pixelkiln 0.69.0 → 0.70.1
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/dist/cli.js +660 -431
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +6 -1
- package/dist/client/gallery.js +2981 -2368
- package/dist/index.cjs +309 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -75
- package/dist/index.d.ts +196 -75
- package/dist/index.js +305 -139
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +11 -0
- package/docs/REVISIONS.md +12 -0
- package/package.json +3 -2
- package/skills/pixelkiln/references/revisions.md +7 -4
package/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
71
|
-
import { readFile as
|
|
70
|
+
import { existsSync as existsSync36 } from "fs";
|
|
71
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -108,7 +108,8 @@ var VALUE_FLAGS = [
|
|
|
108
108
|
"--description",
|
|
109
109
|
"--weight",
|
|
110
110
|
"--glyph-px",
|
|
111
|
-
"--frames"
|
|
111
|
+
"--frames",
|
|
112
|
+
"--estimate-limit"
|
|
112
113
|
];
|
|
113
114
|
var BOOL_FLAGS = [
|
|
114
115
|
"--force",
|
|
@@ -430,7 +431,8 @@ function parseArgs(argv) {
|
|
|
430
431
|
description: get("--description"),
|
|
431
432
|
weight: get("--weight"),
|
|
432
433
|
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
|
|
433
|
-
frames: numberOption("--frames", { min: 3, max: 15, integer: true })
|
|
434
|
+
frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
|
|
435
|
+
estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true })
|
|
434
436
|
};
|
|
435
437
|
}
|
|
436
438
|
|
|
@@ -543,6 +545,7 @@ Options
|
|
|
543
545
|
--no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
|
|
544
546
|
--edit gallery: allow manifest edits from the page (never spends)
|
|
545
547
|
--no-editor gallery: do not offer or serve the in-browser editor
|
|
548
|
+
--estimate-limit <n> gallery: PixelLab skeleton estimates the page may make (default 10)
|
|
546
549
|
--tag Also push tags upstream after fetch
|
|
547
550
|
--refresh fetch: re-download and replace files whose object changed
|
|
548
551
|
upstream (e.g. edited in PixelLab's editor); no generation
|
|
@@ -662,10 +665,10 @@ import { createReadStream } from "fs";
|
|
|
662
665
|
function sha256(data) {
|
|
663
666
|
return createHash("sha256").update(data).digest("hex");
|
|
664
667
|
}
|
|
665
|
-
async function sha256File(
|
|
668
|
+
async function sha256File(path49) {
|
|
666
669
|
return new Promise((resolve, reject) => {
|
|
667
670
|
const hash = createHash("sha256");
|
|
668
|
-
const stream = createReadStream(
|
|
671
|
+
const stream = createReadStream(path49);
|
|
669
672
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
670
673
|
stream.on("error", reject);
|
|
671
674
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -5837,6 +5840,10 @@ function parseSkeletonSet(json, file) {
|
|
|
5837
5840
|
}
|
|
5838
5841
|
return result.data;
|
|
5839
5842
|
}
|
|
5843
|
+
var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
|
|
5844
|
+
function estimateSkeletonSizeProblem(width, height) {
|
|
5845
|
+
return width === height && ESTIMATE_SKELETON_SIZES.includes(width) ? null : `${width}x${height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`;
|
|
5846
|
+
}
|
|
5840
5847
|
var DEFAULT_SCAFFOLD_FRAMES = 4;
|
|
5841
5848
|
function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
|
|
5842
5849
|
const copy = () => keypoints.map((joint) => ({ ...joint }));
|
|
@@ -6156,9 +6163,9 @@ var PixelLabClient = class {
|
|
|
6156
6163
|
apiKey;
|
|
6157
6164
|
http;
|
|
6158
6165
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6159
|
-
async request(
|
|
6166
|
+
async request(path49, init) {
|
|
6160
6167
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6161
|
-
const res = await this.http(`${BASE}${
|
|
6168
|
+
const res = await this.http(`${BASE}${path49}`, {
|
|
6162
6169
|
...init,
|
|
6163
6170
|
headers: {
|
|
6164
6171
|
Authorization: auth,
|
|
@@ -6168,13 +6175,13 @@ var PixelLabClient = class {
|
|
|
6168
6175
|
});
|
|
6169
6176
|
const text = await res.text();
|
|
6170
6177
|
if (!res.ok) {
|
|
6171
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6178
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path49} \u2192 ${res.status}`, res.status, text);
|
|
6172
6179
|
}
|
|
6173
6180
|
if (!text) return {};
|
|
6174
6181
|
try {
|
|
6175
6182
|
return JSON.parse(text);
|
|
6176
6183
|
} catch {
|
|
6177
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6184
|
+
throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
|
|
6178
6185
|
}
|
|
6179
6186
|
}
|
|
6180
6187
|
async balance() {
|
|
@@ -6566,10 +6573,10 @@ var PixelLabClient = class {
|
|
|
6566
6573
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6567
6574
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6568
6575
|
const south = args.reference?.south;
|
|
6569
|
-
let
|
|
6576
|
+
let path49;
|
|
6570
6577
|
let body;
|
|
6571
6578
|
if (args.mode === "v3") {
|
|
6572
|
-
|
|
6579
|
+
path49 = "/create-character-v3";
|
|
6573
6580
|
body = {
|
|
6574
6581
|
description: args.description,
|
|
6575
6582
|
image_size: imageSize,
|
|
@@ -6583,7 +6590,7 @@ var PixelLabClient = class {
|
|
|
6583
6590
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6584
6591
|
};
|
|
6585
6592
|
} else if (args.mode === "pro-flash") {
|
|
6586
|
-
|
|
6593
|
+
path49 = "/create-character-pro-flash";
|
|
6587
6594
|
const traits = args.styleTraits;
|
|
6588
6595
|
body = {
|
|
6589
6596
|
description: args.description,
|
|
@@ -6607,7 +6614,7 @@ var PixelLabClient = class {
|
|
|
6607
6614
|
} : {}
|
|
6608
6615
|
};
|
|
6609
6616
|
} else if (args.mode === "pro") {
|
|
6610
|
-
|
|
6617
|
+
path49 = "/create-character-pro";
|
|
6611
6618
|
body = {
|
|
6612
6619
|
description: args.description,
|
|
6613
6620
|
image_size: imageSize,
|
|
@@ -6621,7 +6628,7 @@ var PixelLabClient = class {
|
|
|
6621
6628
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6622
6629
|
};
|
|
6623
6630
|
} else {
|
|
6624
|
-
|
|
6631
|
+
path49 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6625
6632
|
body = {
|
|
6626
6633
|
description: args.description,
|
|
6627
6634
|
image_size: imageSize,
|
|
@@ -6638,8 +6645,8 @@ var PixelLabClient = class {
|
|
|
6638
6645
|
...palette
|
|
6639
6646
|
};
|
|
6640
6647
|
}
|
|
6641
|
-
const raw = await this.request(
|
|
6642
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6648
|
+
const raw = await this.request(path49, { method: "POST", body: JSON.stringify(body) });
|
|
6649
|
+
return validateResponse(CharacterSubmitSchema, raw, path49.slice(1));
|
|
6643
6650
|
}
|
|
6644
6651
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6645
6652
|
async createCharacterState(args) {
|
|
@@ -9486,9 +9493,9 @@ var RetroDiffusionClient = class {
|
|
|
9486
9493
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9487
9494
|
return Buffer.from(await response.arrayBuffer());
|
|
9488
9495
|
}
|
|
9489
|
-
async call(
|
|
9496
|
+
async call(path49, init = {}) {
|
|
9490
9497
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9491
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9498
|
+
const response = await this.request(`${this.baseUrl}${path49}`, {
|
|
9492
9499
|
...init,
|
|
9493
9500
|
headers: {
|
|
9494
9501
|
"Content-Type": "application/json",
|
|
@@ -11793,10 +11800,10 @@ function isSha256Hash(value) {
|
|
|
11793
11800
|
function parseCache(value) {
|
|
11794
11801
|
return HashCacheSchema.parse(value);
|
|
11795
11802
|
}
|
|
11796
|
-
async function loadCache(
|
|
11797
|
-
if (!existsSync10(
|
|
11803
|
+
async function loadCache(path49) {
|
|
11804
|
+
if (!existsSync10(path49)) return { version: 1, hashes: {} };
|
|
11798
11805
|
try {
|
|
11799
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
11806
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
|
|
11800
11807
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11801
11808
|
return {
|
|
11802
11809
|
version: 1,
|
|
@@ -11808,18 +11815,18 @@ async function loadCache(path48) {
|
|
|
11808
11815
|
return { version: 1, hashes: {} };
|
|
11809
11816
|
}
|
|
11810
11817
|
}
|
|
11811
|
-
async function saveCache(
|
|
11818
|
+
async function saveCache(path49, cache) {
|
|
11812
11819
|
const sorted = {};
|
|
11813
11820
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11814
11821
|
const hash = cache.hashes[key];
|
|
11815
11822
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11816
11823
|
sorted[key] = hash;
|
|
11817
11824
|
}
|
|
11818
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11819
|
-
const tmp = `${
|
|
11825
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path49)), { recursive: true });
|
|
11826
|
+
const tmp = `${path49}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11820
11827
|
try {
|
|
11821
11828
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11822
|
-
await rename5(tmp,
|
|
11829
|
+
await rename5(tmp, path49);
|
|
11823
11830
|
} finally {
|
|
11824
11831
|
await rm5(tmp, { force: true });
|
|
11825
11832
|
}
|
|
@@ -13823,15 +13830,27 @@ var NewAssetSchema = z6.object({
|
|
|
13823
13830
|
/**
|
|
13824
13831
|
* A controlled regeneration of another asset in the same manifest,
|
|
13825
13832
|
* instead of a fresh generation. `inpaint` needs a mask upload the
|
|
13826
|
-
* gallery does not offer yet, so only `image-to-image`
|
|
13827
|
-
*
|
|
13833
|
+
* gallery does not offer yet, so only `image-to-image` and
|
|
13834
|
+
* `animate-skeleton` (whose keypoints file the gallery writes first) can
|
|
13835
|
+
* be created here; hand-edit the manifest for the other modes.
|
|
13828
13836
|
*/
|
|
13829
|
-
revision: z6.
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13837
|
+
revision: z6.discriminatedUnion("mode", [
|
|
13838
|
+
z6.object({
|
|
13839
|
+
mode: z6.literal("image-to-image"),
|
|
13840
|
+
/** Asset id of the parent this revises; must already exist. */
|
|
13841
|
+
from: z6.string().min(1),
|
|
13842
|
+
strength: z6.number().min(0).max(1).optional()
|
|
13843
|
+
}).strict(),
|
|
13844
|
+
z6.object({
|
|
13845
|
+
mode: z6.literal("animate-skeleton"),
|
|
13846
|
+
from: z6.string().min(1),
|
|
13847
|
+
/** Manifest-relative; checked when the manifest is loaded, like every revision input. */
|
|
13848
|
+
keypointsFile: z6.string().min(1),
|
|
13849
|
+
direction: CharacterDirectionSchema,
|
|
13850
|
+
description: z6.string().min(1).optional(),
|
|
13851
|
+
skeletonTemplate: z6.string().min(1).optional()
|
|
13852
|
+
}).strict()
|
|
13853
|
+
]).optional(),
|
|
13835
13854
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
13836
13855
|
state: NewStateSchema.optional(),
|
|
13837
13856
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
@@ -14356,52 +14375,188 @@ async function runEdit(args) {
|
|
|
14356
14375
|
}
|
|
14357
14376
|
|
|
14358
14377
|
// src/cli/commands/gallery.ts
|
|
14359
|
-
import
|
|
14360
|
-
import { existsSync as
|
|
14378
|
+
import path33 from "path";
|
|
14379
|
+
import { existsSync as existsSync25 } from "fs";
|
|
14361
14380
|
|
|
14362
14381
|
// src/gallery/snapshot.ts
|
|
14363
14382
|
import { createHash as createHash3 } from "crypto";
|
|
14364
|
-
import { existsSync as
|
|
14365
|
-
import { readFile as
|
|
14366
|
-
import
|
|
14383
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14384
|
+
import { readFile as readFile18, stat as stat2 } from "fs/promises";
|
|
14385
|
+
import path29 from "path";
|
|
14367
14386
|
|
|
14368
14387
|
// src/gallery/edit.ts
|
|
14388
|
+
import path27 from "path";
|
|
14389
|
+
import { z as z9 } from "zod";
|
|
14390
|
+
|
|
14391
|
+
// src/gallery/skeleton.ts
|
|
14392
|
+
import { existsSync as existsSync18 } from "fs";
|
|
14393
|
+
import { mkdir as mkdir10, readFile as readFile17, rm as rm9, writeFile as writeFile12 } from "fs/promises";
|
|
14369
14394
|
import path26 from "path";
|
|
14370
14395
|
import { z as z8 } from "zod";
|
|
14371
|
-
var
|
|
14372
|
-
|
|
14373
|
-
z8.
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14396
|
+
var SkeletonEstimateRequestSchema = z8.object({
|
|
14397
|
+
project: z8.string().min(1).optional(),
|
|
14398
|
+
styleId: z8.string().min(1),
|
|
14399
|
+
assetId: z8.string().min(1),
|
|
14400
|
+
frames: z8.number().int().min(3).max(15).optional()
|
|
14401
|
+
}).strict();
|
|
14402
|
+
var SkeletonAnimationRequestSchema = z8.object({
|
|
14403
|
+
action: z8.literal("create-skeleton-animation"),
|
|
14404
|
+
project: z8.string().min(1).optional(),
|
|
14405
|
+
expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
|
|
14406
|
+
styleId: z8.string().min(1),
|
|
14407
|
+
/** The sprite the animation poses. */
|
|
14408
|
+
from: z8.string().min(1),
|
|
14409
|
+
assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14410
|
+
/** The motion, sent as PixelLab's `action`. */
|
|
14411
|
+
prompt: z8.string().min(1),
|
|
14412
|
+
direction: CharacterDirectionSchema,
|
|
14413
|
+
description: z8.string().min(1).optional(),
|
|
14414
|
+
skeletonTemplate: z8.string().min(1).optional(),
|
|
14415
|
+
/** Manifest-relative JSON path inside the project. */
|
|
14416
|
+
keypointsFile: z8.string().min(1),
|
|
14417
|
+
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14418
|
+
set: SkeletonSetSchema.optional(),
|
|
14419
|
+
/** Replace an existing `keypointsFile` with `set`. */
|
|
14420
|
+
overwrite: z8.boolean().optional()
|
|
14421
|
+
}).strict();
|
|
14422
|
+
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14423
|
+
function skeletonSourceFile(spec) {
|
|
14424
|
+
if (spec.source) return path26.resolve(spec.root, spec.source);
|
|
14425
|
+
if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
|
|
14426
|
+
return spec.outFile;
|
|
14427
|
+
}
|
|
14428
|
+
function createGallerySkeletonHandlers(opts) {
|
|
14429
|
+
const log2 = opts.onProgress ?? (() => {
|
|
14430
|
+
});
|
|
14431
|
+
const limit = opts.limit ?? DEFAULT_ESTIMATE_LIMIT;
|
|
14432
|
+
let used = 0;
|
|
14433
|
+
const spent = {};
|
|
14434
|
+
const status = () => ({ used, limit, spent: { ...spent } });
|
|
14435
|
+
return {
|
|
14436
|
+
status,
|
|
14437
|
+
async estimate(body) {
|
|
14438
|
+
const parsed = SkeletonEstimateRequestSchema.safeParse(body);
|
|
14439
|
+
if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
|
|
14440
|
+
const request = parsed.data;
|
|
14441
|
+
const ctx = await opts.loadProject(request.project);
|
|
14442
|
+
const key = lockKey(request.styleId, request.assetId);
|
|
14443
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14444
|
+
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14445
|
+
const file = skeletonSourceFile(spec);
|
|
14446
|
+
if (!existsSync18(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
14447
|
+
const bytes = await readFile17(file);
|
|
14448
|
+
const metadata = imageMetadata(bytes);
|
|
14449
|
+
if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
|
|
14450
|
+
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14451
|
+
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14452
|
+
if (used >= limit) {
|
|
14453
|
+
throw Object.assign(
|
|
14454
|
+
new Error(`this gallery has made its ${limit} skeleton estimate${limit === 1 ? "" : "s"}; restart it with --estimate-limit to allow more`),
|
|
14455
|
+
{ status: 429 }
|
|
14456
|
+
);
|
|
14457
|
+
}
|
|
14458
|
+
const client2 = (opts.client ?? clientFromEnv)();
|
|
14459
|
+
used++;
|
|
14460
|
+
const res = await client2.estimateSkeleton({ image: { base64: bytes.toString("base64"), format: metadata.format } });
|
|
14461
|
+
const billed = billedAmount(res.usage);
|
|
14462
|
+
if (billed) spent[billed.unit] = (spent[billed.unit] ?? 0) + billed.amount;
|
|
14463
|
+
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14464
|
+
return {
|
|
14465
|
+
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14466
|
+
source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/"),
|
|
14467
|
+
session: status()
|
|
14468
|
+
};
|
|
14469
|
+
}
|
|
14470
|
+
};
|
|
14471
|
+
}
|
|
14472
|
+
function billedAmount(usage) {
|
|
14473
|
+
if (!usage || typeof usage !== "object") return null;
|
|
14474
|
+
const { type } = usage;
|
|
14475
|
+
if (type !== "usd" && type !== "generations") return null;
|
|
14476
|
+
const amount = usage[type];
|
|
14477
|
+
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14478
|
+
}
|
|
14479
|
+
async function createSkeletonAnimation(manifestPath, request) {
|
|
14480
|
+
const root = path26.dirname(path26.resolve(manifestPath));
|
|
14481
|
+
const file = path26.resolve(root, request.keypointsFile);
|
|
14482
|
+
const relative = path26.relative(root, file);
|
|
14483
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path26.isAbsolute(relative)) {
|
|
14484
|
+
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14485
|
+
}
|
|
14486
|
+
const existed = existsSync18(file);
|
|
14487
|
+
let previous = null;
|
|
14488
|
+
if (request.set) {
|
|
14489
|
+
if (existed && !request.overwrite) {
|
|
14490
|
+
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14491
|
+
}
|
|
14492
|
+
if (existed) previous = await readFile17(file);
|
|
14493
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
14494
|
+
await writeFile12(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14495
|
+
} else if (!existed) {
|
|
14496
|
+
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14497
|
+
}
|
|
14498
|
+
try {
|
|
14499
|
+
await applyManifestEdit(manifestPath, {
|
|
14500
|
+
action: "add-asset",
|
|
14501
|
+
assetId: request.assetId,
|
|
14502
|
+
expectedSha256: request.expectedSha256,
|
|
14503
|
+
asset: {
|
|
14504
|
+
prompt: request.prompt,
|
|
14505
|
+
styles: [request.styleId],
|
|
14506
|
+
revision: {
|
|
14507
|
+
mode: "animate-skeleton",
|
|
14508
|
+
from: request.from,
|
|
14509
|
+
keypointsFile: relative.split(path26.sep).join("/"),
|
|
14510
|
+
direction: request.direction,
|
|
14511
|
+
...request.description ? { description: request.description } : {},
|
|
14512
|
+
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
14513
|
+
}
|
|
14514
|
+
}
|
|
14515
|
+
});
|
|
14516
|
+
} catch (error) {
|
|
14517
|
+
if (request.set) {
|
|
14518
|
+
if (previous) await writeFile12(file, previous);
|
|
14519
|
+
else await rm9(file, { force: true });
|
|
14520
|
+
}
|
|
14521
|
+
throw error;
|
|
14522
|
+
}
|
|
14523
|
+
}
|
|
14524
|
+
|
|
14525
|
+
// src/gallery/edit.ts
|
|
14526
|
+
var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
14527
|
+
var HandEditRequestSchema = z9.discriminatedUnion("action", [
|
|
14528
|
+
z9.object({
|
|
14529
|
+
action: z9.literal("start-edit"),
|
|
14530
|
+
project: z9.string().min(1).optional(),
|
|
14531
|
+
assetId: z9.string().min(1),
|
|
14532
|
+
styleId: z9.string().min(1),
|
|
14533
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14379
14534
|
/** Also launch the author's editor on the file. */
|
|
14380
|
-
open:
|
|
14535
|
+
open: z9.boolean().optional()
|
|
14381
14536
|
}).strict(),
|
|
14382
|
-
|
|
14383
|
-
action:
|
|
14384
|
-
project:
|
|
14385
|
-
assetId:
|
|
14386
|
-
styleId:
|
|
14387
|
-
expectedSha256:
|
|
14537
|
+
z9.object({
|
|
14538
|
+
action: z9.literal("detach-edit"),
|
|
14539
|
+
project: z9.string().min(1).optional(),
|
|
14540
|
+
assetId: z9.string().min(1),
|
|
14541
|
+
styleId: z9.string().min(1),
|
|
14542
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/)
|
|
14388
14543
|
}).strict(),
|
|
14389
14544
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14390
|
-
|
|
14391
|
-
action:
|
|
14392
|
-
project:
|
|
14393
|
-
assetId:
|
|
14394
|
-
styleId:
|
|
14395
|
-
expectedSha256:
|
|
14545
|
+
z9.object({
|
|
14546
|
+
action: z9.literal("save-edit"),
|
|
14547
|
+
project: z9.string().min(1).optional(),
|
|
14548
|
+
assetId: z9.string().min(1),
|
|
14549
|
+
styleId: z9.string().min(1),
|
|
14550
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14396
14551
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14397
14552
|
png: Base64.min(1).optional(),
|
|
14398
14553
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14399
|
-
frames:
|
|
14554
|
+
frames: z9.array(z9.object({ role: z9.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14400
14555
|
/** The editor's layered project file, kept beside the edit. */
|
|
14401
14556
|
pxo: Base64.optional(),
|
|
14402
14557
|
/** Editor identity from the bridge's ready message. */
|
|
14403
|
-
editor:
|
|
14404
|
-
protocol:
|
|
14558
|
+
editor: z9.string().min(1).max(200),
|
|
14559
|
+
protocol: z9.number().int().positive()
|
|
14405
14560
|
}).strict()
|
|
14406
14561
|
]);
|
|
14407
14562
|
function createGalleryEditHandler(opts) {
|
|
@@ -14442,6 +14597,23 @@ function createGalleryEditHandler(opts) {
|
|
|
14442
14597
|
}
|
|
14443
14598
|
return opts.reload();
|
|
14444
14599
|
}
|
|
14600
|
+
const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
|
|
14601
|
+
if (skeleton.success) {
|
|
14602
|
+
let manifestPath2;
|
|
14603
|
+
try {
|
|
14604
|
+
manifestPath2 = await opts.manifestFor(skeleton.data.project);
|
|
14605
|
+
} catch (error) {
|
|
14606
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14607
|
+
}
|
|
14608
|
+
await createSkeletonAnimation(manifestPath2, skeleton.data);
|
|
14609
|
+
log2(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
|
|
14610
|
+
return opts.reload();
|
|
14611
|
+
}
|
|
14612
|
+
if (body?.action === "create-skeleton-animation") {
|
|
14613
|
+
throw new ManifestEditError(
|
|
14614
|
+
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
14615
|
+
);
|
|
14616
|
+
}
|
|
14445
14617
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
14446
14618
|
if (!parsed.success) {
|
|
14447
14619
|
throw new ManifestEditError(
|
|
@@ -14456,7 +14628,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14456
14628
|
}
|
|
14457
14629
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14458
14630
|
if (result.changed) {
|
|
14459
|
-
const relative =
|
|
14631
|
+
const relative = path27.relative(process.cwd(), result.manifestPath);
|
|
14460
14632
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14461
14633
|
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
|
|
14462
14634
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -14466,8 +14638,8 @@ function createGalleryEditHandler(opts) {
|
|
|
14466
14638
|
}
|
|
14467
14639
|
|
|
14468
14640
|
// src/pipeline/history.ts
|
|
14469
|
-
import { existsSync as
|
|
14470
|
-
import
|
|
14641
|
+
import { existsSync as existsSync19 } from "fs";
|
|
14642
|
+
import path28 from "path";
|
|
14471
14643
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
14472
14644
|
function historyLimit(manifest, env = process.env) {
|
|
14473
14645
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -14554,10 +14726,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14554
14726
|
}
|
|
14555
14727
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
14556
14728
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
14557
|
-
if (!
|
|
14729
|
+
if (!existsSync19(file)) continue;
|
|
14558
14730
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
14559
14731
|
throw new OverwriteRefusedError(
|
|
14560
|
-
`${
|
|
14732
|
+
`${path28.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
14561
14733
|
[file]
|
|
14562
14734
|
);
|
|
14563
14735
|
}
|
|
@@ -14610,7 +14782,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14610
14782
|
// src/gallery/snapshot.ts
|
|
14611
14783
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14612
14784
|
function galleryMediaId(absolutePath) {
|
|
14613
|
-
return createHash3("sha256").update(
|
|
14785
|
+
return createHash3("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14614
14786
|
}
|
|
14615
14787
|
function galleryMediaRoute(id) {
|
|
14616
14788
|
return `/media/${id}`;
|
|
@@ -14629,9 +14801,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14629
14801
|
return (entry.history ?? []).map((generation, i) => {
|
|
14630
14802
|
const outputs = generation.outputs.map((output) => {
|
|
14631
14803
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14632
|
-
const file =
|
|
14804
|
+
const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14633
14805
|
let url = null;
|
|
14634
|
-
if (
|
|
14806
|
+
if (existsSync20(file)) {
|
|
14635
14807
|
const id = galleryMediaId(file);
|
|
14636
14808
|
media.set(id, { path: file, contentType: mediaType });
|
|
14637
14809
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14663,7 +14835,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14663
14835
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
14664
14836
|
async function samePixels(a, b) {
|
|
14665
14837
|
try {
|
|
14666
|
-
const [left, right] = await Promise.all([
|
|
14838
|
+
const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
|
|
14667
14839
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
14668
14840
|
const x = decodePng(left);
|
|
14669
14841
|
const y = decodePng(right);
|
|
@@ -14761,10 +14933,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14761
14933
|
async function describeQuality(media, root, inspection) {
|
|
14762
14934
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14763
14935
|
const outputs = await Promise.all(
|
|
14764
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
14936
|
+
outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
|
|
14765
14937
|
);
|
|
14766
|
-
const recordPath =
|
|
14767
|
-
const recordExists =
|
|
14938
|
+
const recordPath = path29.resolve(root, inspection.record);
|
|
14939
|
+
const recordExists = existsSync20(recordPath);
|
|
14768
14940
|
let options = null;
|
|
14769
14941
|
let check = null;
|
|
14770
14942
|
if (recordExists) {
|
|
@@ -14783,7 +14955,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14783
14955
|
return {
|
|
14784
14956
|
state: inspection.state,
|
|
14785
14957
|
reason: inspection.reason,
|
|
14786
|
-
output: portableOutputPath(
|
|
14958
|
+
output: portableOutputPath(path29.resolve(root, inspection.output), root),
|
|
14787
14959
|
record: portableOutputPath(recordPath, root),
|
|
14788
14960
|
recordExists,
|
|
14789
14961
|
outputs,
|
|
@@ -14821,9 +14993,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
14821
14993
|
const media = /* @__PURE__ */ new Map();
|
|
14822
14994
|
const plan = await buildPlan(specs, lock);
|
|
14823
14995
|
const items = [];
|
|
14824
|
-
const manifestText = await
|
|
14996
|
+
const manifestText = await readFile18(loaded.path, "utf8");
|
|
14825
14997
|
const manifestSha256 = sha256(manifestText);
|
|
14826
|
-
const cacheDir =
|
|
14998
|
+
const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
14827
14999
|
let rawStyles = {};
|
|
14828
15000
|
try {
|
|
14829
15001
|
const raw = JSON.parse(manifestText);
|
|
@@ -14847,12 +15019,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
14847
15019
|
)
|
|
14848
15020
|
);
|
|
14849
15021
|
if (spec.source) {
|
|
14850
|
-
const editPath =
|
|
15022
|
+
const editPath = path29.resolve(root, spec.source);
|
|
14851
15023
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
14852
15024
|
const editShas = await Promise.all(
|
|
14853
15025
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
14854
15026
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
14855
|
-
return { file, sha256:
|
|
15027
|
+
return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
|
|
14856
15028
|
})
|
|
14857
15029
|
);
|
|
14858
15030
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -14860,7 +15032,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14860
15032
|
const companion = await readHandEditCompanion(editPath);
|
|
14861
15033
|
if (companion) {
|
|
14862
15034
|
const projectPath = handEditProjectPath(editPath);
|
|
14863
|
-
const project = companion.project &&
|
|
15035
|
+
const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
|
|
14864
15036
|
let projectUrl = null;
|
|
14865
15037
|
if (project) {
|
|
14866
15038
|
const id = galleryMediaId(projectPath);
|
|
@@ -14896,8 +15068,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
14896
15068
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
14897
15069
|
}
|
|
14898
15070
|
} else if (spec.source) {
|
|
14899
|
-
outputs = [await describeOutput(media, root,
|
|
14900
|
-
} else if (!entry &&
|
|
15071
|
+
outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
|
|
15072
|
+
} else if (!entry && existsSync20(spec.outFile)) {
|
|
14901
15073
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
14902
15074
|
} else {
|
|
14903
15075
|
outputs = [];
|
|
@@ -14938,6 +15110,10 @@ async function buildGallerySnapshot(opts) {
|
|
|
14938
15110
|
revision: entry?.revision ?? null,
|
|
14939
15111
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
14940
15112
|
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
15113
|
+
skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
|
|
15114
|
+
keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
|
|
15115
|
+
set: spec.revision.skeleton ?? null
|
|
15116
|
+
} : null,
|
|
14941
15117
|
character: describeCharacter(spec, entry),
|
|
14942
15118
|
outputs,
|
|
14943
15119
|
quality,
|
|
@@ -15002,6 +15178,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15002
15178
|
revision: entry.revision,
|
|
15003
15179
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
15004
15180
|
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
15181
|
+
skeleton: null,
|
|
15005
15182
|
character: describeCharacter(void 0, entry),
|
|
15006
15183
|
outputs,
|
|
15007
15184
|
quality: null,
|
|
@@ -15092,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15092
15269
|
id: loaded.manifest.name,
|
|
15093
15270
|
name: loaded.manifest.name,
|
|
15094
15271
|
manifest: loaded.path,
|
|
15095
|
-
lock:
|
|
15272
|
+
lock: path29.resolve(opts.lockPath),
|
|
15096
15273
|
root,
|
|
15097
15274
|
provider: loaded.manifest.provider,
|
|
15098
15275
|
account: null,
|
|
@@ -15118,7 +15295,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15118
15295
|
return { snapshot, media };
|
|
15119
15296
|
}
|
|
15120
15297
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15121
|
-
const dir =
|
|
15298
|
+
const dir = path29.dirname(path29.resolve(opts.workspacePath));
|
|
15122
15299
|
const media = /* @__PURE__ */ new Map();
|
|
15123
15300
|
const projects = [];
|
|
15124
15301
|
const items = [];
|
|
@@ -15161,11 +15338,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15161
15338
|
id: project.id,
|
|
15162
15339
|
name: loaded.manifest.name,
|
|
15163
15340
|
manifest: manifestPath,
|
|
15164
|
-
lock:
|
|
15341
|
+
lock: path29.resolve(lockPath),
|
|
15165
15342
|
root: loaded.root,
|
|
15166
15343
|
provider: loaded.manifest.provider,
|
|
15167
15344
|
account: project.account ?? null,
|
|
15168
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
15345
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
|
|
15169
15346
|
historyLimit: historyLimit(loaded.manifest),
|
|
15170
15347
|
entries: Object.keys(lock.entries).length,
|
|
15171
15348
|
items: projectItems.length,
|
|
@@ -15177,8 +15354,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15177
15354
|
id: project.id,
|
|
15178
15355
|
name: project.id,
|
|
15179
15356
|
manifest: manifestPath,
|
|
15180
|
-
lock:
|
|
15181
|
-
root:
|
|
15357
|
+
lock: path29.resolve(lockPath),
|
|
15358
|
+
root: path29.dirname(manifestPath),
|
|
15182
15359
|
provider: project.provider,
|
|
15183
15360
|
account: project.account ?? null,
|
|
15184
15361
|
manifestSha256: null,
|
|
@@ -15205,7 +15382,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15205
15382
|
version: 1,
|
|
15206
15383
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15207
15384
|
project: null,
|
|
15208
|
-
workspace: { path:
|
|
15385
|
+
workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
|
|
15209
15386
|
filter,
|
|
15210
15387
|
totals: {
|
|
15211
15388
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15224,16 +15401,51 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15224
15401
|
// src/gallery/server.ts
|
|
15225
15402
|
import { randomBytes } from "crypto";
|
|
15226
15403
|
import { createServer as createServer2 } from "http";
|
|
15227
|
-
import { readFile as
|
|
15404
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
15405
|
+
|
|
15406
|
+
// src/gallery/page.ts
|
|
15407
|
+
import { existsSync as existsSync21, readFileSync as readFileSync4 } from "fs";
|
|
15408
|
+
|
|
15409
|
+
// src/gallery/client-bundle.ts
|
|
15410
|
+
import { createRequire } from "module";
|
|
15411
|
+
import { fileURLToPath } from "url";
|
|
15412
|
+
function bundleGalleryClient(entry = fileURLToPath(new URL("./client/src/main.ts", import.meta.url))) {
|
|
15413
|
+
const esbuild = createRequire(import.meta.url)("esbuild");
|
|
15414
|
+
const result = esbuild.buildSync({
|
|
15415
|
+
entryPoints: [entry],
|
|
15416
|
+
bundle: true,
|
|
15417
|
+
format: "iife",
|
|
15418
|
+
target: "es2020",
|
|
15419
|
+
platform: "browser",
|
|
15420
|
+
write: false,
|
|
15421
|
+
legalComments: "none",
|
|
15422
|
+
charset: "utf8"
|
|
15423
|
+
});
|
|
15424
|
+
return result.outputFiles[0].text;
|
|
15425
|
+
}
|
|
15228
15426
|
|
|
15229
15427
|
// src/gallery/page.ts
|
|
15230
|
-
|
|
15428
|
+
function galleryContentSecurityPolicy(nonce) {
|
|
15429
|
+
return [
|
|
15430
|
+
"default-src 'self'",
|
|
15431
|
+
`script-src 'nonce-${nonce}'`,
|
|
15432
|
+
`style-src 'nonce-${nonce}'`,
|
|
15433
|
+
"img-src 'self' data: blob:",
|
|
15434
|
+
"frame-src 'self'",
|
|
15435
|
+
"connect-src 'self'",
|
|
15436
|
+
"object-src 'none'",
|
|
15437
|
+
"base-uri 'none'",
|
|
15438
|
+
"form-action 'none'",
|
|
15439
|
+
"frame-ancestors 'none'"
|
|
15440
|
+
].join("; ");
|
|
15441
|
+
}
|
|
15231
15442
|
var CLIENT_DIR = new URL("./client/", import.meta.url);
|
|
15232
15443
|
var client;
|
|
15233
15444
|
function clientAssets() {
|
|
15445
|
+
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15234
15446
|
client ??= {
|
|
15235
15447
|
css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
15236
|
-
js:
|
|
15448
|
+
js: existsSync21(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
|
|
15237
15449
|
};
|
|
15238
15450
|
return client;
|
|
15239
15451
|
}
|
|
@@ -15245,6 +15457,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
15245
15457
|
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
15246
15458
|
const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
|
|
15247
15459
|
const { css, js } = clientAssets();
|
|
15460
|
+
const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
|
|
15248
15461
|
return `<!doctype html>
|
|
15249
15462
|
<html lang="en">
|
|
15250
15463
|
<head>
|
|
@@ -15252,7 +15465,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
15252
15465
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
15253
15466
|
<title>${escapeHtml2(title)}</title>
|
|
15254
15467
|
<link rel="icon" href="data:,">
|
|
15255
|
-
<style>
|
|
15468
|
+
<style${nonce}>
|
|
15256
15469
|
${css}</style>
|
|
15257
15470
|
</head>
|
|
15258
15471
|
<body>
|
|
@@ -15294,7 +15507,7 @@ ${css}</style>
|
|
|
15294
15507
|
<div id="drawer-host"></div>
|
|
15295
15508
|
<div id="dialog-host"></div>
|
|
15296
15509
|
<datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
|
|
15297
|
-
<script>
|
|
15510
|
+
<script${nonce}>
|
|
15298
15511
|
const INITIAL = ${data};
|
|
15299
15512
|
const SESSION = ${session};
|
|
15300
15513
|
const EDITABLE = ${editable};
|
|
@@ -15334,7 +15547,7 @@ async function readJsonBody(req) {
|
|
|
15334
15547
|
async function serveGallery(opts) {
|
|
15335
15548
|
const log2 = opts.onProgress ?? (() => {
|
|
15336
15549
|
});
|
|
15337
|
-
const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
|
|
15550
|
+
const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
|
|
15338
15551
|
let media = /* @__PURE__ */ new Map();
|
|
15339
15552
|
let loading = null;
|
|
15340
15553
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -15409,6 +15622,17 @@ async function serveGallery(opts) {
|
|
|
15409
15622
|
}
|
|
15410
15623
|
return;
|
|
15411
15624
|
}
|
|
15625
|
+
if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
|
|
15626
|
+
if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
|
|
15627
|
+
try {
|
|
15628
|
+
const body = await guardedBody("skeleton estimates");
|
|
15629
|
+
if (body === void 0) return;
|
|
15630
|
+
json(200, await opts.skeleton.estimate(body));
|
|
15631
|
+
} catch (err) {
|
|
15632
|
+
reportError("skeleton estimate", err);
|
|
15633
|
+
}
|
|
15634
|
+
return;
|
|
15635
|
+
}
|
|
15412
15636
|
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
15413
15637
|
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
15414
15638
|
try {
|
|
@@ -15465,7 +15689,7 @@ async function serveGallery(opts) {
|
|
|
15465
15689
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
15466
15690
|
if (!asset) return fail(404, "no such review asset");
|
|
15467
15691
|
try {
|
|
15468
|
-
const bytes = await
|
|
15692
|
+
const bytes = await readFile19(asset.path);
|
|
15469
15693
|
res.writeHead(200, {
|
|
15470
15694
|
"Content-Type": asset.contentType,
|
|
15471
15695
|
"Cache-Control": "no-store",
|
|
@@ -15479,6 +15703,10 @@ async function serveGallery(opts) {
|
|
|
15479
15703
|
}
|
|
15480
15704
|
}
|
|
15481
15705
|
if (req.method !== "GET" && req.method !== "HEAD") return fail(405, "the gallery is read-only");
|
|
15706
|
+
if (url2.pathname === "/api/skeleton") {
|
|
15707
|
+
if (!opts.skeleton) return fail(404, "skeleton estimates need the gallery started with --edit");
|
|
15708
|
+
return json(200, opts.skeleton.status());
|
|
15709
|
+
}
|
|
15482
15710
|
if (url2.pathname === "/api/jobs") {
|
|
15483
15711
|
if (!opts.generate) return fail(404, "generation is off");
|
|
15484
15712
|
return json(200, opts.generate.status());
|
|
@@ -15512,12 +15740,15 @@ async function serveGallery(opts) {
|
|
|
15512
15740
|
try {
|
|
15513
15741
|
if (url2.pathname === "/") {
|
|
15514
15742
|
const { snapshot } = await load();
|
|
15743
|
+
const nonce = randomBytes(16).toString("base64");
|
|
15515
15744
|
res.writeHead(200, {
|
|
15516
15745
|
"Content-Type": "text/html; charset=utf-8",
|
|
15517
15746
|
"Cache-Control": "no-store",
|
|
15518
|
-
"X-Content-Type-Options": "nosniff"
|
|
15747
|
+
"X-Content-Type-Options": "nosniff",
|
|
15748
|
+
"Content-Security-Policy": galleryContentSecurityPolicy(nonce)
|
|
15519
15749
|
});
|
|
15520
15750
|
res.end(renderGallery(snapshot, {
|
|
15751
|
+
nonce,
|
|
15521
15752
|
...session ? { session } : {},
|
|
15522
15753
|
editable: Boolean(opts.edit),
|
|
15523
15754
|
generation: Boolean(opts.generate),
|
|
@@ -15541,7 +15772,7 @@ async function serveGallery(opts) {
|
|
|
15541
15772
|
if (!asset) return fail(404, "no such gallery media");
|
|
15542
15773
|
let bytes;
|
|
15543
15774
|
try {
|
|
15544
|
-
bytes = await
|
|
15775
|
+
bytes = await readFile19(asset.path);
|
|
15545
15776
|
} catch {
|
|
15546
15777
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
15547
15778
|
}
|
|
@@ -15585,7 +15816,7 @@ async function serveGallery(opts) {
|
|
|
15585
15816
|
|
|
15586
15817
|
// src/gallery/generate.ts
|
|
15587
15818
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15588
|
-
import { z as
|
|
15819
|
+
import { z as z10 } from "zod";
|
|
15589
15820
|
|
|
15590
15821
|
// src/pipeline/poll.ts
|
|
15591
15822
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -15996,13 +16227,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
15996
16227
|
}
|
|
15997
16228
|
|
|
15998
16229
|
// src/pick/server.ts
|
|
15999
|
-
import { existsSync as
|
|
16000
|
-
import { readFile as
|
|
16230
|
+
import { existsSync as existsSync23 } from "fs";
|
|
16231
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
16001
16232
|
|
|
16002
16233
|
// src/pipeline/init.ts
|
|
16003
|
-
import { readdir, readFile as
|
|
16004
|
-
import { existsSync as
|
|
16005
|
-
import
|
|
16234
|
+
import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
|
|
16235
|
+
import { existsSync as existsSync22 } from "fs";
|
|
16236
|
+
import path30 from "path";
|
|
16006
16237
|
function pngSize(buf) {
|
|
16007
16238
|
if (buf.length < 24) return null;
|
|
16008
16239
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16016,7 +16247,7 @@ function slugify(value) {
|
|
|
16016
16247
|
async function walk(dir, exclude) {
|
|
16017
16248
|
const out = [];
|
|
16018
16249
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16019
|
-
const full =
|
|
16250
|
+
const full = path30.join(dir, entry.name);
|
|
16020
16251
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16021
16252
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16022
16253
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16030,14 +16261,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
16030
16261
|
const skipped = [];
|
|
16031
16262
|
const seen = /* @__PURE__ */ new Map();
|
|
16032
16263
|
for (const file of files.sort()) {
|
|
16033
|
-
const size = pngSize(await
|
|
16264
|
+
const size = pngSize(await readFile20(file));
|
|
16034
16265
|
if (!size) {
|
|
16035
16266
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16036
16267
|
continue;
|
|
16037
16268
|
}
|
|
16038
|
-
const rel =
|
|
16039
|
-
const category =
|
|
16040
|
-
let id = slugify(
|
|
16269
|
+
const rel = path30.relative(root, file);
|
|
16270
|
+
const category = path30.dirname(rel) === "." ? "" : path30.dirname(rel);
|
|
16271
|
+
let id = slugify(path30.basename(rel));
|
|
16041
16272
|
if (seen.has(id)) {
|
|
16042
16273
|
const n = seen.get(id) + 1;
|
|
16043
16274
|
seen.set(id, n);
|
|
@@ -16095,10 +16326,10 @@ function mode(values) {
|
|
|
16095
16326
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16096
16327
|
}
|
|
16097
16328
|
async function writeManifestFile(target, manifest) {
|
|
16098
|
-
if (
|
|
16329
|
+
if (existsSync22(target)) {
|
|
16099
16330
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16100
16331
|
}
|
|
16101
|
-
await
|
|
16332
|
+
await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16102
16333
|
}
|
|
16103
16334
|
|
|
16104
16335
|
// src/pick/sheet.ts
|
|
@@ -16511,13 +16742,13 @@ async function currentArt(entry, spec) {
|
|
|
16511
16742
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
16512
16743
|
if (!previous) return null;
|
|
16513
16744
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
16514
|
-
if (!
|
|
16745
|
+
if (!existsSync23(file)) return null;
|
|
16515
16746
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
16516
16747
|
if (!contentType) return null;
|
|
16517
16748
|
let width = entry.width;
|
|
16518
16749
|
let height = entry.height;
|
|
16519
16750
|
if (contentType === "image/png") {
|
|
16520
|
-
const size = pngSize(await
|
|
16751
|
+
const size = pngSize(await readFile21(file));
|
|
16521
16752
|
if (size) ({ width, height } = size);
|
|
16522
16753
|
}
|
|
16523
16754
|
return { path: file, contentType, width, height };
|
|
@@ -16685,26 +16916,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
16685
16916
|
}
|
|
16686
16917
|
|
|
16687
16918
|
// src/gallery/generate.ts
|
|
16688
|
-
var GenerateRequestSchema =
|
|
16689
|
-
project:
|
|
16690
|
-
keys:
|
|
16919
|
+
var GenerateRequestSchema = z10.object({
|
|
16920
|
+
project: z10.string().min(1).optional(),
|
|
16921
|
+
keys: z10.array(z10.string().min(1)).min(1).max(500),
|
|
16691
16922
|
/**
|
|
16692
16923
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
16693
16924
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
16694
16925
|
* the recorded bytes back over a modified file.
|
|
16695
16926
|
*/
|
|
16696
|
-
force:
|
|
16927
|
+
force: z10.boolean().optional(),
|
|
16697
16928
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
16698
|
-
resume:
|
|
16929
|
+
resume: z10.boolean().optional(),
|
|
16699
16930
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
16700
|
-
refresh:
|
|
16931
|
+
refresh: z10.boolean().optional(),
|
|
16701
16932
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
16702
|
-
restore:
|
|
16933
|
+
restore: z10.boolean().optional(),
|
|
16703
16934
|
/**
|
|
16704
16935
|
* Bring a replaced generation back: 1-based position in the record's
|
|
16705
16936
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
16706
16937
|
*/
|
|
16707
|
-
revert:
|
|
16938
|
+
revert: z10.string().min(1).max(64).optional()
|
|
16708
16939
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
16709
16940
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
16710
16941
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17014,15 +17245,15 @@ function createGenerateHandlers(opts) {
|
|
|
17014
17245
|
}
|
|
17015
17246
|
|
|
17016
17247
|
// src/gallery/editor.ts
|
|
17017
|
-
import { readFile as
|
|
17018
|
-
import
|
|
17248
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
17249
|
+
import path32 from "path";
|
|
17019
17250
|
|
|
17020
17251
|
// src/editor/install.ts
|
|
17021
17252
|
import { createHash as createHash4 } from "crypto";
|
|
17022
|
-
import { existsSync as
|
|
17023
|
-
import { mkdir as
|
|
17253
|
+
import { existsSync as existsSync24 } from "fs";
|
|
17254
|
+
import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
|
|
17024
17255
|
import os2 from "os";
|
|
17025
|
-
import
|
|
17256
|
+
import path31 from "path";
|
|
17026
17257
|
|
|
17027
17258
|
// tools/pixelorama-bridge/pin.json
|
|
17028
17259
|
var pin_default = {
|
|
@@ -17086,15 +17317,15 @@ var EditorInstallError = class extends Error {
|
|
|
17086
17317
|
};
|
|
17087
17318
|
function editorToolsRoot() {
|
|
17088
17319
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17089
|
-
if (override) return
|
|
17090
|
-
if (process.platform === "darwin") return
|
|
17320
|
+
if (override) return path31.resolve(override);
|
|
17321
|
+
if (process.platform === "darwin") return path31.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17091
17322
|
if (process.platform === "win32") {
|
|
17092
|
-
return
|
|
17323
|
+
return path31.join(process.env.LOCALAPPDATA ?? path31.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17093
17324
|
}
|
|
17094
|
-
return
|
|
17325
|
+
return path31.join(process.env.XDG_CACHE_HOME ?? path31.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17095
17326
|
}
|
|
17096
17327
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17097
|
-
return
|
|
17328
|
+
return path31.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17098
17329
|
}
|
|
17099
17330
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17100
17331
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17104,7 +17335,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17104
17335
|
}
|
|
17105
17336
|
async function sha256Of(file) {
|
|
17106
17337
|
try {
|
|
17107
|
-
return createHash4("sha256").update(await
|
|
17338
|
+
return createHash4("sha256").update(await readFile22(file)).digest("hex");
|
|
17108
17339
|
} catch {
|
|
17109
17340
|
return null;
|
|
17110
17341
|
}
|
|
@@ -17117,8 +17348,8 @@ async function editorStatus(opts = {}) {
|
|
|
17117
17348
|
let totalBytes = 0;
|
|
17118
17349
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17119
17350
|
totalBytes += expected.bytes;
|
|
17120
|
-
const file =
|
|
17121
|
-
const info =
|
|
17351
|
+
const file = path31.join(dir, name);
|
|
17352
|
+
const info = existsSync24(file) ? await stat3(file).catch(() => null) : null;
|
|
17122
17353
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17123
17354
|
installedBytes += expected.bytes;
|
|
17124
17355
|
} else {
|
|
@@ -17147,7 +17378,7 @@ async function installEditor(opts = {}) {
|
|
|
17147
17378
|
);
|
|
17148
17379
|
}
|
|
17149
17380
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17150
|
-
await
|
|
17381
|
+
await mkdir11(dir, { recursive: true });
|
|
17151
17382
|
const downloaded = [];
|
|
17152
17383
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17153
17384
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17175,13 +17406,13 @@ async function installEditor(opts = {}) {
|
|
|
17175
17406
|
if (digest2 !== expected.sha256) {
|
|
17176
17407
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17177
17408
|
}
|
|
17178
|
-
const final =
|
|
17409
|
+
const final = path31.join(dir, name);
|
|
17179
17410
|
const tmp = `${final}.${process.pid}.part`;
|
|
17180
17411
|
try {
|
|
17181
|
-
await
|
|
17412
|
+
await writeFile14(tmp, bytes);
|
|
17182
17413
|
await rename9(tmp, final);
|
|
17183
17414
|
} finally {
|
|
17184
|
-
await
|
|
17415
|
+
await rm10(tmp, { force: true });
|
|
17185
17416
|
}
|
|
17186
17417
|
fetchedBytes += bytes.length;
|
|
17187
17418
|
downloaded.push(name);
|
|
@@ -17221,7 +17452,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17221
17452
|
".json": "application/json; charset=utf-8"
|
|
17222
17453
|
};
|
|
17223
17454
|
function editorContentType(name) {
|
|
17224
|
-
return EDITOR_CONTENT_TYPES[
|
|
17455
|
+
return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
|
|
17225
17456
|
}
|
|
17226
17457
|
|
|
17227
17458
|
// src/gallery/editor.ts
|
|
@@ -17297,7 +17528,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17297
17528
|
if (!status.installed) return null;
|
|
17298
17529
|
let bytes;
|
|
17299
17530
|
try {
|
|
17300
|
-
bytes = await
|
|
17531
|
+
bytes = await readFile23(path32.join(dir, name));
|
|
17301
17532
|
} catch {
|
|
17302
17533
|
verified = null;
|
|
17303
17534
|
return null;
|
|
@@ -17337,13 +17568,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17337
17568
|
};
|
|
17338
17569
|
const loadProject = async (project) => {
|
|
17339
17570
|
const ctx = await access2.loadProject(project);
|
|
17340
|
-
const dir =
|
|
17571
|
+
const dir = path33.dirname(ctx.loaded.path);
|
|
17341
17572
|
const declared = readEnvFiles(dir);
|
|
17342
17573
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
17343
17574
|
for (const [name, value] of Object.entries(declared)) {
|
|
17344
17575
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
17345
17576
|
throw new Error(
|
|
17346
|
-
`${
|
|
17577
|
+
`${path33.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
17347
17578
|
);
|
|
17348
17579
|
}
|
|
17349
17580
|
}
|
|
@@ -17371,6 +17602,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17371
17602
|
),
|
|
17372
17603
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
17373
17604
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
17605
|
+
// Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
|
|
17606
|
+
...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
|
|
17374
17607
|
// The editor exists to write hand edits back, so it follows the write gate.
|
|
17375
17608
|
...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
|
|
17376
17609
|
});
|
|
@@ -17388,8 +17621,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17388
17621
|
}
|
|
17389
17622
|
async function runGallery(args) {
|
|
17390
17623
|
if (args.workspace) {
|
|
17391
|
-
const workspacePath =
|
|
17392
|
-
if (!
|
|
17624
|
+
const workspacePath = path33.resolve(args.workspace);
|
|
17625
|
+
if (!existsSync25(workspacePath)) {
|
|
17393
17626
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
17394
17627
|
}
|
|
17395
17628
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -17409,7 +17642,7 @@ async function runGallery(args) {
|
|
|
17409
17642
|
const registered = async (projectId) => {
|
|
17410
17643
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
17411
17644
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
17412
|
-
return resolveProject(
|
|
17645
|
+
return resolveProject(path33.dirname(workspacePath), project);
|
|
17413
17646
|
};
|
|
17414
17647
|
await serveUntilStopped(initial, build2, args, {
|
|
17415
17648
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -17434,7 +17667,7 @@ async function runGallery(args) {
|
|
|
17434
17667
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
17435
17668
|
};
|
|
17436
17669
|
await serveUntilStopped(await build(), reload, args, {
|
|
17437
|
-
manifestFor: () =>
|
|
17670
|
+
manifestFor: () => path33.resolve(args.manifest),
|
|
17438
17671
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
17439
17672
|
});
|
|
17440
17673
|
}
|
|
@@ -17743,18 +17976,18 @@ async function runRestoreGeneration(args) {
|
|
|
17743
17976
|
}
|
|
17744
17977
|
|
|
17745
17978
|
// src/cli/commands/housekeeping.ts
|
|
17746
|
-
import { existsSync as
|
|
17979
|
+
import { existsSync as existsSync27 } from "fs";
|
|
17747
17980
|
|
|
17748
17981
|
// src/pipeline/cache-health.ts
|
|
17749
|
-
import { existsSync as
|
|
17750
|
-
import { readFile as
|
|
17751
|
-
import
|
|
17982
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17983
|
+
import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
|
|
17984
|
+
import path34 from "path";
|
|
17752
17985
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
17753
|
-
if (options.prune && !
|
|
17754
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
17986
|
+
if (options.prune && !existsSync26(lockPath)) {
|
|
17987
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
|
|
17755
17988
|
}
|
|
17756
|
-
const contentDir =
|
|
17757
|
-
const remotePath =
|
|
17989
|
+
const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
|
|
17990
|
+
const remotePath = path34.resolve(cachePathFor(lockPath));
|
|
17758
17991
|
const referenced = referencedHashes(lock);
|
|
17759
17992
|
let content = await inspectContentCache(contentDir, referenced);
|
|
17760
17993
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -17766,7 +17999,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17766
17999
|
]);
|
|
17767
18000
|
for (const name of names) {
|
|
17768
18001
|
try {
|
|
17769
|
-
await
|
|
18002
|
+
await rm11(path34.join(contentDir, name), { force: true });
|
|
17770
18003
|
removed.contentFiles++;
|
|
17771
18004
|
} catch {
|
|
17772
18005
|
}
|
|
@@ -17775,7 +18008,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17775
18008
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
17776
18009
|
removed.resetRemoteHashCache = true;
|
|
17777
18010
|
} else if (remoteHashes.invalidIds.length) {
|
|
17778
|
-
const cache = parseCache(JSON.parse(await
|
|
18011
|
+
const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17779
18012
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
17780
18013
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
17781
18014
|
await saveCache(remotePath, cache);
|
|
@@ -17793,7 +18026,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17793
18026
|
async function inspectContentCache(contentDir, referenced) {
|
|
17794
18027
|
const report = {
|
|
17795
18028
|
path: contentDir,
|
|
17796
|
-
exists:
|
|
18029
|
+
exists: existsSync26(contentDir),
|
|
17797
18030
|
files: 0,
|
|
17798
18031
|
bytes: 0,
|
|
17799
18032
|
valid: 0,
|
|
@@ -17814,12 +18047,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17814
18047
|
continue;
|
|
17815
18048
|
}
|
|
17816
18049
|
report.files++;
|
|
17817
|
-
const file =
|
|
18050
|
+
const file = path34.join(contentDir, entry.name);
|
|
17818
18051
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
17819
18052
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
17820
18053
|
let bytes;
|
|
17821
18054
|
try {
|
|
17822
|
-
bytes = await
|
|
18055
|
+
bytes = await readFile24(file);
|
|
17823
18056
|
report.bytes += bytes.length;
|
|
17824
18057
|
} catch (err) {
|
|
17825
18058
|
report.invalid.push({
|
|
@@ -17857,7 +18090,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17857
18090
|
async function inspectRemoteHashCache(remotePath) {
|
|
17858
18091
|
const report = {
|
|
17859
18092
|
path: remotePath,
|
|
17860
|
-
exists:
|
|
18093
|
+
exists: existsSync26(remotePath),
|
|
17861
18094
|
entries: 0,
|
|
17862
18095
|
valid: 0,
|
|
17863
18096
|
invalidIds: [],
|
|
@@ -17866,7 +18099,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
17866
18099
|
if (!report.exists) return report;
|
|
17867
18100
|
let cache;
|
|
17868
18101
|
try {
|
|
17869
|
-
cache = parseCache(JSON.parse(await
|
|
18102
|
+
cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17870
18103
|
} catch (err) {
|
|
17871
18104
|
report.error = err instanceof Error ? err.message : String(err);
|
|
17872
18105
|
return report;
|
|
@@ -17930,7 +18163,7 @@ async function runAccept(args) {
|
|
|
17930
18163
|
let intact = true;
|
|
17931
18164
|
for (const output of entry.outputs) {
|
|
17932
18165
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
17933
|
-
if (!
|
|
18166
|
+
if (!existsSync27(file) || await sha256File(file) !== output.sha256) {
|
|
17934
18167
|
intact = false;
|
|
17935
18168
|
break;
|
|
17936
18169
|
}
|
|
@@ -17985,42 +18218,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
17985
18218
|
}
|
|
17986
18219
|
|
|
17987
18220
|
// src/cli/commands/init.ts
|
|
17988
|
-
import
|
|
17989
|
-
import { existsSync as
|
|
18221
|
+
import path35 from "path";
|
|
18222
|
+
import { existsSync as existsSync28 } from "fs";
|
|
17990
18223
|
async function runInit(args) {
|
|
17991
18224
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
17992
|
-
const root =
|
|
17993
|
-
if (!
|
|
18225
|
+
const root = path35.resolve(args.from);
|
|
18226
|
+
if (!existsSync28(root)) throw new Error(`No directory at ${root}`);
|
|
17994
18227
|
const generator = args.generator ?? "map";
|
|
17995
18228
|
if (generator !== "1dir" && generator !== "map") {
|
|
17996
18229
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
17997
18230
|
}
|
|
17998
|
-
const target =
|
|
18231
|
+
const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
17999
18232
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18000
18233
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18001
18234
|
const manifest = buildManifest(
|
|
18002
|
-
args.name ??
|
|
18235
|
+
args.name ?? path35.basename(path35.dirname(target)),
|
|
18003
18236
|
args.styles[0] ?? "base",
|
|
18004
18237
|
generator,
|
|
18005
|
-
|
|
18238
|
+
path35.relative(path35.dirname(target), root) || ".",
|
|
18006
18239
|
assets
|
|
18007
18240
|
);
|
|
18008
18241
|
await writeManifestFile(target, manifest);
|
|
18009
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
18242
|
+
log(` scanned ${assets.length} PNG(s) under ${path35.relative(process.cwd(), root)}`);
|
|
18010
18243
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18011
|
-
log(` wrote ${
|
|
18244
|
+
log(` wrote ${path35.relative(process.cwd(), target)}`);
|
|
18012
18245
|
log(`
|
|
18013
18246
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18014
18247
|
log(` PixelLab account instead of inventing them:`);
|
|
18015
18248
|
log(`
|
|
18016
|
-
pixelkiln adopt --manifest ${
|
|
18249
|
+
pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
|
|
18017
18250
|
`);
|
|
18018
18251
|
}
|
|
18019
18252
|
|
|
18020
18253
|
// src/pipeline/doctor.ts
|
|
18021
|
-
import { constants, existsSync as
|
|
18254
|
+
import { constants, existsSync as existsSync29 } from "fs";
|
|
18022
18255
|
import { access, stat as stat4 } from "fs/promises";
|
|
18023
|
-
import
|
|
18256
|
+
import path36 from "path";
|
|
18024
18257
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18025
18258
|
const checks = [];
|
|
18026
18259
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18031,8 +18264,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18031
18264
|
);
|
|
18032
18265
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18033
18266
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18034
|
-
|
|
18035
|
-
...spec.quality ? [
|
|
18267
|
+
path36.dirname(spec.outFile),
|
|
18268
|
+
...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
|
|
18036
18269
|
]))];
|
|
18037
18270
|
const unwritable = [];
|
|
18038
18271
|
for (const dir of dirs) {
|
|
@@ -18055,7 +18288,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18055
18288
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18056
18289
|
const output = entry.outputs[index];
|
|
18057
18290
|
const spec = specByKey.get(key);
|
|
18058
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
18291
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path36.dirname(lockPath));
|
|
18059
18292
|
const owner = outputOwners.get(absolute);
|
|
18060
18293
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18061
18294
|
outputOwners.set(absolute, key);
|
|
@@ -18070,7 +18303,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18070
18303
|
} else {
|
|
18071
18304
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18072
18305
|
}
|
|
18073
|
-
const cacheDir =
|
|
18306
|
+
const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
|
|
18074
18307
|
const stranded = [];
|
|
18075
18308
|
const invalidState = [];
|
|
18076
18309
|
const staleMapUrls = [];
|
|
@@ -18086,11 +18319,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18086
18319
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18087
18320
|
let hasCache = false;
|
|
18088
18321
|
for (const output of entry.outputs) {
|
|
18089
|
-
const cached =
|
|
18322
|
+
const cached = path36.join(
|
|
18090
18323
|
cacheDir,
|
|
18091
18324
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18092
18325
|
);
|
|
18093
|
-
if (
|
|
18326
|
+
if (existsSync29(cached) && await sha256File(cached) === output.sha256) {
|
|
18094
18327
|
hasCache = true;
|
|
18095
18328
|
break;
|
|
18096
18329
|
}
|
|
@@ -18186,16 +18419,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18186
18419
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18187
18420
|
} catch {
|
|
18188
18421
|
}
|
|
18189
|
-
const parent =
|
|
18422
|
+
const parent = path36.dirname(current);
|
|
18190
18423
|
if (parent === current) return current;
|
|
18191
18424
|
current = parent;
|
|
18192
18425
|
}
|
|
18193
18426
|
}
|
|
18194
18427
|
|
|
18195
18428
|
// src/pipeline/audit.ts
|
|
18196
|
-
import { readFile as
|
|
18197
|
-
import { existsSync as
|
|
18198
|
-
import
|
|
18429
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18430
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18431
|
+
import path37 from "path";
|
|
18199
18432
|
function paletteDistance(asset, reference) {
|
|
18200
18433
|
if (!asset.length || !reference.length) return 0;
|
|
18201
18434
|
let total = 0;
|
|
@@ -18231,12 +18464,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18231
18464
|
const unreadable = [];
|
|
18232
18465
|
for (const spec of mine) {
|
|
18233
18466
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18234
|
-
if (!
|
|
18467
|
+
if (!existsSync30(output.absolutePath)) {
|
|
18235
18468
|
missing.push(output.id);
|
|
18236
18469
|
continue;
|
|
18237
18470
|
}
|
|
18238
18471
|
try {
|
|
18239
|
-
const png = decodePng(await
|
|
18472
|
+
const png = decodePng(await readFile25(output.absolutePath));
|
|
18240
18473
|
const palette = extractPalette(png, 12);
|
|
18241
18474
|
assets.push({
|
|
18242
18475
|
assetId: spec.assetId,
|
|
@@ -18260,10 +18493,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18260
18493
|
let referenceFromStyleImages = false;
|
|
18261
18494
|
const refPalettes = [];
|
|
18262
18495
|
for (const img of style.styleImages) {
|
|
18263
|
-
const abs =
|
|
18264
|
-
if (!
|
|
18496
|
+
const abs = path37.resolve(loaded.root, img.path);
|
|
18497
|
+
if (!existsSync30(abs)) continue;
|
|
18265
18498
|
try {
|
|
18266
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
18499
|
+
refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
|
|
18267
18500
|
} catch {
|
|
18268
18501
|
}
|
|
18269
18502
|
}
|
|
@@ -18491,24 +18724,24 @@ async function runAudit(args) {
|
|
|
18491
18724
|
}
|
|
18492
18725
|
|
|
18493
18726
|
// src/cli/commands/pack.ts
|
|
18494
|
-
import
|
|
18495
|
-
import { readFile as
|
|
18727
|
+
import path39 from "path";
|
|
18728
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
18496
18729
|
|
|
18497
18730
|
// src/pipeline/pack.ts
|
|
18498
18731
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18499
|
-
import
|
|
18732
|
+
import path38 from "path";
|
|
18500
18733
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
18501
18734
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
18502
18735
|
if (!Array.isArray(raw) || !raw.length) {
|
|
18503
18736
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
18504
18737
|
}
|
|
18505
|
-
const baseDir =
|
|
18738
|
+
const baseDir = path38.dirname(path38.resolve(inputsFilePath));
|
|
18506
18739
|
return raw.map((entry, i) => {
|
|
18507
18740
|
const e = entry;
|
|
18508
18741
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
18509
18742
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
18510
18743
|
}
|
|
18511
|
-
return { id: e.id, path:
|
|
18744
|
+
return { id: e.id, path: path38.resolve(baseDir, e.path) };
|
|
18512
18745
|
});
|
|
18513
18746
|
}
|
|
18514
18747
|
function packSprites(inputs, options = {}) {
|
|
@@ -18592,7 +18825,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18592
18825
|
}
|
|
18593
18826
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
18594
18827
|
id,
|
|
18595
|
-
path:
|
|
18828
|
+
path: path38.resolve(source)
|
|
18596
18829
|
}));
|
|
18597
18830
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
18598
18831
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -18616,7 +18849,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18616
18849
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
18617
18850
|
const source = sources[id];
|
|
18618
18851
|
if (source && outputs.length <= 1) {
|
|
18619
|
-
inputs.push({ id, path:
|
|
18852
|
+
inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18620
18853
|
continue;
|
|
18621
18854
|
}
|
|
18622
18855
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -18650,7 +18883,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18650
18883
|
}
|
|
18651
18884
|
}
|
|
18652
18885
|
for (const [id, source] of Object.entries(sources)) {
|
|
18653
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
18886
|
+
if (!locked.has(id)) inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18654
18887
|
}
|
|
18655
18888
|
if (!inputs.length) {
|
|
18656
18889
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -18758,7 +18991,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18758
18991
|
const source = sources[id];
|
|
18759
18992
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
18760
18993
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
18761
|
-
placements.push({ id, path:
|
|
18994
|
+
placements.push({ id, path: path38.resolve(manifestDir, source), cell });
|
|
18762
18995
|
continue;
|
|
18763
18996
|
}
|
|
18764
18997
|
if (source && entry) {
|
|
@@ -18791,7 +19024,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18791
19024
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
18792
19025
|
);
|
|
18793
19026
|
}
|
|
18794
|
-
const basePng = mount.base ? readFileSync5(
|
|
19027
|
+
const basePng = mount.base ? readFileSync5(path38.resolve(manifestDir, mount.base)) : void 0;
|
|
18795
19028
|
const mounted = mountSprites(placements, {
|
|
18796
19029
|
cellWidth: mount.cellWidth,
|
|
18797
19030
|
cellHeight: mount.cellHeight,
|
|
@@ -18801,7 +19034,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18801
19034
|
...mounted,
|
|
18802
19035
|
sources: mount.base ? [{
|
|
18803
19036
|
id: "$base",
|
|
18804
|
-
path:
|
|
19037
|
+
path: path38.resolve(manifestDir, mount.base),
|
|
18805
19038
|
sha256: sha256(basePng),
|
|
18806
19039
|
included: true
|
|
18807
19040
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19205,12 +19438,12 @@ async function runPack(args) {
|
|
|
19205
19438
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19206
19439
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19207
19440
|
}
|
|
19208
|
-
const raw = JSON.parse(await
|
|
19441
|
+
const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
|
|
19209
19442
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19210
19443
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19211
19444
|
const format = sheetFormat(args);
|
|
19212
|
-
const base =
|
|
19213
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19445
|
+
const base = path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
19446
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
|
|
19214
19447
|
const outputs = [
|
|
19215
19448
|
{ path: `${base}.png`, data: png },
|
|
19216
19449
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19223,7 +19456,7 @@ async function runPack(args) {
|
|
|
19223
19456
|
log(
|
|
19224
19457
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
19225
19458
|
);
|
|
19226
|
-
log(` ${
|
|
19459
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19227
19460
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19228
19461
|
return;
|
|
19229
19462
|
}
|
|
@@ -19231,7 +19464,7 @@ async function runPack(args) {
|
|
|
19231
19464
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19232
19465
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19233
19466
|
}
|
|
19234
|
-
const manifestDir =
|
|
19467
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19235
19468
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19236
19469
|
for (const styleId of styleIds) {
|
|
19237
19470
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19246,9 +19479,9 @@ async function runPack(args) {
|
|
|
19246
19479
|
sourceOverrides: qualitySources,
|
|
19247
19480
|
sources: manifestSources(loaded.manifest, styleId)
|
|
19248
19481
|
});
|
|
19249
|
-
const base = args.out ?
|
|
19482
|
+
const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path39.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
19250
19483
|
const format = sheetFormat(args);
|
|
19251
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19484
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
|
|
19252
19485
|
const outputs = [
|
|
19253
19486
|
{ path: `${base}.png`, data: png },
|
|
19254
19487
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19280,13 +19513,13 @@ async function runPack(args) {
|
|
|
19280
19513
|
log(
|
|
19281
19514
|
` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
|
|
19282
19515
|
);
|
|
19283
|
-
log(` ${
|
|
19516
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19284
19517
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19285
19518
|
}
|
|
19286
19519
|
}
|
|
19287
19520
|
async function runMount(args) {
|
|
19288
19521
|
const { loaded, lock } = await openProject2(args);
|
|
19289
|
-
const manifestDir =
|
|
19522
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19290
19523
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19291
19524
|
for (const styleId of styleIds) {
|
|
19292
19525
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19325,9 +19558,9 @@ async function runMount(args) {
|
|
|
19325
19558
|
sources,
|
|
19326
19559
|
outputRoles
|
|
19327
19560
|
);
|
|
19328
|
-
const out =
|
|
19561
|
+
const out = path39.resolve(manifestDir, style.mount.out);
|
|
19329
19562
|
const format = sheetFormat(args);
|
|
19330
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19563
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
|
|
19331
19564
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
19332
19565
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
19333
19566
|
const outputs = [
|
|
@@ -19347,7 +19580,7 @@ async function runMount(args) {
|
|
|
19347
19580
|
await provenanceFile("$lock", args.lock),
|
|
19348
19581
|
...qualityRecords,
|
|
19349
19582
|
...artifactSources.filter(
|
|
19350
|
-
(source) => source.id !== "$base" ||
|
|
19583
|
+
(source) => source.id !== "$base" || path39.resolve(source.path) !== out
|
|
19351
19584
|
)
|
|
19352
19585
|
],
|
|
19353
19586
|
options: {
|
|
@@ -19361,14 +19594,14 @@ async function runMount(args) {
|
|
|
19361
19594
|
log(
|
|
19362
19595
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
19363
19596
|
);
|
|
19364
|
-
log(` ${
|
|
19597
|
+
log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
19365
19598
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19366
19599
|
}
|
|
19367
19600
|
}
|
|
19368
19601
|
async function runExport(args) {
|
|
19369
19602
|
const { loaded, specs, lock } = await openProject2(args);
|
|
19370
19603
|
const format = args.format ?? "generic";
|
|
19371
|
-
const manifestDir =
|
|
19604
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19372
19605
|
const selected = specs.filter((spec) => {
|
|
19373
19606
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
19374
19607
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -19384,12 +19617,12 @@ async function runExport(args) {
|
|
|
19384
19617
|
for (const spec of selected) {
|
|
19385
19618
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
19386
19619
|
const style = loaded.manifest.styles[spec.styleId];
|
|
19387
|
-
const defaultBase =
|
|
19388
|
-
const base = args.out ?
|
|
19620
|
+
const defaultBase = path39.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
19621
|
+
const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
19389
19622
|
const result = exportTileset(entry, spec, {
|
|
19390
19623
|
format,
|
|
19391
19624
|
manifestDir,
|
|
19392
|
-
imageName:
|
|
19625
|
+
imageName: path39.basename(`${base}.png`),
|
|
19393
19626
|
columns: args.columns
|
|
19394
19627
|
});
|
|
19395
19628
|
const outputs = [
|
|
@@ -19407,7 +19640,7 @@ async function runExport(args) {
|
|
|
19407
19640
|
asset: spec.assetId,
|
|
19408
19641
|
columns: args.columns ?? null,
|
|
19409
19642
|
format,
|
|
19410
|
-
image:
|
|
19643
|
+
image: path39.basename(`${base}.png`),
|
|
19411
19644
|
providerRules: result.generic.providerRules,
|
|
19412
19645
|
style: spec.styleId,
|
|
19413
19646
|
tileType: spec.tileType ?? null
|
|
@@ -19417,7 +19650,7 @@ async function runExport(args) {
|
|
|
19417
19650
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
19418
19651
|
);
|
|
19419
19652
|
log(
|
|
19420
|
-
` ${
|
|
19653
|
+
` ${path39.relative(process.cwd(), base)}.png + ${path39.basename(base)}${result.extension} + .pixelkiln.json`
|
|
19421
19654
|
);
|
|
19422
19655
|
}
|
|
19423
19656
|
}
|
|
@@ -19430,23 +19663,23 @@ function sheetFormat(args) {
|
|
|
19430
19663
|
}
|
|
19431
19664
|
|
|
19432
19665
|
// src/cli/commands/pixellab-utils.ts
|
|
19433
|
-
import
|
|
19666
|
+
import path41 from "path";
|
|
19434
19667
|
|
|
19435
19668
|
// src/pixellab-utils.ts
|
|
19436
|
-
import { existsSync as
|
|
19437
|
-
import { mkdir as
|
|
19438
|
-
import
|
|
19669
|
+
import { existsSync as existsSync31 } from "fs";
|
|
19670
|
+
import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
|
|
19671
|
+
import path40 from "path";
|
|
19439
19672
|
var UNZOOM_MIN_SIDE = 256;
|
|
19440
19673
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
19441
19674
|
function defaultUnzoomOut(input) {
|
|
19442
|
-
const ext =
|
|
19675
|
+
const ext = path40.extname(input);
|
|
19443
19676
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
19444
19677
|
}
|
|
19445
19678
|
async function unzoomFile(client2, input, opts = {}) {
|
|
19446
|
-
const out =
|
|
19447
|
-
if (
|
|
19448
|
-
if (
|
|
19449
|
-
const bytes = await
|
|
19679
|
+
const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
19680
|
+
if (path40.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
19681
|
+
if (existsSync31(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
19682
|
+
const bytes = await readFile27(input);
|
|
19450
19683
|
const metadata = imageMetadata(bytes);
|
|
19451
19684
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
19452
19685
|
const { width, height, format } = metadata;
|
|
@@ -19459,22 +19692,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
19459
19692
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
19460
19693
|
}
|
|
19461
19694
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
19462
|
-
await
|
|
19463
|
-
await
|
|
19695
|
+
await mkdir12(path40.dirname(out), { recursive: true });
|
|
19696
|
+
await writeFile15(out, res.png);
|
|
19464
19697
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
19465
19698
|
}
|
|
19466
19699
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
19467
19700
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
19468
19701
|
var FONT_COST_GENERATIONS = 25;
|
|
19469
19702
|
function fontOutputPaths(out) {
|
|
19470
|
-
const base =
|
|
19703
|
+
const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
19471
19704
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
19472
19705
|
}
|
|
19473
19706
|
async function generateFont(client2, opts) {
|
|
19474
19707
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
19475
19708
|
if (!opts.force) {
|
|
19476
19709
|
for (const file of [ttf, atlas]) {
|
|
19477
|
-
if (
|
|
19710
|
+
if (existsSync31(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
19478
19711
|
}
|
|
19479
19712
|
}
|
|
19480
19713
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -19501,9 +19734,9 @@ async function generateFont(client2, opts) {
|
|
|
19501
19734
|
client2.download(job.download_ttf_url),
|
|
19502
19735
|
client2.download(job.download_atlas_url)
|
|
19503
19736
|
]);
|
|
19504
|
-
await
|
|
19505
|
-
await
|
|
19506
|
-
await
|
|
19737
|
+
await mkdir12(path40.dirname(ttf), { recursive: true });
|
|
19738
|
+
await writeFile15(ttf, ttfBytes);
|
|
19739
|
+
await writeFile15(atlas, atlasBytes);
|
|
19507
19740
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
19508
19741
|
}
|
|
19509
19742
|
if (Date.now() - started > timeout) {
|
|
@@ -19517,7 +19750,7 @@ async function generateFont(client2, opts) {
|
|
|
19517
19750
|
|
|
19518
19751
|
// src/cli/commands/pixellab-utils.ts
|
|
19519
19752
|
function loadKeys(args) {
|
|
19520
|
-
loadEnvFiles(
|
|
19753
|
+
loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
|
|
19521
19754
|
loadEnvFiles(process.cwd());
|
|
19522
19755
|
}
|
|
19523
19756
|
async function runUnzoom(args) {
|
|
@@ -19583,19 +19816,19 @@ async function runFont(args) {
|
|
|
19583
19816
|
}
|
|
19584
19817
|
|
|
19585
19818
|
// src/cli/commands/quality.ts
|
|
19586
|
-
import
|
|
19587
|
-
import { readFile as
|
|
19819
|
+
import path43 from "path";
|
|
19820
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
19588
19821
|
|
|
19589
19822
|
// src/pipeline/quality-regression.ts
|
|
19590
|
-
import
|
|
19591
|
-
import { existsSync as
|
|
19592
|
-
import { readFile as
|
|
19593
|
-
import { z as
|
|
19823
|
+
import path42 from "path";
|
|
19824
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19825
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
19826
|
+
import { z as z11 } from "zod";
|
|
19594
19827
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19595
19828
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
19596
19829
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
19597
|
-
var QualityPathSchema =
|
|
19598
|
-
(value) => !
|
|
19830
|
+
var QualityPathSchema = z11.string().min(1).refine(
|
|
19831
|
+
(value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
|
|
19599
19832
|
"expected a portable relative path"
|
|
19600
19833
|
);
|
|
19601
19834
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -19608,53 +19841,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
19608
19841
|
maxEdgeContrastDrop: 0.03,
|
|
19609
19842
|
maxIsolatedPixelIncrease: 5e-3
|
|
19610
19843
|
};
|
|
19611
|
-
var QualityToleranceSchema =
|
|
19612
|
-
requireExactHash:
|
|
19613
|
-
maxColorCountIncrease:
|
|
19614
|
-
maxNewColors:
|
|
19615
|
-
maxTransparencyDelta:
|
|
19616
|
-
maxPartialAlphaIncrease:
|
|
19617
|
-
maxEdgeDensityDelta:
|
|
19618
|
-
maxEdgeContrastDrop:
|
|
19619
|
-
maxIsolatedPixelIncrease:
|
|
19844
|
+
var QualityToleranceSchema = z11.object({
|
|
19845
|
+
requireExactHash: z11.boolean(),
|
|
19846
|
+
maxColorCountIncrease: z11.number().int().min(0),
|
|
19847
|
+
maxNewColors: z11.number().int().min(0),
|
|
19848
|
+
maxTransparencyDelta: z11.number().min(0).max(1),
|
|
19849
|
+
maxPartialAlphaIncrease: z11.number().min(0).max(1),
|
|
19850
|
+
maxEdgeDensityDelta: z11.number().min(0).max(1),
|
|
19851
|
+
maxEdgeContrastDrop: z11.number().min(0).max(1),
|
|
19852
|
+
maxIsolatedPixelIncrease: z11.number().min(0).max(1)
|
|
19620
19853
|
}).strict();
|
|
19621
|
-
var ImageQualityMetricsSchema =
|
|
19622
|
-
sha256:
|
|
19623
|
-
width:
|
|
19624
|
-
height:
|
|
19625
|
-
colorCount:
|
|
19626
|
-
colors:
|
|
19627
|
-
transparency:
|
|
19628
|
-
partialAlpha:
|
|
19629
|
-
edgeDensity:
|
|
19630
|
-
meanEdgeContrast:
|
|
19631
|
-
isolatedPixelRatio:
|
|
19854
|
+
var ImageQualityMetricsSchema = z11.object({
|
|
19855
|
+
sha256: z11.string().regex(SHA256_RE),
|
|
19856
|
+
width: z11.number().int().min(1),
|
|
19857
|
+
height: z11.number().int().min(1),
|
|
19858
|
+
colorCount: z11.number().int().min(0),
|
|
19859
|
+
colors: z11.array(z11.string().regex(HEX_RE)),
|
|
19860
|
+
transparency: z11.number().min(0).max(1),
|
|
19861
|
+
partialAlpha: z11.number().min(0).max(1),
|
|
19862
|
+
edgeDensity: z11.number().min(0).max(1),
|
|
19863
|
+
meanEdgeContrast: z11.number().min(0).max(1),
|
|
19864
|
+
isolatedPixelRatio: z11.number().min(0).max(1)
|
|
19632
19865
|
}).strict();
|
|
19633
|
-
var QualityBaselineCaseSchema =
|
|
19634
|
-
id:
|
|
19866
|
+
var QualityBaselineCaseSchema = z11.object({
|
|
19867
|
+
id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19635
19868
|
file: QualityPathSchema,
|
|
19636
|
-
record:
|
|
19869
|
+
record: z11.object({
|
|
19637
19870
|
path: QualityPathSchema,
|
|
19638
|
-
sha256:
|
|
19871
|
+
sha256: z11.string().regex(SHA256_RE)
|
|
19639
19872
|
}).strict().optional(),
|
|
19640
19873
|
expected: ImageQualityMetricsSchema,
|
|
19641
19874
|
tolerances: QualityToleranceSchema
|
|
19642
19875
|
}).strict();
|
|
19643
|
-
var QualityBaselineSchema =
|
|
19644
|
-
$schema:
|
|
19645
|
-
format:
|
|
19646
|
-
schemaVersion:
|
|
19647
|
-
cases:
|
|
19876
|
+
var QualityBaselineSchema = z11.object({
|
|
19877
|
+
$schema: z11.string().url().optional(),
|
|
19878
|
+
format: z11.literal("pixelkiln-quality-baseline"),
|
|
19879
|
+
schemaVersion: z11.literal(1),
|
|
19880
|
+
cases: z11.array(QualityBaselineCaseSchema).min(1)
|
|
19648
19881
|
}).strict().superRefine((baseline, context) => {
|
|
19649
19882
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
19650
19883
|
if (new Set(ids).size !== ids.length) {
|
|
19651
|
-
context.addIssue({ code:
|
|
19884
|
+
context.addIssue({ code: z11.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
19652
19885
|
}
|
|
19653
19886
|
});
|
|
19654
|
-
var QualityInputSchema =
|
|
19655
|
-
id:
|
|
19656
|
-
path:
|
|
19657
|
-
record:
|
|
19887
|
+
var QualityInputSchema = z11.object({
|
|
19888
|
+
id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19889
|
+
path: z11.string().min(1),
|
|
19890
|
+
record: z11.string().min(1).optional(),
|
|
19658
19891
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
19659
19892
|
}).strict();
|
|
19660
19893
|
function message4(error) {
|
|
@@ -19742,10 +19975,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
19742
19975
|
};
|
|
19743
19976
|
}
|
|
19744
19977
|
async function measureImageQuality(file) {
|
|
19745
|
-
const absolute =
|
|
19978
|
+
const absolute = path42.resolve(file);
|
|
19746
19979
|
let bytes;
|
|
19747
19980
|
try {
|
|
19748
|
-
bytes = await
|
|
19981
|
+
bytes = await readFile28(absolute);
|
|
19749
19982
|
} catch (error) {
|
|
19750
19983
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
19751
19984
|
}
|
|
@@ -19756,7 +19989,7 @@ async function measureImageQuality(file) {
|
|
|
19756
19989
|
}
|
|
19757
19990
|
}
|
|
19758
19991
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
19759
|
-
const parsed =
|
|
19992
|
+
const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
|
|
19760
19993
|
if (!parsed.success) {
|
|
19761
19994
|
throw new Error(
|
|
19762
19995
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -19764,11 +19997,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19764
19997
|
}
|
|
19765
19998
|
const ids = parsed.data.map((entry) => entry.id);
|
|
19766
19999
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
19767
|
-
const root =
|
|
20000
|
+
const root = path42.dirname(path42.resolve(inputsFilePath));
|
|
19768
20001
|
return parsed.data.map((entry) => ({
|
|
19769
20002
|
id: entry.id,
|
|
19770
|
-
file:
|
|
19771
|
-
...entry.record ? { record:
|
|
20003
|
+
file: path42.resolve(root, entry.path),
|
|
20004
|
+
...entry.record ? { record: path42.resolve(root, entry.record) } : {},
|
|
19772
20005
|
tolerances: QualityToleranceSchema.parse({
|
|
19773
20006
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
19774
20007
|
...entry.tolerances
|
|
@@ -19776,7 +20009,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19776
20009
|
}));
|
|
19777
20010
|
}
|
|
19778
20011
|
function portableRelative2(from, to) {
|
|
19779
|
-
return
|
|
20012
|
+
return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
|
|
19780
20013
|
}
|
|
19781
20014
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
19782
20015
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -19791,20 +20024,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
19791
20024
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
19792
20025
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
19793
20026
|
}
|
|
19794
|
-
const outputs = record.outputs.map((output) =>
|
|
19795
|
-
if (!outputs.includes(
|
|
20027
|
+
const outputs = record.outputs.map((output) => path42.resolve(path42.dirname(recordPath), output.path));
|
|
20028
|
+
if (!outputs.includes(path42.resolve(imagePath))) {
|
|
19796
20029
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
19797
20030
|
}
|
|
19798
20031
|
}
|
|
19799
20032
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
19800
20033
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
19801
|
-
const absolute =
|
|
20034
|
+
const absolute = path42.resolve(baselinePath);
|
|
19802
20035
|
for (const input of inputs) {
|
|
19803
|
-
if (
|
|
20036
|
+
if (path42.resolve(input.file) === absolute || input.record && path42.resolve(input.record) === absolute) {
|
|
19804
20037
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
19805
20038
|
}
|
|
19806
20039
|
}
|
|
19807
|
-
const root =
|
|
20040
|
+
const root = path42.dirname(absolute);
|
|
19808
20041
|
const cases = [];
|
|
19809
20042
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
19810
20043
|
const expected = await measureImageQuality(input.file);
|
|
@@ -19831,8 +20064,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19831
20064
|
cases
|
|
19832
20065
|
});
|
|
19833
20066
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
19834
|
-
if (
|
|
19835
|
-
const current = await
|
|
20067
|
+
if (existsSync32(absolute) && !options.force) {
|
|
20068
|
+
const current = await readFile28(absolute);
|
|
19836
20069
|
if (!current.equals(data)) {
|
|
19837
20070
|
throw new OverwriteRefusedError(
|
|
19838
20071
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -19844,10 +20077,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19844
20077
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
19845
20078
|
}
|
|
19846
20079
|
async function readQualityBaseline(baselinePath) {
|
|
19847
|
-
const absolute =
|
|
20080
|
+
const absolute = path42.resolve(baselinePath);
|
|
19848
20081
|
let raw;
|
|
19849
20082
|
try {
|
|
19850
|
-
raw = JSON.parse(await
|
|
20083
|
+
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
19851
20084
|
} catch (error) {
|
|
19852
20085
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
19853
20086
|
}
|
|
@@ -19917,12 +20150,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
19917
20150
|
return { violations, warnings };
|
|
19918
20151
|
}
|
|
19919
20152
|
async function checkQualityBaseline(baselinePath) {
|
|
19920
|
-
const absolute =
|
|
20153
|
+
const absolute = path42.resolve(baselinePath);
|
|
19921
20154
|
const baseline = await readQualityBaseline(absolute);
|
|
19922
|
-
const root =
|
|
20155
|
+
const root = path42.dirname(absolute);
|
|
19923
20156
|
const cases = [];
|
|
19924
20157
|
for (const entry of baseline.cases) {
|
|
19925
|
-
const file =
|
|
20158
|
+
const file = path42.resolve(root, entry.file);
|
|
19926
20159
|
let actual = null;
|
|
19927
20160
|
const violations = [];
|
|
19928
20161
|
const warnings = [];
|
|
@@ -19935,7 +20168,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
19935
20168
|
violations.push(message4(error));
|
|
19936
20169
|
}
|
|
19937
20170
|
if (entry.record) {
|
|
19938
|
-
const recordPath =
|
|
20171
|
+
const recordPath = path42.resolve(root, entry.record.path);
|
|
19939
20172
|
try {
|
|
19940
20173
|
const recordHash = await sha256File(recordPath);
|
|
19941
20174
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -19977,15 +20210,15 @@ async function runQuality(args) {
|
|
|
19977
20210
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
19978
20211
|
let raw;
|
|
19979
20212
|
try {
|
|
19980
|
-
raw = JSON.parse(await
|
|
20213
|
+
raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
|
|
19981
20214
|
} catch (error) {
|
|
19982
20215
|
throw new Error(
|
|
19983
|
-
`Could not read quality inputs ${
|
|
20216
|
+
`Could not read quality inputs ${path43.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
19984
20217
|
{ cause: error }
|
|
19985
20218
|
);
|
|
19986
20219
|
}
|
|
19987
20220
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
19988
|
-
if (
|
|
20221
|
+
if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
|
|
19989
20222
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
19990
20223
|
}
|
|
19991
20224
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -19998,7 +20231,7 @@ async function runQuality(args) {
|
|
|
19998
20231
|
}, null, 2));
|
|
19999
20232
|
} else {
|
|
20000
20233
|
log(
|
|
20001
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
20234
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path43.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20002
20235
|
);
|
|
20003
20236
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20004
20237
|
}
|
|
@@ -20024,54 +20257,54 @@ async function runQuality(args) {
|
|
|
20024
20257
|
}
|
|
20025
20258
|
|
|
20026
20259
|
// src/cli/commands/recipe.ts
|
|
20027
|
-
import
|
|
20260
|
+
import path45 from "path";
|
|
20028
20261
|
|
|
20029
20262
|
// src/recipes.ts
|
|
20030
|
-
import
|
|
20031
|
-
import { existsSync as
|
|
20032
|
-
import { readdir as readdir3, readFile as
|
|
20033
|
-
import { fileURLToPath } from "url";
|
|
20034
|
-
import { z as
|
|
20263
|
+
import path44 from "path";
|
|
20264
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20265
|
+
import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
|
|
20266
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20267
|
+
import { z as z12 } from "zod";
|
|
20035
20268
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20036
20269
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20037
20270
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20038
20271
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20039
20272
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20040
|
-
var PortablePathSchema =
|
|
20041
|
-
if (value.includes("\\") ||
|
|
20273
|
+
var PortablePathSchema = z12.string().min(1).refine((value) => {
|
|
20274
|
+
if (value.includes("\\") || path44.posix.isAbsolute(value)) return false;
|
|
20042
20275
|
const parts = value.split("/");
|
|
20043
20276
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20044
20277
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20045
|
-
var RecipeBindingSchema =
|
|
20046
|
-
nodeId:
|
|
20047
|
-
input:
|
|
20278
|
+
var RecipeBindingSchema = z12.object({
|
|
20279
|
+
nodeId: z12.string().min(1),
|
|
20280
|
+
input: z12.string().min(1)
|
|
20048
20281
|
}).strict();
|
|
20049
|
-
var RecipeSchema =
|
|
20050
|
-
$schema:
|
|
20051
|
-
format:
|
|
20052
|
-
schemaVersion:
|
|
20053
|
-
id:
|
|
20054
|
-
version:
|
|
20055
|
-
provider:
|
|
20056
|
-
summary:
|
|
20057
|
-
files:
|
|
20282
|
+
var RecipeSchema = z12.object({
|
|
20283
|
+
$schema: z12.string().url().optional(),
|
|
20284
|
+
format: z12.literal("pixelkiln-recipe"),
|
|
20285
|
+
schemaVersion: z12.literal(1),
|
|
20286
|
+
id: z12.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
20287
|
+
version: z12.string().regex(VERSION_RE, "expected x.y.z"),
|
|
20288
|
+
provider: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20289
|
+
summary: z12.string().min(1).max(240),
|
|
20290
|
+
files: z12.array(z12.object({
|
|
20058
20291
|
path: PortablePathSchema,
|
|
20059
|
-
role:
|
|
20060
|
-
sha256:
|
|
20292
|
+
role: z12.enum(["workflow", "reference"]),
|
|
20293
|
+
sha256: z12.string().regex(SHA256_RE2)
|
|
20061
20294
|
}).strict()).default([]),
|
|
20062
|
-
models:
|
|
20295
|
+
models: z12.array(z12.object({
|
|
20063
20296
|
path: PortablePathSchema,
|
|
20064
|
-
sha256:
|
|
20065
|
-
source:
|
|
20066
|
-
license:
|
|
20297
|
+
sha256: z12.string().regex(SHA256_RE2),
|
|
20298
|
+
source: z12.string().url(),
|
|
20299
|
+
license: z12.string().min(1)
|
|
20067
20300
|
}).strict()).default([]),
|
|
20068
|
-
styleId:
|
|
20301
|
+
styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20069
20302
|
style: StyleSchema,
|
|
20070
|
-
workflow:
|
|
20303
|
+
workflow: z12.object({
|
|
20071
20304
|
path: PortablePathSchema,
|
|
20072
|
-
outputNodeId:
|
|
20073
|
-
numImages:
|
|
20074
|
-
bindings:
|
|
20305
|
+
outputNodeId: z12.string().min(1),
|
|
20306
|
+
numImages: z12.number().int().min(1).max(16),
|
|
20307
|
+
bindings: z12.object({
|
|
20075
20308
|
prompt: RecipeBindingSchema,
|
|
20076
20309
|
width: RecipeBindingSchema.optional(),
|
|
20077
20310
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20082,67 +20315,67 @@ var RecipeSchema = z11.object({
|
|
|
20082
20315
|
strength: RecipeBindingSchema.optional()
|
|
20083
20316
|
}).strict()
|
|
20084
20317
|
}).strict().optional(),
|
|
20085
|
-
quality:
|
|
20086
|
-
stage:
|
|
20087
|
-
workingCanvas:
|
|
20088
|
-
width:
|
|
20089
|
-
height:
|
|
20318
|
+
quality: z12.object({
|
|
20319
|
+
stage: z12.enum(["composition-source", "production-candidate"]),
|
|
20320
|
+
workingCanvas: z12.object({
|
|
20321
|
+
width: z12.number().int().min(16),
|
|
20322
|
+
height: z12.number().int().min(16)
|
|
20090
20323
|
}).strict(),
|
|
20091
|
-
recommendedNativeSize:
|
|
20092
|
-
min:
|
|
20093
|
-
max:
|
|
20324
|
+
recommendedNativeSize: z12.object({
|
|
20325
|
+
min: z12.number().int().min(1),
|
|
20326
|
+
max: z12.number().int().min(1)
|
|
20094
20327
|
}).strict(),
|
|
20095
|
-
paletteColors:
|
|
20096
|
-
min:
|
|
20097
|
-
max:
|
|
20328
|
+
paletteColors: z12.object({
|
|
20329
|
+
min: z12.number().int().min(1),
|
|
20330
|
+
max: z12.number().int().min(1)
|
|
20098
20331
|
}).strict(),
|
|
20099
|
-
background:
|
|
20100
|
-
checks:
|
|
20332
|
+
background: z12.enum(["opaque", "transparent", "full-bleed"]),
|
|
20333
|
+
checks: z12.array(z12.enum([
|
|
20101
20334
|
"prompt-coverage",
|
|
20102
20335
|
"native-grid",
|
|
20103
20336
|
"final-palette",
|
|
20104
20337
|
"alpha-edge",
|
|
20105
20338
|
"human-1x"
|
|
20106
20339
|
])).min(1),
|
|
20107
|
-
notes:
|
|
20340
|
+
notes: z12.array(z12.string().min(1)).default([])
|
|
20108
20341
|
}).strict(),
|
|
20109
|
-
integrity:
|
|
20110
|
-
algorithm:
|
|
20111
|
-
digest:
|
|
20342
|
+
integrity: z12.object({
|
|
20343
|
+
algorithm: z12.literal("sha256"),
|
|
20344
|
+
digest: z12.string().regex(SHA256_RE2)
|
|
20112
20345
|
}).strict()
|
|
20113
20346
|
}).strict().superRefine((recipe, context) => {
|
|
20114
20347
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20115
20348
|
context.addIssue({
|
|
20116
|
-
code:
|
|
20349
|
+
code: z12.ZodIssueCode.custom,
|
|
20117
20350
|
path: ["style", "provider"],
|
|
20118
20351
|
message: "must match the recipe provider"
|
|
20119
20352
|
});
|
|
20120
20353
|
}
|
|
20121
20354
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20122
20355
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20123
|
-
context.addIssue({ code:
|
|
20356
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20124
20357
|
}
|
|
20125
20358
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20126
20359
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20127
|
-
context.addIssue({ code:
|
|
20360
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20128
20361
|
}
|
|
20129
20362
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20130
20363
|
context.addIssue({
|
|
20131
|
-
code:
|
|
20364
|
+
code: z12.ZodIssueCode.custom,
|
|
20132
20365
|
path: ["files"],
|
|
20133
20366
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20134
20367
|
});
|
|
20135
20368
|
}
|
|
20136
20369
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20137
20370
|
context.addIssue({
|
|
20138
|
-
code:
|
|
20371
|
+
code: z12.ZodIssueCode.custom,
|
|
20139
20372
|
path: ["quality", "recommendedNativeSize"],
|
|
20140
20373
|
message: "min cannot exceed max"
|
|
20141
20374
|
});
|
|
20142
20375
|
}
|
|
20143
20376
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20144
20377
|
context.addIssue({
|
|
20145
|
-
code:
|
|
20378
|
+
code: z12.ZodIssueCode.custom,
|
|
20146
20379
|
path: ["quality", "paletteColors"],
|
|
20147
20380
|
message: "min cannot exceed max"
|
|
20148
20381
|
});
|
|
@@ -20151,7 +20384,7 @@ var RecipeSchema = z11.object({
|
|
|
20151
20384
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20152
20385
|
)) {
|
|
20153
20386
|
context.addIssue({
|
|
20154
|
-
code:
|
|
20387
|
+
code: z12.ZodIssueCode.custom,
|
|
20155
20388
|
path: ["workflow", "path"],
|
|
20156
20389
|
message: "must name a file whose role is workflow"
|
|
20157
20390
|
});
|
|
@@ -20159,7 +20392,7 @@ var RecipeSchema = z11.object({
|
|
|
20159
20392
|
if (recipe.provider === "comfyui") {
|
|
20160
20393
|
if (!recipe.workflow) {
|
|
20161
20394
|
context.addIssue({
|
|
20162
|
-
code:
|
|
20395
|
+
code: z12.ZodIssueCode.custom,
|
|
20163
20396
|
path: ["workflow"],
|
|
20164
20397
|
message: "ComfyUI recipes require workflow metadata"
|
|
20165
20398
|
});
|
|
@@ -20168,7 +20401,7 @@ var RecipeSchema = z11.object({
|
|
|
20168
20401
|
const options = recipe.style.providerOptions.comfyui;
|
|
20169
20402
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20170
20403
|
context.addIssue({
|
|
20171
|
-
code:
|
|
20404
|
+
code: z12.ZodIssueCode.custom,
|
|
20172
20405
|
path: ["style", "providerOptions", "comfyui"],
|
|
20173
20406
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20174
20407
|
});
|
|
@@ -20182,7 +20415,7 @@ var RecipeSchema = z11.object({
|
|
|
20182
20415
|
};
|
|
20183
20416
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20184
20417
|
context.addIssue({
|
|
20185
|
-
code:
|
|
20418
|
+
code: z12.ZodIssueCode.custom,
|
|
20186
20419
|
path: ["style", "providerOptions", "comfyui"],
|
|
20187
20420
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20188
20421
|
});
|
|
@@ -20214,10 +20447,10 @@ function message5(error) {
|
|
|
20214
20447
|
return error instanceof Error ? error.message : String(error);
|
|
20215
20448
|
}
|
|
20216
20449
|
async function readRecipeFile(recipePath, bundled) {
|
|
20217
|
-
const absolute =
|
|
20450
|
+
const absolute = path44.resolve(recipePath);
|
|
20218
20451
|
let raw;
|
|
20219
20452
|
try {
|
|
20220
|
-
raw = JSON.parse(await
|
|
20453
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20221
20454
|
} catch (error) {
|
|
20222
20455
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20223
20456
|
}
|
|
@@ -20228,24 +20461,24 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20228
20461
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20229
20462
|
);
|
|
20230
20463
|
}
|
|
20231
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
20464
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
|
|
20232
20465
|
}
|
|
20233
20466
|
async function walkRecipeFiles(root) {
|
|
20234
|
-
if (!
|
|
20467
|
+
if (!existsSync33(root)) return [];
|
|
20235
20468
|
const out = [];
|
|
20236
20469
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20237
|
-
const resolved =
|
|
20470
|
+
const resolved = path44.join(root, entry.name);
|
|
20238
20471
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20239
20472
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20240
20473
|
}
|
|
20241
20474
|
return out;
|
|
20242
20475
|
}
|
|
20243
20476
|
function bundledRecipeRoot() {
|
|
20244
|
-
return
|
|
20477
|
+
return fileURLToPath2(new URL("../recipes/", import.meta.url));
|
|
20245
20478
|
}
|
|
20246
20479
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
20247
20480
|
const recipes = await Promise.all(
|
|
20248
|
-
(await walkRecipeFiles(
|
|
20481
|
+
(await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
20249
20482
|
);
|
|
20250
20483
|
return recipes.sort(
|
|
20251
20484
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -20268,9 +20501,9 @@ function parseSelector(selector) {
|
|
|
20268
20501
|
return { id: match[1], version: match[2] };
|
|
20269
20502
|
}
|
|
20270
20503
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
20271
|
-
const local =
|
|
20272
|
-
if (
|
|
20273
|
-
const recipePath =
|
|
20504
|
+
const local = path44.resolve(target);
|
|
20505
|
+
if (existsSync33(local)) {
|
|
20506
|
+
const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
|
|
20274
20507
|
return readRecipeFile(recipePath, false);
|
|
20275
20508
|
}
|
|
20276
20509
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -20284,8 +20517,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
20284
20517
|
return matches[0];
|
|
20285
20518
|
}
|
|
20286
20519
|
async function verifyFile(root, file) {
|
|
20287
|
-
const absolute =
|
|
20288
|
-
if (!
|
|
20520
|
+
const absolute = path44.resolve(root, ...file.path.split("/"));
|
|
20521
|
+
if (!existsSync33(absolute)) {
|
|
20289
20522
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
20290
20523
|
}
|
|
20291
20524
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -20300,7 +20533,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20300
20533
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20301
20534
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
20302
20535
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
20303
|
-
const modelRoot = options.modelRoot ?
|
|
20536
|
+
const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
|
|
20304
20537
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
20305
20538
|
...await verifyFile(modelRoot, model),
|
|
20306
20539
|
source: model.source,
|
|
@@ -20334,7 +20567,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20334
20567
|
};
|
|
20335
20568
|
}
|
|
20336
20569
|
function renderStyle(style, destination, cwd) {
|
|
20337
|
-
const portableDir =
|
|
20570
|
+
const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
|
|
20338
20571
|
const replace = (value) => {
|
|
20339
20572
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
20340
20573
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -20346,25 +20579,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
20346
20579
|
return replace(style);
|
|
20347
20580
|
}
|
|
20348
20581
|
async function installRecipe(target, options = {}) {
|
|
20349
|
-
const cwd =
|
|
20582
|
+
const cwd = path44.resolve(options.cwd ?? process.cwd());
|
|
20350
20583
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20351
20584
|
const verification = await verifyRecipe(loaded.path);
|
|
20352
20585
|
if (!verification.ok) {
|
|
20353
20586
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
20354
20587
|
}
|
|
20355
|
-
const destination = options.out ?
|
|
20588
|
+
const destination = options.out ? path44.resolve(cwd, options.out) : path44.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
20356
20589
|
const sources = [
|
|
20357
20590
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
20358
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
20591
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path44.join(loaded.dir, ...file.path.split("/")) }))
|
|
20359
20592
|
];
|
|
20360
20593
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
20361
|
-
path:
|
|
20362
|
-
data: await
|
|
20594
|
+
path: path44.join(destination, ...file.path.split("/")),
|
|
20595
|
+
data: await readFile30(file.source)
|
|
20363
20596
|
})));
|
|
20364
20597
|
if (!options.force) {
|
|
20365
20598
|
for (const file of files) {
|
|
20366
|
-
if (!
|
|
20367
|
-
const current = await
|
|
20599
|
+
if (!existsSync33(file.path)) continue;
|
|
20600
|
+
const current = await readFile30(file.path);
|
|
20368
20601
|
if (!current.equals(file.data)) {
|
|
20369
20602
|
throw new OverwriteRefusedError(
|
|
20370
20603
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -20458,14 +20691,14 @@ async function runRecipe(args) {
|
|
|
20458
20691
|
}, null, 2));
|
|
20459
20692
|
} else {
|
|
20460
20693
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
20461
|
-
log(` ${
|
|
20694
|
+
log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
|
|
20462
20695
|
log(`
|
|
20463
20696
|
Add this entry under your manifest's styles object:`);
|
|
20464
20697
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
20465
20698
|
if (result.recipe.models.length) {
|
|
20466
20699
|
log(`
|
|
20467
20700
|
Models are not downloaded automatically. Verify them with:`);
|
|
20468
|
-
log(` pixelkiln recipe verify ${
|
|
20701
|
+
log(` pixelkiln recipe verify ${path45.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
20469
20702
|
}
|
|
20470
20703
|
}
|
|
20471
20704
|
return;
|
|
@@ -20473,7 +20706,7 @@ async function runRecipe(args) {
|
|
|
20473
20706
|
}
|
|
20474
20707
|
|
|
20475
20708
|
// src/cli/commands/refine.ts
|
|
20476
|
-
import
|
|
20709
|
+
import path46 from "path";
|
|
20477
20710
|
async function runRefine(args) {
|
|
20478
20711
|
if (args.from || args.subcommand === "approve") {
|
|
20479
20712
|
if (!args.from) {
|
|
@@ -20503,12 +20736,12 @@ async function runRefine(args) {
|
|
|
20503
20736
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
20504
20737
|
);
|
|
20505
20738
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
20506
|
-
log(` wrote ${
|
|
20507
|
-
log(` quality record: ${
|
|
20739
|
+
log(` wrote ${path46.relative(process.cwd(), result3.output)}`);
|
|
20740
|
+
log(` quality record: ${path46.relative(process.cwd(), result3.record)}`);
|
|
20508
20741
|
log(`
|
|
20509
20742
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
20510
20743
|
log(
|
|
20511
|
-
` pixelkiln refine approve --from ${
|
|
20744
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
20512
20745
|
);
|
|
20513
20746
|
}
|
|
20514
20747
|
return;
|
|
@@ -20536,7 +20769,7 @@ async function runRefine(args) {
|
|
|
20536
20769
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
20537
20770
|
else {
|
|
20538
20771
|
log(
|
|
20539
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
20772
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path46.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path46.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
20540
20773
|
);
|
|
20541
20774
|
}
|
|
20542
20775
|
return;
|
|
@@ -20545,7 +20778,7 @@ async function runRefine(args) {
|
|
|
20545
20778
|
if (args.json) {
|
|
20546
20779
|
log(JSON.stringify(result2, null, 2));
|
|
20547
20780
|
} else {
|
|
20548
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
20781
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path46.relative(process.cwd(), result2.output || result2.record)}`);
|
|
20549
20782
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
20550
20783
|
}
|
|
20551
20784
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -20591,7 +20824,7 @@ async function runRefine(args) {
|
|
|
20591
20824
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
20592
20825
|
if (item.state === "needs-approval") {
|
|
20593
20826
|
log(
|
|
20594
|
-
` pixelkiln refine approve --from ${
|
|
20827
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
20595
20828
|
);
|
|
20596
20829
|
}
|
|
20597
20830
|
}
|
|
@@ -20602,9 +20835,9 @@ async function runRefine(args) {
|
|
|
20602
20835
|
}
|
|
20603
20836
|
|
|
20604
20837
|
// src/cli/commands/skeleton.ts
|
|
20605
|
-
import
|
|
20606
|
-
import { existsSync as
|
|
20607
|
-
import { readFile as
|
|
20838
|
+
import path47 from "path";
|
|
20839
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20840
|
+
import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
|
|
20608
20841
|
|
|
20609
20842
|
// src/skeleton-preview.ts
|
|
20610
20843
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -20712,23 +20945,19 @@ var Canvas = class {
|
|
|
20712
20945
|
};
|
|
20713
20946
|
|
|
20714
20947
|
// src/cli/commands/skeleton.ts
|
|
20715
|
-
var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
|
|
20716
20948
|
async function runEstimateSkeleton(args) {
|
|
20717
20949
|
const image = args.target;
|
|
20718
20950
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
20719
|
-
if (args.out &&
|
|
20951
|
+
if (args.out && existsSync34(path47.resolve(args.out)) && !args.force) {
|
|
20720
20952
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
20721
20953
|
}
|
|
20722
|
-
loadEnvFiles(
|
|
20954
|
+
loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
|
|
20723
20955
|
loadEnvFiles(process.cwd());
|
|
20724
|
-
const bytes = await
|
|
20956
|
+
const bytes = await readFile31(path47.resolve(image));
|
|
20725
20957
|
const metadata = imageMetadata(bytes);
|
|
20726
20958
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
`${image} is ${metadata.width}x${metadata.height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`
|
|
20730
|
-
);
|
|
20731
|
-
}
|
|
20959
|
+
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
20960
|
+
if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
|
|
20732
20961
|
const client2 = clientFromEnv();
|
|
20733
20962
|
const res = await client2.estimateSkeleton({
|
|
20734
20963
|
image: { base64: bytes.toString("base64"), format: metadata.format }
|
|
@@ -20736,7 +20965,7 @@ async function runEstimateSkeleton(args) {
|
|
|
20736
20965
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
20737
20966
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
20738
20967
|
if (args.out) {
|
|
20739
|
-
await
|
|
20968
|
+
await writeFile16(path47.resolve(args.out), json + "\n");
|
|
20740
20969
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
20741
20970
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
20742
20971
|
} else {
|
|
@@ -20749,8 +20978,8 @@ async function runSkeletonPreview(args) {
|
|
|
20749
20978
|
let sourceFile;
|
|
20750
20979
|
let label;
|
|
20751
20980
|
if (target.endsWith(".json")) {
|
|
20752
|
-
keypointsFile =
|
|
20753
|
-
sourceFile = args.from ?
|
|
20981
|
+
keypointsFile = path47.resolve(target);
|
|
20982
|
+
sourceFile = args.from ? path47.resolve(args.from) : void 0;
|
|
20754
20983
|
label = target;
|
|
20755
20984
|
} else {
|
|
20756
20985
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -20762,31 +20991,31 @@ async function runSkeletonPreview(args) {
|
|
|
20762
20991
|
);
|
|
20763
20992
|
}
|
|
20764
20993
|
keypointsFile = spec.revision.keypointsFile;
|
|
20765
|
-
sourceFile = args.from ?
|
|
20994
|
+
sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
|
|
20766
20995
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
20767
20996
|
}
|
|
20768
|
-
if (!
|
|
20997
|
+
if (!existsSync34(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
20769
20998
|
let json;
|
|
20770
20999
|
try {
|
|
20771
|
-
json = JSON.parse(await
|
|
21000
|
+
json = JSON.parse(await readFile31(keypointsFile, "utf8"));
|
|
20772
21001
|
} catch (error) {
|
|
20773
21002
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
20774
21003
|
}
|
|
20775
21004
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
20776
21005
|
let source;
|
|
20777
21006
|
if (sourceFile) {
|
|
20778
|
-
if (!
|
|
21007
|
+
if (!existsSync34(sourceFile)) {
|
|
20779
21008
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
20780
21009
|
} else {
|
|
20781
|
-
const bytes = await
|
|
21010
|
+
const bytes = await readFile31(sourceFile);
|
|
20782
21011
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
20783
21012
|
source = decodePng(bytes);
|
|
20784
21013
|
}
|
|
20785
21014
|
}
|
|
20786
|
-
const out =
|
|
20787
|
-
await
|
|
21015
|
+
const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21016
|
+
await writeFile16(out, renderSkeletonSheet(set, { source }));
|
|
20788
21017
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
20789
|
-
const shown =
|
|
21018
|
+
const shown = path47.relative(process.cwd(), out);
|
|
20790
21019
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
20791
21020
|
}
|
|
20792
21021
|
|
|
@@ -20832,15 +21061,15 @@ async function runTools(args) {
|
|
|
20832
21061
|
}
|
|
20833
21062
|
|
|
20834
21063
|
// src/cli/commands/workspace.ts
|
|
20835
|
-
import
|
|
20836
|
-
import { existsSync as
|
|
21064
|
+
import path48 from "path";
|
|
21065
|
+
import { existsSync as existsSync35 } from "fs";
|
|
20837
21066
|
async function runWorkspace(args) {
|
|
20838
|
-
const workspacePath =
|
|
20839
|
-
const dir =
|
|
21067
|
+
const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21068
|
+
const dir = path48.dirname(workspacePath);
|
|
20840
21069
|
if (args.subcommand === "add") {
|
|
20841
|
-
const manifestPath =
|
|
21070
|
+
const manifestPath = path48.resolve(args.target);
|
|
20842
21071
|
const loadedTarget = await loadManifest(manifestPath);
|
|
20843
|
-
const lockPath = args.explicitLock ?
|
|
21072
|
+
const lockPath = args.explicitLock ? path48.resolve(args.explicitLock) : path48.join(path48.dirname(manifestPath), "pixelkiln.lock.json");
|
|
20844
21073
|
const ws = await loadWorkspace(workspacePath);
|
|
20845
21074
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
20846
21075
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -20860,10 +21089,10 @@ async function runWorkspace(args) {
|
|
|
20860
21089
|
...args.account ? { account: args.account } : {}
|
|
20861
21090
|
};
|
|
20862
21091
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
20863
|
-
log(` registered "${id}" in ${
|
|
21092
|
+
log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20864
21093
|
log(` manifest: ${project.manifest}`);
|
|
20865
21094
|
log(` lock: ${project.lock}`);
|
|
20866
|
-
if (!
|
|
21095
|
+
if (!existsSync35(lockPath)) {
|
|
20867
21096
|
log(
|
|
20868
21097
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
20869
21098
|
);
|
|
@@ -20872,7 +21101,7 @@ async function runWorkspace(args) {
|
|
|
20872
21101
|
}
|
|
20873
21102
|
if (args.subcommand === "remove") {
|
|
20874
21103
|
const ws = await loadWorkspace(workspacePath);
|
|
20875
|
-
const resolvedTarget =
|
|
21104
|
+
const resolvedTarget = path48.resolve(args.target);
|
|
20876
21105
|
const match = ws.projects.find(
|
|
20877
21106
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
20878
21107
|
);
|
|
@@ -20883,11 +21112,11 @@ async function runWorkspace(args) {
|
|
|
20883
21112
|
version: 1,
|
|
20884
21113
|
projects: ws.projects.filter((p) => p !== match)
|
|
20885
21114
|
});
|
|
20886
|
-
log(` removed "${match.id}" from ${
|
|
21115
|
+
log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20887
21116
|
return;
|
|
20888
21117
|
}
|
|
20889
21118
|
if (args.subcommand === "list") {
|
|
20890
|
-
if (!
|
|
21119
|
+
if (!existsSync35(workspacePath)) {
|
|
20891
21120
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20892
21121
|
}
|
|
20893
21122
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20895,9 +21124,9 @@ async function runWorkspace(args) {
|
|
|
20895
21124
|
if (args.json) {
|
|
20896
21125
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
20897
21126
|
} else if (!ws.projects.length) {
|
|
20898
|
-
log(` no projects registered in ${
|
|
21127
|
+
log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20899
21128
|
} else {
|
|
20900
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21129
|
+
log(` ${ws.projects.length} project(s) in ${path48.relative(process.cwd(), workspacePath)}:`);
|
|
20901
21130
|
for (const p of ws.projects) {
|
|
20902
21131
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
20903
21132
|
}
|
|
@@ -20907,7 +21136,7 @@ async function runWorkspace(args) {
|
|
|
20907
21136
|
return;
|
|
20908
21137
|
}
|
|
20909
21138
|
if (args.subcommand === "status") {
|
|
20910
|
-
if (!
|
|
21139
|
+
if (!existsSync35(workspacePath)) {
|
|
20911
21140
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20912
21141
|
}
|
|
20913
21142
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20915,7 +21144,7 @@ async function runWorkspace(args) {
|
|
|
20915
21144
|
if (args.json) {
|
|
20916
21145
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
20917
21146
|
} else {
|
|
20918
|
-
log(` workspace: ${
|
|
21147
|
+
log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20919
21148
|
for (const p of report.projects) {
|
|
20920
21149
|
if (p.error) {
|
|
20921
21150
|
log(`
|
|
@@ -20969,8 +21198,8 @@ async function runHelp() {
|
|
|
20969
21198
|
async function runVersion() {
|
|
20970
21199
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
20971
21200
|
const url = new URL(up, import.meta.url);
|
|
20972
|
-
if (!
|
|
20973
|
-
const pkg = JSON.parse(await
|
|
21201
|
+
if (!existsSync36(url)) continue;
|
|
21202
|
+
const pkg = JSON.parse(await readFile32(url, "utf8"));
|
|
20974
21203
|
if (pkg.name === "pixelkiln") {
|
|
20975
21204
|
log(`${pkg.name} ${pkg.version}`);
|
|
20976
21205
|
return;
|