pixelkiln 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +582 -422
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +5 -0
- package/dist/client/gallery.js +223 -1
- package/dist/index.cjs +261 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -74
- package/dist/index.d.ts +170 -74
- package/dist/index.js +256 -134
- package/dist/index.js.map +1 -1
- package/docs/REVISIONS.md +11 -0
- package/package.json +1 -1
- 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 existsSync35 } from "fs";
|
|
71
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -662,10 +662,10 @@ import { createReadStream } from "fs";
|
|
|
662
662
|
function sha256(data) {
|
|
663
663
|
return createHash("sha256").update(data).digest("hex");
|
|
664
664
|
}
|
|
665
|
-
async function sha256File(
|
|
665
|
+
async function sha256File(path49) {
|
|
666
666
|
return new Promise((resolve, reject) => {
|
|
667
667
|
const hash = createHash("sha256");
|
|
668
|
-
const stream = createReadStream(
|
|
668
|
+
const stream = createReadStream(path49);
|
|
669
669
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
670
670
|
stream.on("error", reject);
|
|
671
671
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -5837,6 +5837,10 @@ function parseSkeletonSet(json, file) {
|
|
|
5837
5837
|
}
|
|
5838
5838
|
return result.data;
|
|
5839
5839
|
}
|
|
5840
|
+
var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
|
|
5841
|
+
function estimateSkeletonSizeProblem(width, height) {
|
|
5842
|
+
return width === height && ESTIMATE_SKELETON_SIZES.includes(width) ? null : `${width}x${height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`;
|
|
5843
|
+
}
|
|
5840
5844
|
var DEFAULT_SCAFFOLD_FRAMES = 4;
|
|
5841
5845
|
function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
|
|
5842
5846
|
const copy = () => keypoints.map((joint) => ({ ...joint }));
|
|
@@ -6156,9 +6160,9 @@ var PixelLabClient = class {
|
|
|
6156
6160
|
apiKey;
|
|
6157
6161
|
http;
|
|
6158
6162
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6159
|
-
async request(
|
|
6163
|
+
async request(path49, init) {
|
|
6160
6164
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6161
|
-
const res = await this.http(`${BASE}${
|
|
6165
|
+
const res = await this.http(`${BASE}${path49}`, {
|
|
6162
6166
|
...init,
|
|
6163
6167
|
headers: {
|
|
6164
6168
|
Authorization: auth,
|
|
@@ -6168,13 +6172,13 @@ var PixelLabClient = class {
|
|
|
6168
6172
|
});
|
|
6169
6173
|
const text = await res.text();
|
|
6170
6174
|
if (!res.ok) {
|
|
6171
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6175
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path49} \u2192 ${res.status}`, res.status, text);
|
|
6172
6176
|
}
|
|
6173
6177
|
if (!text) return {};
|
|
6174
6178
|
try {
|
|
6175
6179
|
return JSON.parse(text);
|
|
6176
6180
|
} catch {
|
|
6177
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6181
|
+
throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
|
|
6178
6182
|
}
|
|
6179
6183
|
}
|
|
6180
6184
|
async balance() {
|
|
@@ -6566,10 +6570,10 @@ var PixelLabClient = class {
|
|
|
6566
6570
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6567
6571
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6568
6572
|
const south = args.reference?.south;
|
|
6569
|
-
let
|
|
6573
|
+
let path49;
|
|
6570
6574
|
let body;
|
|
6571
6575
|
if (args.mode === "v3") {
|
|
6572
|
-
|
|
6576
|
+
path49 = "/create-character-v3";
|
|
6573
6577
|
body = {
|
|
6574
6578
|
description: args.description,
|
|
6575
6579
|
image_size: imageSize,
|
|
@@ -6583,7 +6587,7 @@ var PixelLabClient = class {
|
|
|
6583
6587
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6584
6588
|
};
|
|
6585
6589
|
} else if (args.mode === "pro-flash") {
|
|
6586
|
-
|
|
6590
|
+
path49 = "/create-character-pro-flash";
|
|
6587
6591
|
const traits = args.styleTraits;
|
|
6588
6592
|
body = {
|
|
6589
6593
|
description: args.description,
|
|
@@ -6607,7 +6611,7 @@ var PixelLabClient = class {
|
|
|
6607
6611
|
} : {}
|
|
6608
6612
|
};
|
|
6609
6613
|
} else if (args.mode === "pro") {
|
|
6610
|
-
|
|
6614
|
+
path49 = "/create-character-pro";
|
|
6611
6615
|
body = {
|
|
6612
6616
|
description: args.description,
|
|
6613
6617
|
image_size: imageSize,
|
|
@@ -6621,7 +6625,7 @@ var PixelLabClient = class {
|
|
|
6621
6625
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6622
6626
|
};
|
|
6623
6627
|
} else {
|
|
6624
|
-
|
|
6628
|
+
path49 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6625
6629
|
body = {
|
|
6626
6630
|
description: args.description,
|
|
6627
6631
|
image_size: imageSize,
|
|
@@ -6638,8 +6642,8 @@ var PixelLabClient = class {
|
|
|
6638
6642
|
...palette
|
|
6639
6643
|
};
|
|
6640
6644
|
}
|
|
6641
|
-
const raw = await this.request(
|
|
6642
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6645
|
+
const raw = await this.request(path49, { method: "POST", body: JSON.stringify(body) });
|
|
6646
|
+
return validateResponse(CharacterSubmitSchema, raw, path49.slice(1));
|
|
6643
6647
|
}
|
|
6644
6648
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6645
6649
|
async createCharacterState(args) {
|
|
@@ -9486,9 +9490,9 @@ var RetroDiffusionClient = class {
|
|
|
9486
9490
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9487
9491
|
return Buffer.from(await response.arrayBuffer());
|
|
9488
9492
|
}
|
|
9489
|
-
async call(
|
|
9493
|
+
async call(path49, init = {}) {
|
|
9490
9494
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9491
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9495
|
+
const response = await this.request(`${this.baseUrl}${path49}`, {
|
|
9492
9496
|
...init,
|
|
9493
9497
|
headers: {
|
|
9494
9498
|
"Content-Type": "application/json",
|
|
@@ -11793,10 +11797,10 @@ function isSha256Hash(value) {
|
|
|
11793
11797
|
function parseCache(value) {
|
|
11794
11798
|
return HashCacheSchema.parse(value);
|
|
11795
11799
|
}
|
|
11796
|
-
async function loadCache(
|
|
11797
|
-
if (!existsSync10(
|
|
11800
|
+
async function loadCache(path49) {
|
|
11801
|
+
if (!existsSync10(path49)) return { version: 1, hashes: {} };
|
|
11798
11802
|
try {
|
|
11799
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
11803
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
|
|
11800
11804
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11801
11805
|
return {
|
|
11802
11806
|
version: 1,
|
|
@@ -11808,18 +11812,18 @@ async function loadCache(path48) {
|
|
|
11808
11812
|
return { version: 1, hashes: {} };
|
|
11809
11813
|
}
|
|
11810
11814
|
}
|
|
11811
|
-
async function saveCache(
|
|
11815
|
+
async function saveCache(path49, cache) {
|
|
11812
11816
|
const sorted = {};
|
|
11813
11817
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11814
11818
|
const hash = cache.hashes[key];
|
|
11815
11819
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11816
11820
|
sorted[key] = hash;
|
|
11817
11821
|
}
|
|
11818
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11819
|
-
const tmp = `${
|
|
11822
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path49)), { recursive: true });
|
|
11823
|
+
const tmp = `${path49}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11820
11824
|
try {
|
|
11821
11825
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11822
|
-
await rename5(tmp,
|
|
11826
|
+
await rename5(tmp, path49);
|
|
11823
11827
|
} finally {
|
|
11824
11828
|
await rm5(tmp, { force: true });
|
|
11825
11829
|
}
|
|
@@ -13823,15 +13827,27 @@ var NewAssetSchema = z6.object({
|
|
|
13823
13827
|
/**
|
|
13824
13828
|
* A controlled regeneration of another asset in the same manifest,
|
|
13825
13829
|
* instead of a fresh generation. `inpaint` needs a mask upload the
|
|
13826
|
-
* gallery does not offer yet, so only `image-to-image`
|
|
13827
|
-
*
|
|
13830
|
+
* gallery does not offer yet, so only `image-to-image` and
|
|
13831
|
+
* `animate-skeleton` (whose keypoints file the gallery writes first) can
|
|
13832
|
+
* be created here; hand-edit the manifest for the other modes.
|
|
13828
13833
|
*/
|
|
13829
|
-
revision: z6.
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13834
|
+
revision: z6.discriminatedUnion("mode", [
|
|
13835
|
+
z6.object({
|
|
13836
|
+
mode: z6.literal("image-to-image"),
|
|
13837
|
+
/** Asset id of the parent this revises; must already exist. */
|
|
13838
|
+
from: z6.string().min(1),
|
|
13839
|
+
strength: z6.number().min(0).max(1).optional()
|
|
13840
|
+
}).strict(),
|
|
13841
|
+
z6.object({
|
|
13842
|
+
mode: z6.literal("animate-skeleton"),
|
|
13843
|
+
from: z6.string().min(1),
|
|
13844
|
+
/** Manifest-relative; checked when the manifest is loaded, like every revision input. */
|
|
13845
|
+
keypointsFile: z6.string().min(1),
|
|
13846
|
+
direction: CharacterDirectionSchema,
|
|
13847
|
+
description: z6.string().min(1).optional(),
|
|
13848
|
+
skeletonTemplate: z6.string().min(1).optional()
|
|
13849
|
+
}).strict()
|
|
13850
|
+
]).optional(),
|
|
13835
13851
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
13836
13852
|
state: NewStateSchema.optional(),
|
|
13837
13853
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
@@ -14356,52 +14372,165 @@ async function runEdit(args) {
|
|
|
14356
14372
|
}
|
|
14357
14373
|
|
|
14358
14374
|
// src/cli/commands/gallery.ts
|
|
14359
|
-
import
|
|
14360
|
-
import { existsSync as
|
|
14375
|
+
import path33 from "path";
|
|
14376
|
+
import { existsSync as existsSync24 } from "fs";
|
|
14361
14377
|
|
|
14362
14378
|
// src/gallery/snapshot.ts
|
|
14363
14379
|
import { createHash as createHash3 } from "crypto";
|
|
14364
|
-
import { existsSync as
|
|
14365
|
-
import { readFile as
|
|
14366
|
-
import
|
|
14380
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14381
|
+
import { readFile as readFile18, stat as stat2 } from "fs/promises";
|
|
14382
|
+
import path29 from "path";
|
|
14367
14383
|
|
|
14368
14384
|
// src/gallery/edit.ts
|
|
14385
|
+
import path27 from "path";
|
|
14386
|
+
import { z as z9 } from "zod";
|
|
14387
|
+
|
|
14388
|
+
// src/gallery/skeleton.ts
|
|
14389
|
+
import { existsSync as existsSync18 } from "fs";
|
|
14390
|
+
import { mkdir as mkdir10, readFile as readFile17, rm as rm9, writeFile as writeFile12 } from "fs/promises";
|
|
14369
14391
|
import path26 from "path";
|
|
14370
14392
|
import { z as z8 } from "zod";
|
|
14371
|
-
var
|
|
14372
|
-
|
|
14373
|
-
z8.
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14393
|
+
var SkeletonEstimateRequestSchema = z8.object({
|
|
14394
|
+
project: z8.string().min(1).optional(),
|
|
14395
|
+
styleId: z8.string().min(1),
|
|
14396
|
+
assetId: z8.string().min(1),
|
|
14397
|
+
frames: z8.number().int().min(3).max(15).optional()
|
|
14398
|
+
}).strict();
|
|
14399
|
+
var SkeletonAnimationRequestSchema = z8.object({
|
|
14400
|
+
action: z8.literal("create-skeleton-animation"),
|
|
14401
|
+
project: z8.string().min(1).optional(),
|
|
14402
|
+
expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
|
|
14403
|
+
styleId: z8.string().min(1),
|
|
14404
|
+
/** The sprite the animation poses. */
|
|
14405
|
+
from: z8.string().min(1),
|
|
14406
|
+
assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14407
|
+
/** The motion, sent as PixelLab's `action`. */
|
|
14408
|
+
prompt: z8.string().min(1),
|
|
14409
|
+
direction: CharacterDirectionSchema,
|
|
14410
|
+
description: z8.string().min(1).optional(),
|
|
14411
|
+
skeletonTemplate: z8.string().min(1).optional(),
|
|
14412
|
+
/** Manifest-relative JSON path inside the project. */
|
|
14413
|
+
keypointsFile: z8.string().min(1),
|
|
14414
|
+
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14415
|
+
set: SkeletonSetSchema.optional(),
|
|
14416
|
+
/** Replace an existing `keypointsFile` with `set`. */
|
|
14417
|
+
overwrite: z8.boolean().optional()
|
|
14418
|
+
}).strict();
|
|
14419
|
+
function skeletonSourceFile(spec) {
|
|
14420
|
+
if (spec.source) return path26.resolve(spec.root, spec.source);
|
|
14421
|
+
if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
|
|
14422
|
+
return spec.outFile;
|
|
14423
|
+
}
|
|
14424
|
+
function createGallerySkeletonHandlers(opts) {
|
|
14425
|
+
const log2 = opts.onProgress ?? (() => {
|
|
14426
|
+
});
|
|
14427
|
+
return {
|
|
14428
|
+
async estimate(body) {
|
|
14429
|
+
const parsed = SkeletonEstimateRequestSchema.safeParse(body);
|
|
14430
|
+
if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
|
|
14431
|
+
const request = parsed.data;
|
|
14432
|
+
const ctx = await opts.loadProject(request.project);
|
|
14433
|
+
const key = lockKey(request.styleId, request.assetId);
|
|
14434
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14435
|
+
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14436
|
+
const file = skeletonSourceFile(spec);
|
|
14437
|
+
if (!existsSync18(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
14438
|
+
const bytes = await readFile17(file);
|
|
14439
|
+
const metadata = imageMetadata(bytes);
|
|
14440
|
+
if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
|
|
14441
|
+
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14442
|
+
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14443
|
+
const client2 = (opts.client ?? clientFromEnv)();
|
|
14444
|
+
const res = await client2.estimateSkeleton({ image: { base64: bytes.toString("base64"), format: metadata.format } });
|
|
14445
|
+
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, not budgeted)`);
|
|
14446
|
+
return {
|
|
14447
|
+
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14448
|
+
source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/")
|
|
14449
|
+
};
|
|
14450
|
+
}
|
|
14451
|
+
};
|
|
14452
|
+
}
|
|
14453
|
+
async function createSkeletonAnimation(manifestPath, request) {
|
|
14454
|
+
const root = path26.dirname(path26.resolve(manifestPath));
|
|
14455
|
+
const file = path26.resolve(root, request.keypointsFile);
|
|
14456
|
+
const relative = path26.relative(root, file);
|
|
14457
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path26.isAbsolute(relative)) {
|
|
14458
|
+
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14459
|
+
}
|
|
14460
|
+
const existed = existsSync18(file);
|
|
14461
|
+
let previous = null;
|
|
14462
|
+
if (request.set) {
|
|
14463
|
+
if (existed && !request.overwrite) {
|
|
14464
|
+
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14465
|
+
}
|
|
14466
|
+
if (existed) previous = await readFile17(file);
|
|
14467
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
14468
|
+
await writeFile12(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14469
|
+
} else if (!existed) {
|
|
14470
|
+
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14471
|
+
}
|
|
14472
|
+
try {
|
|
14473
|
+
await applyManifestEdit(manifestPath, {
|
|
14474
|
+
action: "add-asset",
|
|
14475
|
+
assetId: request.assetId,
|
|
14476
|
+
expectedSha256: request.expectedSha256,
|
|
14477
|
+
asset: {
|
|
14478
|
+
prompt: request.prompt,
|
|
14479
|
+
styles: [request.styleId],
|
|
14480
|
+
revision: {
|
|
14481
|
+
mode: "animate-skeleton",
|
|
14482
|
+
from: request.from,
|
|
14483
|
+
keypointsFile: relative.split(path26.sep).join("/"),
|
|
14484
|
+
direction: request.direction,
|
|
14485
|
+
...request.description ? { description: request.description } : {},
|
|
14486
|
+
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
14487
|
+
}
|
|
14488
|
+
}
|
|
14489
|
+
});
|
|
14490
|
+
} catch (error) {
|
|
14491
|
+
if (request.set) {
|
|
14492
|
+
if (previous) await writeFile12(file, previous);
|
|
14493
|
+
else await rm9(file, { force: true });
|
|
14494
|
+
}
|
|
14495
|
+
throw error;
|
|
14496
|
+
}
|
|
14497
|
+
}
|
|
14498
|
+
|
|
14499
|
+
// src/gallery/edit.ts
|
|
14500
|
+
var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
14501
|
+
var HandEditRequestSchema = z9.discriminatedUnion("action", [
|
|
14502
|
+
z9.object({
|
|
14503
|
+
action: z9.literal("start-edit"),
|
|
14504
|
+
project: z9.string().min(1).optional(),
|
|
14505
|
+
assetId: z9.string().min(1),
|
|
14506
|
+
styleId: z9.string().min(1),
|
|
14507
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14379
14508
|
/** Also launch the author's editor on the file. */
|
|
14380
|
-
open:
|
|
14509
|
+
open: z9.boolean().optional()
|
|
14381
14510
|
}).strict(),
|
|
14382
|
-
|
|
14383
|
-
action:
|
|
14384
|
-
project:
|
|
14385
|
-
assetId:
|
|
14386
|
-
styleId:
|
|
14387
|
-
expectedSha256:
|
|
14511
|
+
z9.object({
|
|
14512
|
+
action: z9.literal("detach-edit"),
|
|
14513
|
+
project: z9.string().min(1).optional(),
|
|
14514
|
+
assetId: z9.string().min(1),
|
|
14515
|
+
styleId: z9.string().min(1),
|
|
14516
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/)
|
|
14388
14517
|
}).strict(),
|
|
14389
14518
|
/** 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:
|
|
14519
|
+
z9.object({
|
|
14520
|
+
action: z9.literal("save-edit"),
|
|
14521
|
+
project: z9.string().min(1).optional(),
|
|
14522
|
+
assetId: z9.string().min(1),
|
|
14523
|
+
styleId: z9.string().min(1),
|
|
14524
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14396
14525
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14397
14526
|
png: Base64.min(1).optional(),
|
|
14398
14527
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14399
|
-
frames:
|
|
14528
|
+
frames: z9.array(z9.object({ role: z9.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14400
14529
|
/** The editor's layered project file, kept beside the edit. */
|
|
14401
14530
|
pxo: Base64.optional(),
|
|
14402
14531
|
/** Editor identity from the bridge's ready message. */
|
|
14403
|
-
editor:
|
|
14404
|
-
protocol:
|
|
14532
|
+
editor: z9.string().min(1).max(200),
|
|
14533
|
+
protocol: z9.number().int().positive()
|
|
14405
14534
|
}).strict()
|
|
14406
14535
|
]);
|
|
14407
14536
|
function createGalleryEditHandler(opts) {
|
|
@@ -14442,6 +14571,23 @@ function createGalleryEditHandler(opts) {
|
|
|
14442
14571
|
}
|
|
14443
14572
|
return opts.reload();
|
|
14444
14573
|
}
|
|
14574
|
+
const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
|
|
14575
|
+
if (skeleton.success) {
|
|
14576
|
+
let manifestPath2;
|
|
14577
|
+
try {
|
|
14578
|
+
manifestPath2 = await opts.manifestFor(skeleton.data.project);
|
|
14579
|
+
} catch (error) {
|
|
14580
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14581
|
+
}
|
|
14582
|
+
await createSkeletonAnimation(manifestPath2, skeleton.data);
|
|
14583
|
+
log2(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
|
|
14584
|
+
return opts.reload();
|
|
14585
|
+
}
|
|
14586
|
+
if (body?.action === "create-skeleton-animation") {
|
|
14587
|
+
throw new ManifestEditError(
|
|
14588
|
+
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
14589
|
+
);
|
|
14590
|
+
}
|
|
14445
14591
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
14446
14592
|
if (!parsed.success) {
|
|
14447
14593
|
throw new ManifestEditError(
|
|
@@ -14456,7 +14602,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14456
14602
|
}
|
|
14457
14603
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14458
14604
|
if (result.changed) {
|
|
14459
|
-
const relative =
|
|
14605
|
+
const relative = path27.relative(process.cwd(), result.manifestPath);
|
|
14460
14606
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14461
14607
|
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
14608
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -14466,8 +14612,8 @@ function createGalleryEditHandler(opts) {
|
|
|
14466
14612
|
}
|
|
14467
14613
|
|
|
14468
14614
|
// src/pipeline/history.ts
|
|
14469
|
-
import { existsSync as
|
|
14470
|
-
import
|
|
14615
|
+
import { existsSync as existsSync19 } from "fs";
|
|
14616
|
+
import path28 from "path";
|
|
14471
14617
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
14472
14618
|
function historyLimit(manifest, env = process.env) {
|
|
14473
14619
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -14554,10 +14700,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14554
14700
|
}
|
|
14555
14701
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
14556
14702
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
14557
|
-
if (!
|
|
14703
|
+
if (!existsSync19(file)) continue;
|
|
14558
14704
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
14559
14705
|
throw new OverwriteRefusedError(
|
|
14560
|
-
`${
|
|
14706
|
+
`${path28.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
14561
14707
|
[file]
|
|
14562
14708
|
);
|
|
14563
14709
|
}
|
|
@@ -14610,7 +14756,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14610
14756
|
// src/gallery/snapshot.ts
|
|
14611
14757
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14612
14758
|
function galleryMediaId(absolutePath) {
|
|
14613
|
-
return createHash3("sha256").update(
|
|
14759
|
+
return createHash3("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14614
14760
|
}
|
|
14615
14761
|
function galleryMediaRoute(id) {
|
|
14616
14762
|
return `/media/${id}`;
|
|
@@ -14629,9 +14775,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14629
14775
|
return (entry.history ?? []).map((generation, i) => {
|
|
14630
14776
|
const outputs = generation.outputs.map((output) => {
|
|
14631
14777
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14632
|
-
const file =
|
|
14778
|
+
const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14633
14779
|
let url = null;
|
|
14634
|
-
if (
|
|
14780
|
+
if (existsSync20(file)) {
|
|
14635
14781
|
const id = galleryMediaId(file);
|
|
14636
14782
|
media.set(id, { path: file, contentType: mediaType });
|
|
14637
14783
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14663,7 +14809,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14663
14809
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
14664
14810
|
async function samePixels(a, b) {
|
|
14665
14811
|
try {
|
|
14666
|
-
const [left, right] = await Promise.all([
|
|
14812
|
+
const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
|
|
14667
14813
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
14668
14814
|
const x = decodePng(left);
|
|
14669
14815
|
const y = decodePng(right);
|
|
@@ -14761,10 +14907,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14761
14907
|
async function describeQuality(media, root, inspection) {
|
|
14762
14908
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14763
14909
|
const outputs = await Promise.all(
|
|
14764
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
14910
|
+
outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
|
|
14765
14911
|
);
|
|
14766
|
-
const recordPath =
|
|
14767
|
-
const recordExists =
|
|
14912
|
+
const recordPath = path29.resolve(root, inspection.record);
|
|
14913
|
+
const recordExists = existsSync20(recordPath);
|
|
14768
14914
|
let options = null;
|
|
14769
14915
|
let check = null;
|
|
14770
14916
|
if (recordExists) {
|
|
@@ -14783,7 +14929,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14783
14929
|
return {
|
|
14784
14930
|
state: inspection.state,
|
|
14785
14931
|
reason: inspection.reason,
|
|
14786
|
-
output: portableOutputPath(
|
|
14932
|
+
output: portableOutputPath(path29.resolve(root, inspection.output), root),
|
|
14787
14933
|
record: portableOutputPath(recordPath, root),
|
|
14788
14934
|
recordExists,
|
|
14789
14935
|
outputs,
|
|
@@ -14821,9 +14967,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
14821
14967
|
const media = /* @__PURE__ */ new Map();
|
|
14822
14968
|
const plan = await buildPlan(specs, lock);
|
|
14823
14969
|
const items = [];
|
|
14824
|
-
const manifestText = await
|
|
14970
|
+
const manifestText = await readFile18(loaded.path, "utf8");
|
|
14825
14971
|
const manifestSha256 = sha256(manifestText);
|
|
14826
|
-
const cacheDir =
|
|
14972
|
+
const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
14827
14973
|
let rawStyles = {};
|
|
14828
14974
|
try {
|
|
14829
14975
|
const raw = JSON.parse(manifestText);
|
|
@@ -14847,12 +14993,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
14847
14993
|
)
|
|
14848
14994
|
);
|
|
14849
14995
|
if (spec.source) {
|
|
14850
|
-
const editPath =
|
|
14996
|
+
const editPath = path29.resolve(root, spec.source);
|
|
14851
14997
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
14852
14998
|
const editShas = await Promise.all(
|
|
14853
14999
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
14854
15000
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
14855
|
-
return { file, sha256:
|
|
15001
|
+
return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
|
|
14856
15002
|
})
|
|
14857
15003
|
);
|
|
14858
15004
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -14860,7 +15006,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14860
15006
|
const companion = await readHandEditCompanion(editPath);
|
|
14861
15007
|
if (companion) {
|
|
14862
15008
|
const projectPath = handEditProjectPath(editPath);
|
|
14863
|
-
const project = companion.project &&
|
|
15009
|
+
const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
|
|
14864
15010
|
let projectUrl = null;
|
|
14865
15011
|
if (project) {
|
|
14866
15012
|
const id = galleryMediaId(projectPath);
|
|
@@ -14896,8 +15042,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
14896
15042
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
14897
15043
|
}
|
|
14898
15044
|
} else if (spec.source) {
|
|
14899
|
-
outputs = [await describeOutput(media, root,
|
|
14900
|
-
} else if (!entry &&
|
|
15045
|
+
outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
|
|
15046
|
+
} else if (!entry && existsSync20(spec.outFile)) {
|
|
14901
15047
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
14902
15048
|
} else {
|
|
14903
15049
|
outputs = [];
|
|
@@ -14938,6 +15084,10 @@ async function buildGallerySnapshot(opts) {
|
|
|
14938
15084
|
revision: entry?.revision ?? null,
|
|
14939
15085
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
14940
15086
|
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
15087
|
+
skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
|
|
15088
|
+
keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
|
|
15089
|
+
set: spec.revision.skeleton ?? null
|
|
15090
|
+
} : null,
|
|
14941
15091
|
character: describeCharacter(spec, entry),
|
|
14942
15092
|
outputs,
|
|
14943
15093
|
quality,
|
|
@@ -15002,6 +15152,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15002
15152
|
revision: entry.revision,
|
|
15003
15153
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
15004
15154
|
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
15155
|
+
skeleton: null,
|
|
15005
15156
|
character: describeCharacter(void 0, entry),
|
|
15006
15157
|
outputs,
|
|
15007
15158
|
quality: null,
|
|
@@ -15092,7 +15243,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15092
15243
|
id: loaded.manifest.name,
|
|
15093
15244
|
name: loaded.manifest.name,
|
|
15094
15245
|
manifest: loaded.path,
|
|
15095
|
-
lock:
|
|
15246
|
+
lock: path29.resolve(opts.lockPath),
|
|
15096
15247
|
root,
|
|
15097
15248
|
provider: loaded.manifest.provider,
|
|
15098
15249
|
account: null,
|
|
@@ -15118,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15118
15269
|
return { snapshot, media };
|
|
15119
15270
|
}
|
|
15120
15271
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15121
|
-
const dir =
|
|
15272
|
+
const dir = path29.dirname(path29.resolve(opts.workspacePath));
|
|
15122
15273
|
const media = /* @__PURE__ */ new Map();
|
|
15123
15274
|
const projects = [];
|
|
15124
15275
|
const items = [];
|
|
@@ -15161,11 +15312,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15161
15312
|
id: project.id,
|
|
15162
15313
|
name: loaded.manifest.name,
|
|
15163
15314
|
manifest: manifestPath,
|
|
15164
|
-
lock:
|
|
15315
|
+
lock: path29.resolve(lockPath),
|
|
15165
15316
|
root: loaded.root,
|
|
15166
15317
|
provider: loaded.manifest.provider,
|
|
15167
15318
|
account: project.account ?? null,
|
|
15168
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
15319
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
|
|
15169
15320
|
historyLimit: historyLimit(loaded.manifest),
|
|
15170
15321
|
entries: Object.keys(lock.entries).length,
|
|
15171
15322
|
items: projectItems.length,
|
|
@@ -15177,8 +15328,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15177
15328
|
id: project.id,
|
|
15178
15329
|
name: project.id,
|
|
15179
15330
|
manifest: manifestPath,
|
|
15180
|
-
lock:
|
|
15181
|
-
root:
|
|
15331
|
+
lock: path29.resolve(lockPath),
|
|
15332
|
+
root: path29.dirname(manifestPath),
|
|
15182
15333
|
provider: project.provider,
|
|
15183
15334
|
account: project.account ?? null,
|
|
15184
15335
|
manifestSha256: null,
|
|
@@ -15205,7 +15356,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15205
15356
|
version: 1,
|
|
15206
15357
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15207
15358
|
project: null,
|
|
15208
|
-
workspace: { path:
|
|
15359
|
+
workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
|
|
15209
15360
|
filter,
|
|
15210
15361
|
totals: {
|
|
15211
15362
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15224,7 +15375,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15224
15375
|
// src/gallery/server.ts
|
|
15225
15376
|
import { randomBytes } from "crypto";
|
|
15226
15377
|
import { createServer as createServer2 } from "http";
|
|
15227
|
-
import { readFile as
|
|
15378
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
15228
15379
|
|
|
15229
15380
|
// src/gallery/page.ts
|
|
15230
15381
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -15334,7 +15485,7 @@ async function readJsonBody(req) {
|
|
|
15334
15485
|
async function serveGallery(opts) {
|
|
15335
15486
|
const log2 = opts.onProgress ?? (() => {
|
|
15336
15487
|
});
|
|
15337
|
-
const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
|
|
15488
|
+
const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
|
|
15338
15489
|
let media = /* @__PURE__ */ new Map();
|
|
15339
15490
|
let loading = null;
|
|
15340
15491
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -15409,6 +15560,17 @@ async function serveGallery(opts) {
|
|
|
15409
15560
|
}
|
|
15410
15561
|
return;
|
|
15411
15562
|
}
|
|
15563
|
+
if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
|
|
15564
|
+
if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
|
|
15565
|
+
try {
|
|
15566
|
+
const body = await guardedBody("skeleton estimates");
|
|
15567
|
+
if (body === void 0) return;
|
|
15568
|
+
json(200, await opts.skeleton.estimate(body));
|
|
15569
|
+
} catch (err) {
|
|
15570
|
+
reportError("skeleton estimate", err);
|
|
15571
|
+
}
|
|
15572
|
+
return;
|
|
15573
|
+
}
|
|
15412
15574
|
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
15413
15575
|
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
15414
15576
|
try {
|
|
@@ -15465,7 +15627,7 @@ async function serveGallery(opts) {
|
|
|
15465
15627
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
15466
15628
|
if (!asset) return fail(404, "no such review asset");
|
|
15467
15629
|
try {
|
|
15468
|
-
const bytes = await
|
|
15630
|
+
const bytes = await readFile19(asset.path);
|
|
15469
15631
|
res.writeHead(200, {
|
|
15470
15632
|
"Content-Type": asset.contentType,
|
|
15471
15633
|
"Cache-Control": "no-store",
|
|
@@ -15541,7 +15703,7 @@ async function serveGallery(opts) {
|
|
|
15541
15703
|
if (!asset) return fail(404, "no such gallery media");
|
|
15542
15704
|
let bytes;
|
|
15543
15705
|
try {
|
|
15544
|
-
bytes = await
|
|
15706
|
+
bytes = await readFile19(asset.path);
|
|
15545
15707
|
} catch {
|
|
15546
15708
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
15547
15709
|
}
|
|
@@ -15585,7 +15747,7 @@ async function serveGallery(opts) {
|
|
|
15585
15747
|
|
|
15586
15748
|
// src/gallery/generate.ts
|
|
15587
15749
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15588
|
-
import { z as
|
|
15750
|
+
import { z as z10 } from "zod";
|
|
15589
15751
|
|
|
15590
15752
|
// src/pipeline/poll.ts
|
|
15591
15753
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -15996,13 +16158,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
15996
16158
|
}
|
|
15997
16159
|
|
|
15998
16160
|
// src/pick/server.ts
|
|
15999
|
-
import { existsSync as
|
|
16000
|
-
import { readFile as
|
|
16161
|
+
import { existsSync as existsSync22 } from "fs";
|
|
16162
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
16001
16163
|
|
|
16002
16164
|
// src/pipeline/init.ts
|
|
16003
|
-
import { readdir, readFile as
|
|
16004
|
-
import { existsSync as
|
|
16005
|
-
import
|
|
16165
|
+
import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
|
|
16166
|
+
import { existsSync as existsSync21 } from "fs";
|
|
16167
|
+
import path30 from "path";
|
|
16006
16168
|
function pngSize(buf) {
|
|
16007
16169
|
if (buf.length < 24) return null;
|
|
16008
16170
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16016,7 +16178,7 @@ function slugify(value) {
|
|
|
16016
16178
|
async function walk(dir, exclude) {
|
|
16017
16179
|
const out = [];
|
|
16018
16180
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16019
|
-
const full =
|
|
16181
|
+
const full = path30.join(dir, entry.name);
|
|
16020
16182
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16021
16183
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16022
16184
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16030,14 +16192,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
16030
16192
|
const skipped = [];
|
|
16031
16193
|
const seen = /* @__PURE__ */ new Map();
|
|
16032
16194
|
for (const file of files.sort()) {
|
|
16033
|
-
const size = pngSize(await
|
|
16195
|
+
const size = pngSize(await readFile20(file));
|
|
16034
16196
|
if (!size) {
|
|
16035
16197
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16036
16198
|
continue;
|
|
16037
16199
|
}
|
|
16038
|
-
const rel =
|
|
16039
|
-
const category =
|
|
16040
|
-
let id = slugify(
|
|
16200
|
+
const rel = path30.relative(root, file);
|
|
16201
|
+
const category = path30.dirname(rel) === "." ? "" : path30.dirname(rel);
|
|
16202
|
+
let id = slugify(path30.basename(rel));
|
|
16041
16203
|
if (seen.has(id)) {
|
|
16042
16204
|
const n = seen.get(id) + 1;
|
|
16043
16205
|
seen.set(id, n);
|
|
@@ -16095,10 +16257,10 @@ function mode(values) {
|
|
|
16095
16257
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16096
16258
|
}
|
|
16097
16259
|
async function writeManifestFile(target, manifest) {
|
|
16098
|
-
if (
|
|
16260
|
+
if (existsSync21(target)) {
|
|
16099
16261
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16100
16262
|
}
|
|
16101
|
-
await
|
|
16263
|
+
await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16102
16264
|
}
|
|
16103
16265
|
|
|
16104
16266
|
// src/pick/sheet.ts
|
|
@@ -16511,13 +16673,13 @@ async function currentArt(entry, spec) {
|
|
|
16511
16673
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
16512
16674
|
if (!previous) return null;
|
|
16513
16675
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
16514
|
-
if (!
|
|
16676
|
+
if (!existsSync22(file)) return null;
|
|
16515
16677
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
16516
16678
|
if (!contentType) return null;
|
|
16517
16679
|
let width = entry.width;
|
|
16518
16680
|
let height = entry.height;
|
|
16519
16681
|
if (contentType === "image/png") {
|
|
16520
|
-
const size = pngSize(await
|
|
16682
|
+
const size = pngSize(await readFile21(file));
|
|
16521
16683
|
if (size) ({ width, height } = size);
|
|
16522
16684
|
}
|
|
16523
16685
|
return { path: file, contentType, width, height };
|
|
@@ -16685,26 +16847,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
16685
16847
|
}
|
|
16686
16848
|
|
|
16687
16849
|
// src/gallery/generate.ts
|
|
16688
|
-
var GenerateRequestSchema =
|
|
16689
|
-
project:
|
|
16690
|
-
keys:
|
|
16850
|
+
var GenerateRequestSchema = z10.object({
|
|
16851
|
+
project: z10.string().min(1).optional(),
|
|
16852
|
+
keys: z10.array(z10.string().min(1)).min(1).max(500),
|
|
16691
16853
|
/**
|
|
16692
16854
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
16693
16855
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
16694
16856
|
* the recorded bytes back over a modified file.
|
|
16695
16857
|
*/
|
|
16696
|
-
force:
|
|
16858
|
+
force: z10.boolean().optional(),
|
|
16697
16859
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
16698
|
-
resume:
|
|
16860
|
+
resume: z10.boolean().optional(),
|
|
16699
16861
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
16700
|
-
refresh:
|
|
16862
|
+
refresh: z10.boolean().optional(),
|
|
16701
16863
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
16702
|
-
restore:
|
|
16864
|
+
restore: z10.boolean().optional(),
|
|
16703
16865
|
/**
|
|
16704
16866
|
* Bring a replaced generation back: 1-based position in the record's
|
|
16705
16867
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
16706
16868
|
*/
|
|
16707
|
-
revert:
|
|
16869
|
+
revert: z10.string().min(1).max(64).optional()
|
|
16708
16870
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
16709
16871
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
16710
16872
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17014,15 +17176,15 @@ function createGenerateHandlers(opts) {
|
|
|
17014
17176
|
}
|
|
17015
17177
|
|
|
17016
17178
|
// src/gallery/editor.ts
|
|
17017
|
-
import { readFile as
|
|
17018
|
-
import
|
|
17179
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
17180
|
+
import path32 from "path";
|
|
17019
17181
|
|
|
17020
17182
|
// src/editor/install.ts
|
|
17021
17183
|
import { createHash as createHash4 } from "crypto";
|
|
17022
|
-
import { existsSync as
|
|
17023
|
-
import { mkdir as
|
|
17184
|
+
import { existsSync as existsSync23 } from "fs";
|
|
17185
|
+
import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
|
|
17024
17186
|
import os2 from "os";
|
|
17025
|
-
import
|
|
17187
|
+
import path31 from "path";
|
|
17026
17188
|
|
|
17027
17189
|
// tools/pixelorama-bridge/pin.json
|
|
17028
17190
|
var pin_default = {
|
|
@@ -17086,15 +17248,15 @@ var EditorInstallError = class extends Error {
|
|
|
17086
17248
|
};
|
|
17087
17249
|
function editorToolsRoot() {
|
|
17088
17250
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17089
|
-
if (override) return
|
|
17090
|
-
if (process.platform === "darwin") return
|
|
17251
|
+
if (override) return path31.resolve(override);
|
|
17252
|
+
if (process.platform === "darwin") return path31.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17091
17253
|
if (process.platform === "win32") {
|
|
17092
|
-
return
|
|
17254
|
+
return path31.join(process.env.LOCALAPPDATA ?? path31.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17093
17255
|
}
|
|
17094
|
-
return
|
|
17256
|
+
return path31.join(process.env.XDG_CACHE_HOME ?? path31.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17095
17257
|
}
|
|
17096
17258
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17097
|
-
return
|
|
17259
|
+
return path31.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17098
17260
|
}
|
|
17099
17261
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17100
17262
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17104,7 +17266,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17104
17266
|
}
|
|
17105
17267
|
async function sha256Of(file) {
|
|
17106
17268
|
try {
|
|
17107
|
-
return createHash4("sha256").update(await
|
|
17269
|
+
return createHash4("sha256").update(await readFile22(file)).digest("hex");
|
|
17108
17270
|
} catch {
|
|
17109
17271
|
return null;
|
|
17110
17272
|
}
|
|
@@ -17117,8 +17279,8 @@ async function editorStatus(opts = {}) {
|
|
|
17117
17279
|
let totalBytes = 0;
|
|
17118
17280
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17119
17281
|
totalBytes += expected.bytes;
|
|
17120
|
-
const file =
|
|
17121
|
-
const info =
|
|
17282
|
+
const file = path31.join(dir, name);
|
|
17283
|
+
const info = existsSync23(file) ? await stat3(file).catch(() => null) : null;
|
|
17122
17284
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17123
17285
|
installedBytes += expected.bytes;
|
|
17124
17286
|
} else {
|
|
@@ -17147,7 +17309,7 @@ async function installEditor(opts = {}) {
|
|
|
17147
17309
|
);
|
|
17148
17310
|
}
|
|
17149
17311
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17150
|
-
await
|
|
17312
|
+
await mkdir11(dir, { recursive: true });
|
|
17151
17313
|
const downloaded = [];
|
|
17152
17314
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17153
17315
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17175,13 +17337,13 @@ async function installEditor(opts = {}) {
|
|
|
17175
17337
|
if (digest2 !== expected.sha256) {
|
|
17176
17338
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17177
17339
|
}
|
|
17178
|
-
const final =
|
|
17340
|
+
const final = path31.join(dir, name);
|
|
17179
17341
|
const tmp = `${final}.${process.pid}.part`;
|
|
17180
17342
|
try {
|
|
17181
|
-
await
|
|
17343
|
+
await writeFile14(tmp, bytes);
|
|
17182
17344
|
await rename9(tmp, final);
|
|
17183
17345
|
} finally {
|
|
17184
|
-
await
|
|
17346
|
+
await rm10(tmp, { force: true });
|
|
17185
17347
|
}
|
|
17186
17348
|
fetchedBytes += bytes.length;
|
|
17187
17349
|
downloaded.push(name);
|
|
@@ -17221,7 +17383,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17221
17383
|
".json": "application/json; charset=utf-8"
|
|
17222
17384
|
};
|
|
17223
17385
|
function editorContentType(name) {
|
|
17224
|
-
return EDITOR_CONTENT_TYPES[
|
|
17386
|
+
return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
|
|
17225
17387
|
}
|
|
17226
17388
|
|
|
17227
17389
|
// src/gallery/editor.ts
|
|
@@ -17297,7 +17459,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17297
17459
|
if (!status.installed) return null;
|
|
17298
17460
|
let bytes;
|
|
17299
17461
|
try {
|
|
17300
|
-
bytes = await
|
|
17462
|
+
bytes = await readFile23(path32.join(dir, name));
|
|
17301
17463
|
} catch {
|
|
17302
17464
|
verified = null;
|
|
17303
17465
|
return null;
|
|
@@ -17337,13 +17499,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17337
17499
|
};
|
|
17338
17500
|
const loadProject = async (project) => {
|
|
17339
17501
|
const ctx = await access2.loadProject(project);
|
|
17340
|
-
const dir =
|
|
17502
|
+
const dir = path33.dirname(ctx.loaded.path);
|
|
17341
17503
|
const declared = readEnvFiles(dir);
|
|
17342
17504
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
17343
17505
|
for (const [name, value] of Object.entries(declared)) {
|
|
17344
17506
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
17345
17507
|
throw new Error(
|
|
17346
|
-
`${
|
|
17508
|
+
`${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
17509
|
);
|
|
17348
17510
|
}
|
|
17349
17511
|
}
|
|
@@ -17371,6 +17533,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17371
17533
|
),
|
|
17372
17534
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
17373
17535
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
17536
|
+
// Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
|
|
17537
|
+
...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, onProgress: log }) } : {},
|
|
17374
17538
|
// The editor exists to write hand edits back, so it follows the write gate.
|
|
17375
17539
|
...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
|
|
17376
17540
|
});
|
|
@@ -17388,8 +17552,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17388
17552
|
}
|
|
17389
17553
|
async function runGallery(args) {
|
|
17390
17554
|
if (args.workspace) {
|
|
17391
|
-
const workspacePath =
|
|
17392
|
-
if (!
|
|
17555
|
+
const workspacePath = path33.resolve(args.workspace);
|
|
17556
|
+
if (!existsSync24(workspacePath)) {
|
|
17393
17557
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
17394
17558
|
}
|
|
17395
17559
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -17409,7 +17573,7 @@ async function runGallery(args) {
|
|
|
17409
17573
|
const registered = async (projectId) => {
|
|
17410
17574
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
17411
17575
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
17412
|
-
return resolveProject(
|
|
17576
|
+
return resolveProject(path33.dirname(workspacePath), project);
|
|
17413
17577
|
};
|
|
17414
17578
|
await serveUntilStopped(initial, build2, args, {
|
|
17415
17579
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -17434,7 +17598,7 @@ async function runGallery(args) {
|
|
|
17434
17598
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
17435
17599
|
};
|
|
17436
17600
|
await serveUntilStopped(await build(), reload, args, {
|
|
17437
|
-
manifestFor: () =>
|
|
17601
|
+
manifestFor: () => path33.resolve(args.manifest),
|
|
17438
17602
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
17439
17603
|
});
|
|
17440
17604
|
}
|
|
@@ -17743,18 +17907,18 @@ async function runRestoreGeneration(args) {
|
|
|
17743
17907
|
}
|
|
17744
17908
|
|
|
17745
17909
|
// src/cli/commands/housekeeping.ts
|
|
17746
|
-
import { existsSync as
|
|
17910
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17747
17911
|
|
|
17748
17912
|
// src/pipeline/cache-health.ts
|
|
17749
|
-
import { existsSync as
|
|
17750
|
-
import { readFile as
|
|
17751
|
-
import
|
|
17913
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17914
|
+
import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
|
|
17915
|
+
import path34 from "path";
|
|
17752
17916
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
17753
|
-
if (options.prune && !
|
|
17754
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
17917
|
+
if (options.prune && !existsSync25(lockPath)) {
|
|
17918
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
|
|
17755
17919
|
}
|
|
17756
|
-
const contentDir =
|
|
17757
|
-
const remotePath =
|
|
17920
|
+
const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
|
|
17921
|
+
const remotePath = path34.resolve(cachePathFor(lockPath));
|
|
17758
17922
|
const referenced = referencedHashes(lock);
|
|
17759
17923
|
let content = await inspectContentCache(contentDir, referenced);
|
|
17760
17924
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -17766,7 +17930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17766
17930
|
]);
|
|
17767
17931
|
for (const name of names) {
|
|
17768
17932
|
try {
|
|
17769
|
-
await
|
|
17933
|
+
await rm11(path34.join(contentDir, name), { force: true });
|
|
17770
17934
|
removed.contentFiles++;
|
|
17771
17935
|
} catch {
|
|
17772
17936
|
}
|
|
@@ -17775,7 +17939,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17775
17939
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
17776
17940
|
removed.resetRemoteHashCache = true;
|
|
17777
17941
|
} else if (remoteHashes.invalidIds.length) {
|
|
17778
|
-
const cache = parseCache(JSON.parse(await
|
|
17942
|
+
const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17779
17943
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
17780
17944
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
17781
17945
|
await saveCache(remotePath, cache);
|
|
@@ -17793,7 +17957,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17793
17957
|
async function inspectContentCache(contentDir, referenced) {
|
|
17794
17958
|
const report = {
|
|
17795
17959
|
path: contentDir,
|
|
17796
|
-
exists:
|
|
17960
|
+
exists: existsSync25(contentDir),
|
|
17797
17961
|
files: 0,
|
|
17798
17962
|
bytes: 0,
|
|
17799
17963
|
valid: 0,
|
|
@@ -17814,12 +17978,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17814
17978
|
continue;
|
|
17815
17979
|
}
|
|
17816
17980
|
report.files++;
|
|
17817
|
-
const file =
|
|
17981
|
+
const file = path34.join(contentDir, entry.name);
|
|
17818
17982
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
17819
17983
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
17820
17984
|
let bytes;
|
|
17821
17985
|
try {
|
|
17822
|
-
bytes = await
|
|
17986
|
+
bytes = await readFile24(file);
|
|
17823
17987
|
report.bytes += bytes.length;
|
|
17824
17988
|
} catch (err) {
|
|
17825
17989
|
report.invalid.push({
|
|
@@ -17857,7 +18021,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17857
18021
|
async function inspectRemoteHashCache(remotePath) {
|
|
17858
18022
|
const report = {
|
|
17859
18023
|
path: remotePath,
|
|
17860
|
-
exists:
|
|
18024
|
+
exists: existsSync25(remotePath),
|
|
17861
18025
|
entries: 0,
|
|
17862
18026
|
valid: 0,
|
|
17863
18027
|
invalidIds: [],
|
|
@@ -17866,7 +18030,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
17866
18030
|
if (!report.exists) return report;
|
|
17867
18031
|
let cache;
|
|
17868
18032
|
try {
|
|
17869
|
-
cache = parseCache(JSON.parse(await
|
|
18033
|
+
cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17870
18034
|
} catch (err) {
|
|
17871
18035
|
report.error = err instanceof Error ? err.message : String(err);
|
|
17872
18036
|
return report;
|
|
@@ -17930,7 +18094,7 @@ async function runAccept(args) {
|
|
|
17930
18094
|
let intact = true;
|
|
17931
18095
|
for (const output of entry.outputs) {
|
|
17932
18096
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
17933
|
-
if (!
|
|
18097
|
+
if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
|
|
17934
18098
|
intact = false;
|
|
17935
18099
|
break;
|
|
17936
18100
|
}
|
|
@@ -17985,42 +18149,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
17985
18149
|
}
|
|
17986
18150
|
|
|
17987
18151
|
// src/cli/commands/init.ts
|
|
17988
|
-
import
|
|
17989
|
-
import { existsSync as
|
|
18152
|
+
import path35 from "path";
|
|
18153
|
+
import { existsSync as existsSync27 } from "fs";
|
|
17990
18154
|
async function runInit(args) {
|
|
17991
18155
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
17992
|
-
const root =
|
|
17993
|
-
if (!
|
|
18156
|
+
const root = path35.resolve(args.from);
|
|
18157
|
+
if (!existsSync27(root)) throw new Error(`No directory at ${root}`);
|
|
17994
18158
|
const generator = args.generator ?? "map";
|
|
17995
18159
|
if (generator !== "1dir" && generator !== "map") {
|
|
17996
18160
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
17997
18161
|
}
|
|
17998
|
-
const target =
|
|
18162
|
+
const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
17999
18163
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18000
18164
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18001
18165
|
const manifest = buildManifest(
|
|
18002
|
-
args.name ??
|
|
18166
|
+
args.name ?? path35.basename(path35.dirname(target)),
|
|
18003
18167
|
args.styles[0] ?? "base",
|
|
18004
18168
|
generator,
|
|
18005
|
-
|
|
18169
|
+
path35.relative(path35.dirname(target), root) || ".",
|
|
18006
18170
|
assets
|
|
18007
18171
|
);
|
|
18008
18172
|
await writeManifestFile(target, manifest);
|
|
18009
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
18173
|
+
log(` scanned ${assets.length} PNG(s) under ${path35.relative(process.cwd(), root)}`);
|
|
18010
18174
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18011
|
-
log(` wrote ${
|
|
18175
|
+
log(` wrote ${path35.relative(process.cwd(), target)}`);
|
|
18012
18176
|
log(`
|
|
18013
18177
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18014
18178
|
log(` PixelLab account instead of inventing them:`);
|
|
18015
18179
|
log(`
|
|
18016
|
-
pixelkiln adopt --manifest ${
|
|
18180
|
+
pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
|
|
18017
18181
|
`);
|
|
18018
18182
|
}
|
|
18019
18183
|
|
|
18020
18184
|
// src/pipeline/doctor.ts
|
|
18021
|
-
import { constants, existsSync as
|
|
18185
|
+
import { constants, existsSync as existsSync28 } from "fs";
|
|
18022
18186
|
import { access, stat as stat4 } from "fs/promises";
|
|
18023
|
-
import
|
|
18187
|
+
import path36 from "path";
|
|
18024
18188
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18025
18189
|
const checks = [];
|
|
18026
18190
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18031,8 +18195,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18031
18195
|
);
|
|
18032
18196
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18033
18197
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18034
|
-
|
|
18035
|
-
...spec.quality ? [
|
|
18198
|
+
path36.dirname(spec.outFile),
|
|
18199
|
+
...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
|
|
18036
18200
|
]))];
|
|
18037
18201
|
const unwritable = [];
|
|
18038
18202
|
for (const dir of dirs) {
|
|
@@ -18055,7 +18219,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18055
18219
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18056
18220
|
const output = entry.outputs[index];
|
|
18057
18221
|
const spec = specByKey.get(key);
|
|
18058
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
18222
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path36.dirname(lockPath));
|
|
18059
18223
|
const owner = outputOwners.get(absolute);
|
|
18060
18224
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18061
18225
|
outputOwners.set(absolute, key);
|
|
@@ -18070,7 +18234,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18070
18234
|
} else {
|
|
18071
18235
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18072
18236
|
}
|
|
18073
|
-
const cacheDir =
|
|
18237
|
+
const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
|
|
18074
18238
|
const stranded = [];
|
|
18075
18239
|
const invalidState = [];
|
|
18076
18240
|
const staleMapUrls = [];
|
|
@@ -18086,11 +18250,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18086
18250
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18087
18251
|
let hasCache = false;
|
|
18088
18252
|
for (const output of entry.outputs) {
|
|
18089
|
-
const cached =
|
|
18253
|
+
const cached = path36.join(
|
|
18090
18254
|
cacheDir,
|
|
18091
18255
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18092
18256
|
);
|
|
18093
|
-
if (
|
|
18257
|
+
if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
|
|
18094
18258
|
hasCache = true;
|
|
18095
18259
|
break;
|
|
18096
18260
|
}
|
|
@@ -18186,16 +18350,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18186
18350
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18187
18351
|
} catch {
|
|
18188
18352
|
}
|
|
18189
|
-
const parent =
|
|
18353
|
+
const parent = path36.dirname(current);
|
|
18190
18354
|
if (parent === current) return current;
|
|
18191
18355
|
current = parent;
|
|
18192
18356
|
}
|
|
18193
18357
|
}
|
|
18194
18358
|
|
|
18195
18359
|
// src/pipeline/audit.ts
|
|
18196
|
-
import { readFile as
|
|
18197
|
-
import { existsSync as
|
|
18198
|
-
import
|
|
18360
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18361
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18362
|
+
import path37 from "path";
|
|
18199
18363
|
function paletteDistance(asset, reference) {
|
|
18200
18364
|
if (!asset.length || !reference.length) return 0;
|
|
18201
18365
|
let total = 0;
|
|
@@ -18231,12 +18395,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18231
18395
|
const unreadable = [];
|
|
18232
18396
|
for (const spec of mine) {
|
|
18233
18397
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18234
|
-
if (!
|
|
18398
|
+
if (!existsSync29(output.absolutePath)) {
|
|
18235
18399
|
missing.push(output.id);
|
|
18236
18400
|
continue;
|
|
18237
18401
|
}
|
|
18238
18402
|
try {
|
|
18239
|
-
const png = decodePng(await
|
|
18403
|
+
const png = decodePng(await readFile25(output.absolutePath));
|
|
18240
18404
|
const palette = extractPalette(png, 12);
|
|
18241
18405
|
assets.push({
|
|
18242
18406
|
assetId: spec.assetId,
|
|
@@ -18260,10 +18424,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18260
18424
|
let referenceFromStyleImages = false;
|
|
18261
18425
|
const refPalettes = [];
|
|
18262
18426
|
for (const img of style.styleImages) {
|
|
18263
|
-
const abs =
|
|
18264
|
-
if (!
|
|
18427
|
+
const abs = path37.resolve(loaded.root, img.path);
|
|
18428
|
+
if (!existsSync29(abs)) continue;
|
|
18265
18429
|
try {
|
|
18266
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
18430
|
+
refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
|
|
18267
18431
|
} catch {
|
|
18268
18432
|
}
|
|
18269
18433
|
}
|
|
@@ -18491,24 +18655,24 @@ async function runAudit(args) {
|
|
|
18491
18655
|
}
|
|
18492
18656
|
|
|
18493
18657
|
// src/cli/commands/pack.ts
|
|
18494
|
-
import
|
|
18495
|
-
import { readFile as
|
|
18658
|
+
import path39 from "path";
|
|
18659
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
18496
18660
|
|
|
18497
18661
|
// src/pipeline/pack.ts
|
|
18498
18662
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18499
|
-
import
|
|
18663
|
+
import path38 from "path";
|
|
18500
18664
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
18501
18665
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
18502
18666
|
if (!Array.isArray(raw) || !raw.length) {
|
|
18503
18667
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
18504
18668
|
}
|
|
18505
|
-
const baseDir =
|
|
18669
|
+
const baseDir = path38.dirname(path38.resolve(inputsFilePath));
|
|
18506
18670
|
return raw.map((entry, i) => {
|
|
18507
18671
|
const e = entry;
|
|
18508
18672
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
18509
18673
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
18510
18674
|
}
|
|
18511
|
-
return { id: e.id, path:
|
|
18675
|
+
return { id: e.id, path: path38.resolve(baseDir, e.path) };
|
|
18512
18676
|
});
|
|
18513
18677
|
}
|
|
18514
18678
|
function packSprites(inputs, options = {}) {
|
|
@@ -18592,7 +18756,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18592
18756
|
}
|
|
18593
18757
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
18594
18758
|
id,
|
|
18595
|
-
path:
|
|
18759
|
+
path: path38.resolve(source)
|
|
18596
18760
|
}));
|
|
18597
18761
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
18598
18762
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -18616,7 +18780,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18616
18780
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
18617
18781
|
const source = sources[id];
|
|
18618
18782
|
if (source && outputs.length <= 1) {
|
|
18619
|
-
inputs.push({ id, path:
|
|
18783
|
+
inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18620
18784
|
continue;
|
|
18621
18785
|
}
|
|
18622
18786
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -18650,7 +18814,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18650
18814
|
}
|
|
18651
18815
|
}
|
|
18652
18816
|
for (const [id, source] of Object.entries(sources)) {
|
|
18653
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
18817
|
+
if (!locked.has(id)) inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18654
18818
|
}
|
|
18655
18819
|
if (!inputs.length) {
|
|
18656
18820
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -18758,7 +18922,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18758
18922
|
const source = sources[id];
|
|
18759
18923
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
18760
18924
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
18761
|
-
placements.push({ id, path:
|
|
18925
|
+
placements.push({ id, path: path38.resolve(manifestDir, source), cell });
|
|
18762
18926
|
continue;
|
|
18763
18927
|
}
|
|
18764
18928
|
if (source && entry) {
|
|
@@ -18791,7 +18955,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18791
18955
|
`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
18956
|
);
|
|
18793
18957
|
}
|
|
18794
|
-
const basePng = mount.base ? readFileSync5(
|
|
18958
|
+
const basePng = mount.base ? readFileSync5(path38.resolve(manifestDir, mount.base)) : void 0;
|
|
18795
18959
|
const mounted = mountSprites(placements, {
|
|
18796
18960
|
cellWidth: mount.cellWidth,
|
|
18797
18961
|
cellHeight: mount.cellHeight,
|
|
@@ -18801,7 +18965,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18801
18965
|
...mounted,
|
|
18802
18966
|
sources: mount.base ? [{
|
|
18803
18967
|
id: "$base",
|
|
18804
|
-
path:
|
|
18968
|
+
path: path38.resolve(manifestDir, mount.base),
|
|
18805
18969
|
sha256: sha256(basePng),
|
|
18806
18970
|
included: true
|
|
18807
18971
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19205,12 +19369,12 @@ async function runPack(args) {
|
|
|
19205
19369
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19206
19370
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19207
19371
|
}
|
|
19208
|
-
const raw = JSON.parse(await
|
|
19372
|
+
const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
|
|
19209
19373
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19210
19374
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19211
19375
|
const format = sheetFormat(args);
|
|
19212
|
-
const base =
|
|
19213
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19376
|
+
const base = path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
19377
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
|
|
19214
19378
|
const outputs = [
|
|
19215
19379
|
{ path: `${base}.png`, data: png },
|
|
19216
19380
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19223,7 +19387,7 @@ async function runPack(args) {
|
|
|
19223
19387
|
log(
|
|
19224
19388
|
` ${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
19389
|
);
|
|
19226
|
-
log(` ${
|
|
19390
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19227
19391
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19228
19392
|
return;
|
|
19229
19393
|
}
|
|
@@ -19231,7 +19395,7 @@ async function runPack(args) {
|
|
|
19231
19395
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19232
19396
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19233
19397
|
}
|
|
19234
|
-
const manifestDir =
|
|
19398
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19235
19399
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19236
19400
|
for (const styleId of styleIds) {
|
|
19237
19401
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19246,9 +19410,9 @@ async function runPack(args) {
|
|
|
19246
19410
|
sourceOverrides: qualitySources,
|
|
19247
19411
|
sources: manifestSources(loaded.manifest, styleId)
|
|
19248
19412
|
});
|
|
19249
|
-
const base = args.out ?
|
|
19413
|
+
const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path39.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
19250
19414
|
const format = sheetFormat(args);
|
|
19251
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19415
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
|
|
19252
19416
|
const outputs = [
|
|
19253
19417
|
{ path: `${base}.png`, data: png },
|
|
19254
19418
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19280,13 +19444,13 @@ async function runPack(args) {
|
|
|
19280
19444
|
log(
|
|
19281
19445
|
` ${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
19446
|
);
|
|
19283
|
-
log(` ${
|
|
19447
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19284
19448
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19285
19449
|
}
|
|
19286
19450
|
}
|
|
19287
19451
|
async function runMount(args) {
|
|
19288
19452
|
const { loaded, lock } = await openProject2(args);
|
|
19289
|
-
const manifestDir =
|
|
19453
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19290
19454
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19291
19455
|
for (const styleId of styleIds) {
|
|
19292
19456
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19325,9 +19489,9 @@ async function runMount(args) {
|
|
|
19325
19489
|
sources,
|
|
19326
19490
|
outputRoles
|
|
19327
19491
|
);
|
|
19328
|
-
const out =
|
|
19492
|
+
const out = path39.resolve(manifestDir, style.mount.out);
|
|
19329
19493
|
const format = sheetFormat(args);
|
|
19330
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19494
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
|
|
19331
19495
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
19332
19496
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
19333
19497
|
const outputs = [
|
|
@@ -19347,7 +19511,7 @@ async function runMount(args) {
|
|
|
19347
19511
|
await provenanceFile("$lock", args.lock),
|
|
19348
19512
|
...qualityRecords,
|
|
19349
19513
|
...artifactSources.filter(
|
|
19350
|
-
(source) => source.id !== "$base" ||
|
|
19514
|
+
(source) => source.id !== "$base" || path39.resolve(source.path) !== out
|
|
19351
19515
|
)
|
|
19352
19516
|
],
|
|
19353
19517
|
options: {
|
|
@@ -19361,14 +19525,14 @@ async function runMount(args) {
|
|
|
19361
19525
|
log(
|
|
19362
19526
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
19363
19527
|
);
|
|
19364
|
-
log(` ${
|
|
19528
|
+
log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
19365
19529
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19366
19530
|
}
|
|
19367
19531
|
}
|
|
19368
19532
|
async function runExport(args) {
|
|
19369
19533
|
const { loaded, specs, lock } = await openProject2(args);
|
|
19370
19534
|
const format = args.format ?? "generic";
|
|
19371
|
-
const manifestDir =
|
|
19535
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19372
19536
|
const selected = specs.filter((spec) => {
|
|
19373
19537
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
19374
19538
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -19384,12 +19548,12 @@ async function runExport(args) {
|
|
|
19384
19548
|
for (const spec of selected) {
|
|
19385
19549
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
19386
19550
|
const style = loaded.manifest.styles[spec.styleId];
|
|
19387
|
-
const defaultBase =
|
|
19388
|
-
const base = args.out ?
|
|
19551
|
+
const defaultBase = path39.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
19552
|
+
const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
19389
19553
|
const result = exportTileset(entry, spec, {
|
|
19390
19554
|
format,
|
|
19391
19555
|
manifestDir,
|
|
19392
|
-
imageName:
|
|
19556
|
+
imageName: path39.basename(`${base}.png`),
|
|
19393
19557
|
columns: args.columns
|
|
19394
19558
|
});
|
|
19395
19559
|
const outputs = [
|
|
@@ -19407,7 +19571,7 @@ async function runExport(args) {
|
|
|
19407
19571
|
asset: spec.assetId,
|
|
19408
19572
|
columns: args.columns ?? null,
|
|
19409
19573
|
format,
|
|
19410
|
-
image:
|
|
19574
|
+
image: path39.basename(`${base}.png`),
|
|
19411
19575
|
providerRules: result.generic.providerRules,
|
|
19412
19576
|
style: spec.styleId,
|
|
19413
19577
|
tileType: spec.tileType ?? null
|
|
@@ -19417,7 +19581,7 @@ async function runExport(args) {
|
|
|
19417
19581
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
19418
19582
|
);
|
|
19419
19583
|
log(
|
|
19420
|
-
` ${
|
|
19584
|
+
` ${path39.relative(process.cwd(), base)}.png + ${path39.basename(base)}${result.extension} + .pixelkiln.json`
|
|
19421
19585
|
);
|
|
19422
19586
|
}
|
|
19423
19587
|
}
|
|
@@ -19430,23 +19594,23 @@ function sheetFormat(args) {
|
|
|
19430
19594
|
}
|
|
19431
19595
|
|
|
19432
19596
|
// src/cli/commands/pixellab-utils.ts
|
|
19433
|
-
import
|
|
19597
|
+
import path41 from "path";
|
|
19434
19598
|
|
|
19435
19599
|
// src/pixellab-utils.ts
|
|
19436
|
-
import { existsSync as
|
|
19437
|
-
import { mkdir as
|
|
19438
|
-
import
|
|
19600
|
+
import { existsSync as existsSync30 } from "fs";
|
|
19601
|
+
import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
|
|
19602
|
+
import path40 from "path";
|
|
19439
19603
|
var UNZOOM_MIN_SIDE = 256;
|
|
19440
19604
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
19441
19605
|
function defaultUnzoomOut(input) {
|
|
19442
|
-
const ext =
|
|
19606
|
+
const ext = path40.extname(input);
|
|
19443
19607
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
19444
19608
|
}
|
|
19445
19609
|
async function unzoomFile(client2, input, opts = {}) {
|
|
19446
|
-
const out =
|
|
19447
|
-
if (
|
|
19448
|
-
if (
|
|
19449
|
-
const bytes = await
|
|
19610
|
+
const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
19611
|
+
if (path40.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
19612
|
+
if (existsSync30(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
19613
|
+
const bytes = await readFile27(input);
|
|
19450
19614
|
const metadata = imageMetadata(bytes);
|
|
19451
19615
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
19452
19616
|
const { width, height, format } = metadata;
|
|
@@ -19459,22 +19623,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
19459
19623
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
19460
19624
|
}
|
|
19461
19625
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
19462
|
-
await
|
|
19463
|
-
await
|
|
19626
|
+
await mkdir12(path40.dirname(out), { recursive: true });
|
|
19627
|
+
await writeFile15(out, res.png);
|
|
19464
19628
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
19465
19629
|
}
|
|
19466
19630
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
19467
19631
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
19468
19632
|
var FONT_COST_GENERATIONS = 25;
|
|
19469
19633
|
function fontOutputPaths(out) {
|
|
19470
|
-
const base =
|
|
19634
|
+
const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
19471
19635
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
19472
19636
|
}
|
|
19473
19637
|
async function generateFont(client2, opts) {
|
|
19474
19638
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
19475
19639
|
if (!opts.force) {
|
|
19476
19640
|
for (const file of [ttf, atlas]) {
|
|
19477
|
-
if (
|
|
19641
|
+
if (existsSync30(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
19478
19642
|
}
|
|
19479
19643
|
}
|
|
19480
19644
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -19501,9 +19665,9 @@ async function generateFont(client2, opts) {
|
|
|
19501
19665
|
client2.download(job.download_ttf_url),
|
|
19502
19666
|
client2.download(job.download_atlas_url)
|
|
19503
19667
|
]);
|
|
19504
|
-
await
|
|
19505
|
-
await
|
|
19506
|
-
await
|
|
19668
|
+
await mkdir12(path40.dirname(ttf), { recursive: true });
|
|
19669
|
+
await writeFile15(ttf, ttfBytes);
|
|
19670
|
+
await writeFile15(atlas, atlasBytes);
|
|
19507
19671
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
19508
19672
|
}
|
|
19509
19673
|
if (Date.now() - started > timeout) {
|
|
@@ -19517,7 +19681,7 @@ async function generateFont(client2, opts) {
|
|
|
19517
19681
|
|
|
19518
19682
|
// src/cli/commands/pixellab-utils.ts
|
|
19519
19683
|
function loadKeys(args) {
|
|
19520
|
-
loadEnvFiles(
|
|
19684
|
+
loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
|
|
19521
19685
|
loadEnvFiles(process.cwd());
|
|
19522
19686
|
}
|
|
19523
19687
|
async function runUnzoom(args) {
|
|
@@ -19583,19 +19747,19 @@ async function runFont(args) {
|
|
|
19583
19747
|
}
|
|
19584
19748
|
|
|
19585
19749
|
// src/cli/commands/quality.ts
|
|
19586
|
-
import
|
|
19587
|
-
import { readFile as
|
|
19750
|
+
import path43 from "path";
|
|
19751
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
19588
19752
|
|
|
19589
19753
|
// src/pipeline/quality-regression.ts
|
|
19590
|
-
import
|
|
19591
|
-
import { existsSync as
|
|
19592
|
-
import { readFile as
|
|
19593
|
-
import { z as
|
|
19754
|
+
import path42 from "path";
|
|
19755
|
+
import { existsSync as existsSync31 } from "fs";
|
|
19756
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
19757
|
+
import { z as z11 } from "zod";
|
|
19594
19758
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19595
19759
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
19596
19760
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
19597
|
-
var QualityPathSchema =
|
|
19598
|
-
(value) => !
|
|
19761
|
+
var QualityPathSchema = z11.string().min(1).refine(
|
|
19762
|
+
(value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
|
|
19599
19763
|
"expected a portable relative path"
|
|
19600
19764
|
);
|
|
19601
19765
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -19608,53 +19772,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
19608
19772
|
maxEdgeContrastDrop: 0.03,
|
|
19609
19773
|
maxIsolatedPixelIncrease: 5e-3
|
|
19610
19774
|
};
|
|
19611
|
-
var QualityToleranceSchema =
|
|
19612
|
-
requireExactHash:
|
|
19613
|
-
maxColorCountIncrease:
|
|
19614
|
-
maxNewColors:
|
|
19615
|
-
maxTransparencyDelta:
|
|
19616
|
-
maxPartialAlphaIncrease:
|
|
19617
|
-
maxEdgeDensityDelta:
|
|
19618
|
-
maxEdgeContrastDrop:
|
|
19619
|
-
maxIsolatedPixelIncrease:
|
|
19775
|
+
var QualityToleranceSchema = z11.object({
|
|
19776
|
+
requireExactHash: z11.boolean(),
|
|
19777
|
+
maxColorCountIncrease: z11.number().int().min(0),
|
|
19778
|
+
maxNewColors: z11.number().int().min(0),
|
|
19779
|
+
maxTransparencyDelta: z11.number().min(0).max(1),
|
|
19780
|
+
maxPartialAlphaIncrease: z11.number().min(0).max(1),
|
|
19781
|
+
maxEdgeDensityDelta: z11.number().min(0).max(1),
|
|
19782
|
+
maxEdgeContrastDrop: z11.number().min(0).max(1),
|
|
19783
|
+
maxIsolatedPixelIncrease: z11.number().min(0).max(1)
|
|
19620
19784
|
}).strict();
|
|
19621
|
-
var ImageQualityMetricsSchema =
|
|
19622
|
-
sha256:
|
|
19623
|
-
width:
|
|
19624
|
-
height:
|
|
19625
|
-
colorCount:
|
|
19626
|
-
colors:
|
|
19627
|
-
transparency:
|
|
19628
|
-
partialAlpha:
|
|
19629
|
-
edgeDensity:
|
|
19630
|
-
meanEdgeContrast:
|
|
19631
|
-
isolatedPixelRatio:
|
|
19785
|
+
var ImageQualityMetricsSchema = z11.object({
|
|
19786
|
+
sha256: z11.string().regex(SHA256_RE),
|
|
19787
|
+
width: z11.number().int().min(1),
|
|
19788
|
+
height: z11.number().int().min(1),
|
|
19789
|
+
colorCount: z11.number().int().min(0),
|
|
19790
|
+
colors: z11.array(z11.string().regex(HEX_RE)),
|
|
19791
|
+
transparency: z11.number().min(0).max(1),
|
|
19792
|
+
partialAlpha: z11.number().min(0).max(1),
|
|
19793
|
+
edgeDensity: z11.number().min(0).max(1),
|
|
19794
|
+
meanEdgeContrast: z11.number().min(0).max(1),
|
|
19795
|
+
isolatedPixelRatio: z11.number().min(0).max(1)
|
|
19632
19796
|
}).strict();
|
|
19633
|
-
var QualityBaselineCaseSchema =
|
|
19634
|
-
id:
|
|
19797
|
+
var QualityBaselineCaseSchema = z11.object({
|
|
19798
|
+
id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19635
19799
|
file: QualityPathSchema,
|
|
19636
|
-
record:
|
|
19800
|
+
record: z11.object({
|
|
19637
19801
|
path: QualityPathSchema,
|
|
19638
|
-
sha256:
|
|
19802
|
+
sha256: z11.string().regex(SHA256_RE)
|
|
19639
19803
|
}).strict().optional(),
|
|
19640
19804
|
expected: ImageQualityMetricsSchema,
|
|
19641
19805
|
tolerances: QualityToleranceSchema
|
|
19642
19806
|
}).strict();
|
|
19643
|
-
var QualityBaselineSchema =
|
|
19644
|
-
$schema:
|
|
19645
|
-
format:
|
|
19646
|
-
schemaVersion:
|
|
19647
|
-
cases:
|
|
19807
|
+
var QualityBaselineSchema = z11.object({
|
|
19808
|
+
$schema: z11.string().url().optional(),
|
|
19809
|
+
format: z11.literal("pixelkiln-quality-baseline"),
|
|
19810
|
+
schemaVersion: z11.literal(1),
|
|
19811
|
+
cases: z11.array(QualityBaselineCaseSchema).min(1)
|
|
19648
19812
|
}).strict().superRefine((baseline, context) => {
|
|
19649
19813
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
19650
19814
|
if (new Set(ids).size !== ids.length) {
|
|
19651
|
-
context.addIssue({ code:
|
|
19815
|
+
context.addIssue({ code: z11.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
19652
19816
|
}
|
|
19653
19817
|
});
|
|
19654
|
-
var QualityInputSchema =
|
|
19655
|
-
id:
|
|
19656
|
-
path:
|
|
19657
|
-
record:
|
|
19818
|
+
var QualityInputSchema = z11.object({
|
|
19819
|
+
id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19820
|
+
path: z11.string().min(1),
|
|
19821
|
+
record: z11.string().min(1).optional(),
|
|
19658
19822
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
19659
19823
|
}).strict();
|
|
19660
19824
|
function message4(error) {
|
|
@@ -19742,10 +19906,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
19742
19906
|
};
|
|
19743
19907
|
}
|
|
19744
19908
|
async function measureImageQuality(file) {
|
|
19745
|
-
const absolute =
|
|
19909
|
+
const absolute = path42.resolve(file);
|
|
19746
19910
|
let bytes;
|
|
19747
19911
|
try {
|
|
19748
|
-
bytes = await
|
|
19912
|
+
bytes = await readFile28(absolute);
|
|
19749
19913
|
} catch (error) {
|
|
19750
19914
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
19751
19915
|
}
|
|
@@ -19756,7 +19920,7 @@ async function measureImageQuality(file) {
|
|
|
19756
19920
|
}
|
|
19757
19921
|
}
|
|
19758
19922
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
19759
|
-
const parsed =
|
|
19923
|
+
const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
|
|
19760
19924
|
if (!parsed.success) {
|
|
19761
19925
|
throw new Error(
|
|
19762
19926
|
"--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 +19928,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19764
19928
|
}
|
|
19765
19929
|
const ids = parsed.data.map((entry) => entry.id);
|
|
19766
19930
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
19767
|
-
const root =
|
|
19931
|
+
const root = path42.dirname(path42.resolve(inputsFilePath));
|
|
19768
19932
|
return parsed.data.map((entry) => ({
|
|
19769
19933
|
id: entry.id,
|
|
19770
|
-
file:
|
|
19771
|
-
...entry.record ? { record:
|
|
19934
|
+
file: path42.resolve(root, entry.path),
|
|
19935
|
+
...entry.record ? { record: path42.resolve(root, entry.record) } : {},
|
|
19772
19936
|
tolerances: QualityToleranceSchema.parse({
|
|
19773
19937
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
19774
19938
|
...entry.tolerances
|
|
@@ -19776,7 +19940,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19776
19940
|
}));
|
|
19777
19941
|
}
|
|
19778
19942
|
function portableRelative2(from, to) {
|
|
19779
|
-
return
|
|
19943
|
+
return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
|
|
19780
19944
|
}
|
|
19781
19945
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
19782
19946
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -19791,20 +19955,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
19791
19955
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
19792
19956
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
19793
19957
|
}
|
|
19794
|
-
const outputs = record.outputs.map((output) =>
|
|
19795
|
-
if (!outputs.includes(
|
|
19958
|
+
const outputs = record.outputs.map((output) => path42.resolve(path42.dirname(recordPath), output.path));
|
|
19959
|
+
if (!outputs.includes(path42.resolve(imagePath))) {
|
|
19796
19960
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
19797
19961
|
}
|
|
19798
19962
|
}
|
|
19799
19963
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
19800
19964
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
19801
|
-
const absolute =
|
|
19965
|
+
const absolute = path42.resolve(baselinePath);
|
|
19802
19966
|
for (const input of inputs) {
|
|
19803
|
-
if (
|
|
19967
|
+
if (path42.resolve(input.file) === absolute || input.record && path42.resolve(input.record) === absolute) {
|
|
19804
19968
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
19805
19969
|
}
|
|
19806
19970
|
}
|
|
19807
|
-
const root =
|
|
19971
|
+
const root = path42.dirname(absolute);
|
|
19808
19972
|
const cases = [];
|
|
19809
19973
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
19810
19974
|
const expected = await measureImageQuality(input.file);
|
|
@@ -19831,8 +19995,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19831
19995
|
cases
|
|
19832
19996
|
});
|
|
19833
19997
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
19834
|
-
if (
|
|
19835
|
-
const current = await
|
|
19998
|
+
if (existsSync31(absolute) && !options.force) {
|
|
19999
|
+
const current = await readFile28(absolute);
|
|
19836
20000
|
if (!current.equals(data)) {
|
|
19837
20001
|
throw new OverwriteRefusedError(
|
|
19838
20002
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -19844,10 +20008,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19844
20008
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
19845
20009
|
}
|
|
19846
20010
|
async function readQualityBaseline(baselinePath) {
|
|
19847
|
-
const absolute =
|
|
20011
|
+
const absolute = path42.resolve(baselinePath);
|
|
19848
20012
|
let raw;
|
|
19849
20013
|
try {
|
|
19850
|
-
raw = JSON.parse(await
|
|
20014
|
+
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
19851
20015
|
} catch (error) {
|
|
19852
20016
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
19853
20017
|
}
|
|
@@ -19917,12 +20081,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
19917
20081
|
return { violations, warnings };
|
|
19918
20082
|
}
|
|
19919
20083
|
async function checkQualityBaseline(baselinePath) {
|
|
19920
|
-
const absolute =
|
|
20084
|
+
const absolute = path42.resolve(baselinePath);
|
|
19921
20085
|
const baseline = await readQualityBaseline(absolute);
|
|
19922
|
-
const root =
|
|
20086
|
+
const root = path42.dirname(absolute);
|
|
19923
20087
|
const cases = [];
|
|
19924
20088
|
for (const entry of baseline.cases) {
|
|
19925
|
-
const file =
|
|
20089
|
+
const file = path42.resolve(root, entry.file);
|
|
19926
20090
|
let actual = null;
|
|
19927
20091
|
const violations = [];
|
|
19928
20092
|
const warnings = [];
|
|
@@ -19935,7 +20099,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
19935
20099
|
violations.push(message4(error));
|
|
19936
20100
|
}
|
|
19937
20101
|
if (entry.record) {
|
|
19938
|
-
const recordPath =
|
|
20102
|
+
const recordPath = path42.resolve(root, entry.record.path);
|
|
19939
20103
|
try {
|
|
19940
20104
|
const recordHash = await sha256File(recordPath);
|
|
19941
20105
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -19977,15 +20141,15 @@ async function runQuality(args) {
|
|
|
19977
20141
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
19978
20142
|
let raw;
|
|
19979
20143
|
try {
|
|
19980
|
-
raw = JSON.parse(await
|
|
20144
|
+
raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
|
|
19981
20145
|
} catch (error) {
|
|
19982
20146
|
throw new Error(
|
|
19983
|
-
`Could not read quality inputs ${
|
|
20147
|
+
`Could not read quality inputs ${path43.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
19984
20148
|
{ cause: error }
|
|
19985
20149
|
);
|
|
19986
20150
|
}
|
|
19987
20151
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
19988
|
-
if (
|
|
20152
|
+
if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
|
|
19989
20153
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
19990
20154
|
}
|
|
19991
20155
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -19998,7 +20162,7 @@ async function runQuality(args) {
|
|
|
19998
20162
|
}, null, 2));
|
|
19999
20163
|
} else {
|
|
20000
20164
|
log(
|
|
20001
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
20165
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path43.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20002
20166
|
);
|
|
20003
20167
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20004
20168
|
}
|
|
@@ -20024,54 +20188,54 @@ async function runQuality(args) {
|
|
|
20024
20188
|
}
|
|
20025
20189
|
|
|
20026
20190
|
// src/cli/commands/recipe.ts
|
|
20027
|
-
import
|
|
20191
|
+
import path45 from "path";
|
|
20028
20192
|
|
|
20029
20193
|
// src/recipes.ts
|
|
20030
|
-
import
|
|
20031
|
-
import { existsSync as
|
|
20032
|
-
import { readdir as readdir3, readFile as
|
|
20194
|
+
import path44 from "path";
|
|
20195
|
+
import { existsSync as existsSync32 } from "fs";
|
|
20196
|
+
import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
|
|
20033
20197
|
import { fileURLToPath } from "url";
|
|
20034
|
-
import { z as
|
|
20198
|
+
import { z as z12 } from "zod";
|
|
20035
20199
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20036
20200
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20037
20201
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20038
20202
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20039
20203
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20040
|
-
var PortablePathSchema =
|
|
20041
|
-
if (value.includes("\\") ||
|
|
20204
|
+
var PortablePathSchema = z12.string().min(1).refine((value) => {
|
|
20205
|
+
if (value.includes("\\") || path44.posix.isAbsolute(value)) return false;
|
|
20042
20206
|
const parts = value.split("/");
|
|
20043
20207
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20044
20208
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20045
|
-
var RecipeBindingSchema =
|
|
20046
|
-
nodeId:
|
|
20047
|
-
input:
|
|
20209
|
+
var RecipeBindingSchema = z12.object({
|
|
20210
|
+
nodeId: z12.string().min(1),
|
|
20211
|
+
input: z12.string().min(1)
|
|
20048
20212
|
}).strict();
|
|
20049
|
-
var RecipeSchema =
|
|
20050
|
-
$schema:
|
|
20051
|
-
format:
|
|
20052
|
-
schemaVersion:
|
|
20053
|
-
id:
|
|
20054
|
-
version:
|
|
20055
|
-
provider:
|
|
20056
|
-
summary:
|
|
20057
|
-
files:
|
|
20213
|
+
var RecipeSchema = z12.object({
|
|
20214
|
+
$schema: z12.string().url().optional(),
|
|
20215
|
+
format: z12.literal("pixelkiln-recipe"),
|
|
20216
|
+
schemaVersion: z12.literal(1),
|
|
20217
|
+
id: z12.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
20218
|
+
version: z12.string().regex(VERSION_RE, "expected x.y.z"),
|
|
20219
|
+
provider: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20220
|
+
summary: z12.string().min(1).max(240),
|
|
20221
|
+
files: z12.array(z12.object({
|
|
20058
20222
|
path: PortablePathSchema,
|
|
20059
|
-
role:
|
|
20060
|
-
sha256:
|
|
20223
|
+
role: z12.enum(["workflow", "reference"]),
|
|
20224
|
+
sha256: z12.string().regex(SHA256_RE2)
|
|
20061
20225
|
}).strict()).default([]),
|
|
20062
|
-
models:
|
|
20226
|
+
models: z12.array(z12.object({
|
|
20063
20227
|
path: PortablePathSchema,
|
|
20064
|
-
sha256:
|
|
20065
|
-
source:
|
|
20066
|
-
license:
|
|
20228
|
+
sha256: z12.string().regex(SHA256_RE2),
|
|
20229
|
+
source: z12.string().url(),
|
|
20230
|
+
license: z12.string().min(1)
|
|
20067
20231
|
}).strict()).default([]),
|
|
20068
|
-
styleId:
|
|
20232
|
+
styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20069
20233
|
style: StyleSchema,
|
|
20070
|
-
workflow:
|
|
20234
|
+
workflow: z12.object({
|
|
20071
20235
|
path: PortablePathSchema,
|
|
20072
|
-
outputNodeId:
|
|
20073
|
-
numImages:
|
|
20074
|
-
bindings:
|
|
20236
|
+
outputNodeId: z12.string().min(1),
|
|
20237
|
+
numImages: z12.number().int().min(1).max(16),
|
|
20238
|
+
bindings: z12.object({
|
|
20075
20239
|
prompt: RecipeBindingSchema,
|
|
20076
20240
|
width: RecipeBindingSchema.optional(),
|
|
20077
20241
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20082,67 +20246,67 @@ var RecipeSchema = z11.object({
|
|
|
20082
20246
|
strength: RecipeBindingSchema.optional()
|
|
20083
20247
|
}).strict()
|
|
20084
20248
|
}).strict().optional(),
|
|
20085
|
-
quality:
|
|
20086
|
-
stage:
|
|
20087
|
-
workingCanvas:
|
|
20088
|
-
width:
|
|
20089
|
-
height:
|
|
20249
|
+
quality: z12.object({
|
|
20250
|
+
stage: z12.enum(["composition-source", "production-candidate"]),
|
|
20251
|
+
workingCanvas: z12.object({
|
|
20252
|
+
width: z12.number().int().min(16),
|
|
20253
|
+
height: z12.number().int().min(16)
|
|
20090
20254
|
}).strict(),
|
|
20091
|
-
recommendedNativeSize:
|
|
20092
|
-
min:
|
|
20093
|
-
max:
|
|
20255
|
+
recommendedNativeSize: z12.object({
|
|
20256
|
+
min: z12.number().int().min(1),
|
|
20257
|
+
max: z12.number().int().min(1)
|
|
20094
20258
|
}).strict(),
|
|
20095
|
-
paletteColors:
|
|
20096
|
-
min:
|
|
20097
|
-
max:
|
|
20259
|
+
paletteColors: z12.object({
|
|
20260
|
+
min: z12.number().int().min(1),
|
|
20261
|
+
max: z12.number().int().min(1)
|
|
20098
20262
|
}).strict(),
|
|
20099
|
-
background:
|
|
20100
|
-
checks:
|
|
20263
|
+
background: z12.enum(["opaque", "transparent", "full-bleed"]),
|
|
20264
|
+
checks: z12.array(z12.enum([
|
|
20101
20265
|
"prompt-coverage",
|
|
20102
20266
|
"native-grid",
|
|
20103
20267
|
"final-palette",
|
|
20104
20268
|
"alpha-edge",
|
|
20105
20269
|
"human-1x"
|
|
20106
20270
|
])).min(1),
|
|
20107
|
-
notes:
|
|
20271
|
+
notes: z12.array(z12.string().min(1)).default([])
|
|
20108
20272
|
}).strict(),
|
|
20109
|
-
integrity:
|
|
20110
|
-
algorithm:
|
|
20111
|
-
digest:
|
|
20273
|
+
integrity: z12.object({
|
|
20274
|
+
algorithm: z12.literal("sha256"),
|
|
20275
|
+
digest: z12.string().regex(SHA256_RE2)
|
|
20112
20276
|
}).strict()
|
|
20113
20277
|
}).strict().superRefine((recipe, context) => {
|
|
20114
20278
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20115
20279
|
context.addIssue({
|
|
20116
|
-
code:
|
|
20280
|
+
code: z12.ZodIssueCode.custom,
|
|
20117
20281
|
path: ["style", "provider"],
|
|
20118
20282
|
message: "must match the recipe provider"
|
|
20119
20283
|
});
|
|
20120
20284
|
}
|
|
20121
20285
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20122
20286
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20123
|
-
context.addIssue({ code:
|
|
20287
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20124
20288
|
}
|
|
20125
20289
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20126
20290
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20127
|
-
context.addIssue({ code:
|
|
20291
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20128
20292
|
}
|
|
20129
20293
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20130
20294
|
context.addIssue({
|
|
20131
|
-
code:
|
|
20295
|
+
code: z12.ZodIssueCode.custom,
|
|
20132
20296
|
path: ["files"],
|
|
20133
20297
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20134
20298
|
});
|
|
20135
20299
|
}
|
|
20136
20300
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20137
20301
|
context.addIssue({
|
|
20138
|
-
code:
|
|
20302
|
+
code: z12.ZodIssueCode.custom,
|
|
20139
20303
|
path: ["quality", "recommendedNativeSize"],
|
|
20140
20304
|
message: "min cannot exceed max"
|
|
20141
20305
|
});
|
|
20142
20306
|
}
|
|
20143
20307
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20144
20308
|
context.addIssue({
|
|
20145
|
-
code:
|
|
20309
|
+
code: z12.ZodIssueCode.custom,
|
|
20146
20310
|
path: ["quality", "paletteColors"],
|
|
20147
20311
|
message: "min cannot exceed max"
|
|
20148
20312
|
});
|
|
@@ -20151,7 +20315,7 @@ var RecipeSchema = z11.object({
|
|
|
20151
20315
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20152
20316
|
)) {
|
|
20153
20317
|
context.addIssue({
|
|
20154
|
-
code:
|
|
20318
|
+
code: z12.ZodIssueCode.custom,
|
|
20155
20319
|
path: ["workflow", "path"],
|
|
20156
20320
|
message: "must name a file whose role is workflow"
|
|
20157
20321
|
});
|
|
@@ -20159,7 +20323,7 @@ var RecipeSchema = z11.object({
|
|
|
20159
20323
|
if (recipe.provider === "comfyui") {
|
|
20160
20324
|
if (!recipe.workflow) {
|
|
20161
20325
|
context.addIssue({
|
|
20162
|
-
code:
|
|
20326
|
+
code: z12.ZodIssueCode.custom,
|
|
20163
20327
|
path: ["workflow"],
|
|
20164
20328
|
message: "ComfyUI recipes require workflow metadata"
|
|
20165
20329
|
});
|
|
@@ -20168,7 +20332,7 @@ var RecipeSchema = z11.object({
|
|
|
20168
20332
|
const options = recipe.style.providerOptions.comfyui;
|
|
20169
20333
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20170
20334
|
context.addIssue({
|
|
20171
|
-
code:
|
|
20335
|
+
code: z12.ZodIssueCode.custom,
|
|
20172
20336
|
path: ["style", "providerOptions", "comfyui"],
|
|
20173
20337
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20174
20338
|
});
|
|
@@ -20182,7 +20346,7 @@ var RecipeSchema = z11.object({
|
|
|
20182
20346
|
};
|
|
20183
20347
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20184
20348
|
context.addIssue({
|
|
20185
|
-
code:
|
|
20349
|
+
code: z12.ZodIssueCode.custom,
|
|
20186
20350
|
path: ["style", "providerOptions", "comfyui"],
|
|
20187
20351
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20188
20352
|
});
|
|
@@ -20214,10 +20378,10 @@ function message5(error) {
|
|
|
20214
20378
|
return error instanceof Error ? error.message : String(error);
|
|
20215
20379
|
}
|
|
20216
20380
|
async function readRecipeFile(recipePath, bundled) {
|
|
20217
|
-
const absolute =
|
|
20381
|
+
const absolute = path44.resolve(recipePath);
|
|
20218
20382
|
let raw;
|
|
20219
20383
|
try {
|
|
20220
|
-
raw = JSON.parse(await
|
|
20384
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20221
20385
|
} catch (error) {
|
|
20222
20386
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20223
20387
|
}
|
|
@@ -20228,13 +20392,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20228
20392
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20229
20393
|
);
|
|
20230
20394
|
}
|
|
20231
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
20395
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
|
|
20232
20396
|
}
|
|
20233
20397
|
async function walkRecipeFiles(root) {
|
|
20234
|
-
if (!
|
|
20398
|
+
if (!existsSync32(root)) return [];
|
|
20235
20399
|
const out = [];
|
|
20236
20400
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20237
|
-
const resolved =
|
|
20401
|
+
const resolved = path44.join(root, entry.name);
|
|
20238
20402
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20239
20403
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20240
20404
|
}
|
|
@@ -20245,7 +20409,7 @@ function bundledRecipeRoot() {
|
|
|
20245
20409
|
}
|
|
20246
20410
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
20247
20411
|
const recipes = await Promise.all(
|
|
20248
|
-
(await walkRecipeFiles(
|
|
20412
|
+
(await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
20249
20413
|
);
|
|
20250
20414
|
return recipes.sort(
|
|
20251
20415
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -20268,9 +20432,9 @@ function parseSelector(selector) {
|
|
|
20268
20432
|
return { id: match[1], version: match[2] };
|
|
20269
20433
|
}
|
|
20270
20434
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
20271
|
-
const local =
|
|
20272
|
-
if (
|
|
20273
|
-
const recipePath =
|
|
20435
|
+
const local = path44.resolve(target);
|
|
20436
|
+
if (existsSync32(local)) {
|
|
20437
|
+
const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
|
|
20274
20438
|
return readRecipeFile(recipePath, false);
|
|
20275
20439
|
}
|
|
20276
20440
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -20284,8 +20448,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
20284
20448
|
return matches[0];
|
|
20285
20449
|
}
|
|
20286
20450
|
async function verifyFile(root, file) {
|
|
20287
|
-
const absolute =
|
|
20288
|
-
if (!
|
|
20451
|
+
const absolute = path44.resolve(root, ...file.path.split("/"));
|
|
20452
|
+
if (!existsSync32(absolute)) {
|
|
20289
20453
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
20290
20454
|
}
|
|
20291
20455
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -20300,7 +20464,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20300
20464
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20301
20465
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
20302
20466
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
20303
|
-
const modelRoot = options.modelRoot ?
|
|
20467
|
+
const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
|
|
20304
20468
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
20305
20469
|
...await verifyFile(modelRoot, model),
|
|
20306
20470
|
source: model.source,
|
|
@@ -20334,7 +20498,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20334
20498
|
};
|
|
20335
20499
|
}
|
|
20336
20500
|
function renderStyle(style, destination, cwd) {
|
|
20337
|
-
const portableDir =
|
|
20501
|
+
const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
|
|
20338
20502
|
const replace = (value) => {
|
|
20339
20503
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
20340
20504
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -20346,25 +20510,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
20346
20510
|
return replace(style);
|
|
20347
20511
|
}
|
|
20348
20512
|
async function installRecipe(target, options = {}) {
|
|
20349
|
-
const cwd =
|
|
20513
|
+
const cwd = path44.resolve(options.cwd ?? process.cwd());
|
|
20350
20514
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20351
20515
|
const verification = await verifyRecipe(loaded.path);
|
|
20352
20516
|
if (!verification.ok) {
|
|
20353
20517
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
20354
20518
|
}
|
|
20355
|
-
const destination = options.out ?
|
|
20519
|
+
const destination = options.out ? path44.resolve(cwd, options.out) : path44.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
20356
20520
|
const sources = [
|
|
20357
20521
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
20358
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
20522
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path44.join(loaded.dir, ...file.path.split("/")) }))
|
|
20359
20523
|
];
|
|
20360
20524
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
20361
|
-
path:
|
|
20362
|
-
data: await
|
|
20525
|
+
path: path44.join(destination, ...file.path.split("/")),
|
|
20526
|
+
data: await readFile30(file.source)
|
|
20363
20527
|
})));
|
|
20364
20528
|
if (!options.force) {
|
|
20365
20529
|
for (const file of files) {
|
|
20366
|
-
if (!
|
|
20367
|
-
const current = await
|
|
20530
|
+
if (!existsSync32(file.path)) continue;
|
|
20531
|
+
const current = await readFile30(file.path);
|
|
20368
20532
|
if (!current.equals(file.data)) {
|
|
20369
20533
|
throw new OverwriteRefusedError(
|
|
20370
20534
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -20458,14 +20622,14 @@ async function runRecipe(args) {
|
|
|
20458
20622
|
}, null, 2));
|
|
20459
20623
|
} else {
|
|
20460
20624
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
20461
|
-
log(` ${
|
|
20625
|
+
log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
|
|
20462
20626
|
log(`
|
|
20463
20627
|
Add this entry under your manifest's styles object:`);
|
|
20464
20628
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
20465
20629
|
if (result.recipe.models.length) {
|
|
20466
20630
|
log(`
|
|
20467
20631
|
Models are not downloaded automatically. Verify them with:`);
|
|
20468
|
-
log(` pixelkiln recipe verify ${
|
|
20632
|
+
log(` pixelkiln recipe verify ${path45.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
20469
20633
|
}
|
|
20470
20634
|
}
|
|
20471
20635
|
return;
|
|
@@ -20473,7 +20637,7 @@ async function runRecipe(args) {
|
|
|
20473
20637
|
}
|
|
20474
20638
|
|
|
20475
20639
|
// src/cli/commands/refine.ts
|
|
20476
|
-
import
|
|
20640
|
+
import path46 from "path";
|
|
20477
20641
|
async function runRefine(args) {
|
|
20478
20642
|
if (args.from || args.subcommand === "approve") {
|
|
20479
20643
|
if (!args.from) {
|
|
@@ -20503,12 +20667,12 @@ async function runRefine(args) {
|
|
|
20503
20667
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
20504
20668
|
);
|
|
20505
20669
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
20506
|
-
log(` wrote ${
|
|
20507
|
-
log(` quality record: ${
|
|
20670
|
+
log(` wrote ${path46.relative(process.cwd(), result3.output)}`);
|
|
20671
|
+
log(` quality record: ${path46.relative(process.cwd(), result3.record)}`);
|
|
20508
20672
|
log(`
|
|
20509
20673
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
20510
20674
|
log(
|
|
20511
|
-
` pixelkiln refine approve --from ${
|
|
20675
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
20512
20676
|
);
|
|
20513
20677
|
}
|
|
20514
20678
|
return;
|
|
@@ -20536,7 +20700,7 @@ async function runRefine(args) {
|
|
|
20536
20700
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
20537
20701
|
else {
|
|
20538
20702
|
log(
|
|
20539
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
20703
|
+
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
20704
|
);
|
|
20541
20705
|
}
|
|
20542
20706
|
return;
|
|
@@ -20545,7 +20709,7 @@ async function runRefine(args) {
|
|
|
20545
20709
|
if (args.json) {
|
|
20546
20710
|
log(JSON.stringify(result2, null, 2));
|
|
20547
20711
|
} else {
|
|
20548
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
20712
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path46.relative(process.cwd(), result2.output || result2.record)}`);
|
|
20549
20713
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
20550
20714
|
}
|
|
20551
20715
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -20591,7 +20755,7 @@ async function runRefine(args) {
|
|
|
20591
20755
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
20592
20756
|
if (item.state === "needs-approval") {
|
|
20593
20757
|
log(
|
|
20594
|
-
` pixelkiln refine approve --from ${
|
|
20758
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
20595
20759
|
);
|
|
20596
20760
|
}
|
|
20597
20761
|
}
|
|
@@ -20602,9 +20766,9 @@ async function runRefine(args) {
|
|
|
20602
20766
|
}
|
|
20603
20767
|
|
|
20604
20768
|
// src/cli/commands/skeleton.ts
|
|
20605
|
-
import
|
|
20606
|
-
import { existsSync as
|
|
20607
|
-
import { readFile as
|
|
20769
|
+
import path47 from "path";
|
|
20770
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20771
|
+
import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
|
|
20608
20772
|
|
|
20609
20773
|
// src/skeleton-preview.ts
|
|
20610
20774
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -20712,23 +20876,19 @@ var Canvas = class {
|
|
|
20712
20876
|
};
|
|
20713
20877
|
|
|
20714
20878
|
// src/cli/commands/skeleton.ts
|
|
20715
|
-
var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
|
|
20716
20879
|
async function runEstimateSkeleton(args) {
|
|
20717
20880
|
const image = args.target;
|
|
20718
20881
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
20719
|
-
if (args.out &&
|
|
20882
|
+
if (args.out && existsSync33(path47.resolve(args.out)) && !args.force) {
|
|
20720
20883
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
20721
20884
|
}
|
|
20722
|
-
loadEnvFiles(
|
|
20885
|
+
loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
|
|
20723
20886
|
loadEnvFiles(process.cwd());
|
|
20724
|
-
const bytes = await
|
|
20887
|
+
const bytes = await readFile31(path47.resolve(image));
|
|
20725
20888
|
const metadata = imageMetadata(bytes);
|
|
20726
20889
|
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
|
-
}
|
|
20890
|
+
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
20891
|
+
if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
|
|
20732
20892
|
const client2 = clientFromEnv();
|
|
20733
20893
|
const res = await client2.estimateSkeleton({
|
|
20734
20894
|
image: { base64: bytes.toString("base64"), format: metadata.format }
|
|
@@ -20736,7 +20896,7 @@ async function runEstimateSkeleton(args) {
|
|
|
20736
20896
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
20737
20897
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
20738
20898
|
if (args.out) {
|
|
20739
|
-
await
|
|
20899
|
+
await writeFile16(path47.resolve(args.out), json + "\n");
|
|
20740
20900
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
20741
20901
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
20742
20902
|
} else {
|
|
@@ -20749,8 +20909,8 @@ async function runSkeletonPreview(args) {
|
|
|
20749
20909
|
let sourceFile;
|
|
20750
20910
|
let label;
|
|
20751
20911
|
if (target.endsWith(".json")) {
|
|
20752
|
-
keypointsFile =
|
|
20753
|
-
sourceFile = args.from ?
|
|
20912
|
+
keypointsFile = path47.resolve(target);
|
|
20913
|
+
sourceFile = args.from ? path47.resolve(args.from) : void 0;
|
|
20754
20914
|
label = target;
|
|
20755
20915
|
} else {
|
|
20756
20916
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -20762,31 +20922,31 @@ async function runSkeletonPreview(args) {
|
|
|
20762
20922
|
);
|
|
20763
20923
|
}
|
|
20764
20924
|
keypointsFile = spec.revision.keypointsFile;
|
|
20765
|
-
sourceFile = args.from ?
|
|
20925
|
+
sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
|
|
20766
20926
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
20767
20927
|
}
|
|
20768
|
-
if (!
|
|
20928
|
+
if (!existsSync33(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
20769
20929
|
let json;
|
|
20770
20930
|
try {
|
|
20771
|
-
json = JSON.parse(await
|
|
20931
|
+
json = JSON.parse(await readFile31(keypointsFile, "utf8"));
|
|
20772
20932
|
} catch (error) {
|
|
20773
20933
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
20774
20934
|
}
|
|
20775
20935
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
20776
20936
|
let source;
|
|
20777
20937
|
if (sourceFile) {
|
|
20778
|
-
if (!
|
|
20938
|
+
if (!existsSync33(sourceFile)) {
|
|
20779
20939
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
20780
20940
|
} else {
|
|
20781
|
-
const bytes = await
|
|
20941
|
+
const bytes = await readFile31(sourceFile);
|
|
20782
20942
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
20783
20943
|
source = decodePng(bytes);
|
|
20784
20944
|
}
|
|
20785
20945
|
}
|
|
20786
|
-
const out =
|
|
20787
|
-
await
|
|
20946
|
+
const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
20947
|
+
await writeFile16(out, renderSkeletonSheet(set, { source }));
|
|
20788
20948
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
20789
|
-
const shown =
|
|
20949
|
+
const shown = path47.relative(process.cwd(), out);
|
|
20790
20950
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
20791
20951
|
}
|
|
20792
20952
|
|
|
@@ -20832,15 +20992,15 @@ async function runTools(args) {
|
|
|
20832
20992
|
}
|
|
20833
20993
|
|
|
20834
20994
|
// src/cli/commands/workspace.ts
|
|
20835
|
-
import
|
|
20836
|
-
import { existsSync as
|
|
20995
|
+
import path48 from "path";
|
|
20996
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20837
20997
|
async function runWorkspace(args) {
|
|
20838
|
-
const workspacePath =
|
|
20839
|
-
const dir =
|
|
20998
|
+
const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
20999
|
+
const dir = path48.dirname(workspacePath);
|
|
20840
21000
|
if (args.subcommand === "add") {
|
|
20841
|
-
const manifestPath =
|
|
21001
|
+
const manifestPath = path48.resolve(args.target);
|
|
20842
21002
|
const loadedTarget = await loadManifest(manifestPath);
|
|
20843
|
-
const lockPath = args.explicitLock ?
|
|
21003
|
+
const lockPath = args.explicitLock ? path48.resolve(args.explicitLock) : path48.join(path48.dirname(manifestPath), "pixelkiln.lock.json");
|
|
20844
21004
|
const ws = await loadWorkspace(workspacePath);
|
|
20845
21005
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
20846
21006
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -20860,10 +21020,10 @@ async function runWorkspace(args) {
|
|
|
20860
21020
|
...args.account ? { account: args.account } : {}
|
|
20861
21021
|
};
|
|
20862
21022
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
20863
|
-
log(` registered "${id}" in ${
|
|
21023
|
+
log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20864
21024
|
log(` manifest: ${project.manifest}`);
|
|
20865
21025
|
log(` lock: ${project.lock}`);
|
|
20866
|
-
if (!
|
|
21026
|
+
if (!existsSync34(lockPath)) {
|
|
20867
21027
|
log(
|
|
20868
21028
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
20869
21029
|
);
|
|
@@ -20872,7 +21032,7 @@ async function runWorkspace(args) {
|
|
|
20872
21032
|
}
|
|
20873
21033
|
if (args.subcommand === "remove") {
|
|
20874
21034
|
const ws = await loadWorkspace(workspacePath);
|
|
20875
|
-
const resolvedTarget =
|
|
21035
|
+
const resolvedTarget = path48.resolve(args.target);
|
|
20876
21036
|
const match = ws.projects.find(
|
|
20877
21037
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
20878
21038
|
);
|
|
@@ -20883,11 +21043,11 @@ async function runWorkspace(args) {
|
|
|
20883
21043
|
version: 1,
|
|
20884
21044
|
projects: ws.projects.filter((p) => p !== match)
|
|
20885
21045
|
});
|
|
20886
|
-
log(` removed "${match.id}" from ${
|
|
21046
|
+
log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20887
21047
|
return;
|
|
20888
21048
|
}
|
|
20889
21049
|
if (args.subcommand === "list") {
|
|
20890
|
-
if (!
|
|
21050
|
+
if (!existsSync34(workspacePath)) {
|
|
20891
21051
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20892
21052
|
}
|
|
20893
21053
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20895,9 +21055,9 @@ async function runWorkspace(args) {
|
|
|
20895
21055
|
if (args.json) {
|
|
20896
21056
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
20897
21057
|
} else if (!ws.projects.length) {
|
|
20898
|
-
log(` no projects registered in ${
|
|
21058
|
+
log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20899
21059
|
} else {
|
|
20900
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21060
|
+
log(` ${ws.projects.length} project(s) in ${path48.relative(process.cwd(), workspacePath)}:`);
|
|
20901
21061
|
for (const p of ws.projects) {
|
|
20902
21062
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
20903
21063
|
}
|
|
@@ -20907,7 +21067,7 @@ async function runWorkspace(args) {
|
|
|
20907
21067
|
return;
|
|
20908
21068
|
}
|
|
20909
21069
|
if (args.subcommand === "status") {
|
|
20910
|
-
if (!
|
|
21070
|
+
if (!existsSync34(workspacePath)) {
|
|
20911
21071
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20912
21072
|
}
|
|
20913
21073
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20915,7 +21075,7 @@ async function runWorkspace(args) {
|
|
|
20915
21075
|
if (args.json) {
|
|
20916
21076
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
20917
21077
|
} else {
|
|
20918
|
-
log(` workspace: ${
|
|
21078
|
+
log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20919
21079
|
for (const p of report.projects) {
|
|
20920
21080
|
if (p.error) {
|
|
20921
21081
|
log(`
|
|
@@ -20969,8 +21129,8 @@ async function runHelp() {
|
|
|
20969
21129
|
async function runVersion() {
|
|
20970
21130
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
20971
21131
|
const url = new URL(up, import.meta.url);
|
|
20972
|
-
if (!
|
|
20973
|
-
const pkg = JSON.parse(await
|
|
21132
|
+
if (!existsSync35(url)) continue;
|
|
21133
|
+
const pkg = JSON.parse(await readFile32(url, "utf8"));
|
|
20974
21134
|
if (pkg.name === "pixelkiln") {
|
|
20975
21135
|
log(`${pkg.name} ${pkg.version}`);
|
|
20976
21136
|
return;
|