pixelkiln 0.68.1 → 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/README.md +1 -1
- package/dist/cli.js +808 -416
- 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 +292 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -132
- package/dist/index.d.ts +243 -132
- package/dist/index.js +287 -137
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +41 -13
- package/docs/REVISIONS.md +21 -4
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +2 -2
- package/skills/pixelkiln/references/pixellab.md +2 -1
- package/skills/pixelkiln/references/revisions.md +12 -5
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";
|
|
@@ -107,7 +107,8 @@ var VALUE_FLAGS = [
|
|
|
107
107
|
"--quantize",
|
|
108
108
|
"--description",
|
|
109
109
|
"--weight",
|
|
110
|
-
"--glyph-px"
|
|
110
|
+
"--glyph-px",
|
|
111
|
+
"--frames"
|
|
111
112
|
];
|
|
112
113
|
var BOOL_FLAGS = [
|
|
113
114
|
"--force",
|
|
@@ -158,6 +159,7 @@ var COMMANDS = [
|
|
|
158
159
|
"recipe",
|
|
159
160
|
"workspace",
|
|
160
161
|
"estimate-skeleton",
|
|
162
|
+
"skeleton-preview",
|
|
161
163
|
"unzoom",
|
|
162
164
|
"font",
|
|
163
165
|
"help",
|
|
@@ -256,6 +258,12 @@ function parseArgs(argv) {
|
|
|
256
258
|
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
257
259
|
if (target === void 0) throw new UsageError("estimate-skeleton needs an image path");
|
|
258
260
|
rest = rest.slice(1);
|
|
261
|
+
} else if (command === "skeleton-preview") {
|
|
262
|
+
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
263
|
+
if (target === void 0) {
|
|
264
|
+
throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
|
|
265
|
+
}
|
|
266
|
+
rest = rest.slice(1);
|
|
259
267
|
} else if (command === "refine") {
|
|
260
268
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
261
269
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -421,7 +429,8 @@ function parseArgs(argv) {
|
|
|
421
429
|
quantize: numberOption("--quantize", { min: -1, max: 256, integer: true }),
|
|
422
430
|
description: get("--description"),
|
|
423
431
|
weight: get("--weight"),
|
|
424
|
-
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true })
|
|
432
|
+
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
|
|
433
|
+
frames: numberOption("--frames", { min: 3, max: 15, integer: true })
|
|
425
434
|
};
|
|
426
435
|
}
|
|
427
436
|
|
|
@@ -461,11 +470,16 @@ Commands
|
|
|
461
470
|
first. Local art is untouched.
|
|
462
471
|
tag Push manifest tags to the objects upstream (free).
|
|
463
472
|
balance Show the provider's remaining balance.
|
|
464
|
-
estimate-skeleton <image> [--out <file>]
|
|
465
|
-
PixelLab only: derive an 18-joint skeleton from
|
|
466
|
-
and un-budgeted (like balance, not a generation).
|
|
467
|
-
|
|
468
|
-
|
|
473
|
+
estimate-skeleton <image> [--out <file>] [--frames <n>] [--force]
|
|
474
|
+
PixelLab only: derive an 18-joint skeleton from a square 16-256px
|
|
475
|
+
image, direct and un-budgeted (like balance, not a generation).
|
|
476
|
+
Prints, or writes with --out, a ready keypoints file for an
|
|
477
|
+
animate-skeleton revision: the estimated pose plus --frames copies
|
|
478
|
+
(default 4) to edit. --out refuses to replace a file without --force.
|
|
479
|
+
skeleton-preview <keypoints.json | asset> [--from <image>] [--out <file>]
|
|
480
|
+
Draw every pose of a keypoints file over its source image as one
|
|
481
|
+
PNG, locally and free. Given an animate-skeleton asset id, reads
|
|
482
|
+
its keypointsFile and source from the manifest.
|
|
469
483
|
status Summarise the lockfile.
|
|
470
484
|
edit Hand-edit an asset in your own editor: copies the generated PNG to
|
|
471
485
|
<outDir>/edits/, declares it as the asset's source, and opens it.
|
|
@@ -648,10 +662,10 @@ import { createReadStream } from "fs";
|
|
|
648
662
|
function sha256(data) {
|
|
649
663
|
return createHash("sha256").update(data).digest("hex");
|
|
650
664
|
}
|
|
651
|
-
async function sha256File(
|
|
665
|
+
async function sha256File(path49) {
|
|
652
666
|
return new Promise((resolve, reject) => {
|
|
653
667
|
const hash = createHash("sha256");
|
|
654
|
-
const stream = createReadStream(
|
|
668
|
+
const stream = createReadStream(path49);
|
|
655
669
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
656
670
|
stream.on("error", reject);
|
|
657
671
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -5756,14 +5770,61 @@ import { z as z3 } from "zod";
|
|
|
5756
5770
|
|
|
5757
5771
|
// src/skeleton.ts
|
|
5758
5772
|
import { z as z2 } from "zod";
|
|
5773
|
+
var SKELETON_LABELS = [
|
|
5774
|
+
"NOSE",
|
|
5775
|
+
"NECK",
|
|
5776
|
+
"RIGHT SHOULDER",
|
|
5777
|
+
"RIGHT ELBOW",
|
|
5778
|
+
"RIGHT ARM",
|
|
5779
|
+
"LEFT SHOULDER",
|
|
5780
|
+
"LEFT ELBOW",
|
|
5781
|
+
"LEFT ARM",
|
|
5782
|
+
"RIGHT HIP",
|
|
5783
|
+
"RIGHT KNEE",
|
|
5784
|
+
"RIGHT LEG",
|
|
5785
|
+
"LEFT HIP",
|
|
5786
|
+
"LEFT KNEE",
|
|
5787
|
+
"LEFT LEG",
|
|
5788
|
+
"RIGHT EYE",
|
|
5789
|
+
"LEFT EYE",
|
|
5790
|
+
"RIGHT EAR",
|
|
5791
|
+
"LEFT EAR"
|
|
5792
|
+
];
|
|
5793
|
+
var SKELETON_BONES = [
|
|
5794
|
+
["NOSE", "NECK"],
|
|
5795
|
+
["NECK", "RIGHT SHOULDER"],
|
|
5796
|
+
["RIGHT SHOULDER", "RIGHT ELBOW"],
|
|
5797
|
+
["RIGHT ELBOW", "RIGHT ARM"],
|
|
5798
|
+
["NECK", "LEFT SHOULDER"],
|
|
5799
|
+
["LEFT SHOULDER", "LEFT ELBOW"],
|
|
5800
|
+
["LEFT ELBOW", "LEFT ARM"],
|
|
5801
|
+
["NECK", "RIGHT HIP"],
|
|
5802
|
+
["RIGHT HIP", "RIGHT KNEE"],
|
|
5803
|
+
["RIGHT KNEE", "RIGHT LEG"],
|
|
5804
|
+
["NECK", "LEFT HIP"],
|
|
5805
|
+
["LEFT HIP", "LEFT KNEE"],
|
|
5806
|
+
["LEFT KNEE", "LEFT LEG"],
|
|
5807
|
+
["NOSE", "RIGHT EYE"],
|
|
5808
|
+
["RIGHT EYE", "RIGHT EAR"],
|
|
5809
|
+
["NOSE", "LEFT EYE"],
|
|
5810
|
+
["LEFT EYE", "LEFT EAR"]
|
|
5811
|
+
];
|
|
5759
5812
|
var SkeletonKeypointSchema = z2.object({
|
|
5760
|
-
label: z2.
|
|
5813
|
+
label: z2.enum(SKELETON_LABELS),
|
|
5761
5814
|
x: z2.number().min(0).max(1),
|
|
5762
5815
|
y: z2.number().min(0).max(1),
|
|
5763
5816
|
z_index: z2.number(),
|
|
5764
5817
|
depth: z2.number().optional()
|
|
5765
5818
|
}).strict();
|
|
5766
|
-
var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18)
|
|
5819
|
+
var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18).superRefine((frame, context) => {
|
|
5820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5821
|
+
for (const [index, joint] of frame.entries()) {
|
|
5822
|
+
if (seen.has(joint.label)) {
|
|
5823
|
+
context.addIssue({ code: z2.ZodIssueCode.custom, message: `"${joint.label}" appears twice`, path: [index, "label"] });
|
|
5824
|
+
}
|
|
5825
|
+
seen.add(joint.label);
|
|
5826
|
+
}
|
|
5827
|
+
});
|
|
5767
5828
|
var SkeletonSetSchema = z2.object({
|
|
5768
5829
|
firstFrameKeypoints: SkeletonFrameSchema,
|
|
5769
5830
|
frames: z2.array(SkeletonFrameSchema).min(3).max(15)
|
|
@@ -5776,6 +5837,15 @@ function parseSkeletonSet(json, file) {
|
|
|
5776
5837
|
}
|
|
5777
5838
|
return result.data;
|
|
5778
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
|
+
}
|
|
5844
|
+
var DEFAULT_SCAFFOLD_FRAMES = 4;
|
|
5845
|
+
function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
|
|
5846
|
+
const copy = () => keypoints.map((joint) => ({ ...joint }));
|
|
5847
|
+
return { firstFrameKeypoints: copy(), frames: Array.from({ length: frames }, copy) };
|
|
5848
|
+
}
|
|
5779
5849
|
|
|
5780
5850
|
// src/client.ts
|
|
5781
5851
|
var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
|
|
@@ -6090,9 +6160,9 @@ var PixelLabClient = class {
|
|
|
6090
6160
|
apiKey;
|
|
6091
6161
|
http;
|
|
6092
6162
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6093
|
-
async request(
|
|
6163
|
+
async request(path49, init) {
|
|
6094
6164
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6095
|
-
const res = await this.http(`${BASE}${
|
|
6165
|
+
const res = await this.http(`${BASE}${path49}`, {
|
|
6096
6166
|
...init,
|
|
6097
6167
|
headers: {
|
|
6098
6168
|
Authorization: auth,
|
|
@@ -6102,13 +6172,13 @@ var PixelLabClient = class {
|
|
|
6102
6172
|
});
|
|
6103
6173
|
const text = await res.text();
|
|
6104
6174
|
if (!res.ok) {
|
|
6105
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6175
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path49} \u2192 ${res.status}`, res.status, text);
|
|
6106
6176
|
}
|
|
6107
6177
|
if (!text) return {};
|
|
6108
6178
|
try {
|
|
6109
6179
|
return JSON.parse(text);
|
|
6110
6180
|
} catch {
|
|
6111
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6181
|
+
throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
|
|
6112
6182
|
}
|
|
6113
6183
|
}
|
|
6114
6184
|
async balance() {
|
|
@@ -6500,10 +6570,10 @@ var PixelLabClient = class {
|
|
|
6500
6570
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6501
6571
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6502
6572
|
const south = args.reference?.south;
|
|
6503
|
-
let
|
|
6573
|
+
let path49;
|
|
6504
6574
|
let body;
|
|
6505
6575
|
if (args.mode === "v3") {
|
|
6506
|
-
|
|
6576
|
+
path49 = "/create-character-v3";
|
|
6507
6577
|
body = {
|
|
6508
6578
|
description: args.description,
|
|
6509
6579
|
image_size: imageSize,
|
|
@@ -6517,7 +6587,7 @@ var PixelLabClient = class {
|
|
|
6517
6587
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6518
6588
|
};
|
|
6519
6589
|
} else if (args.mode === "pro-flash") {
|
|
6520
|
-
|
|
6590
|
+
path49 = "/create-character-pro-flash";
|
|
6521
6591
|
const traits = args.styleTraits;
|
|
6522
6592
|
body = {
|
|
6523
6593
|
description: args.description,
|
|
@@ -6541,7 +6611,7 @@ var PixelLabClient = class {
|
|
|
6541
6611
|
} : {}
|
|
6542
6612
|
};
|
|
6543
6613
|
} else if (args.mode === "pro") {
|
|
6544
|
-
|
|
6614
|
+
path49 = "/create-character-pro";
|
|
6545
6615
|
body = {
|
|
6546
6616
|
description: args.description,
|
|
6547
6617
|
image_size: imageSize,
|
|
@@ -6555,7 +6625,7 @@ var PixelLabClient = class {
|
|
|
6555
6625
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6556
6626
|
};
|
|
6557
6627
|
} else {
|
|
6558
|
-
|
|
6628
|
+
path49 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6559
6629
|
body = {
|
|
6560
6630
|
description: args.description,
|
|
6561
6631
|
image_size: imageSize,
|
|
@@ -6572,8 +6642,8 @@ var PixelLabClient = class {
|
|
|
6572
6642
|
...palette
|
|
6573
6643
|
};
|
|
6574
6644
|
}
|
|
6575
|
-
const raw = await this.request(
|
|
6576
|
-
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));
|
|
6577
6647
|
}
|
|
6578
6648
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6579
6649
|
async createCharacterState(args) {
|
|
@@ -7128,7 +7198,7 @@ var PixelLabClient = class {
|
|
|
7128
7198
|
* account (see `EstimateSkeletonResponseSchema`'s own caveat).
|
|
7129
7199
|
*/
|
|
7130
7200
|
async estimateSkeleton(args) {
|
|
7131
|
-
const body = { image: args.image };
|
|
7201
|
+
const body = { image: { type: "base64", base64: args.image.base64, format: args.image.format } };
|
|
7132
7202
|
const res = validateResponse(
|
|
7133
7203
|
EstimateSkeletonResponseSchema,
|
|
7134
7204
|
await this.request("/estimate-skeleton", { method: "POST", body: JSON.stringify(body) }),
|
|
@@ -9420,9 +9490,9 @@ var RetroDiffusionClient = class {
|
|
|
9420
9490
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9421
9491
|
return Buffer.from(await response.arrayBuffer());
|
|
9422
9492
|
}
|
|
9423
|
-
async call(
|
|
9493
|
+
async call(path49, init = {}) {
|
|
9424
9494
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9425
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9495
|
+
const response = await this.request(`${this.baseUrl}${path49}`, {
|
|
9426
9496
|
...init,
|
|
9427
9497
|
headers: {
|
|
9428
9498
|
"Content-Type": "application/json",
|
|
@@ -11727,10 +11797,10 @@ function isSha256Hash(value) {
|
|
|
11727
11797
|
function parseCache(value) {
|
|
11728
11798
|
return HashCacheSchema.parse(value);
|
|
11729
11799
|
}
|
|
11730
|
-
async function loadCache(
|
|
11731
|
-
if (!existsSync10(
|
|
11800
|
+
async function loadCache(path49) {
|
|
11801
|
+
if (!existsSync10(path49)) return { version: 1, hashes: {} };
|
|
11732
11802
|
try {
|
|
11733
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
11803
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
|
|
11734
11804
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11735
11805
|
return {
|
|
11736
11806
|
version: 1,
|
|
@@ -11742,18 +11812,18 @@ async function loadCache(path48) {
|
|
|
11742
11812
|
return { version: 1, hashes: {} };
|
|
11743
11813
|
}
|
|
11744
11814
|
}
|
|
11745
|
-
async function saveCache(
|
|
11815
|
+
async function saveCache(path49, cache) {
|
|
11746
11816
|
const sorted = {};
|
|
11747
11817
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11748
11818
|
const hash = cache.hashes[key];
|
|
11749
11819
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11750
11820
|
sorted[key] = hash;
|
|
11751
11821
|
}
|
|
11752
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11753
|
-
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`;
|
|
11754
11824
|
try {
|
|
11755
11825
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11756
|
-
await rename5(tmp,
|
|
11826
|
+
await rename5(tmp, path49);
|
|
11757
11827
|
} finally {
|
|
11758
11828
|
await rm5(tmp, { force: true });
|
|
11759
11829
|
}
|
|
@@ -13757,15 +13827,27 @@ var NewAssetSchema = z6.object({
|
|
|
13757
13827
|
/**
|
|
13758
13828
|
* A controlled regeneration of another asset in the same manifest,
|
|
13759
13829
|
* instead of a fresh generation. `inpaint` needs a mask upload the
|
|
13760
|
-
* gallery does not offer yet, so only `image-to-image`
|
|
13761
|
-
*
|
|
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.
|
|
13762
13833
|
*/
|
|
13763
|
-
revision: z6.
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
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(),
|
|
13769
13851
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
13770
13852
|
state: NewStateSchema.optional(),
|
|
13771
13853
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
@@ -14290,52 +14372,165 @@ async function runEdit(args) {
|
|
|
14290
14372
|
}
|
|
14291
14373
|
|
|
14292
14374
|
// src/cli/commands/gallery.ts
|
|
14293
|
-
import
|
|
14294
|
-
import { existsSync as
|
|
14375
|
+
import path33 from "path";
|
|
14376
|
+
import { existsSync as existsSync24 } from "fs";
|
|
14295
14377
|
|
|
14296
14378
|
// src/gallery/snapshot.ts
|
|
14297
14379
|
import { createHash as createHash3 } from "crypto";
|
|
14298
|
-
import { existsSync as
|
|
14299
|
-
import { readFile as
|
|
14300
|
-
import
|
|
14380
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14381
|
+
import { readFile as readFile18, stat as stat2 } from "fs/promises";
|
|
14382
|
+
import path29 from "path";
|
|
14301
14383
|
|
|
14302
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";
|
|
14303
14391
|
import path26 from "path";
|
|
14304
14392
|
import { z as z8 } from "zod";
|
|
14305
|
-
var
|
|
14306
|
-
|
|
14307
|
-
z8.
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14312
|
-
|
|
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}$/),
|
|
14313
14508
|
/** Also launch the author's editor on the file. */
|
|
14314
|
-
open:
|
|
14509
|
+
open: z9.boolean().optional()
|
|
14315
14510
|
}).strict(),
|
|
14316
|
-
|
|
14317
|
-
action:
|
|
14318
|
-
project:
|
|
14319
|
-
assetId:
|
|
14320
|
-
styleId:
|
|
14321
|
-
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}$/)
|
|
14322
14517
|
}).strict(),
|
|
14323
14518
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14324
|
-
|
|
14325
|
-
action:
|
|
14326
|
-
project:
|
|
14327
|
-
assetId:
|
|
14328
|
-
styleId:
|
|
14329
|
-
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}$/),
|
|
14330
14525
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14331
14526
|
png: Base64.min(1).optional(),
|
|
14332
14527
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14333
|
-
frames:
|
|
14528
|
+
frames: z9.array(z9.object({ role: z9.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14334
14529
|
/** The editor's layered project file, kept beside the edit. */
|
|
14335
14530
|
pxo: Base64.optional(),
|
|
14336
14531
|
/** Editor identity from the bridge's ready message. */
|
|
14337
|
-
editor:
|
|
14338
|
-
protocol:
|
|
14532
|
+
editor: z9.string().min(1).max(200),
|
|
14533
|
+
protocol: z9.number().int().positive()
|
|
14339
14534
|
}).strict()
|
|
14340
14535
|
]);
|
|
14341
14536
|
function createGalleryEditHandler(opts) {
|
|
@@ -14376,6 +14571,23 @@ function createGalleryEditHandler(opts) {
|
|
|
14376
14571
|
}
|
|
14377
14572
|
return opts.reload();
|
|
14378
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
|
+
}
|
|
14379
14591
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
14380
14592
|
if (!parsed.success) {
|
|
14381
14593
|
throw new ManifestEditError(
|
|
@@ -14390,7 +14602,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14390
14602
|
}
|
|
14391
14603
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14392
14604
|
if (result.changed) {
|
|
14393
|
-
const relative =
|
|
14605
|
+
const relative = path27.relative(process.cwd(), result.manifestPath);
|
|
14394
14606
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14395
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;
|
|
14396
14608
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -14400,8 +14612,8 @@ function createGalleryEditHandler(opts) {
|
|
|
14400
14612
|
}
|
|
14401
14613
|
|
|
14402
14614
|
// src/pipeline/history.ts
|
|
14403
|
-
import { existsSync as
|
|
14404
|
-
import
|
|
14615
|
+
import { existsSync as existsSync19 } from "fs";
|
|
14616
|
+
import path28 from "path";
|
|
14405
14617
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
14406
14618
|
function historyLimit(manifest, env = process.env) {
|
|
14407
14619
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -14488,10 +14700,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14488
14700
|
}
|
|
14489
14701
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
14490
14702
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
14491
|
-
if (!
|
|
14703
|
+
if (!existsSync19(file)) continue;
|
|
14492
14704
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
14493
14705
|
throw new OverwriteRefusedError(
|
|
14494
|
-
`${
|
|
14706
|
+
`${path28.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
14495
14707
|
[file]
|
|
14496
14708
|
);
|
|
14497
14709
|
}
|
|
@@ -14544,7 +14756,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14544
14756
|
// src/gallery/snapshot.ts
|
|
14545
14757
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14546
14758
|
function galleryMediaId(absolutePath) {
|
|
14547
|
-
return createHash3("sha256").update(
|
|
14759
|
+
return createHash3("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14548
14760
|
}
|
|
14549
14761
|
function galleryMediaRoute(id) {
|
|
14550
14762
|
return `/media/${id}`;
|
|
@@ -14563,9 +14775,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14563
14775
|
return (entry.history ?? []).map((generation, i) => {
|
|
14564
14776
|
const outputs = generation.outputs.map((output) => {
|
|
14565
14777
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14566
|
-
const file =
|
|
14778
|
+
const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14567
14779
|
let url = null;
|
|
14568
|
-
if (
|
|
14780
|
+
if (existsSync20(file)) {
|
|
14569
14781
|
const id = galleryMediaId(file);
|
|
14570
14782
|
media.set(id, { path: file, contentType: mediaType });
|
|
14571
14783
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14597,7 +14809,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14597
14809
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
14598
14810
|
async function samePixels(a, b) {
|
|
14599
14811
|
try {
|
|
14600
|
-
const [left, right] = await Promise.all([
|
|
14812
|
+
const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
|
|
14601
14813
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
14602
14814
|
const x = decodePng(left);
|
|
14603
14815
|
const y = decodePng(right);
|
|
@@ -14695,10 +14907,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14695
14907
|
async function describeQuality(media, root, inspection) {
|
|
14696
14908
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14697
14909
|
const outputs = await Promise.all(
|
|
14698
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
14910
|
+
outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
|
|
14699
14911
|
);
|
|
14700
|
-
const recordPath =
|
|
14701
|
-
const recordExists =
|
|
14912
|
+
const recordPath = path29.resolve(root, inspection.record);
|
|
14913
|
+
const recordExists = existsSync20(recordPath);
|
|
14702
14914
|
let options = null;
|
|
14703
14915
|
let check = null;
|
|
14704
14916
|
if (recordExists) {
|
|
@@ -14717,7 +14929,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14717
14929
|
return {
|
|
14718
14930
|
state: inspection.state,
|
|
14719
14931
|
reason: inspection.reason,
|
|
14720
|
-
output: portableOutputPath(
|
|
14932
|
+
output: portableOutputPath(path29.resolve(root, inspection.output), root),
|
|
14721
14933
|
record: portableOutputPath(recordPath, root),
|
|
14722
14934
|
recordExists,
|
|
14723
14935
|
outputs,
|
|
@@ -14755,9 +14967,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
14755
14967
|
const media = /* @__PURE__ */ new Map();
|
|
14756
14968
|
const plan = await buildPlan(specs, lock);
|
|
14757
14969
|
const items = [];
|
|
14758
|
-
const manifestText = await
|
|
14970
|
+
const manifestText = await readFile18(loaded.path, "utf8");
|
|
14759
14971
|
const manifestSha256 = sha256(manifestText);
|
|
14760
|
-
const cacheDir =
|
|
14972
|
+
const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
14761
14973
|
let rawStyles = {};
|
|
14762
14974
|
try {
|
|
14763
14975
|
const raw = JSON.parse(manifestText);
|
|
@@ -14781,12 +14993,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
14781
14993
|
)
|
|
14782
14994
|
);
|
|
14783
14995
|
if (spec.source) {
|
|
14784
|
-
const editPath =
|
|
14996
|
+
const editPath = path29.resolve(root, spec.source);
|
|
14785
14997
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
14786
14998
|
const editShas = await Promise.all(
|
|
14787
14999
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
14788
15000
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
14789
|
-
return { file, sha256:
|
|
15001
|
+
return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
|
|
14790
15002
|
})
|
|
14791
15003
|
);
|
|
14792
15004
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -14794,7 +15006,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14794
15006
|
const companion = await readHandEditCompanion(editPath);
|
|
14795
15007
|
if (companion) {
|
|
14796
15008
|
const projectPath = handEditProjectPath(editPath);
|
|
14797
|
-
const project = companion.project &&
|
|
15009
|
+
const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
|
|
14798
15010
|
let projectUrl = null;
|
|
14799
15011
|
if (project) {
|
|
14800
15012
|
const id = galleryMediaId(projectPath);
|
|
@@ -14830,8 +15042,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
14830
15042
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
14831
15043
|
}
|
|
14832
15044
|
} else if (spec.source) {
|
|
14833
|
-
outputs = [await describeOutput(media, root,
|
|
14834
|
-
} else if (!entry &&
|
|
15045
|
+
outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
|
|
15046
|
+
} else if (!entry && existsSync20(spec.outFile)) {
|
|
14835
15047
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
14836
15048
|
} else {
|
|
14837
15049
|
outputs = [];
|
|
@@ -14872,6 +15084,10 @@ async function buildGallerySnapshot(opts) {
|
|
|
14872
15084
|
revision: entry?.revision ?? null,
|
|
14873
15085
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
14874
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,
|
|
14875
15091
|
character: describeCharacter(spec, entry),
|
|
14876
15092
|
outputs,
|
|
14877
15093
|
quality,
|
|
@@ -14936,6 +15152,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14936
15152
|
revision: entry.revision,
|
|
14937
15153
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
14938
15154
|
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
15155
|
+
skeleton: null,
|
|
14939
15156
|
character: describeCharacter(void 0, entry),
|
|
14940
15157
|
outputs,
|
|
14941
15158
|
quality: null,
|
|
@@ -15026,7 +15243,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15026
15243
|
id: loaded.manifest.name,
|
|
15027
15244
|
name: loaded.manifest.name,
|
|
15028
15245
|
manifest: loaded.path,
|
|
15029
|
-
lock:
|
|
15246
|
+
lock: path29.resolve(opts.lockPath),
|
|
15030
15247
|
root,
|
|
15031
15248
|
provider: loaded.manifest.provider,
|
|
15032
15249
|
account: null,
|
|
@@ -15052,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15052
15269
|
return { snapshot, media };
|
|
15053
15270
|
}
|
|
15054
15271
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15055
|
-
const dir =
|
|
15272
|
+
const dir = path29.dirname(path29.resolve(opts.workspacePath));
|
|
15056
15273
|
const media = /* @__PURE__ */ new Map();
|
|
15057
15274
|
const projects = [];
|
|
15058
15275
|
const items = [];
|
|
@@ -15095,11 +15312,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15095
15312
|
id: project.id,
|
|
15096
15313
|
name: loaded.manifest.name,
|
|
15097
15314
|
manifest: manifestPath,
|
|
15098
|
-
lock:
|
|
15315
|
+
lock: path29.resolve(lockPath),
|
|
15099
15316
|
root: loaded.root,
|
|
15100
15317
|
provider: loaded.manifest.provider,
|
|
15101
15318
|
account: project.account ?? null,
|
|
15102
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
15319
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
|
|
15103
15320
|
historyLimit: historyLimit(loaded.manifest),
|
|
15104
15321
|
entries: Object.keys(lock.entries).length,
|
|
15105
15322
|
items: projectItems.length,
|
|
@@ -15111,8 +15328,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15111
15328
|
id: project.id,
|
|
15112
15329
|
name: project.id,
|
|
15113
15330
|
manifest: manifestPath,
|
|
15114
|
-
lock:
|
|
15115
|
-
root:
|
|
15331
|
+
lock: path29.resolve(lockPath),
|
|
15332
|
+
root: path29.dirname(manifestPath),
|
|
15116
15333
|
provider: project.provider,
|
|
15117
15334
|
account: project.account ?? null,
|
|
15118
15335
|
manifestSha256: null,
|
|
@@ -15139,7 +15356,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15139
15356
|
version: 1,
|
|
15140
15357
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15141
15358
|
project: null,
|
|
15142
|
-
workspace: { path:
|
|
15359
|
+
workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
|
|
15143
15360
|
filter,
|
|
15144
15361
|
totals: {
|
|
15145
15362
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15158,7 +15375,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15158
15375
|
// src/gallery/server.ts
|
|
15159
15376
|
import { randomBytes } from "crypto";
|
|
15160
15377
|
import { createServer as createServer2 } from "http";
|
|
15161
|
-
import { readFile as
|
|
15378
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
15162
15379
|
|
|
15163
15380
|
// src/gallery/page.ts
|
|
15164
15381
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -15268,7 +15485,7 @@ async function readJsonBody(req) {
|
|
|
15268
15485
|
async function serveGallery(opts) {
|
|
15269
15486
|
const log2 = opts.onProgress ?? (() => {
|
|
15270
15487
|
});
|
|
15271
|
-
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;
|
|
15272
15489
|
let media = /* @__PURE__ */ new Map();
|
|
15273
15490
|
let loading = null;
|
|
15274
15491
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -15343,6 +15560,17 @@ async function serveGallery(opts) {
|
|
|
15343
15560
|
}
|
|
15344
15561
|
return;
|
|
15345
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
|
+
}
|
|
15346
15574
|
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
15347
15575
|
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
15348
15576
|
try {
|
|
@@ -15399,7 +15627,7 @@ async function serveGallery(opts) {
|
|
|
15399
15627
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
15400
15628
|
if (!asset) return fail(404, "no such review asset");
|
|
15401
15629
|
try {
|
|
15402
|
-
const bytes = await
|
|
15630
|
+
const bytes = await readFile19(asset.path);
|
|
15403
15631
|
res.writeHead(200, {
|
|
15404
15632
|
"Content-Type": asset.contentType,
|
|
15405
15633
|
"Cache-Control": "no-store",
|
|
@@ -15475,7 +15703,7 @@ async function serveGallery(opts) {
|
|
|
15475
15703
|
if (!asset) return fail(404, "no such gallery media");
|
|
15476
15704
|
let bytes;
|
|
15477
15705
|
try {
|
|
15478
|
-
bytes = await
|
|
15706
|
+
bytes = await readFile19(asset.path);
|
|
15479
15707
|
} catch {
|
|
15480
15708
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
15481
15709
|
}
|
|
@@ -15519,7 +15747,7 @@ async function serveGallery(opts) {
|
|
|
15519
15747
|
|
|
15520
15748
|
// src/gallery/generate.ts
|
|
15521
15749
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15522
|
-
import { z as
|
|
15750
|
+
import { z as z10 } from "zod";
|
|
15523
15751
|
|
|
15524
15752
|
// src/pipeline/poll.ts
|
|
15525
15753
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -15930,13 +16158,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
15930
16158
|
}
|
|
15931
16159
|
|
|
15932
16160
|
// src/pick/server.ts
|
|
15933
|
-
import { existsSync as
|
|
15934
|
-
import { readFile as
|
|
16161
|
+
import { existsSync as existsSync22 } from "fs";
|
|
16162
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
15935
16163
|
|
|
15936
16164
|
// src/pipeline/init.ts
|
|
15937
|
-
import { readdir, readFile as
|
|
15938
|
-
import { existsSync as
|
|
15939
|
-
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";
|
|
15940
16168
|
function pngSize(buf) {
|
|
15941
16169
|
if (buf.length < 24) return null;
|
|
15942
16170
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -15950,7 +16178,7 @@ function slugify(value) {
|
|
|
15950
16178
|
async function walk(dir, exclude) {
|
|
15951
16179
|
const out = [];
|
|
15952
16180
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
15953
|
-
const full =
|
|
16181
|
+
const full = path30.join(dir, entry.name);
|
|
15954
16182
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
15955
16183
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
15956
16184
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -15964,14 +16192,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
15964
16192
|
const skipped = [];
|
|
15965
16193
|
const seen = /* @__PURE__ */ new Map();
|
|
15966
16194
|
for (const file of files.sort()) {
|
|
15967
|
-
const size = pngSize(await
|
|
16195
|
+
const size = pngSize(await readFile20(file));
|
|
15968
16196
|
if (!size) {
|
|
15969
16197
|
skipped.push(`${file} (not a readable PNG)`);
|
|
15970
16198
|
continue;
|
|
15971
16199
|
}
|
|
15972
|
-
const rel =
|
|
15973
|
-
const category =
|
|
15974
|
-
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));
|
|
15975
16203
|
if (seen.has(id)) {
|
|
15976
16204
|
const n = seen.get(id) + 1;
|
|
15977
16205
|
seen.set(id, n);
|
|
@@ -16029,10 +16257,10 @@ function mode(values) {
|
|
|
16029
16257
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16030
16258
|
}
|
|
16031
16259
|
async function writeManifestFile(target, manifest) {
|
|
16032
|
-
if (
|
|
16260
|
+
if (existsSync21(target)) {
|
|
16033
16261
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16034
16262
|
}
|
|
16035
|
-
await
|
|
16263
|
+
await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16036
16264
|
}
|
|
16037
16265
|
|
|
16038
16266
|
// src/pick/sheet.ts
|
|
@@ -16445,13 +16673,13 @@ async function currentArt(entry, spec) {
|
|
|
16445
16673
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
16446
16674
|
if (!previous) return null;
|
|
16447
16675
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
16448
|
-
if (!
|
|
16676
|
+
if (!existsSync22(file)) return null;
|
|
16449
16677
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
16450
16678
|
if (!contentType) return null;
|
|
16451
16679
|
let width = entry.width;
|
|
16452
16680
|
let height = entry.height;
|
|
16453
16681
|
if (contentType === "image/png") {
|
|
16454
|
-
const size = pngSize(await
|
|
16682
|
+
const size = pngSize(await readFile21(file));
|
|
16455
16683
|
if (size) ({ width, height } = size);
|
|
16456
16684
|
}
|
|
16457
16685
|
return { path: file, contentType, width, height };
|
|
@@ -16619,26 +16847,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
16619
16847
|
}
|
|
16620
16848
|
|
|
16621
16849
|
// src/gallery/generate.ts
|
|
16622
|
-
var GenerateRequestSchema =
|
|
16623
|
-
project:
|
|
16624
|
-
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),
|
|
16625
16853
|
/**
|
|
16626
16854
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
16627
16855
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
16628
16856
|
* the recorded bytes back over a modified file.
|
|
16629
16857
|
*/
|
|
16630
|
-
force:
|
|
16858
|
+
force: z10.boolean().optional(),
|
|
16631
16859
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
16632
|
-
resume:
|
|
16860
|
+
resume: z10.boolean().optional(),
|
|
16633
16861
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
16634
|
-
refresh:
|
|
16862
|
+
refresh: z10.boolean().optional(),
|
|
16635
16863
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
16636
|
-
restore:
|
|
16864
|
+
restore: z10.boolean().optional(),
|
|
16637
16865
|
/**
|
|
16638
16866
|
* Bring a replaced generation back: 1-based position in the record's
|
|
16639
16867
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
16640
16868
|
*/
|
|
16641
|
-
revert:
|
|
16869
|
+
revert: z10.string().min(1).max(64).optional()
|
|
16642
16870
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
16643
16871
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
16644
16872
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -16948,15 +17176,15 @@ function createGenerateHandlers(opts) {
|
|
|
16948
17176
|
}
|
|
16949
17177
|
|
|
16950
17178
|
// src/gallery/editor.ts
|
|
16951
|
-
import { readFile as
|
|
16952
|
-
import
|
|
17179
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
17180
|
+
import path32 from "path";
|
|
16953
17181
|
|
|
16954
17182
|
// src/editor/install.ts
|
|
16955
17183
|
import { createHash as createHash4 } from "crypto";
|
|
16956
|
-
import { existsSync as
|
|
16957
|
-
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";
|
|
16958
17186
|
import os2 from "os";
|
|
16959
|
-
import
|
|
17187
|
+
import path31 from "path";
|
|
16960
17188
|
|
|
16961
17189
|
// tools/pixelorama-bridge/pin.json
|
|
16962
17190
|
var pin_default = {
|
|
@@ -17020,15 +17248,15 @@ var EditorInstallError = class extends Error {
|
|
|
17020
17248
|
};
|
|
17021
17249
|
function editorToolsRoot() {
|
|
17022
17250
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17023
|
-
if (override) return
|
|
17024
|
-
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");
|
|
17025
17253
|
if (process.platform === "win32") {
|
|
17026
|
-
return
|
|
17254
|
+
return path31.join(process.env.LOCALAPPDATA ?? path31.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17027
17255
|
}
|
|
17028
|
-
return
|
|
17256
|
+
return path31.join(process.env.XDG_CACHE_HOME ?? path31.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17029
17257
|
}
|
|
17030
17258
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17031
|
-
return
|
|
17259
|
+
return path31.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17032
17260
|
}
|
|
17033
17261
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17034
17262
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17038,7 +17266,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17038
17266
|
}
|
|
17039
17267
|
async function sha256Of(file) {
|
|
17040
17268
|
try {
|
|
17041
|
-
return createHash4("sha256").update(await
|
|
17269
|
+
return createHash4("sha256").update(await readFile22(file)).digest("hex");
|
|
17042
17270
|
} catch {
|
|
17043
17271
|
return null;
|
|
17044
17272
|
}
|
|
@@ -17051,8 +17279,8 @@ async function editorStatus(opts = {}) {
|
|
|
17051
17279
|
let totalBytes = 0;
|
|
17052
17280
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17053
17281
|
totalBytes += expected.bytes;
|
|
17054
|
-
const file =
|
|
17055
|
-
const info =
|
|
17282
|
+
const file = path31.join(dir, name);
|
|
17283
|
+
const info = existsSync23(file) ? await stat3(file).catch(() => null) : null;
|
|
17056
17284
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17057
17285
|
installedBytes += expected.bytes;
|
|
17058
17286
|
} else {
|
|
@@ -17081,7 +17309,7 @@ async function installEditor(opts = {}) {
|
|
|
17081
17309
|
);
|
|
17082
17310
|
}
|
|
17083
17311
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17084
|
-
await
|
|
17312
|
+
await mkdir11(dir, { recursive: true });
|
|
17085
17313
|
const downloaded = [];
|
|
17086
17314
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17087
17315
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17109,13 +17337,13 @@ async function installEditor(opts = {}) {
|
|
|
17109
17337
|
if (digest2 !== expected.sha256) {
|
|
17110
17338
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17111
17339
|
}
|
|
17112
|
-
const final =
|
|
17340
|
+
const final = path31.join(dir, name);
|
|
17113
17341
|
const tmp = `${final}.${process.pid}.part`;
|
|
17114
17342
|
try {
|
|
17115
|
-
await
|
|
17343
|
+
await writeFile14(tmp, bytes);
|
|
17116
17344
|
await rename9(tmp, final);
|
|
17117
17345
|
} finally {
|
|
17118
|
-
await
|
|
17346
|
+
await rm10(tmp, { force: true });
|
|
17119
17347
|
}
|
|
17120
17348
|
fetchedBytes += bytes.length;
|
|
17121
17349
|
downloaded.push(name);
|
|
@@ -17155,7 +17383,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17155
17383
|
".json": "application/json; charset=utf-8"
|
|
17156
17384
|
};
|
|
17157
17385
|
function editorContentType(name) {
|
|
17158
|
-
return EDITOR_CONTENT_TYPES[
|
|
17386
|
+
return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
|
|
17159
17387
|
}
|
|
17160
17388
|
|
|
17161
17389
|
// src/gallery/editor.ts
|
|
@@ -17231,7 +17459,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17231
17459
|
if (!status.installed) return null;
|
|
17232
17460
|
let bytes;
|
|
17233
17461
|
try {
|
|
17234
|
-
bytes = await
|
|
17462
|
+
bytes = await readFile23(path32.join(dir, name));
|
|
17235
17463
|
} catch {
|
|
17236
17464
|
verified = null;
|
|
17237
17465
|
return null;
|
|
@@ -17271,13 +17499,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17271
17499
|
};
|
|
17272
17500
|
const loadProject = async (project) => {
|
|
17273
17501
|
const ctx = await access2.loadProject(project);
|
|
17274
|
-
const dir =
|
|
17502
|
+
const dir = path33.dirname(ctx.loaded.path);
|
|
17275
17503
|
const declared = readEnvFiles(dir);
|
|
17276
17504
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
17277
17505
|
for (const [name, value] of Object.entries(declared)) {
|
|
17278
17506
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
17279
17507
|
throw new Error(
|
|
17280
|
-
`${
|
|
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.`
|
|
17281
17509
|
);
|
|
17282
17510
|
}
|
|
17283
17511
|
}
|
|
@@ -17305,6 +17533,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17305
17533
|
),
|
|
17306
17534
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
17307
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 }) } : {},
|
|
17308
17538
|
// The editor exists to write hand edits back, so it follows the write gate.
|
|
17309
17539
|
...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
|
|
17310
17540
|
});
|
|
@@ -17322,8 +17552,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17322
17552
|
}
|
|
17323
17553
|
async function runGallery(args) {
|
|
17324
17554
|
if (args.workspace) {
|
|
17325
|
-
const workspacePath =
|
|
17326
|
-
if (!
|
|
17555
|
+
const workspacePath = path33.resolve(args.workspace);
|
|
17556
|
+
if (!existsSync24(workspacePath)) {
|
|
17327
17557
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
17328
17558
|
}
|
|
17329
17559
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -17343,7 +17573,7 @@ async function runGallery(args) {
|
|
|
17343
17573
|
const registered = async (projectId) => {
|
|
17344
17574
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
17345
17575
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
17346
|
-
return resolveProject(
|
|
17576
|
+
return resolveProject(path33.dirname(workspacePath), project);
|
|
17347
17577
|
};
|
|
17348
17578
|
await serveUntilStopped(initial, build2, args, {
|
|
17349
17579
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -17368,7 +17598,7 @@ async function runGallery(args) {
|
|
|
17368
17598
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
17369
17599
|
};
|
|
17370
17600
|
await serveUntilStopped(await build(), reload, args, {
|
|
17371
|
-
manifestFor: () =>
|
|
17601
|
+
manifestFor: () => path33.resolve(args.manifest),
|
|
17372
17602
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
17373
17603
|
});
|
|
17374
17604
|
}
|
|
@@ -17677,18 +17907,18 @@ async function runRestoreGeneration(args) {
|
|
|
17677
17907
|
}
|
|
17678
17908
|
|
|
17679
17909
|
// src/cli/commands/housekeeping.ts
|
|
17680
|
-
import { existsSync as
|
|
17910
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17681
17911
|
|
|
17682
17912
|
// src/pipeline/cache-health.ts
|
|
17683
|
-
import { existsSync as
|
|
17684
|
-
import { readFile as
|
|
17685
|
-
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";
|
|
17686
17916
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
17687
|
-
if (options.prune && !
|
|
17688
|
-
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)}`);
|
|
17689
17919
|
}
|
|
17690
|
-
const contentDir =
|
|
17691
|
-
const remotePath =
|
|
17920
|
+
const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
|
|
17921
|
+
const remotePath = path34.resolve(cachePathFor(lockPath));
|
|
17692
17922
|
const referenced = referencedHashes(lock);
|
|
17693
17923
|
let content = await inspectContentCache(contentDir, referenced);
|
|
17694
17924
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -17700,7 +17930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17700
17930
|
]);
|
|
17701
17931
|
for (const name of names) {
|
|
17702
17932
|
try {
|
|
17703
|
-
await
|
|
17933
|
+
await rm11(path34.join(contentDir, name), { force: true });
|
|
17704
17934
|
removed.contentFiles++;
|
|
17705
17935
|
} catch {
|
|
17706
17936
|
}
|
|
@@ -17709,7 +17939,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17709
17939
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
17710
17940
|
removed.resetRemoteHashCache = true;
|
|
17711
17941
|
} else if (remoteHashes.invalidIds.length) {
|
|
17712
|
-
const cache = parseCache(JSON.parse(await
|
|
17942
|
+
const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17713
17943
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
17714
17944
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
17715
17945
|
await saveCache(remotePath, cache);
|
|
@@ -17727,7 +17957,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
17727
17957
|
async function inspectContentCache(contentDir, referenced) {
|
|
17728
17958
|
const report = {
|
|
17729
17959
|
path: contentDir,
|
|
17730
|
-
exists:
|
|
17960
|
+
exists: existsSync25(contentDir),
|
|
17731
17961
|
files: 0,
|
|
17732
17962
|
bytes: 0,
|
|
17733
17963
|
valid: 0,
|
|
@@ -17748,12 +17978,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17748
17978
|
continue;
|
|
17749
17979
|
}
|
|
17750
17980
|
report.files++;
|
|
17751
|
-
const file =
|
|
17981
|
+
const file = path34.join(contentDir, entry.name);
|
|
17752
17982
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
17753
17983
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
17754
17984
|
let bytes;
|
|
17755
17985
|
try {
|
|
17756
|
-
bytes = await
|
|
17986
|
+
bytes = await readFile24(file);
|
|
17757
17987
|
report.bytes += bytes.length;
|
|
17758
17988
|
} catch (err) {
|
|
17759
17989
|
report.invalid.push({
|
|
@@ -17791,7 +18021,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
17791
18021
|
async function inspectRemoteHashCache(remotePath) {
|
|
17792
18022
|
const report = {
|
|
17793
18023
|
path: remotePath,
|
|
17794
|
-
exists:
|
|
18024
|
+
exists: existsSync25(remotePath),
|
|
17795
18025
|
entries: 0,
|
|
17796
18026
|
valid: 0,
|
|
17797
18027
|
invalidIds: [],
|
|
@@ -17800,7 +18030,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
17800
18030
|
if (!report.exists) return report;
|
|
17801
18031
|
let cache;
|
|
17802
18032
|
try {
|
|
17803
|
-
cache = parseCache(JSON.parse(await
|
|
18033
|
+
cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
|
|
17804
18034
|
} catch (err) {
|
|
17805
18035
|
report.error = err instanceof Error ? err.message : String(err);
|
|
17806
18036
|
return report;
|
|
@@ -17864,7 +18094,7 @@ async function runAccept(args) {
|
|
|
17864
18094
|
let intact = true;
|
|
17865
18095
|
for (const output of entry.outputs) {
|
|
17866
18096
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
17867
|
-
if (!
|
|
18097
|
+
if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
|
|
17868
18098
|
intact = false;
|
|
17869
18099
|
break;
|
|
17870
18100
|
}
|
|
@@ -17919,42 +18149,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
17919
18149
|
}
|
|
17920
18150
|
|
|
17921
18151
|
// src/cli/commands/init.ts
|
|
17922
|
-
import
|
|
17923
|
-
import { existsSync as
|
|
18152
|
+
import path35 from "path";
|
|
18153
|
+
import { existsSync as existsSync27 } from "fs";
|
|
17924
18154
|
async function runInit(args) {
|
|
17925
18155
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
17926
|
-
const root =
|
|
17927
|
-
if (!
|
|
18156
|
+
const root = path35.resolve(args.from);
|
|
18157
|
+
if (!existsSync27(root)) throw new Error(`No directory at ${root}`);
|
|
17928
18158
|
const generator = args.generator ?? "map";
|
|
17929
18159
|
if (generator !== "1dir" && generator !== "map") {
|
|
17930
18160
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
17931
18161
|
}
|
|
17932
|
-
const target =
|
|
18162
|
+
const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
17933
18163
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
17934
18164
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
17935
18165
|
const manifest = buildManifest(
|
|
17936
|
-
args.name ??
|
|
18166
|
+
args.name ?? path35.basename(path35.dirname(target)),
|
|
17937
18167
|
args.styles[0] ?? "base",
|
|
17938
18168
|
generator,
|
|
17939
|
-
|
|
18169
|
+
path35.relative(path35.dirname(target), root) || ".",
|
|
17940
18170
|
assets
|
|
17941
18171
|
);
|
|
17942
18172
|
await writeManifestFile(target, manifest);
|
|
17943
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
18173
|
+
log(` scanned ${assets.length} PNG(s) under ${path35.relative(process.cwd(), root)}`);
|
|
17944
18174
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
17945
|
-
log(` wrote ${
|
|
18175
|
+
log(` wrote ${path35.relative(process.cwd(), target)}`);
|
|
17946
18176
|
log(`
|
|
17947
18177
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
17948
18178
|
log(` PixelLab account instead of inventing them:`);
|
|
17949
18179
|
log(`
|
|
17950
|
-
pixelkiln adopt --manifest ${
|
|
18180
|
+
pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
|
|
17951
18181
|
`);
|
|
17952
18182
|
}
|
|
17953
18183
|
|
|
17954
18184
|
// src/pipeline/doctor.ts
|
|
17955
|
-
import { constants, existsSync as
|
|
18185
|
+
import { constants, existsSync as existsSync28 } from "fs";
|
|
17956
18186
|
import { access, stat as stat4 } from "fs/promises";
|
|
17957
|
-
import
|
|
18187
|
+
import path36 from "path";
|
|
17958
18188
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
17959
18189
|
const checks = [];
|
|
17960
18190
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -17965,8 +18195,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
17965
18195
|
);
|
|
17966
18196
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
17967
18197
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
17968
|
-
|
|
17969
|
-
...spec.quality ? [
|
|
18198
|
+
path36.dirname(spec.outFile),
|
|
18199
|
+
...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
|
|
17970
18200
|
]))];
|
|
17971
18201
|
const unwritable = [];
|
|
17972
18202
|
for (const dir of dirs) {
|
|
@@ -17989,7 +18219,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
17989
18219
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
17990
18220
|
const output = entry.outputs[index];
|
|
17991
18221
|
const spec = specByKey.get(key);
|
|
17992
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
18222
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path36.dirname(lockPath));
|
|
17993
18223
|
const owner = outputOwners.get(absolute);
|
|
17994
18224
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
17995
18225
|
outputOwners.set(absolute, key);
|
|
@@ -18004,7 +18234,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18004
18234
|
} else {
|
|
18005
18235
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18006
18236
|
}
|
|
18007
|
-
const cacheDir =
|
|
18237
|
+
const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
|
|
18008
18238
|
const stranded = [];
|
|
18009
18239
|
const invalidState = [];
|
|
18010
18240
|
const staleMapUrls = [];
|
|
@@ -18020,11 +18250,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18020
18250
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18021
18251
|
let hasCache = false;
|
|
18022
18252
|
for (const output of entry.outputs) {
|
|
18023
|
-
const cached =
|
|
18253
|
+
const cached = path36.join(
|
|
18024
18254
|
cacheDir,
|
|
18025
18255
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18026
18256
|
);
|
|
18027
|
-
if (
|
|
18257
|
+
if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
|
|
18028
18258
|
hasCache = true;
|
|
18029
18259
|
break;
|
|
18030
18260
|
}
|
|
@@ -18120,16 +18350,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18120
18350
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18121
18351
|
} catch {
|
|
18122
18352
|
}
|
|
18123
|
-
const parent =
|
|
18353
|
+
const parent = path36.dirname(current);
|
|
18124
18354
|
if (parent === current) return current;
|
|
18125
18355
|
current = parent;
|
|
18126
18356
|
}
|
|
18127
18357
|
}
|
|
18128
18358
|
|
|
18129
18359
|
// src/pipeline/audit.ts
|
|
18130
|
-
import { readFile as
|
|
18131
|
-
import { existsSync as
|
|
18132
|
-
import
|
|
18360
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18361
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18362
|
+
import path37 from "path";
|
|
18133
18363
|
function paletteDistance(asset, reference) {
|
|
18134
18364
|
if (!asset.length || !reference.length) return 0;
|
|
18135
18365
|
let total = 0;
|
|
@@ -18165,12 +18395,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18165
18395
|
const unreadable = [];
|
|
18166
18396
|
for (const spec of mine) {
|
|
18167
18397
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18168
|
-
if (!
|
|
18398
|
+
if (!existsSync29(output.absolutePath)) {
|
|
18169
18399
|
missing.push(output.id);
|
|
18170
18400
|
continue;
|
|
18171
18401
|
}
|
|
18172
18402
|
try {
|
|
18173
|
-
const png = decodePng(await
|
|
18403
|
+
const png = decodePng(await readFile25(output.absolutePath));
|
|
18174
18404
|
const palette = extractPalette(png, 12);
|
|
18175
18405
|
assets.push({
|
|
18176
18406
|
assetId: spec.assetId,
|
|
@@ -18194,10 +18424,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18194
18424
|
let referenceFromStyleImages = false;
|
|
18195
18425
|
const refPalettes = [];
|
|
18196
18426
|
for (const img of style.styleImages) {
|
|
18197
|
-
const abs =
|
|
18198
|
-
if (!
|
|
18427
|
+
const abs = path37.resolve(loaded.root, img.path);
|
|
18428
|
+
if (!existsSync29(abs)) continue;
|
|
18199
18429
|
try {
|
|
18200
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
18430
|
+
refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
|
|
18201
18431
|
} catch {
|
|
18202
18432
|
}
|
|
18203
18433
|
}
|
|
@@ -18425,24 +18655,24 @@ async function runAudit(args) {
|
|
|
18425
18655
|
}
|
|
18426
18656
|
|
|
18427
18657
|
// src/cli/commands/pack.ts
|
|
18428
|
-
import
|
|
18429
|
-
import { readFile as
|
|
18658
|
+
import path39 from "path";
|
|
18659
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
18430
18660
|
|
|
18431
18661
|
// src/pipeline/pack.ts
|
|
18432
18662
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18433
|
-
import
|
|
18663
|
+
import path38 from "path";
|
|
18434
18664
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
18435
18665
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
18436
18666
|
if (!Array.isArray(raw) || !raw.length) {
|
|
18437
18667
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
18438
18668
|
}
|
|
18439
|
-
const baseDir =
|
|
18669
|
+
const baseDir = path38.dirname(path38.resolve(inputsFilePath));
|
|
18440
18670
|
return raw.map((entry, i) => {
|
|
18441
18671
|
const e = entry;
|
|
18442
18672
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
18443
18673
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
18444
18674
|
}
|
|
18445
|
-
return { id: e.id, path:
|
|
18675
|
+
return { id: e.id, path: path38.resolve(baseDir, e.path) };
|
|
18446
18676
|
});
|
|
18447
18677
|
}
|
|
18448
18678
|
function packSprites(inputs, options = {}) {
|
|
@@ -18526,7 +18756,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18526
18756
|
}
|
|
18527
18757
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
18528
18758
|
id,
|
|
18529
|
-
path:
|
|
18759
|
+
path: path38.resolve(source)
|
|
18530
18760
|
}));
|
|
18531
18761
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
18532
18762
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -18550,7 +18780,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18550
18780
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
18551
18781
|
const source = sources[id];
|
|
18552
18782
|
if (source && outputs.length <= 1) {
|
|
18553
|
-
inputs.push({ id, path:
|
|
18783
|
+
inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18554
18784
|
continue;
|
|
18555
18785
|
}
|
|
18556
18786
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -18584,7 +18814,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18584
18814
|
}
|
|
18585
18815
|
}
|
|
18586
18816
|
for (const [id, source] of Object.entries(sources)) {
|
|
18587
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
18817
|
+
if (!locked.has(id)) inputs.push({ id, path: path38.resolve(manifestDir, source) });
|
|
18588
18818
|
}
|
|
18589
18819
|
if (!inputs.length) {
|
|
18590
18820
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -18692,7 +18922,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18692
18922
|
const source = sources[id];
|
|
18693
18923
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
18694
18924
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
18695
|
-
placements.push({ id, path:
|
|
18925
|
+
placements.push({ id, path: path38.resolve(manifestDir, source), cell });
|
|
18696
18926
|
continue;
|
|
18697
18927
|
}
|
|
18698
18928
|
if (source && entry) {
|
|
@@ -18725,7 +18955,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18725
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}`
|
|
18726
18956
|
);
|
|
18727
18957
|
}
|
|
18728
|
-
const basePng = mount.base ? readFileSync5(
|
|
18958
|
+
const basePng = mount.base ? readFileSync5(path38.resolve(manifestDir, mount.base)) : void 0;
|
|
18729
18959
|
const mounted = mountSprites(placements, {
|
|
18730
18960
|
cellWidth: mount.cellWidth,
|
|
18731
18961
|
cellHeight: mount.cellHeight,
|
|
@@ -18735,7 +18965,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
18735
18965
|
...mounted,
|
|
18736
18966
|
sources: mount.base ? [{
|
|
18737
18967
|
id: "$base",
|
|
18738
|
-
path:
|
|
18968
|
+
path: path38.resolve(manifestDir, mount.base),
|
|
18739
18969
|
sha256: sha256(basePng),
|
|
18740
18970
|
included: true
|
|
18741
18971
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19139,12 +19369,12 @@ async function runPack(args) {
|
|
|
19139
19369
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19140
19370
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19141
19371
|
}
|
|
19142
|
-
const raw = JSON.parse(await
|
|
19372
|
+
const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
|
|
19143
19373
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19144
19374
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19145
19375
|
const format = sheetFormat(args);
|
|
19146
|
-
const base =
|
|
19147
|
-
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`) });
|
|
19148
19378
|
const outputs = [
|
|
19149
19379
|
{ path: `${base}.png`, data: png },
|
|
19150
19380
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19157,7 +19387,7 @@ async function runPack(args) {
|
|
|
19157
19387
|
log(
|
|
19158
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})`
|
|
19159
19389
|
);
|
|
19160
|
-
log(` ${
|
|
19390
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19161
19391
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19162
19392
|
return;
|
|
19163
19393
|
}
|
|
@@ -19165,7 +19395,7 @@ async function runPack(args) {
|
|
|
19165
19395
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19166
19396
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19167
19397
|
}
|
|
19168
|
-
const manifestDir =
|
|
19398
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19169
19399
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19170
19400
|
for (const styleId of styleIds) {
|
|
19171
19401
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19180,9 +19410,9 @@ async function runPack(args) {
|
|
|
19180
19410
|
sourceOverrides: qualitySources,
|
|
19181
19411
|
sources: manifestSources(loaded.manifest, styleId)
|
|
19182
19412
|
});
|
|
19183
|
-
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`);
|
|
19184
19414
|
const format = sheetFormat(args);
|
|
19185
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19415
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
|
|
19186
19416
|
const outputs = [
|
|
19187
19417
|
{ path: `${base}.png`, data: png },
|
|
19188
19418
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19214,13 +19444,13 @@ async function runPack(args) {
|
|
|
19214
19444
|
log(
|
|
19215
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})`
|
|
19216
19446
|
);
|
|
19217
|
-
log(` ${
|
|
19447
|
+
log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19218
19448
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19219
19449
|
}
|
|
19220
19450
|
}
|
|
19221
19451
|
async function runMount(args) {
|
|
19222
19452
|
const { loaded, lock } = await openProject2(args);
|
|
19223
|
-
const manifestDir =
|
|
19453
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19224
19454
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19225
19455
|
for (const styleId of styleIds) {
|
|
19226
19456
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19259,9 +19489,9 @@ async function runMount(args) {
|
|
|
19259
19489
|
sources,
|
|
19260
19490
|
outputRoles
|
|
19261
19491
|
);
|
|
19262
|
-
const out =
|
|
19492
|
+
const out = path39.resolve(manifestDir, style.mount.out);
|
|
19263
19493
|
const format = sheetFormat(args);
|
|
19264
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19494
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
|
|
19265
19495
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
19266
19496
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
19267
19497
|
const outputs = [
|
|
@@ -19281,7 +19511,7 @@ async function runMount(args) {
|
|
|
19281
19511
|
await provenanceFile("$lock", args.lock),
|
|
19282
19512
|
...qualityRecords,
|
|
19283
19513
|
...artifactSources.filter(
|
|
19284
|
-
(source) => source.id !== "$base" ||
|
|
19514
|
+
(source) => source.id !== "$base" || path39.resolve(source.path) !== out
|
|
19285
19515
|
)
|
|
19286
19516
|
],
|
|
19287
19517
|
options: {
|
|
@@ -19295,14 +19525,14 @@ async function runMount(args) {
|
|
|
19295
19525
|
log(
|
|
19296
19526
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
19297
19527
|
);
|
|
19298
|
-
log(` ${
|
|
19528
|
+
log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
19299
19529
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19300
19530
|
}
|
|
19301
19531
|
}
|
|
19302
19532
|
async function runExport(args) {
|
|
19303
19533
|
const { loaded, specs, lock } = await openProject2(args);
|
|
19304
19534
|
const format = args.format ?? "generic";
|
|
19305
|
-
const manifestDir =
|
|
19535
|
+
const manifestDir = path39.dirname(path39.resolve(args.manifest));
|
|
19306
19536
|
const selected = specs.filter((spec) => {
|
|
19307
19537
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
19308
19538
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -19318,12 +19548,12 @@ async function runExport(args) {
|
|
|
19318
19548
|
for (const spec of selected) {
|
|
19319
19549
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
19320
19550
|
const style = loaded.manifest.styles[spec.styleId];
|
|
19321
|
-
const defaultBase =
|
|
19322
|
-
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;
|
|
19323
19553
|
const result = exportTileset(entry, spec, {
|
|
19324
19554
|
format,
|
|
19325
19555
|
manifestDir,
|
|
19326
|
-
imageName:
|
|
19556
|
+
imageName: path39.basename(`${base}.png`),
|
|
19327
19557
|
columns: args.columns
|
|
19328
19558
|
});
|
|
19329
19559
|
const outputs = [
|
|
@@ -19341,7 +19571,7 @@ async function runExport(args) {
|
|
|
19341
19571
|
asset: spec.assetId,
|
|
19342
19572
|
columns: args.columns ?? null,
|
|
19343
19573
|
format,
|
|
19344
|
-
image:
|
|
19574
|
+
image: path39.basename(`${base}.png`),
|
|
19345
19575
|
providerRules: result.generic.providerRules,
|
|
19346
19576
|
style: spec.styleId,
|
|
19347
19577
|
tileType: spec.tileType ?? null
|
|
@@ -19351,7 +19581,7 @@ async function runExport(args) {
|
|
|
19351
19581
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
19352
19582
|
);
|
|
19353
19583
|
log(
|
|
19354
|
-
` ${
|
|
19584
|
+
` ${path39.relative(process.cwd(), base)}.png + ${path39.basename(base)}${result.extension} + .pixelkiln.json`
|
|
19355
19585
|
);
|
|
19356
19586
|
}
|
|
19357
19587
|
}
|
|
@@ -19364,23 +19594,23 @@ function sheetFormat(args) {
|
|
|
19364
19594
|
}
|
|
19365
19595
|
|
|
19366
19596
|
// src/cli/commands/pixellab-utils.ts
|
|
19367
|
-
import
|
|
19597
|
+
import path41 from "path";
|
|
19368
19598
|
|
|
19369
19599
|
// src/pixellab-utils.ts
|
|
19370
|
-
import { existsSync as
|
|
19371
|
-
import { mkdir as
|
|
19372
|
-
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";
|
|
19373
19603
|
var UNZOOM_MIN_SIDE = 256;
|
|
19374
19604
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
19375
19605
|
function defaultUnzoomOut(input) {
|
|
19376
|
-
const ext =
|
|
19606
|
+
const ext = path40.extname(input);
|
|
19377
19607
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
19378
19608
|
}
|
|
19379
19609
|
async function unzoomFile(client2, input, opts = {}) {
|
|
19380
|
-
const out =
|
|
19381
|
-
if (
|
|
19382
|
-
if (
|
|
19383
|
-
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);
|
|
19384
19614
|
const metadata = imageMetadata(bytes);
|
|
19385
19615
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
19386
19616
|
const { width, height, format } = metadata;
|
|
@@ -19393,22 +19623,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
19393
19623
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
19394
19624
|
}
|
|
19395
19625
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
19396
|
-
await
|
|
19397
|
-
await
|
|
19626
|
+
await mkdir12(path40.dirname(out), { recursive: true });
|
|
19627
|
+
await writeFile15(out, res.png);
|
|
19398
19628
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
19399
19629
|
}
|
|
19400
19630
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
19401
19631
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
19402
19632
|
var FONT_COST_GENERATIONS = 25;
|
|
19403
19633
|
function fontOutputPaths(out) {
|
|
19404
|
-
const base =
|
|
19634
|
+
const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
19405
19635
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
19406
19636
|
}
|
|
19407
19637
|
async function generateFont(client2, opts) {
|
|
19408
19638
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
19409
19639
|
if (!opts.force) {
|
|
19410
19640
|
for (const file of [ttf, atlas]) {
|
|
19411
|
-
if (
|
|
19641
|
+
if (existsSync30(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
19412
19642
|
}
|
|
19413
19643
|
}
|
|
19414
19644
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -19435,9 +19665,9 @@ async function generateFont(client2, opts) {
|
|
|
19435
19665
|
client2.download(job.download_ttf_url),
|
|
19436
19666
|
client2.download(job.download_atlas_url)
|
|
19437
19667
|
]);
|
|
19438
|
-
await
|
|
19439
|
-
await
|
|
19440
|
-
await
|
|
19668
|
+
await mkdir12(path40.dirname(ttf), { recursive: true });
|
|
19669
|
+
await writeFile15(ttf, ttfBytes);
|
|
19670
|
+
await writeFile15(atlas, atlasBytes);
|
|
19441
19671
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
19442
19672
|
}
|
|
19443
19673
|
if (Date.now() - started > timeout) {
|
|
@@ -19451,7 +19681,7 @@ async function generateFont(client2, opts) {
|
|
|
19451
19681
|
|
|
19452
19682
|
// src/cli/commands/pixellab-utils.ts
|
|
19453
19683
|
function loadKeys(args) {
|
|
19454
|
-
loadEnvFiles(
|
|
19684
|
+
loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
|
|
19455
19685
|
loadEnvFiles(process.cwd());
|
|
19456
19686
|
}
|
|
19457
19687
|
async function runUnzoom(args) {
|
|
@@ -19517,19 +19747,19 @@ async function runFont(args) {
|
|
|
19517
19747
|
}
|
|
19518
19748
|
|
|
19519
19749
|
// src/cli/commands/quality.ts
|
|
19520
|
-
import
|
|
19521
|
-
import { readFile as
|
|
19750
|
+
import path43 from "path";
|
|
19751
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
19522
19752
|
|
|
19523
19753
|
// src/pipeline/quality-regression.ts
|
|
19524
|
-
import
|
|
19525
|
-
import { existsSync as
|
|
19526
|
-
import { readFile as
|
|
19527
|
-
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";
|
|
19528
19758
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19529
19759
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
19530
19760
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
19531
|
-
var QualityPathSchema =
|
|
19532
|
-
(value) => !
|
|
19761
|
+
var QualityPathSchema = z11.string().min(1).refine(
|
|
19762
|
+
(value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
|
|
19533
19763
|
"expected a portable relative path"
|
|
19534
19764
|
);
|
|
19535
19765
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -19542,53 +19772,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
19542
19772
|
maxEdgeContrastDrop: 0.03,
|
|
19543
19773
|
maxIsolatedPixelIncrease: 5e-3
|
|
19544
19774
|
};
|
|
19545
|
-
var QualityToleranceSchema =
|
|
19546
|
-
requireExactHash:
|
|
19547
|
-
maxColorCountIncrease:
|
|
19548
|
-
maxNewColors:
|
|
19549
|
-
maxTransparencyDelta:
|
|
19550
|
-
maxPartialAlphaIncrease:
|
|
19551
|
-
maxEdgeDensityDelta:
|
|
19552
|
-
maxEdgeContrastDrop:
|
|
19553
|
-
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)
|
|
19554
19784
|
}).strict();
|
|
19555
|
-
var ImageQualityMetricsSchema =
|
|
19556
|
-
sha256:
|
|
19557
|
-
width:
|
|
19558
|
-
height:
|
|
19559
|
-
colorCount:
|
|
19560
|
-
colors:
|
|
19561
|
-
transparency:
|
|
19562
|
-
partialAlpha:
|
|
19563
|
-
edgeDensity:
|
|
19564
|
-
meanEdgeContrast:
|
|
19565
|
-
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)
|
|
19566
19796
|
}).strict();
|
|
19567
|
-
var QualityBaselineCaseSchema =
|
|
19568
|
-
id:
|
|
19797
|
+
var QualityBaselineCaseSchema = z11.object({
|
|
19798
|
+
id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19569
19799
|
file: QualityPathSchema,
|
|
19570
|
-
record:
|
|
19800
|
+
record: z11.object({
|
|
19571
19801
|
path: QualityPathSchema,
|
|
19572
|
-
sha256:
|
|
19802
|
+
sha256: z11.string().regex(SHA256_RE)
|
|
19573
19803
|
}).strict().optional(),
|
|
19574
19804
|
expected: ImageQualityMetricsSchema,
|
|
19575
19805
|
tolerances: QualityToleranceSchema
|
|
19576
19806
|
}).strict();
|
|
19577
|
-
var QualityBaselineSchema =
|
|
19578
|
-
$schema:
|
|
19579
|
-
format:
|
|
19580
|
-
schemaVersion:
|
|
19581
|
-
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)
|
|
19582
19812
|
}).strict().superRefine((baseline, context) => {
|
|
19583
19813
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
19584
19814
|
if (new Set(ids).size !== ids.length) {
|
|
19585
|
-
context.addIssue({ code:
|
|
19815
|
+
context.addIssue({ code: z11.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
19586
19816
|
}
|
|
19587
19817
|
});
|
|
19588
|
-
var QualityInputSchema =
|
|
19589
|
-
id:
|
|
19590
|
-
path:
|
|
19591
|
-
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(),
|
|
19592
19822
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
19593
19823
|
}).strict();
|
|
19594
19824
|
function message4(error) {
|
|
@@ -19676,10 +19906,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
19676
19906
|
};
|
|
19677
19907
|
}
|
|
19678
19908
|
async function measureImageQuality(file) {
|
|
19679
|
-
const absolute =
|
|
19909
|
+
const absolute = path42.resolve(file);
|
|
19680
19910
|
let bytes;
|
|
19681
19911
|
try {
|
|
19682
|
-
bytes = await
|
|
19912
|
+
bytes = await readFile28(absolute);
|
|
19683
19913
|
} catch (error) {
|
|
19684
19914
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
19685
19915
|
}
|
|
@@ -19690,7 +19920,7 @@ async function measureImageQuality(file) {
|
|
|
19690
19920
|
}
|
|
19691
19921
|
}
|
|
19692
19922
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
19693
|
-
const parsed =
|
|
19923
|
+
const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
|
|
19694
19924
|
if (!parsed.success) {
|
|
19695
19925
|
throw new Error(
|
|
19696
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")
|
|
@@ -19698,11 +19928,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19698
19928
|
}
|
|
19699
19929
|
const ids = parsed.data.map((entry) => entry.id);
|
|
19700
19930
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
19701
|
-
const root =
|
|
19931
|
+
const root = path42.dirname(path42.resolve(inputsFilePath));
|
|
19702
19932
|
return parsed.data.map((entry) => ({
|
|
19703
19933
|
id: entry.id,
|
|
19704
|
-
file:
|
|
19705
|
-
...entry.record ? { record:
|
|
19934
|
+
file: path42.resolve(root, entry.path),
|
|
19935
|
+
...entry.record ? { record: path42.resolve(root, entry.record) } : {},
|
|
19706
19936
|
tolerances: QualityToleranceSchema.parse({
|
|
19707
19937
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
19708
19938
|
...entry.tolerances
|
|
@@ -19710,7 +19940,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
19710
19940
|
}));
|
|
19711
19941
|
}
|
|
19712
19942
|
function portableRelative2(from, to) {
|
|
19713
|
-
return
|
|
19943
|
+
return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
|
|
19714
19944
|
}
|
|
19715
19945
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
19716
19946
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -19725,20 +19955,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
19725
19955
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
19726
19956
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
19727
19957
|
}
|
|
19728
|
-
const outputs = record.outputs.map((output) =>
|
|
19729
|
-
if (!outputs.includes(
|
|
19958
|
+
const outputs = record.outputs.map((output) => path42.resolve(path42.dirname(recordPath), output.path));
|
|
19959
|
+
if (!outputs.includes(path42.resolve(imagePath))) {
|
|
19730
19960
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
19731
19961
|
}
|
|
19732
19962
|
}
|
|
19733
19963
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
19734
19964
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
19735
|
-
const absolute =
|
|
19965
|
+
const absolute = path42.resolve(baselinePath);
|
|
19736
19966
|
for (const input of inputs) {
|
|
19737
|
-
if (
|
|
19967
|
+
if (path42.resolve(input.file) === absolute || input.record && path42.resolve(input.record) === absolute) {
|
|
19738
19968
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
19739
19969
|
}
|
|
19740
19970
|
}
|
|
19741
|
-
const root =
|
|
19971
|
+
const root = path42.dirname(absolute);
|
|
19742
19972
|
const cases = [];
|
|
19743
19973
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
19744
19974
|
const expected = await measureImageQuality(input.file);
|
|
@@ -19765,8 +19995,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19765
19995
|
cases
|
|
19766
19996
|
});
|
|
19767
19997
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
19768
|
-
if (
|
|
19769
|
-
const current = await
|
|
19998
|
+
if (existsSync31(absolute) && !options.force) {
|
|
19999
|
+
const current = await readFile28(absolute);
|
|
19770
20000
|
if (!current.equals(data)) {
|
|
19771
20001
|
throw new OverwriteRefusedError(
|
|
19772
20002
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -19778,10 +20008,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
19778
20008
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
19779
20009
|
}
|
|
19780
20010
|
async function readQualityBaseline(baselinePath) {
|
|
19781
|
-
const absolute =
|
|
20011
|
+
const absolute = path42.resolve(baselinePath);
|
|
19782
20012
|
let raw;
|
|
19783
20013
|
try {
|
|
19784
|
-
raw = JSON.parse(await
|
|
20014
|
+
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
19785
20015
|
} catch (error) {
|
|
19786
20016
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
19787
20017
|
}
|
|
@@ -19851,12 +20081,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
19851
20081
|
return { violations, warnings };
|
|
19852
20082
|
}
|
|
19853
20083
|
async function checkQualityBaseline(baselinePath) {
|
|
19854
|
-
const absolute =
|
|
20084
|
+
const absolute = path42.resolve(baselinePath);
|
|
19855
20085
|
const baseline = await readQualityBaseline(absolute);
|
|
19856
|
-
const root =
|
|
20086
|
+
const root = path42.dirname(absolute);
|
|
19857
20087
|
const cases = [];
|
|
19858
20088
|
for (const entry of baseline.cases) {
|
|
19859
|
-
const file =
|
|
20089
|
+
const file = path42.resolve(root, entry.file);
|
|
19860
20090
|
let actual = null;
|
|
19861
20091
|
const violations = [];
|
|
19862
20092
|
const warnings = [];
|
|
@@ -19869,7 +20099,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
19869
20099
|
violations.push(message4(error));
|
|
19870
20100
|
}
|
|
19871
20101
|
if (entry.record) {
|
|
19872
|
-
const recordPath =
|
|
20102
|
+
const recordPath = path42.resolve(root, entry.record.path);
|
|
19873
20103
|
try {
|
|
19874
20104
|
const recordHash = await sha256File(recordPath);
|
|
19875
20105
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -19911,15 +20141,15 @@ async function runQuality(args) {
|
|
|
19911
20141
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
19912
20142
|
let raw;
|
|
19913
20143
|
try {
|
|
19914
|
-
raw = JSON.parse(await
|
|
20144
|
+
raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
|
|
19915
20145
|
} catch (error) {
|
|
19916
20146
|
throw new Error(
|
|
19917
|
-
`Could not read quality inputs ${
|
|
20147
|
+
`Could not read quality inputs ${path43.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
19918
20148
|
{ cause: error }
|
|
19919
20149
|
);
|
|
19920
20150
|
}
|
|
19921
20151
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
19922
|
-
if (
|
|
20152
|
+
if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
|
|
19923
20153
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
19924
20154
|
}
|
|
19925
20155
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -19932,7 +20162,7 @@ async function runQuality(args) {
|
|
|
19932
20162
|
}, null, 2));
|
|
19933
20163
|
} else {
|
|
19934
20164
|
log(
|
|
19935
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
20165
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path43.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
19936
20166
|
);
|
|
19937
20167
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
19938
20168
|
}
|
|
@@ -19958,54 +20188,54 @@ async function runQuality(args) {
|
|
|
19958
20188
|
}
|
|
19959
20189
|
|
|
19960
20190
|
// src/cli/commands/recipe.ts
|
|
19961
|
-
import
|
|
20191
|
+
import path45 from "path";
|
|
19962
20192
|
|
|
19963
20193
|
// src/recipes.ts
|
|
19964
|
-
import
|
|
19965
|
-
import { existsSync as
|
|
19966
|
-
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";
|
|
19967
20197
|
import { fileURLToPath } from "url";
|
|
19968
|
-
import { z as
|
|
20198
|
+
import { z as z12 } from "zod";
|
|
19969
20199
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
19970
20200
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
19971
20201
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
19972
20202
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
19973
20203
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
19974
|
-
var PortablePathSchema =
|
|
19975
|
-
if (value.includes("\\") ||
|
|
20204
|
+
var PortablePathSchema = z12.string().min(1).refine((value) => {
|
|
20205
|
+
if (value.includes("\\") || path44.posix.isAbsolute(value)) return false;
|
|
19976
20206
|
const parts = value.split("/");
|
|
19977
20207
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
19978
20208
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
19979
|
-
var RecipeBindingSchema =
|
|
19980
|
-
nodeId:
|
|
19981
|
-
input:
|
|
20209
|
+
var RecipeBindingSchema = z12.object({
|
|
20210
|
+
nodeId: z12.string().min(1),
|
|
20211
|
+
input: z12.string().min(1)
|
|
19982
20212
|
}).strict();
|
|
19983
|
-
var RecipeSchema =
|
|
19984
|
-
$schema:
|
|
19985
|
-
format:
|
|
19986
|
-
schemaVersion:
|
|
19987
|
-
id:
|
|
19988
|
-
version:
|
|
19989
|
-
provider:
|
|
19990
|
-
summary:
|
|
19991
|
-
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({
|
|
19992
20222
|
path: PortablePathSchema,
|
|
19993
|
-
role:
|
|
19994
|
-
sha256:
|
|
20223
|
+
role: z12.enum(["workflow", "reference"]),
|
|
20224
|
+
sha256: z12.string().regex(SHA256_RE2)
|
|
19995
20225
|
}).strict()).default([]),
|
|
19996
|
-
models:
|
|
20226
|
+
models: z12.array(z12.object({
|
|
19997
20227
|
path: PortablePathSchema,
|
|
19998
|
-
sha256:
|
|
19999
|
-
source:
|
|
20000
|
-
license:
|
|
20228
|
+
sha256: z12.string().regex(SHA256_RE2),
|
|
20229
|
+
source: z12.string().url(),
|
|
20230
|
+
license: z12.string().min(1)
|
|
20001
20231
|
}).strict()).default([]),
|
|
20002
|
-
styleId:
|
|
20232
|
+
styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20003
20233
|
style: StyleSchema,
|
|
20004
|
-
workflow:
|
|
20234
|
+
workflow: z12.object({
|
|
20005
20235
|
path: PortablePathSchema,
|
|
20006
|
-
outputNodeId:
|
|
20007
|
-
numImages:
|
|
20008
|
-
bindings:
|
|
20236
|
+
outputNodeId: z12.string().min(1),
|
|
20237
|
+
numImages: z12.number().int().min(1).max(16),
|
|
20238
|
+
bindings: z12.object({
|
|
20009
20239
|
prompt: RecipeBindingSchema,
|
|
20010
20240
|
width: RecipeBindingSchema.optional(),
|
|
20011
20241
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20016,67 +20246,67 @@ var RecipeSchema = z11.object({
|
|
|
20016
20246
|
strength: RecipeBindingSchema.optional()
|
|
20017
20247
|
}).strict()
|
|
20018
20248
|
}).strict().optional(),
|
|
20019
|
-
quality:
|
|
20020
|
-
stage:
|
|
20021
|
-
workingCanvas:
|
|
20022
|
-
width:
|
|
20023
|
-
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)
|
|
20024
20254
|
}).strict(),
|
|
20025
|
-
recommendedNativeSize:
|
|
20026
|
-
min:
|
|
20027
|
-
max:
|
|
20255
|
+
recommendedNativeSize: z12.object({
|
|
20256
|
+
min: z12.number().int().min(1),
|
|
20257
|
+
max: z12.number().int().min(1)
|
|
20028
20258
|
}).strict(),
|
|
20029
|
-
paletteColors:
|
|
20030
|
-
min:
|
|
20031
|
-
max:
|
|
20259
|
+
paletteColors: z12.object({
|
|
20260
|
+
min: z12.number().int().min(1),
|
|
20261
|
+
max: z12.number().int().min(1)
|
|
20032
20262
|
}).strict(),
|
|
20033
|
-
background:
|
|
20034
|
-
checks:
|
|
20263
|
+
background: z12.enum(["opaque", "transparent", "full-bleed"]),
|
|
20264
|
+
checks: z12.array(z12.enum([
|
|
20035
20265
|
"prompt-coverage",
|
|
20036
20266
|
"native-grid",
|
|
20037
20267
|
"final-palette",
|
|
20038
20268
|
"alpha-edge",
|
|
20039
20269
|
"human-1x"
|
|
20040
20270
|
])).min(1),
|
|
20041
|
-
notes:
|
|
20271
|
+
notes: z12.array(z12.string().min(1)).default([])
|
|
20042
20272
|
}).strict(),
|
|
20043
|
-
integrity:
|
|
20044
|
-
algorithm:
|
|
20045
|
-
digest:
|
|
20273
|
+
integrity: z12.object({
|
|
20274
|
+
algorithm: z12.literal("sha256"),
|
|
20275
|
+
digest: z12.string().regex(SHA256_RE2)
|
|
20046
20276
|
}).strict()
|
|
20047
20277
|
}).strict().superRefine((recipe, context) => {
|
|
20048
20278
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20049
20279
|
context.addIssue({
|
|
20050
|
-
code:
|
|
20280
|
+
code: z12.ZodIssueCode.custom,
|
|
20051
20281
|
path: ["style", "provider"],
|
|
20052
20282
|
message: "must match the recipe provider"
|
|
20053
20283
|
});
|
|
20054
20284
|
}
|
|
20055
20285
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20056
20286
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20057
|
-
context.addIssue({ code:
|
|
20287
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20058
20288
|
}
|
|
20059
20289
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20060
20290
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20061
|
-
context.addIssue({ code:
|
|
20291
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20062
20292
|
}
|
|
20063
20293
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20064
20294
|
context.addIssue({
|
|
20065
|
-
code:
|
|
20295
|
+
code: z12.ZodIssueCode.custom,
|
|
20066
20296
|
path: ["files"],
|
|
20067
20297
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20068
20298
|
});
|
|
20069
20299
|
}
|
|
20070
20300
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20071
20301
|
context.addIssue({
|
|
20072
|
-
code:
|
|
20302
|
+
code: z12.ZodIssueCode.custom,
|
|
20073
20303
|
path: ["quality", "recommendedNativeSize"],
|
|
20074
20304
|
message: "min cannot exceed max"
|
|
20075
20305
|
});
|
|
20076
20306
|
}
|
|
20077
20307
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20078
20308
|
context.addIssue({
|
|
20079
|
-
code:
|
|
20309
|
+
code: z12.ZodIssueCode.custom,
|
|
20080
20310
|
path: ["quality", "paletteColors"],
|
|
20081
20311
|
message: "min cannot exceed max"
|
|
20082
20312
|
});
|
|
@@ -20085,7 +20315,7 @@ var RecipeSchema = z11.object({
|
|
|
20085
20315
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20086
20316
|
)) {
|
|
20087
20317
|
context.addIssue({
|
|
20088
|
-
code:
|
|
20318
|
+
code: z12.ZodIssueCode.custom,
|
|
20089
20319
|
path: ["workflow", "path"],
|
|
20090
20320
|
message: "must name a file whose role is workflow"
|
|
20091
20321
|
});
|
|
@@ -20093,7 +20323,7 @@ var RecipeSchema = z11.object({
|
|
|
20093
20323
|
if (recipe.provider === "comfyui") {
|
|
20094
20324
|
if (!recipe.workflow) {
|
|
20095
20325
|
context.addIssue({
|
|
20096
|
-
code:
|
|
20326
|
+
code: z12.ZodIssueCode.custom,
|
|
20097
20327
|
path: ["workflow"],
|
|
20098
20328
|
message: "ComfyUI recipes require workflow metadata"
|
|
20099
20329
|
});
|
|
@@ -20102,7 +20332,7 @@ var RecipeSchema = z11.object({
|
|
|
20102
20332
|
const options = recipe.style.providerOptions.comfyui;
|
|
20103
20333
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20104
20334
|
context.addIssue({
|
|
20105
|
-
code:
|
|
20335
|
+
code: z12.ZodIssueCode.custom,
|
|
20106
20336
|
path: ["style", "providerOptions", "comfyui"],
|
|
20107
20337
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20108
20338
|
});
|
|
@@ -20116,7 +20346,7 @@ var RecipeSchema = z11.object({
|
|
|
20116
20346
|
};
|
|
20117
20347
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20118
20348
|
context.addIssue({
|
|
20119
|
-
code:
|
|
20349
|
+
code: z12.ZodIssueCode.custom,
|
|
20120
20350
|
path: ["style", "providerOptions", "comfyui"],
|
|
20121
20351
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20122
20352
|
});
|
|
@@ -20148,10 +20378,10 @@ function message5(error) {
|
|
|
20148
20378
|
return error instanceof Error ? error.message : String(error);
|
|
20149
20379
|
}
|
|
20150
20380
|
async function readRecipeFile(recipePath, bundled) {
|
|
20151
|
-
const absolute =
|
|
20381
|
+
const absolute = path44.resolve(recipePath);
|
|
20152
20382
|
let raw;
|
|
20153
20383
|
try {
|
|
20154
|
-
raw = JSON.parse(await
|
|
20384
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20155
20385
|
} catch (error) {
|
|
20156
20386
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20157
20387
|
}
|
|
@@ -20162,13 +20392,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20162
20392
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20163
20393
|
);
|
|
20164
20394
|
}
|
|
20165
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
20395
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
|
|
20166
20396
|
}
|
|
20167
20397
|
async function walkRecipeFiles(root) {
|
|
20168
|
-
if (!
|
|
20398
|
+
if (!existsSync32(root)) return [];
|
|
20169
20399
|
const out = [];
|
|
20170
20400
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20171
|
-
const resolved =
|
|
20401
|
+
const resolved = path44.join(root, entry.name);
|
|
20172
20402
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20173
20403
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20174
20404
|
}
|
|
@@ -20179,7 +20409,7 @@ function bundledRecipeRoot() {
|
|
|
20179
20409
|
}
|
|
20180
20410
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
20181
20411
|
const recipes = await Promise.all(
|
|
20182
|
-
(await walkRecipeFiles(
|
|
20412
|
+
(await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
20183
20413
|
);
|
|
20184
20414
|
return recipes.sort(
|
|
20185
20415
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -20202,9 +20432,9 @@ function parseSelector(selector) {
|
|
|
20202
20432
|
return { id: match[1], version: match[2] };
|
|
20203
20433
|
}
|
|
20204
20434
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
20205
|
-
const local =
|
|
20206
|
-
if (
|
|
20207
|
-
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);
|
|
20208
20438
|
return readRecipeFile(recipePath, false);
|
|
20209
20439
|
}
|
|
20210
20440
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -20218,8 +20448,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
20218
20448
|
return matches[0];
|
|
20219
20449
|
}
|
|
20220
20450
|
async function verifyFile(root, file) {
|
|
20221
|
-
const absolute =
|
|
20222
|
-
if (!
|
|
20451
|
+
const absolute = path44.resolve(root, ...file.path.split("/"));
|
|
20452
|
+
if (!existsSync32(absolute)) {
|
|
20223
20453
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
20224
20454
|
}
|
|
20225
20455
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -20234,7 +20464,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20234
20464
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20235
20465
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
20236
20466
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
20237
|
-
const modelRoot = options.modelRoot ?
|
|
20467
|
+
const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
|
|
20238
20468
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
20239
20469
|
...await verifyFile(modelRoot, model),
|
|
20240
20470
|
source: model.source,
|
|
@@ -20268,7 +20498,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20268
20498
|
};
|
|
20269
20499
|
}
|
|
20270
20500
|
function renderStyle(style, destination, cwd) {
|
|
20271
|
-
const portableDir =
|
|
20501
|
+
const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
|
|
20272
20502
|
const replace = (value) => {
|
|
20273
20503
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
20274
20504
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -20280,25 +20510,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
20280
20510
|
return replace(style);
|
|
20281
20511
|
}
|
|
20282
20512
|
async function installRecipe(target, options = {}) {
|
|
20283
|
-
const cwd =
|
|
20513
|
+
const cwd = path44.resolve(options.cwd ?? process.cwd());
|
|
20284
20514
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20285
20515
|
const verification = await verifyRecipe(loaded.path);
|
|
20286
20516
|
if (!verification.ok) {
|
|
20287
20517
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
20288
20518
|
}
|
|
20289
|
-
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);
|
|
20290
20520
|
const sources = [
|
|
20291
20521
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
20292
|
-
...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("/")) }))
|
|
20293
20523
|
];
|
|
20294
20524
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
20295
|
-
path:
|
|
20296
|
-
data: await
|
|
20525
|
+
path: path44.join(destination, ...file.path.split("/")),
|
|
20526
|
+
data: await readFile30(file.source)
|
|
20297
20527
|
})));
|
|
20298
20528
|
if (!options.force) {
|
|
20299
20529
|
for (const file of files) {
|
|
20300
|
-
if (!
|
|
20301
|
-
const current = await
|
|
20530
|
+
if (!existsSync32(file.path)) continue;
|
|
20531
|
+
const current = await readFile30(file.path);
|
|
20302
20532
|
if (!current.equals(file.data)) {
|
|
20303
20533
|
throw new OverwriteRefusedError(
|
|
20304
20534
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -20392,14 +20622,14 @@ async function runRecipe(args) {
|
|
|
20392
20622
|
}, null, 2));
|
|
20393
20623
|
} else {
|
|
20394
20624
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
20395
|
-
log(` ${
|
|
20625
|
+
log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
|
|
20396
20626
|
log(`
|
|
20397
20627
|
Add this entry under your manifest's styles object:`);
|
|
20398
20628
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
20399
20629
|
if (result.recipe.models.length) {
|
|
20400
20630
|
log(`
|
|
20401
20631
|
Models are not downloaded automatically. Verify them with:`);
|
|
20402
|
-
log(` pixelkiln recipe verify ${
|
|
20632
|
+
log(` pixelkiln recipe verify ${path45.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
20403
20633
|
}
|
|
20404
20634
|
}
|
|
20405
20635
|
return;
|
|
@@ -20407,7 +20637,7 @@ async function runRecipe(args) {
|
|
|
20407
20637
|
}
|
|
20408
20638
|
|
|
20409
20639
|
// src/cli/commands/refine.ts
|
|
20410
|
-
import
|
|
20640
|
+
import path46 from "path";
|
|
20411
20641
|
async function runRefine(args) {
|
|
20412
20642
|
if (args.from || args.subcommand === "approve") {
|
|
20413
20643
|
if (!args.from) {
|
|
@@ -20437,12 +20667,12 @@ async function runRefine(args) {
|
|
|
20437
20667
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
20438
20668
|
);
|
|
20439
20669
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
20440
|
-
log(` wrote ${
|
|
20441
|
-
log(` quality record: ${
|
|
20670
|
+
log(` wrote ${path46.relative(process.cwd(), result3.output)}`);
|
|
20671
|
+
log(` quality record: ${path46.relative(process.cwd(), result3.record)}`);
|
|
20442
20672
|
log(`
|
|
20443
20673
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
20444
20674
|
log(
|
|
20445
|
-
` pixelkiln refine approve --from ${
|
|
20675
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
20446
20676
|
);
|
|
20447
20677
|
}
|
|
20448
20678
|
return;
|
|
@@ -20470,7 +20700,7 @@ async function runRefine(args) {
|
|
|
20470
20700
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
20471
20701
|
else {
|
|
20472
20702
|
log(
|
|
20473
|
-
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()}`
|
|
20474
20704
|
);
|
|
20475
20705
|
}
|
|
20476
20706
|
return;
|
|
@@ -20479,7 +20709,7 @@ async function runRefine(args) {
|
|
|
20479
20709
|
if (args.json) {
|
|
20480
20710
|
log(JSON.stringify(result2, null, 2));
|
|
20481
20711
|
} else {
|
|
20482
|
-
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)}`);
|
|
20483
20713
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
20484
20714
|
}
|
|
20485
20715
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -20525,7 +20755,7 @@ async function runRefine(args) {
|
|
|
20525
20755
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
20526
20756
|
if (item.state === "needs-approval") {
|
|
20527
20757
|
log(
|
|
20528
|
-
` pixelkiln refine approve --from ${
|
|
20758
|
+
` pixelkiln refine approve --from ${path46.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
20529
20759
|
);
|
|
20530
20760
|
}
|
|
20531
20761
|
}
|
|
@@ -20536,28 +20766,189 @@ async function runRefine(args) {
|
|
|
20536
20766
|
}
|
|
20537
20767
|
|
|
20538
20768
|
// src/cli/commands/skeleton.ts
|
|
20539
|
-
import
|
|
20540
|
-
import {
|
|
20769
|
+
import path47 from "path";
|
|
20770
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20771
|
+
import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
|
|
20772
|
+
|
|
20773
|
+
// src/skeleton-preview.ts
|
|
20774
|
+
var RIGHT = [255, 140, 40, 255];
|
|
20775
|
+
var LEFT = [60, 150, 255, 255];
|
|
20776
|
+
var CENTER = [240, 240, 240, 255];
|
|
20777
|
+
var GROUND = [28, 28, 34, 255];
|
|
20778
|
+
var OUTLINE = [0, 0, 0, 255];
|
|
20779
|
+
var GUTTER = 4;
|
|
20780
|
+
var MIN_CELL = 192;
|
|
20781
|
+
function renderSkeletonSheet(set, options = {}) {
|
|
20782
|
+
const poses = [set.firstFrameKeypoints, ...set.frames];
|
|
20783
|
+
const source = options.source;
|
|
20784
|
+
const scale = source ? Math.max(1, Math.ceil(MIN_CELL / Math.max(source.width, source.height))) : 1;
|
|
20785
|
+
const cellW = source ? source.width * scale : options.cellSize ?? MIN_CELL;
|
|
20786
|
+
const cellH = source ? source.height * scale : options.cellSize ?? MIN_CELL;
|
|
20787
|
+
const columns = Math.min(options.columns ?? 8, poses.length);
|
|
20788
|
+
const rows = Math.ceil(poses.length / columns);
|
|
20789
|
+
const width = columns * cellW + (columns + 1) * GUTTER;
|
|
20790
|
+
const height = rows * cellH + (rows + 1) * GUTTER;
|
|
20791
|
+
const canvas = new Canvas(width, height);
|
|
20792
|
+
canvas.fill(0, 0, width, height, [12, 12, 16, 255]);
|
|
20793
|
+
poses.forEach((pose, index) => {
|
|
20794
|
+
const left = GUTTER + index % columns * (cellW + GUTTER);
|
|
20795
|
+
const top = GUTTER + Math.floor(index / columns) * (cellH + GUTTER);
|
|
20796
|
+
canvas.fill(left, top, cellW, cellH, GROUND);
|
|
20797
|
+
if (source) canvas.drawImage(source, left, top, scale, index === 0 ? 1 : 0.45);
|
|
20798
|
+
drawPose(canvas, pose, left, top, cellW, cellH, Math.max(1, Math.round(Math.min(cellW, cellH) / 128)));
|
|
20799
|
+
});
|
|
20800
|
+
return encodeRgbaPng(width, height, canvas.data);
|
|
20801
|
+
}
|
|
20802
|
+
function drawPose(canvas, pose, left, top, w, h, stroke) {
|
|
20803
|
+
const at = new Map(pose.map((joint) => [joint.label, joint]));
|
|
20804
|
+
const point = (joint) => [left + joint.x * (w - 1), top + joint.y * (h - 1)];
|
|
20805
|
+
for (const outline of [true, false]) {
|
|
20806
|
+
for (const [from, to] of SKELETON_BONES) {
|
|
20807
|
+
const a = at.get(from);
|
|
20808
|
+
const b = at.get(to);
|
|
20809
|
+
if (!a || !b) continue;
|
|
20810
|
+
const [x0, y0] = point(a);
|
|
20811
|
+
const [x1, y1] = point(b);
|
|
20812
|
+
canvas.line(x0, y0, x1, y1, outline ? OUTLINE : colorOf(to), outline ? stroke + 2 : stroke);
|
|
20813
|
+
}
|
|
20814
|
+
for (const joint of pose) {
|
|
20815
|
+
const [x, y] = point(joint);
|
|
20816
|
+
canvas.dot(x, y, outline ? stroke + 3 : stroke + 1, outline ? OUTLINE : colorOf(joint.label));
|
|
20817
|
+
}
|
|
20818
|
+
}
|
|
20819
|
+
}
|
|
20820
|
+
function colorOf(label) {
|
|
20821
|
+
if (label.startsWith("RIGHT")) return RIGHT;
|
|
20822
|
+
if (label.startsWith("LEFT")) return LEFT;
|
|
20823
|
+
return CENTER;
|
|
20824
|
+
}
|
|
20825
|
+
var Canvas = class {
|
|
20826
|
+
constructor(width, height) {
|
|
20827
|
+
this.width = width;
|
|
20828
|
+
this.height = height;
|
|
20829
|
+
this.data = Buffer.alloc(width * height * 4);
|
|
20830
|
+
}
|
|
20831
|
+
width;
|
|
20832
|
+
height;
|
|
20833
|
+
data;
|
|
20834
|
+
set(x, y, [r, g, b, a]) {
|
|
20835
|
+
const px = Math.round(x);
|
|
20836
|
+
const py = Math.round(y);
|
|
20837
|
+
if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
|
|
20838
|
+
const i = (py * this.width + px) * 4;
|
|
20839
|
+
this.data[i] = r;
|
|
20840
|
+
this.data[i + 1] = g;
|
|
20841
|
+
this.data[i + 2] = b;
|
|
20842
|
+
this.data[i + 3] = a;
|
|
20843
|
+
}
|
|
20844
|
+
fill(x, y, w, h, color) {
|
|
20845
|
+
for (let dy = 0; dy < h; dy++) for (let dx = 0; dx < w; dx++) this.set(x + dx, y + dy, color);
|
|
20846
|
+
}
|
|
20847
|
+
/** Nearest-neighbour upscale, composited over what is there, at `strength`. */
|
|
20848
|
+
drawImage(image, left, top, scale, strength) {
|
|
20849
|
+
for (let y = 0; y < image.height; y++) {
|
|
20850
|
+
for (let x = 0; x < image.width; x++) {
|
|
20851
|
+
const i = (y * image.width + x) * 4;
|
|
20852
|
+
const alpha = image.pixels[i + 3] / 255 * strength;
|
|
20853
|
+
if (alpha === 0) continue;
|
|
20854
|
+
const src = [image.pixels[i], image.pixels[i + 1], image.pixels[i + 2]];
|
|
20855
|
+
for (let dy = 0; dy < scale; dy++) {
|
|
20856
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
20857
|
+
const o = ((top + y * scale + dy) * this.width + left + x * scale + dx) * 4;
|
|
20858
|
+
for (let c = 0; c < 3; c++) this.data[o + c] = Math.round(src[c] * alpha + this.data[o + c] * (1 - alpha));
|
|
20859
|
+
}
|
|
20860
|
+
}
|
|
20861
|
+
}
|
|
20862
|
+
}
|
|
20863
|
+
}
|
|
20864
|
+
line(x0, y0, x1, y1, color, stroke) {
|
|
20865
|
+
const steps = Math.max(1, Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0))));
|
|
20866
|
+
for (let step = 0; step <= steps; step++) {
|
|
20867
|
+
const t = step / steps;
|
|
20868
|
+
this.dot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, stroke, color);
|
|
20869
|
+
}
|
|
20870
|
+
}
|
|
20871
|
+
/** A filled square `size` wide centred on (x, y): crisp at pixel-art scale. */
|
|
20872
|
+
dot(x, y, size, color) {
|
|
20873
|
+
const half = Math.floor(size / 2);
|
|
20874
|
+
for (let dy = -half; dy < size - half; dy++) for (let dx = -half; dx < size - half; dx++) this.set(x + dx, y + dy, color);
|
|
20875
|
+
}
|
|
20876
|
+
};
|
|
20877
|
+
|
|
20878
|
+
// src/cli/commands/skeleton.ts
|
|
20541
20879
|
async function runEstimateSkeleton(args) {
|
|
20542
20880
|
const image = args.target;
|
|
20543
20881
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
20544
|
-
|
|
20882
|
+
if (args.out && existsSync33(path47.resolve(args.out)) && !args.force) {
|
|
20883
|
+
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
20884
|
+
}
|
|
20885
|
+
loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
|
|
20545
20886
|
loadEnvFiles(process.cwd());
|
|
20546
|
-
const bytes = await
|
|
20887
|
+
const bytes = await readFile31(path47.resolve(image));
|
|
20547
20888
|
const metadata = imageMetadata(bytes);
|
|
20548
20889
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
20890
|
+
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
20891
|
+
if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
|
|
20549
20892
|
const client2 = clientFromEnv();
|
|
20550
20893
|
const res = await client2.estimateSkeleton({
|
|
20551
20894
|
image: { base64: bytes.toString("base64"), format: metadata.format }
|
|
20552
20895
|
});
|
|
20553
|
-
const
|
|
20896
|
+
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
20897
|
+
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
20554
20898
|
if (args.out) {
|
|
20555
|
-
await
|
|
20556
|
-
log(` wrote ${
|
|
20899
|
+
await writeFile16(path47.resolve(args.out), json + "\n");
|
|
20900
|
+
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
20901
|
+
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
20557
20902
|
} else {
|
|
20558
20903
|
log(json);
|
|
20559
20904
|
}
|
|
20560
20905
|
}
|
|
20906
|
+
async function runSkeletonPreview(args) {
|
|
20907
|
+
const target = args.target;
|
|
20908
|
+
let keypointsFile;
|
|
20909
|
+
let sourceFile;
|
|
20910
|
+
let label;
|
|
20911
|
+
if (target.endsWith(".json")) {
|
|
20912
|
+
keypointsFile = path47.resolve(target);
|
|
20913
|
+
sourceFile = args.from ? path47.resolve(args.from) : void 0;
|
|
20914
|
+
label = target;
|
|
20915
|
+
} else {
|
|
20916
|
+
const loaded = await loadManifest(args.manifest);
|
|
20917
|
+
const specs = (await resolveSpecs(loaded, { styles: args.styles, assets: [target] })).filter((spec2) => spec2.assetId === target);
|
|
20918
|
+
const spec = specs.find((candidate) => candidate.revision?.mode === "animate-skeleton");
|
|
20919
|
+
if (!spec) {
|
|
20920
|
+
throw new UsageError(
|
|
20921
|
+
specs.length ? `${target} is not an animate-skeleton revision; pass a keypoints file instead` : `No asset "${target}" in ${args.manifest}`
|
|
20922
|
+
);
|
|
20923
|
+
}
|
|
20924
|
+
keypointsFile = spec.revision.keypointsFile;
|
|
20925
|
+
sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
|
|
20926
|
+
label = `${spec.styleId}/${spec.assetId}`;
|
|
20927
|
+
}
|
|
20928
|
+
if (!existsSync33(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
20929
|
+
let json;
|
|
20930
|
+
try {
|
|
20931
|
+
json = JSON.parse(await readFile31(keypointsFile, "utf8"));
|
|
20932
|
+
} catch (error) {
|
|
20933
|
+
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
20934
|
+
}
|
|
20935
|
+
const set = parseSkeletonSet(json, keypointsFile);
|
|
20936
|
+
let source;
|
|
20937
|
+
if (sourceFile) {
|
|
20938
|
+
if (!existsSync33(sourceFile)) {
|
|
20939
|
+
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
20940
|
+
} else {
|
|
20941
|
+
const bytes = await readFile31(sourceFile);
|
|
20942
|
+
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
20943
|
+
source = decodePng(bytes);
|
|
20944
|
+
}
|
|
20945
|
+
}
|
|
20946
|
+
const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
20947
|
+
await writeFile16(out, renderSkeletonSheet(set, { source }));
|
|
20948
|
+
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
20949
|
+
const shown = path47.relative(process.cwd(), out);
|
|
20950
|
+
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
20951
|
+
}
|
|
20561
20952
|
|
|
20562
20953
|
// src/cli/commands/tools.ts
|
|
20563
20954
|
async function runTools(args) {
|
|
@@ -20601,15 +20992,15 @@ async function runTools(args) {
|
|
|
20601
20992
|
}
|
|
20602
20993
|
|
|
20603
20994
|
// src/cli/commands/workspace.ts
|
|
20604
|
-
import
|
|
20605
|
-
import { existsSync as
|
|
20995
|
+
import path48 from "path";
|
|
20996
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20606
20997
|
async function runWorkspace(args) {
|
|
20607
|
-
const workspacePath =
|
|
20608
|
-
const dir =
|
|
20998
|
+
const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
20999
|
+
const dir = path48.dirname(workspacePath);
|
|
20609
21000
|
if (args.subcommand === "add") {
|
|
20610
|
-
const manifestPath =
|
|
21001
|
+
const manifestPath = path48.resolve(args.target);
|
|
20611
21002
|
const loadedTarget = await loadManifest(manifestPath);
|
|
20612
|
-
const lockPath = args.explicitLock ?
|
|
21003
|
+
const lockPath = args.explicitLock ? path48.resolve(args.explicitLock) : path48.join(path48.dirname(manifestPath), "pixelkiln.lock.json");
|
|
20613
21004
|
const ws = await loadWorkspace(workspacePath);
|
|
20614
21005
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
20615
21006
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -20629,10 +21020,10 @@ async function runWorkspace(args) {
|
|
|
20629
21020
|
...args.account ? { account: args.account } : {}
|
|
20630
21021
|
};
|
|
20631
21022
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
20632
|
-
log(` registered "${id}" in ${
|
|
21023
|
+
log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20633
21024
|
log(` manifest: ${project.manifest}`);
|
|
20634
21025
|
log(` lock: ${project.lock}`);
|
|
20635
|
-
if (!
|
|
21026
|
+
if (!existsSync34(lockPath)) {
|
|
20636
21027
|
log(
|
|
20637
21028
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
20638
21029
|
);
|
|
@@ -20641,7 +21032,7 @@ async function runWorkspace(args) {
|
|
|
20641
21032
|
}
|
|
20642
21033
|
if (args.subcommand === "remove") {
|
|
20643
21034
|
const ws = await loadWorkspace(workspacePath);
|
|
20644
|
-
const resolvedTarget =
|
|
21035
|
+
const resolvedTarget = path48.resolve(args.target);
|
|
20645
21036
|
const match = ws.projects.find(
|
|
20646
21037
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
20647
21038
|
);
|
|
@@ -20652,11 +21043,11 @@ async function runWorkspace(args) {
|
|
|
20652
21043
|
version: 1,
|
|
20653
21044
|
projects: ws.projects.filter((p) => p !== match)
|
|
20654
21045
|
});
|
|
20655
|
-
log(` removed "${match.id}" from ${
|
|
21046
|
+
log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20656
21047
|
return;
|
|
20657
21048
|
}
|
|
20658
21049
|
if (args.subcommand === "list") {
|
|
20659
|
-
if (!
|
|
21050
|
+
if (!existsSync34(workspacePath)) {
|
|
20660
21051
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20661
21052
|
}
|
|
20662
21053
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20664,9 +21055,9 @@ async function runWorkspace(args) {
|
|
|
20664
21055
|
if (args.json) {
|
|
20665
21056
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
20666
21057
|
} else if (!ws.projects.length) {
|
|
20667
|
-
log(` no projects registered in ${
|
|
21058
|
+
log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20668
21059
|
} else {
|
|
20669
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21060
|
+
log(` ${ws.projects.length} project(s) in ${path48.relative(process.cwd(), workspacePath)}:`);
|
|
20670
21061
|
for (const p of ws.projects) {
|
|
20671
21062
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
20672
21063
|
}
|
|
@@ -20676,7 +21067,7 @@ async function runWorkspace(args) {
|
|
|
20676
21067
|
return;
|
|
20677
21068
|
}
|
|
20678
21069
|
if (args.subcommand === "status") {
|
|
20679
|
-
if (!
|
|
21070
|
+
if (!existsSync34(workspacePath)) {
|
|
20680
21071
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20681
21072
|
}
|
|
20682
21073
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20684,7 +21075,7 @@ async function runWorkspace(args) {
|
|
|
20684
21075
|
if (args.json) {
|
|
20685
21076
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
20686
21077
|
} else {
|
|
20687
|
-
log(` workspace: ${
|
|
21078
|
+
log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
|
|
20688
21079
|
for (const p of report.projects) {
|
|
20689
21080
|
if (p.error) {
|
|
20690
21081
|
log(`
|
|
@@ -20738,8 +21129,8 @@ async function runHelp() {
|
|
|
20738
21129
|
async function runVersion() {
|
|
20739
21130
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
20740
21131
|
const url = new URL(up, import.meta.url);
|
|
20741
|
-
if (!
|
|
20742
|
-
const pkg = JSON.parse(await
|
|
21132
|
+
if (!existsSync35(url)) continue;
|
|
21133
|
+
const pkg = JSON.parse(await readFile32(url, "utf8"));
|
|
20743
21134
|
if (pkg.name === "pixelkiln") {
|
|
20744
21135
|
log(`${pkg.name} ${pkg.version}`);
|
|
20745
21136
|
return;
|
|
@@ -20784,6 +21175,7 @@ var RUNNERS = {
|
|
|
20784
21175
|
recipe: runRecipe,
|
|
20785
21176
|
workspace: runWorkspace,
|
|
20786
21177
|
"estimate-skeleton": runEstimateSkeleton,
|
|
21178
|
+
"skeleton-preview": runSkeletonPreview,
|
|
20787
21179
|
unzoom: runUnzoom,
|
|
20788
21180
|
font: runFont
|
|
20789
21181
|
};
|