pixelkiln 0.71.0 → 0.73.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/PROVIDERS.md +1 -1
- package/dist/cli.js +732 -500
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +17 -5
- package/dist/index.cjs +436 -250
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1939 -291
- package/dist/index.d.ts +1939 -291
- package/dist/index.js +419 -233
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +13 -0
- package/docs/GENERATORS.md +2 -2
- package/docs/MANIFEST.md +1 -1
- package/package.json +1 -1
- package/schema/manifest.schema.json +1 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +6 -0
- package/skills/pixelkiln/references/pixellab.md +8 -1
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
70
|
+
import { existsSync as existsSync37 } from "fs";
|
|
71
71
|
import { readFile as readFile32 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
@@ -665,10 +665,10 @@ import { createReadStream } from "fs";
|
|
|
665
665
|
function sha256(data) {
|
|
666
666
|
return createHash("sha256").update(data).digest("hex");
|
|
667
667
|
}
|
|
668
|
-
async function sha256File(
|
|
668
|
+
async function sha256File(path50) {
|
|
669
669
|
return new Promise((resolve, reject) => {
|
|
670
670
|
const hash = createHash("sha256");
|
|
671
|
-
const stream = createReadStream(
|
|
671
|
+
const stream = createReadStream(path50);
|
|
672
672
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
673
673
|
stream.on("error", reject);
|
|
674
674
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -934,7 +934,7 @@ var CharacterProportionsSchema = z.union([
|
|
|
934
934
|
hipWidth: z.number().min(0.5).max(2).optional()
|
|
935
935
|
}).strict()
|
|
936
936
|
]);
|
|
937
|
-
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
937
|
+
var CharacterAnimationModeSchema = z.enum(["template", "skeleton-v3", "v3", "pro"]);
|
|
938
938
|
var UiPieceSchema = z.discriminatedUnion("kind", [
|
|
939
939
|
z.object({
|
|
940
940
|
id: z.string().min(1),
|
|
@@ -1048,7 +1048,11 @@ var CharacterAnimationSchema = z.object({
|
|
|
1048
1048
|
fps: z.number().int().min(1).max(60).default(8),
|
|
1049
1049
|
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
1050
1050
|
keepFirstFrame: z.boolean().default(true),
|
|
1051
|
-
/**
|
|
1051
|
+
/**
|
|
1052
|
+
* `template` when a template is named, otherwise `v3`; `skeleton-v3`
|
|
1053
|
+
* poses a template with PixelLab's skeleton model instead; `pro` for the
|
|
1054
|
+
* sequential high-quality engine.
|
|
1055
|
+
*/
|
|
1052
1056
|
mode: CharacterAnimationModeSchema.optional(),
|
|
1053
1057
|
/**
|
|
1054
1058
|
* v3 only: a manifest-relative image of the pose to start from, instead
|
|
@@ -1085,7 +1089,7 @@ var CharacterAnimationSchema = z.object({
|
|
|
1085
1089
|
if (animation[key] !== void 0 && mode2 !== "v3") {
|
|
1086
1090
|
context.addIssue({
|
|
1087
1091
|
code: z.ZodIssueCode.custom,
|
|
1088
|
-
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
|
|
1092
|
+
message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" || mode2 === "skeleton-v3" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
|
|
1089
1093
|
path: [key]
|
|
1090
1094
|
});
|
|
1091
1095
|
}
|
|
@@ -1095,10 +1099,10 @@ var CharacterAnimationSchema = z.object({
|
|
|
1095
1099
|
context.addIssue({ code: z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
|
|
1096
1100
|
}
|
|
1097
1101
|
}
|
|
1098
|
-
if (animation.template && animation.mode && animation.mode !== "template") {
|
|
1102
|
+
if (animation.template && animation.mode && animation.mode !== "template" && animation.mode !== "skeleton-v3") {
|
|
1099
1103
|
context.addIssue({
|
|
1100
1104
|
code: z.ZodIssueCode.custom,
|
|
1101
|
-
message: `a template animation cannot use mode "${animation.mode}"`,
|
|
1105
|
+
message: `a template animation cannot use mode "${animation.mode}"; use "template" or "skeleton-v3"`,
|
|
1102
1106
|
path: ["mode"]
|
|
1103
1107
|
});
|
|
1104
1108
|
}
|
|
@@ -1109,8 +1113,8 @@ var CharacterAnimationSchema = z.object({
|
|
|
1109
1113
|
path: ["frames"]
|
|
1110
1114
|
});
|
|
1111
1115
|
}
|
|
1112
|
-
if (!animation.template && animation.mode === "template") {
|
|
1113
|
-
context.addIssue({ code: z.ZodIssueCode.custom, message:
|
|
1116
|
+
if (!animation.template && (animation.mode === "template" || animation.mode === "skeleton-v3")) {
|
|
1117
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: `${animation.mode} mode needs a template`, path: ["template"] });
|
|
1114
1118
|
}
|
|
1115
1119
|
});
|
|
1116
1120
|
var RevisionDitheringSchema = z.enum(["none", "2x2", "4x4", "8x8"]);
|
|
@@ -1806,7 +1810,7 @@ var AssetSchema = z.object({
|
|
|
1806
1810
|
path: ["mirror"]
|
|
1807
1811
|
});
|
|
1808
1812
|
}
|
|
1809
|
-
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
|
|
1813
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
|
|
1810
1814
|
context.addIssue({
|
|
1811
1815
|
code: z.ZodIssueCode.custom,
|
|
1812
1816
|
message: "Required",
|
|
@@ -6163,9 +6167,9 @@ var PixelLabClient = class {
|
|
|
6163
6167
|
apiKey;
|
|
6164
6168
|
http;
|
|
6165
6169
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6166
|
-
async request(
|
|
6170
|
+
async request(path50, init) {
|
|
6167
6171
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6168
|
-
const res = await this.http(`${BASE}${
|
|
6172
|
+
const res = await this.http(`${BASE}${path50}`, {
|
|
6169
6173
|
...init,
|
|
6170
6174
|
headers: {
|
|
6171
6175
|
Authorization: auth,
|
|
@@ -6175,13 +6179,13 @@ var PixelLabClient = class {
|
|
|
6175
6179
|
});
|
|
6176
6180
|
const text = await res.text();
|
|
6177
6181
|
if (!res.ok) {
|
|
6178
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6182
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path50} \u2192 ${res.status}`, res.status, text);
|
|
6179
6183
|
}
|
|
6180
6184
|
if (!text) return {};
|
|
6181
6185
|
try {
|
|
6182
6186
|
return JSON.parse(text);
|
|
6183
6187
|
} catch {
|
|
6184
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6188
|
+
throw new Error(`${init?.method ?? "GET"} ${path50} returned invalid JSON`);
|
|
6185
6189
|
}
|
|
6186
6190
|
}
|
|
6187
6191
|
async balance() {
|
|
@@ -6573,10 +6577,10 @@ var PixelLabClient = class {
|
|
|
6573
6577
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6574
6578
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6575
6579
|
const south = args.reference?.south;
|
|
6576
|
-
let
|
|
6580
|
+
let path50;
|
|
6577
6581
|
let body;
|
|
6578
6582
|
if (args.mode === "v3") {
|
|
6579
|
-
|
|
6583
|
+
path50 = "/create-character-v3";
|
|
6580
6584
|
body = {
|
|
6581
6585
|
description: args.description,
|
|
6582
6586
|
image_size: imageSize,
|
|
@@ -6590,7 +6594,7 @@ var PixelLabClient = class {
|
|
|
6590
6594
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6591
6595
|
};
|
|
6592
6596
|
} else if (args.mode === "pro-flash") {
|
|
6593
|
-
|
|
6597
|
+
path50 = "/create-character-pro-flash";
|
|
6594
6598
|
const traits = args.styleTraits;
|
|
6595
6599
|
body = {
|
|
6596
6600
|
description: args.description,
|
|
@@ -6614,7 +6618,7 @@ var PixelLabClient = class {
|
|
|
6614
6618
|
} : {}
|
|
6615
6619
|
};
|
|
6616
6620
|
} else if (args.mode === "pro") {
|
|
6617
|
-
|
|
6621
|
+
path50 = "/create-character-pro";
|
|
6618
6622
|
body = {
|
|
6619
6623
|
description: args.description,
|
|
6620
6624
|
image_size: imageSize,
|
|
@@ -6628,7 +6632,7 @@ var PixelLabClient = class {
|
|
|
6628
6632
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6629
6633
|
};
|
|
6630
6634
|
} else {
|
|
6631
|
-
|
|
6635
|
+
path50 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6632
6636
|
body = {
|
|
6633
6637
|
description: args.description,
|
|
6634
6638
|
image_size: imageSize,
|
|
@@ -6645,8 +6649,8 @@ var PixelLabClient = class {
|
|
|
6645
6649
|
...palette
|
|
6646
6650
|
};
|
|
6647
6651
|
}
|
|
6648
|
-
const raw = await this.request(
|
|
6649
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6652
|
+
const raw = await this.request(path50, { method: "POST", body: JSON.stringify(body) });
|
|
6653
|
+
return validateResponse(CharacterSubmitSchema, raw, path50.slice(1));
|
|
6650
6654
|
}
|
|
6651
6655
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6652
6656
|
async createCharacterState(args) {
|
|
@@ -6666,12 +6670,20 @@ var PixelLabClient = class {
|
|
|
6666
6670
|
}
|
|
6667
6671
|
/**
|
|
6668
6672
|
* One animation of one character. Template mode costs a generation per
|
|
6669
|
-
* direction and fixes the frame count; v3
|
|
6670
|
-
* the
|
|
6673
|
+
* direction and fixes the frame count; skeleton-v3 poses the same template
|
|
6674
|
+
* with the skeleton video model (2-4 generations per direction); v3 draws
|
|
6675
|
+
* `frameCount` frames from the action text; pro is the sequential engine.
|
|
6676
|
+
* One job per direction.
|
|
6677
|
+
*
|
|
6678
|
+
* Every mode but skeleton-v3 goes to `/animate-character`, the path whose
|
|
6679
|
+
* shapes were measured live. skeleton-v3 is documented only on
|
|
6680
|
+
* `/characters/animations`, which takes the same request and answers with
|
|
6681
|
+
* the same job list (plus an `animation_group_id`).
|
|
6671
6682
|
*/
|
|
6672
6683
|
async animateCharacter(args) {
|
|
6673
6684
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6674
|
-
const
|
|
6685
|
+
const endpoint = args.mode === "skeleton-v3" ? "/characters/animations" : "/animate-character";
|
|
6686
|
+
const raw = await this.request(endpoint, {
|
|
6675
6687
|
method: "POST",
|
|
6676
6688
|
body: JSON.stringify({
|
|
6677
6689
|
character_id: args.characterId,
|
|
@@ -6695,7 +6707,7 @@ var PixelLabClient = class {
|
|
|
6695
6707
|
...args.seed != null ? { seed: args.seed } : {}
|
|
6696
6708
|
})
|
|
6697
6709
|
});
|
|
6698
|
-
return validateResponse(AnimateSubmitSchema, raw,
|
|
6710
|
+
return validateResponse(AnimateSubmitSchema, raw, endpoint.slice(1));
|
|
6699
6711
|
}
|
|
6700
6712
|
/**
|
|
6701
6713
|
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
@@ -7291,6 +7303,7 @@ function characterCost(spec) {
|
|
|
7291
7303
|
if (character.kind === "animation") {
|
|
7292
7304
|
const animation = character.animation;
|
|
7293
7305
|
if (animation.mode === "template") return 1;
|
|
7306
|
+
if (animation.mode === "skeleton-v3") return 4;
|
|
7294
7307
|
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
7295
7308
|
return generationCost(spec.width, spec.height, "1dir");
|
|
7296
7309
|
}
|
|
@@ -7299,7 +7312,11 @@ function characterCost(spec) {
|
|
|
7299
7312
|
return 20;
|
|
7300
7313
|
}
|
|
7301
7314
|
if (character.mode === "standard") return 1;
|
|
7302
|
-
if (character.mode === "v3")
|
|
7315
|
+
if (character.mode === "v3") {
|
|
7316
|
+
const south = character.reference?.south;
|
|
7317
|
+
if (south) return Math.max(1, Math.ceil(south.width * south.height * 8 / 65536));
|
|
7318
|
+
return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
7319
|
+
}
|
|
7303
7320
|
if (character.mode === "pro-flash") {
|
|
7304
7321
|
const south = character.reference?.south;
|
|
7305
7322
|
return south ? proFlashCharacterCost(south.width, south.height, true) : proFlashCharacterCost(spec.width, spec.height, false);
|
|
@@ -7883,7 +7900,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7883
7900
|
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
7884
7901
|
);
|
|
7885
7902
|
}
|
|
7886
|
-
if (character.animation.mode !== "template" && !spec.prompt.trim()) {
|
|
7903
|
+
if (character.animation.mode !== "template" && character.animation.mode !== "skeleton-v3" && !spec.prompt.trim()) {
|
|
7887
7904
|
throw new Error(`${spec.styleId}/${spec.assetId} needs a prompt describing the motion, or a template`);
|
|
7888
7905
|
}
|
|
7889
7906
|
}
|
|
@@ -9493,9 +9510,9 @@ var RetroDiffusionClient = class {
|
|
|
9493
9510
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9494
9511
|
return Buffer.from(await response.arrayBuffer());
|
|
9495
9512
|
}
|
|
9496
|
-
async call(
|
|
9513
|
+
async call(path50, init = {}) {
|
|
9497
9514
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9498
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9515
|
+
const response = await this.request(`${this.baseUrl}${path50}`, {
|
|
9499
9516
|
...init,
|
|
9500
9517
|
headers: {
|
|
9501
9518
|
"Content-Type": "application/json",
|
|
@@ -11400,8 +11417,8 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11400
11417
|
);
|
|
11401
11418
|
}
|
|
11402
11419
|
}
|
|
11403
|
-
if (asset.animation.mode === "template") {
|
|
11404
|
-
throw new Error(`assets.${assetId}: objectPro animations have no "
|
|
11420
|
+
if (asset.animation.mode === "template" || asset.animation.mode === "skeleton-v3") {
|
|
11421
|
+
throw new Error(`assets.${assetId}: objectPro animations have no "${asset.animation.mode}" mode; use "v3" or "pro"`);
|
|
11405
11422
|
}
|
|
11406
11423
|
const pose = async (rel, what) => {
|
|
11407
11424
|
const image = await files.load(rel, what);
|
|
@@ -11800,10 +11817,10 @@ function isSha256Hash(value) {
|
|
|
11800
11817
|
function parseCache(value) {
|
|
11801
11818
|
return HashCacheSchema.parse(value);
|
|
11802
11819
|
}
|
|
11803
|
-
async function loadCache(
|
|
11804
|
-
if (!existsSync10(
|
|
11820
|
+
async function loadCache(path50) {
|
|
11821
|
+
if (!existsSync10(path50)) return { version: 1, hashes: {} };
|
|
11805
11822
|
try {
|
|
11806
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
11823
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path50, "utf8")));
|
|
11807
11824
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11808
11825
|
return {
|
|
11809
11826
|
version: 1,
|
|
@@ -11815,18 +11832,18 @@ async function loadCache(path49) {
|
|
|
11815
11832
|
return { version: 1, hashes: {} };
|
|
11816
11833
|
}
|
|
11817
11834
|
}
|
|
11818
|
-
async function saveCache(
|
|
11835
|
+
async function saveCache(path50, cache) {
|
|
11819
11836
|
const sorted = {};
|
|
11820
11837
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11821
11838
|
const hash = cache.hashes[key];
|
|
11822
11839
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11823
11840
|
sorted[key] = hash;
|
|
11824
11841
|
}
|
|
11825
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11826
|
-
const tmp = `${
|
|
11842
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path50)), { recursive: true });
|
|
11843
|
+
const tmp = `${path50}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11827
11844
|
try {
|
|
11828
11845
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11829
|
-
await rename5(tmp,
|
|
11846
|
+
await rename5(tmp, path50);
|
|
11830
11847
|
} finally {
|
|
11831
11848
|
await rm5(tmp, { force: true });
|
|
11832
11849
|
}
|
|
@@ -13813,13 +13830,16 @@ var NewAnimationSchema = z6.object({
|
|
|
13813
13830
|
direction: CharacterDirectionSchema.optional(),
|
|
13814
13831
|
frames: z6.number().int().min(4).max(16).optional(),
|
|
13815
13832
|
fps: z6.number().int().min(1).max(60).optional(),
|
|
13833
|
+
/** One loop per named direction, plus free mirrors; expanded when the manifest loads. */
|
|
13834
|
+
directions: z6.array(CharacterDirectionSchema).min(1).optional(),
|
|
13816
13835
|
mode: CharacterAnimationModeSchema.optional(),
|
|
13817
13836
|
/** What is being animated, when the parent's own description would mislead the model (`character` only). */
|
|
13818
13837
|
subject: z6.string().min(1).optional(),
|
|
13819
13838
|
enhancePrompt: z6.boolean().optional()
|
|
13820
13839
|
}).strict();
|
|
13821
13840
|
var NewAssetSchema = z6.object({
|
|
13822
|
-
|
|
13841
|
+
/** A portrait, an outfit, or a mirror has none of its own; the loader says where one is needed. */
|
|
13842
|
+
prompt: z6.string().optional(),
|
|
13823
13843
|
category: z6.string().optional(),
|
|
13824
13844
|
width: z6.number().int().optional(),
|
|
13825
13845
|
height: z6.number().int().optional(),
|
|
@@ -13854,7 +13874,24 @@ var NewAssetSchema = z6.object({
|
|
|
13854
13874
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
13855
13875
|
state: NewStateSchema.optional(),
|
|
13856
13876
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
13857
|
-
animation: NewAnimationSchema.optional()
|
|
13877
|
+
animation: NewAnimationSchema.optional(),
|
|
13878
|
+
/** A bust of an existing character. */
|
|
13879
|
+
portrait: CharacterPortraitSchema.optional(),
|
|
13880
|
+
/** An existing loop re-clothed from a reference image (uploaded first). */
|
|
13881
|
+
outfit: CharacterOutfitSchema.optional(),
|
|
13882
|
+
/** Another asset of the same style, flipped; free. */
|
|
13883
|
+
mirror: z6.string().min(1).optional(),
|
|
13884
|
+
/**
|
|
13885
|
+
* `character`/`objectPro` bases: the subject's own sprite (uploaded
|
|
13886
|
+
* first), or one per direction, which PixelLab rotates instead of drawing.
|
|
13887
|
+
*/
|
|
13888
|
+
reference: z6.union([z6.string().min(1), z6.record(CharacterDirectionSchema, z6.string().min(1))]).optional(),
|
|
13889
|
+
/** `character` pro bases: a concept image that seeds the design. */
|
|
13890
|
+
concept: z6.string().min(1).optional(),
|
|
13891
|
+
/** `character` pro bases: another character in the style whose look this one follows. */
|
|
13892
|
+
styleCharacter: z6.string().min(1).optional(),
|
|
13893
|
+
/** `character` standard humanoid bases: this character's proportions, over the style's. */
|
|
13894
|
+
proportions: CharacterProportionsSchema.optional()
|
|
13858
13895
|
}).strict().refine((asset) => !(asset.revision && (asset.state || asset.animation)), {
|
|
13859
13896
|
message: "an asset is a revision, a state, or an animation \u2014 not more than one"
|
|
13860
13897
|
}).refine((asset) => !(asset.state && asset.animation), {
|
|
@@ -13865,59 +13902,95 @@ var CANDIDATE_OPTION = {
|
|
|
13865
13902
|
comfyui: "numImages",
|
|
13866
13903
|
scenario: "numOutputs"
|
|
13867
13904
|
};
|
|
13905
|
+
var NewStyleSchema = z6.object({
|
|
13906
|
+
generator: z6.enum(["character", "objectPro"]),
|
|
13907
|
+
/** Where the style's art is written, inside the project. */
|
|
13908
|
+
outDir: z6.string().min(1).refine((dir) => !dir.split(/[\\/]/).includes("..") && !/^([a-z]:)?[\\/]/i.test(dir), {
|
|
13909
|
+
message: "outDir must be a path inside the project"
|
|
13910
|
+
}),
|
|
13911
|
+
size: z6.number().int().min(16).max(256).optional(),
|
|
13912
|
+
mode: CharacterModeSchema.optional(),
|
|
13913
|
+
directions: z6.union([z6.literal(4), z6.literal(8)]).optional(),
|
|
13914
|
+
template: z6.string().min(1).optional(),
|
|
13915
|
+
view: z6.string().min(1).max(64).optional(),
|
|
13916
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
13917
|
+
isometric: z6.boolean().optional(),
|
|
13918
|
+
textGuidanceScale: z6.number().min(1).max(20).optional(),
|
|
13919
|
+
enhancePrompt: z6.boolean().optional(),
|
|
13920
|
+
promptPrefix: z6.string().optional(),
|
|
13921
|
+
promptSuffix: z6.string().optional(),
|
|
13922
|
+
palette: z6.array(z6.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).optional()
|
|
13923
|
+
}).strict();
|
|
13924
|
+
var PatchAssetEdit = z6.object({
|
|
13925
|
+
action: z6.literal("patch-asset"),
|
|
13926
|
+
assetId: z6.string().min(1),
|
|
13927
|
+
patch: AssetPatchSchema
|
|
13928
|
+
}).strict();
|
|
13929
|
+
var AddAssetEdit = z6.object({
|
|
13930
|
+
action: z6.literal("add-asset"),
|
|
13931
|
+
assetId: z6.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
13932
|
+
asset: NewAssetSchema
|
|
13933
|
+
}).strict();
|
|
13934
|
+
var SetSourceEdit = z6.object({
|
|
13935
|
+
action: z6.literal("set-source"),
|
|
13936
|
+
assetId: z6.string().min(1),
|
|
13937
|
+
styleId: z6.string().min(1),
|
|
13938
|
+
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
13939
|
+
source: z6.string().min(1)
|
|
13940
|
+
}).strict();
|
|
13941
|
+
var ClearSourceEdit = z6.object({
|
|
13942
|
+
action: z6.literal("clear-source"),
|
|
13943
|
+
assetId: z6.string().min(1),
|
|
13944
|
+
styleId: z6.string().min(1)
|
|
13945
|
+
}).strict();
|
|
13946
|
+
var PatchStyleEdit = z6.object({
|
|
13947
|
+
action: z6.literal("patch-style"),
|
|
13948
|
+
styleId: z6.string().min(1),
|
|
13949
|
+
patch: z6.object({
|
|
13950
|
+
/** Candidates per generation, written to the provider's own option. */
|
|
13951
|
+
candidates: z6.number().int().min(1).max(64).optional(),
|
|
13952
|
+
/** Empty clears the style's own value (inherit, or the default). */
|
|
13953
|
+
promptPrefix: z6.string().optional(),
|
|
13954
|
+
promptSuffix: z6.string().optional(),
|
|
13955
|
+
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
13956
|
+
palette: z6.array(z6.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
13957
|
+
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
13958
|
+
enforcePalette: z6.boolean().optional(),
|
|
13959
|
+
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
13960
|
+
view: z6.string().max(64).nullable().optional(),
|
|
13961
|
+
/** pixflux only; null clears the style's own value. */
|
|
13962
|
+
noBackground: z6.boolean().nullable().optional()
|
|
13963
|
+
}).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
|
|
13964
|
+
}).strict();
|
|
13965
|
+
var AddStyleEdit = z6.object({
|
|
13966
|
+
action: z6.literal("add-style"),
|
|
13967
|
+
styleId: z6.string().min(1).regex(/^[^/\\:]+$/, "style ids cannot contain slashes or colons"),
|
|
13968
|
+
style: NewStyleSchema
|
|
13969
|
+
}).strict();
|
|
13970
|
+
var Envelope = {
|
|
13971
|
+
/** Workspace project id; omitted for a single-project gallery. */
|
|
13972
|
+
project: z6.string().min(1).optional(),
|
|
13973
|
+
/** Manifest bytes the page last saw; a mismatch refuses the write. */
|
|
13974
|
+
expectedSha256: HexSha
|
|
13975
|
+
};
|
|
13976
|
+
var SingleEdit = z6.discriminatedUnion("action", [PatchAssetEdit, AddAssetEdit, SetSourceEdit, ClearSourceEdit, PatchStyleEdit, AddStyleEdit]);
|
|
13868
13977
|
var ManifestEditSchema = z6.discriminatedUnion("action", [
|
|
13978
|
+
PatchAssetEdit.extend(Envelope),
|
|
13979
|
+
AddAssetEdit.extend(Envelope),
|
|
13980
|
+
SetSourceEdit.extend(Envelope),
|
|
13981
|
+
ClearSourceEdit.extend(Envelope),
|
|
13982
|
+
PatchStyleEdit.extend(Envelope),
|
|
13983
|
+
AddStyleEdit.extend(Envelope),
|
|
13984
|
+
/**
|
|
13985
|
+
* Several edits as one write: a new character's style, base, and loops
|
|
13986
|
+
* land together or not at all, validated once as the finished manifest.
|
|
13987
|
+
* Each edit sees the ones before it, so a base can name a style the same
|
|
13988
|
+
* batch adds.
|
|
13989
|
+
*/
|
|
13869
13990
|
z6.object({
|
|
13870
|
-
action: z6.literal("
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
assetId: z6.string().min(1),
|
|
13874
|
-
/** Manifest bytes the page last saw; a mismatch refuses the write. */
|
|
13875
|
-
expectedSha256: HexSha,
|
|
13876
|
-
patch: AssetPatchSchema
|
|
13877
|
-
}).strict(),
|
|
13878
|
-
z6.object({
|
|
13879
|
-
action: z6.literal("add-asset"),
|
|
13880
|
-
project: z6.string().min(1).optional(),
|
|
13881
|
-
assetId: z6.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
13882
|
-
expectedSha256: HexSha,
|
|
13883
|
-
asset: NewAssetSchema
|
|
13884
|
-
}).strict(),
|
|
13885
|
-
z6.object({
|
|
13886
|
-
action: z6.literal("set-source"),
|
|
13887
|
-
project: z6.string().min(1).optional(),
|
|
13888
|
-
assetId: z6.string().min(1),
|
|
13889
|
-
styleId: z6.string().min(1),
|
|
13890
|
-
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
13891
|
-
source: z6.string().min(1),
|
|
13892
|
-
expectedSha256: HexSha
|
|
13893
|
-
}).strict(),
|
|
13894
|
-
z6.object({
|
|
13895
|
-
action: z6.literal("clear-source"),
|
|
13896
|
-
project: z6.string().min(1).optional(),
|
|
13897
|
-
assetId: z6.string().min(1),
|
|
13898
|
-
styleId: z6.string().min(1),
|
|
13899
|
-
expectedSha256: HexSha
|
|
13900
|
-
}).strict(),
|
|
13901
|
-
z6.object({
|
|
13902
|
-
action: z6.literal("patch-style"),
|
|
13903
|
-
project: z6.string().min(1).optional(),
|
|
13904
|
-
styleId: z6.string().min(1),
|
|
13905
|
-
expectedSha256: HexSha,
|
|
13906
|
-
patch: z6.object({
|
|
13907
|
-
/** Candidates per generation, written to the provider's own option. */
|
|
13908
|
-
candidates: z6.number().int().min(1).max(64).optional(),
|
|
13909
|
-
/** Empty clears the style's own value (inherit, or the default). */
|
|
13910
|
-
promptPrefix: z6.string().optional(),
|
|
13911
|
-
promptSuffix: z6.string().optional(),
|
|
13912
|
-
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
13913
|
-
palette: z6.array(z6.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
13914
|
-
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
13915
|
-
enforcePalette: z6.boolean().optional(),
|
|
13916
|
-
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
13917
|
-
view: z6.string().max(64).nullable().optional(),
|
|
13918
|
-
/** pixflux only; null clears the style's own value. */
|
|
13919
|
-
noBackground: z6.boolean().nullable().optional()
|
|
13920
|
-
}).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
|
|
13991
|
+
action: z6.literal("batch"),
|
|
13992
|
+
...Envelope,
|
|
13993
|
+
edits: z6.array(SingleEdit).min(1).max(64)
|
|
13921
13994
|
}).strict()
|
|
13922
13995
|
]);
|
|
13923
13996
|
var ManifestDriftError = class extends Error {
|
|
@@ -13966,6 +14039,18 @@ function undeclaredAsset(raw, assetId) {
|
|
|
13966
14039
|
}
|
|
13967
14040
|
function applyEdit(raw, edit) {
|
|
13968
14041
|
raw.assets ??= {};
|
|
14042
|
+
if (edit.action === "batch") {
|
|
14043
|
+
for (const each of edit.edits) applyEdit(raw, each);
|
|
14044
|
+
return;
|
|
14045
|
+
}
|
|
14046
|
+
if (edit.action === "add-style") {
|
|
14047
|
+
raw.styles ??= {};
|
|
14048
|
+
if (Object.hasOwn(raw.styles, edit.styleId)) throw new ManifestEditError(`style "${edit.styleId}" already exists`);
|
|
14049
|
+
const style = { ...edit.style };
|
|
14050
|
+
if (edit.style.palette) style.palette = edit.style.palette.map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
14051
|
+
raw.styles[edit.styleId] = style;
|
|
14052
|
+
return;
|
|
14053
|
+
}
|
|
13969
14054
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
13970
14055
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
13971
14056
|
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
@@ -14043,6 +14128,15 @@ function applyEdit(raw, edit) {
|
|
|
14043
14128
|
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
14044
14129
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
14045
14130
|
}
|
|
14131
|
+
for (const [what, parent] of [
|
|
14132
|
+
["portrait", edit.asset.portrait?.of],
|
|
14133
|
+
["outfit", edit.asset.outfit?.of],
|
|
14134
|
+
["mirror", edit.asset.mirror]
|
|
14135
|
+
]) {
|
|
14136
|
+
if (parent && !declaredAssets(raw).has(parent)) {
|
|
14137
|
+
throw new ManifestEditError(`${what} source "${parent}" is not declared by the manifest`);
|
|
14138
|
+
}
|
|
14139
|
+
}
|
|
14046
14140
|
raw.assets[edit.assetId] = asset2;
|
|
14047
14141
|
return;
|
|
14048
14142
|
}
|
|
@@ -14070,11 +14164,10 @@ function applyEdit(raw, edit) {
|
|
|
14070
14164
|
else delete asset.tags;
|
|
14071
14165
|
}
|
|
14072
14166
|
}
|
|
14073
|
-
async function
|
|
14167
|
+
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
14074
14168
|
const absolute = path23.resolve(manifestPath);
|
|
14075
14169
|
const original = await readFile15(absolute, "utf8");
|
|
14076
|
-
|
|
14077
|
-
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14170
|
+
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14078
14171
|
let raw;
|
|
14079
14172
|
try {
|
|
14080
14173
|
raw = JSON.parse(original);
|
|
@@ -14085,25 +14178,63 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
14085
14178
|
}
|
|
14086
14179
|
applyEdit(raw, edit);
|
|
14087
14180
|
const next = serializeLike(original, raw);
|
|
14088
|
-
if (next === original) return
|
|
14181
|
+
if (next === original) return unchanged(absolute, original);
|
|
14089
14182
|
const tmp = path23.join(
|
|
14090
14183
|
path23.dirname(absolute),
|
|
14091
14184
|
`.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
14092
14185
|
);
|
|
14093
14186
|
try {
|
|
14094
14187
|
await writeFile10(tmp, next);
|
|
14188
|
+
let specs;
|
|
14095
14189
|
try {
|
|
14096
|
-
|
|
14097
|
-
await resolveSpecs(loaded);
|
|
14190
|
+
specs = await resolveSpecs(await loadManifest(tmp));
|
|
14098
14191
|
} catch (error) {
|
|
14099
14192
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
14100
14193
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
14101
14194
|
}
|
|
14102
|
-
await
|
|
14195
|
+
return await use({ absolute, original, next, tmp, specs });
|
|
14103
14196
|
} finally {
|
|
14104
14197
|
await rm7(tmp, { force: true });
|
|
14105
14198
|
}
|
|
14106
|
-
|
|
14199
|
+
}
|
|
14200
|
+
async function applyManifestEdit(manifestPath, edit) {
|
|
14201
|
+
return withCandidate(
|
|
14202
|
+
manifestPath,
|
|
14203
|
+
edit,
|
|
14204
|
+
async ({ absolute, next, tmp }) => {
|
|
14205
|
+
await rename7(tmp, absolute);
|
|
14206
|
+
return { manifestPath: absolute, sha256: sha256(next), changed: true };
|
|
14207
|
+
},
|
|
14208
|
+
(absolute, original) => ({ manifestPath: absolute, sha256: sha256(original), changed: false })
|
|
14209
|
+
);
|
|
14210
|
+
}
|
|
14211
|
+
async function priceManifestEdit(manifestPath, edit) {
|
|
14212
|
+
return withCandidate(
|
|
14213
|
+
manifestPath,
|
|
14214
|
+
edit,
|
|
14215
|
+
async ({ absolute, specs }) => {
|
|
14216
|
+
const before = new Map((await resolveSpecs(await loadManifest(absolute))).map((spec) => [`${spec.styleId}/${spec.assetId}`, spec.specHash]));
|
|
14217
|
+
const items = [];
|
|
14218
|
+
const totals = {};
|
|
14219
|
+
for (const spec of specs) {
|
|
14220
|
+
const key = `${spec.styleId}/${spec.assetId}`;
|
|
14221
|
+
const was = before.get(key);
|
|
14222
|
+
if (was === spec.specHash) continue;
|
|
14223
|
+
items.push({
|
|
14224
|
+
key,
|
|
14225
|
+
styleId: spec.styleId,
|
|
14226
|
+
assetId: spec.assetId,
|
|
14227
|
+
change: was === void 0 ? "new" : "changed",
|
|
14228
|
+
cost: spec.cost,
|
|
14229
|
+
costUnit: spec.costUnit,
|
|
14230
|
+
candidates: spec.candidates
|
|
14231
|
+
});
|
|
14232
|
+
totals[spec.costUnit] = (totals[spec.costUnit] ?? 0) + spec.cost;
|
|
14233
|
+
}
|
|
14234
|
+
return { items, totals };
|
|
14235
|
+
},
|
|
14236
|
+
() => ({ items: [], totals: {} })
|
|
14237
|
+
);
|
|
14107
14238
|
}
|
|
14108
14239
|
|
|
14109
14240
|
// src/pipeline/hand-edit.ts
|
|
@@ -14375,63 +14506,104 @@ async function runEdit(args) {
|
|
|
14375
14506
|
}
|
|
14376
14507
|
|
|
14377
14508
|
// src/cli/commands/gallery.ts
|
|
14378
|
-
import
|
|
14379
|
-
import { existsSync as
|
|
14509
|
+
import path34 from "path";
|
|
14510
|
+
import { existsSync as existsSync26 } from "fs";
|
|
14380
14511
|
|
|
14381
14512
|
// src/gallery/snapshot.ts
|
|
14382
14513
|
import { createHash as createHash3 } from "crypto";
|
|
14383
|
-
import { existsSync as
|
|
14514
|
+
import { existsSync as existsSync21 } from "fs";
|
|
14384
14515
|
import { readFile as readFile18, stat as stat2 } from "fs/promises";
|
|
14385
|
-
import
|
|
14516
|
+
import path30 from "path";
|
|
14386
14517
|
|
|
14387
14518
|
// src/gallery/edit.ts
|
|
14388
|
-
import
|
|
14389
|
-
import { z as
|
|
14519
|
+
import path28 from "path";
|
|
14520
|
+
import { z as z10 } from "zod";
|
|
14390
14521
|
|
|
14391
|
-
// src/gallery/
|
|
14522
|
+
// src/gallery/upload.ts
|
|
14392
14523
|
import { existsSync as existsSync18 } from "fs";
|
|
14393
|
-
import { mkdir as mkdir10,
|
|
14524
|
+
import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
|
|
14394
14525
|
import path26 from "path";
|
|
14395
14526
|
import { z as z8 } from "zod";
|
|
14396
|
-
var
|
|
14527
|
+
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
14528
|
+
var UploadImageSchema = z8.object({
|
|
14529
|
+
action: z8.literal("upload-image"),
|
|
14397
14530
|
project: z8.string().min(1).optional(),
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14531
|
+
/** Manifest-relative destination, ending .png, .jpg, or .jpeg. */
|
|
14532
|
+
path: z8.string().min(1),
|
|
14533
|
+
base64: z8.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_UPLOAD_BYTES / 3) * 4),
|
|
14534
|
+
/** Replace a file already at `path`. */
|
|
14535
|
+
overwrite: z8.boolean().optional()
|
|
14401
14536
|
}).strict();
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14537
|
+
async function saveProjectImage(root, request) {
|
|
14538
|
+
const file = path26.resolve(root, request.path);
|
|
14539
|
+
const relative = path26.relative(root, file);
|
|
14540
|
+
if (relative.startsWith("..") || path26.isAbsolute(relative)) {
|
|
14541
|
+
throw new ManifestEditError("an upload must stay inside the project");
|
|
14542
|
+
}
|
|
14543
|
+
const extension = path26.extname(file).toLowerCase();
|
|
14544
|
+
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
14545
|
+
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
14546
|
+
const bytes = Buffer.from(request.base64, "base64");
|
|
14547
|
+
if (!bytes.length || bytes.length > MAX_UPLOAD_BYTES) {
|
|
14548
|
+
throw new ManifestEditError(`an upload must be between 1 byte and ${MAX_UPLOAD_BYTES / 1024 / 1024} MB`);
|
|
14549
|
+
}
|
|
14550
|
+
const metadata = imageMetadata(bytes);
|
|
14551
|
+
if (!metadata) throw new ManifestEditError("the upload is not a readable PNG or JPEG");
|
|
14552
|
+
if (metadata.format !== expected) {
|
|
14553
|
+
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
14554
|
+
}
|
|
14555
|
+
if (existsSync18(file) && !request.overwrite) {
|
|
14556
|
+
throw new ManifestEditError(`${relative.split(path26.sep).join("/")} already exists; choose another name, or replace it`);
|
|
14557
|
+
}
|
|
14558
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
14559
|
+
await writeFile12(file, bytes);
|
|
14560
|
+
return relative.split(path26.sep).join("/");
|
|
14561
|
+
}
|
|
14562
|
+
|
|
14563
|
+
// src/gallery/skeleton.ts
|
|
14564
|
+
import { existsSync as existsSync19 } from "fs";
|
|
14565
|
+
import { mkdir as mkdir11, readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
|
|
14566
|
+
import path27 from "path";
|
|
14567
|
+
import { z as z9 } from "zod";
|
|
14568
|
+
var SkeletonEstimateRequestSchema = z9.object({
|
|
14569
|
+
project: z9.string().min(1).optional(),
|
|
14570
|
+
styleId: z9.string().min(1),
|
|
14571
|
+
assetId: z9.string().min(1),
|
|
14572
|
+
frames: z9.number().int().min(3).max(15).optional()
|
|
14573
|
+
}).strict();
|
|
14574
|
+
var SkeletonAnimationRequestSchema = z9.object({
|
|
14575
|
+
action: z9.literal("create-skeleton-animation"),
|
|
14576
|
+
project: z9.string().min(1).optional(),
|
|
14577
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14578
|
+
styleId: z9.string().min(1),
|
|
14407
14579
|
/** The sprite the animation poses. */
|
|
14408
|
-
from:
|
|
14409
|
-
assetId:
|
|
14580
|
+
from: z9.string().min(1),
|
|
14581
|
+
assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14410
14582
|
/** The motion, sent as PixelLab's `action`. */
|
|
14411
|
-
prompt:
|
|
14583
|
+
prompt: z9.string().min(1),
|
|
14412
14584
|
direction: CharacterDirectionSchema,
|
|
14413
|
-
description:
|
|
14414
|
-
skeletonTemplate:
|
|
14585
|
+
description: z9.string().min(1).optional(),
|
|
14586
|
+
skeletonTemplate: z9.string().min(1).optional(),
|
|
14415
14587
|
/** Manifest-relative JSON path inside the project. */
|
|
14416
|
-
keypointsFile:
|
|
14588
|
+
keypointsFile: z9.string().min(1),
|
|
14417
14589
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14418
14590
|
set: SkeletonSetSchema.optional(),
|
|
14419
14591
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
14420
|
-
overwrite:
|
|
14592
|
+
overwrite: z9.boolean().optional()
|
|
14421
14593
|
}).strict();
|
|
14422
|
-
var SkeletonKeypointsUpdateSchema =
|
|
14423
|
-
action:
|
|
14424
|
-
project:
|
|
14425
|
-
styleId:
|
|
14426
|
-
assetId:
|
|
14594
|
+
var SkeletonKeypointsUpdateSchema = z9.object({
|
|
14595
|
+
action: z9.literal("update-skeleton-keypoints"),
|
|
14596
|
+
project: z9.string().min(1).optional(),
|
|
14597
|
+
styleId: z9.string().min(1),
|
|
14598
|
+
assetId: z9.string().min(1),
|
|
14427
14599
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
14428
|
-
expectedSha256:
|
|
14600
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14429
14601
|
set: SkeletonSetSchema
|
|
14430
14602
|
}).strict();
|
|
14431
14603
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14432
14604
|
function skeletonSourceFile(spec) {
|
|
14433
|
-
if (spec.source) return
|
|
14434
|
-
if (spec.quality &&
|
|
14605
|
+
if (spec.source) return path27.resolve(spec.root, spec.source);
|
|
14606
|
+
if (spec.quality && existsSync19(spec.quality.outFile)) return spec.quality.outFile;
|
|
14435
14607
|
return spec.outFile;
|
|
14436
14608
|
}
|
|
14437
14609
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -14452,10 +14624,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14452
14624
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14453
14625
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14454
14626
|
const file = skeletonSourceFile(spec);
|
|
14455
|
-
if (!
|
|
14627
|
+
if (!existsSync19(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
14456
14628
|
const bytes = await readFile17(file);
|
|
14457
14629
|
const metadata = imageMetadata(bytes);
|
|
14458
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
14630
|
+
if (!metadata) throw new ManifestEditError(`${path27.basename(file)} is not a readable PNG or JPEG`);
|
|
14459
14631
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14460
14632
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14461
14633
|
if (used >= limit) {
|
|
@@ -14472,7 +14644,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14472
14644
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14473
14645
|
return {
|
|
14474
14646
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14475
|
-
source:
|
|
14647
|
+
source: path27.relative(ctx.loaded.root, file).split(path27.sep).join("/"),
|
|
14476
14648
|
session: status()
|
|
14477
14649
|
};
|
|
14478
14650
|
}
|
|
@@ -14486,21 +14658,21 @@ function billedAmount(usage) {
|
|
|
14486
14658
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14487
14659
|
}
|
|
14488
14660
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
14489
|
-
const root =
|
|
14490
|
-
const file =
|
|
14491
|
-
const relative =
|
|
14492
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
14661
|
+
const root = path27.dirname(path27.resolve(manifestPath));
|
|
14662
|
+
const file = path27.resolve(root, request.keypointsFile);
|
|
14663
|
+
const relative = path27.relative(root, file);
|
|
14664
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path27.isAbsolute(relative)) {
|
|
14493
14665
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14494
14666
|
}
|
|
14495
|
-
const existed =
|
|
14667
|
+
const existed = existsSync19(file);
|
|
14496
14668
|
let previous = null;
|
|
14497
14669
|
if (request.set) {
|
|
14498
14670
|
if (existed && !request.overwrite) {
|
|
14499
14671
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14500
14672
|
}
|
|
14501
14673
|
if (existed) previous = await readFile17(file);
|
|
14502
|
-
await
|
|
14503
|
-
await
|
|
14674
|
+
await mkdir11(path27.dirname(file), { recursive: true });
|
|
14675
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14504
14676
|
} else if (!existed) {
|
|
14505
14677
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14506
14678
|
}
|
|
@@ -14515,7 +14687,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14515
14687
|
revision: {
|
|
14516
14688
|
mode: "animate-skeleton",
|
|
14517
14689
|
from: request.from,
|
|
14518
|
-
keypointsFile: relative.split(
|
|
14690
|
+
keypointsFile: relative.split(path27.sep).join("/"),
|
|
14519
14691
|
direction: request.direction,
|
|
14520
14692
|
...request.description ? { description: request.description } : {},
|
|
14521
14693
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -14524,7 +14696,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14524
14696
|
});
|
|
14525
14697
|
} catch (error) {
|
|
14526
14698
|
if (request.set) {
|
|
14527
|
-
if (previous) await
|
|
14699
|
+
if (previous) await writeFile13(file, previous);
|
|
14528
14700
|
else await rm9(file, { force: true });
|
|
14529
14701
|
}
|
|
14530
14702
|
throw error;
|
|
@@ -14536,56 +14708,56 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
14536
14708
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14537
14709
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
14538
14710
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
14539
|
-
const relative =
|
|
14540
|
-
if (relative.startsWith("..") ||
|
|
14711
|
+
const relative = path27.relative(ctx.loaded.root, file);
|
|
14712
|
+
if (relative.startsWith("..") || path27.isAbsolute(relative)) {
|
|
14541
14713
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
14542
14714
|
}
|
|
14543
|
-
const current =
|
|
14715
|
+
const current = existsSync19(file) ? sha256(await readFile17(file)) : null;
|
|
14544
14716
|
if (current !== request.expectedSha256) {
|
|
14545
14717
|
throw Object.assign(
|
|
14546
|
-
new Error(`${relative.split(
|
|
14718
|
+
new Error(`${relative.split(path27.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
14547
14719
|
{ status: 409 }
|
|
14548
14720
|
);
|
|
14549
14721
|
}
|
|
14550
|
-
await
|
|
14551
|
-
return relative.split(
|
|
14722
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14723
|
+
return relative.split(path27.sep).join("/");
|
|
14552
14724
|
}
|
|
14553
14725
|
|
|
14554
14726
|
// src/gallery/edit.ts
|
|
14555
|
-
var Base64 =
|
|
14556
|
-
var HandEditRequestSchema =
|
|
14557
|
-
|
|
14558
|
-
action:
|
|
14559
|
-
project:
|
|
14560
|
-
assetId:
|
|
14561
|
-
styleId:
|
|
14562
|
-
expectedSha256:
|
|
14727
|
+
var Base64 = z10.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
14728
|
+
var HandEditRequestSchema = z10.discriminatedUnion("action", [
|
|
14729
|
+
z10.object({
|
|
14730
|
+
action: z10.literal("start-edit"),
|
|
14731
|
+
project: z10.string().min(1).optional(),
|
|
14732
|
+
assetId: z10.string().min(1),
|
|
14733
|
+
styleId: z10.string().min(1),
|
|
14734
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14563
14735
|
/** Also launch the author's editor on the file. */
|
|
14564
|
-
open:
|
|
14736
|
+
open: z10.boolean().optional()
|
|
14565
14737
|
}).strict(),
|
|
14566
|
-
|
|
14567
|
-
action:
|
|
14568
|
-
project:
|
|
14569
|
-
assetId:
|
|
14570
|
-
styleId:
|
|
14571
|
-
expectedSha256:
|
|
14738
|
+
z10.object({
|
|
14739
|
+
action: z10.literal("detach-edit"),
|
|
14740
|
+
project: z10.string().min(1).optional(),
|
|
14741
|
+
assetId: z10.string().min(1),
|
|
14742
|
+
styleId: z10.string().min(1),
|
|
14743
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
|
|
14572
14744
|
}).strict(),
|
|
14573
14745
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14574
|
-
|
|
14575
|
-
action:
|
|
14576
|
-
project:
|
|
14577
|
-
assetId:
|
|
14578
|
-
styleId:
|
|
14579
|
-
expectedSha256:
|
|
14746
|
+
z10.object({
|
|
14747
|
+
action: z10.literal("save-edit"),
|
|
14748
|
+
project: z10.string().min(1).optional(),
|
|
14749
|
+
assetId: z10.string().min(1),
|
|
14750
|
+
styleId: z10.string().min(1),
|
|
14751
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14580
14752
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14581
14753
|
png: Base64.min(1).optional(),
|
|
14582
14754
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14583
|
-
frames:
|
|
14755
|
+
frames: z10.array(z10.object({ role: z10.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14584
14756
|
/** The editor's layered project file, kept beside the edit. */
|
|
14585
14757
|
pxo: Base64.optional(),
|
|
14586
14758
|
/** Editor identity from the bridge's ready message. */
|
|
14587
|
-
editor:
|
|
14588
|
-
protocol:
|
|
14759
|
+
editor: z10.string().min(1).max(200),
|
|
14760
|
+
protocol: z10.number().int().positive()
|
|
14589
14761
|
}).strict()
|
|
14590
14762
|
]);
|
|
14591
14763
|
function createGalleryEditHandler(opts) {
|
|
@@ -14626,6 +14798,21 @@ function createGalleryEditHandler(opts) {
|
|
|
14626
14798
|
}
|
|
14627
14799
|
return opts.reload();
|
|
14628
14800
|
}
|
|
14801
|
+
const upload = UploadImageSchema.safeParse(body);
|
|
14802
|
+
if (upload.success) {
|
|
14803
|
+
let manifestPath2;
|
|
14804
|
+
try {
|
|
14805
|
+
manifestPath2 = await opts.manifestFor(upload.data.project);
|
|
14806
|
+
} catch (error) {
|
|
14807
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14808
|
+
}
|
|
14809
|
+
const written = await saveProjectImage(path28.dirname(path28.resolve(manifestPath2)), upload.data);
|
|
14810
|
+
log2(` image added: ${written}`);
|
|
14811
|
+
return opts.reload();
|
|
14812
|
+
}
|
|
14813
|
+
if (body?.action === "upload-image") {
|
|
14814
|
+
throw new ManifestEditError("invalid upload: " + upload.error.issues.slice(0, 2).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
14815
|
+
}
|
|
14629
14816
|
const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
|
|
14630
14817
|
if (poses.success) {
|
|
14631
14818
|
if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
|
|
@@ -14669,18 +14856,51 @@ function createGalleryEditHandler(opts) {
|
|
|
14669
14856
|
}
|
|
14670
14857
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14671
14858
|
if (result.changed) {
|
|
14672
|
-
const relative =
|
|
14859
|
+
const relative = path28.relative(process.cwd(), result.manifestPath);
|
|
14673
14860
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14674
|
-
|
|
14675
|
-
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
14861
|
+
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
14676
14862
|
}
|
|
14677
14863
|
return opts.reload();
|
|
14678
14864
|
};
|
|
14679
14865
|
}
|
|
14866
|
+
function describeEdit(edit) {
|
|
14867
|
+
switch (edit.action) {
|
|
14868
|
+
case "batch":
|
|
14869
|
+
return `${edit.edits.length} changes (${edit.edits.map(describeEdit).join("; ")})`;
|
|
14870
|
+
case "add-style":
|
|
14871
|
+
return `added style ${edit.styleId}`;
|
|
14872
|
+
case "patch-style":
|
|
14873
|
+
return `changed style ${edit.styleId}`;
|
|
14874
|
+
case "set-source":
|
|
14875
|
+
case "clear-source":
|
|
14876
|
+
return `changed ${edit.styleId}/${edit.assetId} source`;
|
|
14877
|
+
case "add-asset":
|
|
14878
|
+
return `added ${edit.assetId}`;
|
|
14879
|
+
case "patch-asset":
|
|
14880
|
+
return `changed ${edit.assetId}`;
|
|
14881
|
+
}
|
|
14882
|
+
}
|
|
14883
|
+
function createGalleryPriceHandler(opts) {
|
|
14884
|
+
return async (body) => {
|
|
14885
|
+
const parsed = ManifestEditSchema.safeParse(body);
|
|
14886
|
+
if (!parsed.success) {
|
|
14887
|
+
throw new ManifestEditError(
|
|
14888
|
+
"invalid edit: " + parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
14889
|
+
);
|
|
14890
|
+
}
|
|
14891
|
+
let manifestPath;
|
|
14892
|
+
try {
|
|
14893
|
+
manifestPath = await opts.manifestFor(parsed.data.project);
|
|
14894
|
+
} catch (error) {
|
|
14895
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14896
|
+
}
|
|
14897
|
+
return priceManifestEdit(manifestPath, parsed.data);
|
|
14898
|
+
};
|
|
14899
|
+
}
|
|
14680
14900
|
|
|
14681
14901
|
// src/pipeline/history.ts
|
|
14682
|
-
import { existsSync as
|
|
14683
|
-
import
|
|
14902
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14903
|
+
import path29 from "path";
|
|
14684
14904
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
14685
14905
|
function historyLimit(manifest, env = process.env) {
|
|
14686
14906
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -14767,10 +14987,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14767
14987
|
}
|
|
14768
14988
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
14769
14989
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
14770
|
-
if (!
|
|
14990
|
+
if (!existsSync20(file)) continue;
|
|
14771
14991
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
14772
14992
|
throw new OverwriteRefusedError(
|
|
14773
|
-
`${
|
|
14993
|
+
`${path29.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
14774
14994
|
[file]
|
|
14775
14995
|
);
|
|
14776
14996
|
}
|
|
@@ -14823,7 +15043,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14823
15043
|
// src/gallery/snapshot.ts
|
|
14824
15044
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14825
15045
|
function galleryMediaId(absolutePath) {
|
|
14826
|
-
return createHash3("sha256").update(
|
|
15046
|
+
return createHash3("sha256").update(path30.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14827
15047
|
}
|
|
14828
15048
|
function galleryMediaRoute(id) {
|
|
14829
15049
|
return `/media/${id}`;
|
|
@@ -14842,9 +15062,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14842
15062
|
return (entry.history ?? []).map((generation, i) => {
|
|
14843
15063
|
const outputs = generation.outputs.map((output) => {
|
|
14844
15064
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14845
|
-
const file =
|
|
15065
|
+
const file = path30.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14846
15066
|
let url = null;
|
|
14847
|
-
if (
|
|
15067
|
+
if (existsSync21(file)) {
|
|
14848
15068
|
const id = galleryMediaId(file);
|
|
14849
15069
|
media.set(id, { path: file, contentType: mediaType });
|
|
14850
15070
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14974,10 +15194,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14974
15194
|
async function describeQuality(media, root, inspection) {
|
|
14975
15195
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14976
15196
|
const outputs = await Promise.all(
|
|
14977
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
15197
|
+
outputPaths.map((file) => describeOutput(media, root, path30.resolve(root, file)))
|
|
14978
15198
|
);
|
|
14979
|
-
const recordPath =
|
|
14980
|
-
const recordExists =
|
|
15199
|
+
const recordPath = path30.resolve(root, inspection.record);
|
|
15200
|
+
const recordExists = existsSync21(recordPath);
|
|
14981
15201
|
let options = null;
|
|
14982
15202
|
let check = null;
|
|
14983
15203
|
if (recordExists) {
|
|
@@ -14996,7 +15216,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14996
15216
|
return {
|
|
14997
15217
|
state: inspection.state,
|
|
14998
15218
|
reason: inspection.reason,
|
|
14999
|
-
output: portableOutputPath(
|
|
15219
|
+
output: portableOutputPath(path30.resolve(root, inspection.output), root),
|
|
15000
15220
|
record: portableOutputPath(recordPath, root),
|
|
15001
15221
|
recordExists,
|
|
15002
15222
|
outputs,
|
|
@@ -15036,7 +15256,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15036
15256
|
const items = [];
|
|
15037
15257
|
const manifestText = await readFile18(loaded.path, "utf8");
|
|
15038
15258
|
const manifestSha256 = sha256(manifestText);
|
|
15039
|
-
const cacheDir =
|
|
15259
|
+
const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15040
15260
|
let rawStyles = {};
|
|
15041
15261
|
try {
|
|
15042
15262
|
const raw = JSON.parse(manifestText);
|
|
@@ -15060,12 +15280,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15060
15280
|
)
|
|
15061
15281
|
);
|
|
15062
15282
|
if (spec.source) {
|
|
15063
|
-
const editPath =
|
|
15283
|
+
const editPath = path30.resolve(root, spec.source);
|
|
15064
15284
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15065
15285
|
const editShas = await Promise.all(
|
|
15066
15286
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15067
15287
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15068
|
-
return { file, sha256:
|
|
15288
|
+
return { file, sha256: existsSync21(file) ? await sha256File(file) : null };
|
|
15069
15289
|
})
|
|
15070
15290
|
);
|
|
15071
15291
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15073,7 +15293,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15073
15293
|
const companion = await readHandEditCompanion(editPath);
|
|
15074
15294
|
if (companion) {
|
|
15075
15295
|
const projectPath = handEditProjectPath(editPath);
|
|
15076
|
-
const project = companion.project &&
|
|
15296
|
+
const project = companion.project && existsSync21(projectPath) ? await fileInfo(projectPath) : null;
|
|
15077
15297
|
let projectUrl = null;
|
|
15078
15298
|
if (project) {
|
|
15079
15299
|
const id = galleryMediaId(projectPath);
|
|
@@ -15109,8 +15329,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15109
15329
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15110
15330
|
}
|
|
15111
15331
|
} else if (spec.source) {
|
|
15112
|
-
outputs = [await describeOutput(media, root,
|
|
15113
|
-
} else if (!entry &&
|
|
15332
|
+
outputs = [await describeOutput(media, root, path30.resolve(root, spec.source))];
|
|
15333
|
+
} else if (!entry && existsSync21(spec.outFile)) {
|
|
15114
15334
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15115
15335
|
} else {
|
|
15116
15336
|
outputs = [];
|
|
@@ -15311,7 +15531,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15311
15531
|
id: loaded.manifest.name,
|
|
15312
15532
|
name: loaded.manifest.name,
|
|
15313
15533
|
manifest: loaded.path,
|
|
15314
|
-
lock:
|
|
15534
|
+
lock: path30.resolve(opts.lockPath),
|
|
15315
15535
|
root,
|
|
15316
15536
|
provider: loaded.manifest.provider,
|
|
15317
15537
|
account: null,
|
|
@@ -15337,7 +15557,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15337
15557
|
return { snapshot, media };
|
|
15338
15558
|
}
|
|
15339
15559
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15340
|
-
const dir =
|
|
15560
|
+
const dir = path30.dirname(path30.resolve(opts.workspacePath));
|
|
15341
15561
|
const media = /* @__PURE__ */ new Map();
|
|
15342
15562
|
const projects = [];
|
|
15343
15563
|
const items = [];
|
|
@@ -15380,7 +15600,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15380
15600
|
id: project.id,
|
|
15381
15601
|
name: loaded.manifest.name,
|
|
15382
15602
|
manifest: manifestPath,
|
|
15383
|
-
lock:
|
|
15603
|
+
lock: path30.resolve(lockPath),
|
|
15384
15604
|
root: loaded.root,
|
|
15385
15605
|
provider: loaded.manifest.provider,
|
|
15386
15606
|
account: project.account ?? null,
|
|
@@ -15396,8 +15616,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15396
15616
|
id: project.id,
|
|
15397
15617
|
name: project.id,
|
|
15398
15618
|
manifest: manifestPath,
|
|
15399
|
-
lock:
|
|
15400
|
-
root:
|
|
15619
|
+
lock: path30.resolve(lockPath),
|
|
15620
|
+
root: path30.dirname(manifestPath),
|
|
15401
15621
|
provider: project.provider,
|
|
15402
15622
|
account: project.account ?? null,
|
|
15403
15623
|
manifestSha256: null,
|
|
@@ -15424,7 +15644,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15424
15644
|
version: 1,
|
|
15425
15645
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15426
15646
|
project: null,
|
|
15427
|
-
workspace: { path:
|
|
15647
|
+
workspace: { path: path30.resolve(opts.workspacePath), dir, projects },
|
|
15428
15648
|
filter,
|
|
15429
15649
|
totals: {
|
|
15430
15650
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15446,7 +15666,7 @@ import { createServer as createServer2 } from "http";
|
|
|
15446
15666
|
import { readFile as readFile19 } from "fs/promises";
|
|
15447
15667
|
|
|
15448
15668
|
// src/gallery/page.ts
|
|
15449
|
-
import { existsSync as
|
|
15669
|
+
import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
|
|
15450
15670
|
|
|
15451
15671
|
// src/gallery/client-bundle.ts
|
|
15452
15672
|
import { createRequire } from "module";
|
|
@@ -15487,7 +15707,7 @@ function clientAssets() {
|
|
|
15487
15707
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15488
15708
|
client ??= {
|
|
15489
15709
|
css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
15490
|
-
js:
|
|
15710
|
+
js: existsSync22(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
|
|
15491
15711
|
};
|
|
15492
15712
|
return client;
|
|
15493
15713
|
}
|
|
@@ -15589,7 +15809,7 @@ async function readJsonBody(req) {
|
|
|
15589
15809
|
async function serveGallery(opts) {
|
|
15590
15810
|
const log2 = opts.onProgress ?? (() => {
|
|
15591
15811
|
});
|
|
15592
|
-
const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
|
|
15812
|
+
const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? randomBytes(16).toString("hex") : null;
|
|
15593
15813
|
let media = /* @__PURE__ */ new Map();
|
|
15594
15814
|
let loading = null;
|
|
15595
15815
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -15664,6 +15884,17 @@ async function serveGallery(opts) {
|
|
|
15664
15884
|
}
|
|
15665
15885
|
return;
|
|
15666
15886
|
}
|
|
15887
|
+
if (req.method === "POST" && url2.pathname === "/api/price") {
|
|
15888
|
+
if (!opts.price || !session) return fail(405, "pricing an edit needs the gallery started with --edit");
|
|
15889
|
+
try {
|
|
15890
|
+
const body = await guardedBody("price requests");
|
|
15891
|
+
if (body === void 0) return;
|
|
15892
|
+
json(200, await opts.price(body));
|
|
15893
|
+
} catch (err) {
|
|
15894
|
+
reportError("price", err);
|
|
15895
|
+
}
|
|
15896
|
+
return;
|
|
15897
|
+
}
|
|
15667
15898
|
if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
|
|
15668
15899
|
if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
|
|
15669
15900
|
try {
|
|
@@ -15858,7 +16089,7 @@ async function serveGallery(opts) {
|
|
|
15858
16089
|
|
|
15859
16090
|
// src/gallery/generate.ts
|
|
15860
16091
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15861
|
-
import { z as
|
|
16092
|
+
import { z as z11 } from "zod";
|
|
15862
16093
|
|
|
15863
16094
|
// src/pipeline/poll.ts
|
|
15864
16095
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -16269,13 +16500,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
16269
16500
|
}
|
|
16270
16501
|
|
|
16271
16502
|
// src/pick/server.ts
|
|
16272
|
-
import { existsSync as
|
|
16503
|
+
import { existsSync as existsSync24 } from "fs";
|
|
16273
16504
|
import { readFile as readFile21 } from "fs/promises";
|
|
16274
16505
|
|
|
16275
16506
|
// src/pipeline/init.ts
|
|
16276
|
-
import { readdir, readFile as readFile20, writeFile as
|
|
16277
|
-
import { existsSync as
|
|
16278
|
-
import
|
|
16507
|
+
import { readdir, readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
|
|
16508
|
+
import { existsSync as existsSync23 } from "fs";
|
|
16509
|
+
import path31 from "path";
|
|
16279
16510
|
function pngSize(buf) {
|
|
16280
16511
|
if (buf.length < 24) return null;
|
|
16281
16512
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16289,7 +16520,7 @@ function slugify(value) {
|
|
|
16289
16520
|
async function walk(dir, exclude) {
|
|
16290
16521
|
const out = [];
|
|
16291
16522
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16292
|
-
const full =
|
|
16523
|
+
const full = path31.join(dir, entry.name);
|
|
16293
16524
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16294
16525
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16295
16526
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16308,9 +16539,9 @@ async function scanAssets(root, opts = {}) {
|
|
|
16308
16539
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16309
16540
|
continue;
|
|
16310
16541
|
}
|
|
16311
|
-
const rel =
|
|
16312
|
-
const category =
|
|
16313
|
-
let id = slugify(
|
|
16542
|
+
const rel = path31.relative(root, file);
|
|
16543
|
+
const category = path31.dirname(rel) === "." ? "" : path31.dirname(rel);
|
|
16544
|
+
let id = slugify(path31.basename(rel));
|
|
16314
16545
|
if (seen.has(id)) {
|
|
16315
16546
|
const n = seen.get(id) + 1;
|
|
16316
16547
|
seen.set(id, n);
|
|
@@ -16368,10 +16599,10 @@ function mode(values) {
|
|
|
16368
16599
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16369
16600
|
}
|
|
16370
16601
|
async function writeManifestFile(target, manifest) {
|
|
16371
|
-
if (
|
|
16602
|
+
if (existsSync23(target)) {
|
|
16372
16603
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16373
16604
|
}
|
|
16374
|
-
await
|
|
16605
|
+
await writeFile14(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16375
16606
|
}
|
|
16376
16607
|
|
|
16377
16608
|
// src/pick/sheet.ts
|
|
@@ -16784,7 +17015,7 @@ async function currentArt(entry, spec) {
|
|
|
16784
17015
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
16785
17016
|
if (!previous) return null;
|
|
16786
17017
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
16787
|
-
if (!
|
|
17018
|
+
if (!existsSync24(file)) return null;
|
|
16788
17019
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
16789
17020
|
if (!contentType) return null;
|
|
16790
17021
|
let width = entry.width;
|
|
@@ -16958,26 +17189,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
16958
17189
|
}
|
|
16959
17190
|
|
|
16960
17191
|
// src/gallery/generate.ts
|
|
16961
|
-
var GenerateRequestSchema =
|
|
16962
|
-
project:
|
|
16963
|
-
keys:
|
|
17192
|
+
var GenerateRequestSchema = z11.object({
|
|
17193
|
+
project: z11.string().min(1).optional(),
|
|
17194
|
+
keys: z11.array(z11.string().min(1)).min(1).max(500),
|
|
16964
17195
|
/**
|
|
16965
17196
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
16966
17197
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
16967
17198
|
* the recorded bytes back over a modified file.
|
|
16968
17199
|
*/
|
|
16969
|
-
force:
|
|
17200
|
+
force: z11.boolean().optional(),
|
|
16970
17201
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
16971
|
-
resume:
|
|
17202
|
+
resume: z11.boolean().optional(),
|
|
16972
17203
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
16973
|
-
refresh:
|
|
17204
|
+
refresh: z11.boolean().optional(),
|
|
16974
17205
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
16975
|
-
restore:
|
|
17206
|
+
restore: z11.boolean().optional(),
|
|
16976
17207
|
/**
|
|
16977
17208
|
* Bring a replaced generation back: 1-based position in the record's
|
|
16978
17209
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
16979
17210
|
*/
|
|
16980
|
-
revert:
|
|
17211
|
+
revert: z11.string().min(1).max(64).optional()
|
|
16981
17212
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
16982
17213
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
16983
17214
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17288,14 +17519,14 @@ function createGenerateHandlers(opts) {
|
|
|
17288
17519
|
|
|
17289
17520
|
// src/gallery/editor.ts
|
|
17290
17521
|
import { readFile as readFile23 } from "fs/promises";
|
|
17291
|
-
import
|
|
17522
|
+
import path33 from "path";
|
|
17292
17523
|
|
|
17293
17524
|
// src/editor/install.ts
|
|
17294
17525
|
import { createHash as createHash4 } from "crypto";
|
|
17295
|
-
import { existsSync as
|
|
17296
|
-
import { mkdir as
|
|
17526
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17527
|
+
import { mkdir as mkdir12, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile15 } from "fs/promises";
|
|
17297
17528
|
import os2 from "os";
|
|
17298
|
-
import
|
|
17529
|
+
import path32 from "path";
|
|
17299
17530
|
|
|
17300
17531
|
// tools/pixelorama-bridge/pin.json
|
|
17301
17532
|
var pin_default = {
|
|
@@ -17359,15 +17590,15 @@ var EditorInstallError = class extends Error {
|
|
|
17359
17590
|
};
|
|
17360
17591
|
function editorToolsRoot() {
|
|
17361
17592
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17362
|
-
if (override) return
|
|
17363
|
-
if (process.platform === "darwin") return
|
|
17593
|
+
if (override) return path32.resolve(override);
|
|
17594
|
+
if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17364
17595
|
if (process.platform === "win32") {
|
|
17365
|
-
return
|
|
17596
|
+
return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17366
17597
|
}
|
|
17367
|
-
return
|
|
17598
|
+
return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17368
17599
|
}
|
|
17369
17600
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17370
|
-
return
|
|
17601
|
+
return path32.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17371
17602
|
}
|
|
17372
17603
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17373
17604
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17390,8 +17621,8 @@ async function editorStatus(opts = {}) {
|
|
|
17390
17621
|
let totalBytes = 0;
|
|
17391
17622
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17392
17623
|
totalBytes += expected.bytes;
|
|
17393
|
-
const file =
|
|
17394
|
-
const info =
|
|
17624
|
+
const file = path32.join(dir, name);
|
|
17625
|
+
const info = existsSync25(file) ? await stat3(file).catch(() => null) : null;
|
|
17395
17626
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17396
17627
|
installedBytes += expected.bytes;
|
|
17397
17628
|
} else {
|
|
@@ -17420,7 +17651,7 @@ async function installEditor(opts = {}) {
|
|
|
17420
17651
|
);
|
|
17421
17652
|
}
|
|
17422
17653
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17423
|
-
await
|
|
17654
|
+
await mkdir12(dir, { recursive: true });
|
|
17424
17655
|
const downloaded = [];
|
|
17425
17656
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17426
17657
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17448,10 +17679,10 @@ async function installEditor(opts = {}) {
|
|
|
17448
17679
|
if (digest2 !== expected.sha256) {
|
|
17449
17680
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17450
17681
|
}
|
|
17451
|
-
const final =
|
|
17682
|
+
const final = path32.join(dir, name);
|
|
17452
17683
|
const tmp = `${final}.${process.pid}.part`;
|
|
17453
17684
|
try {
|
|
17454
|
-
await
|
|
17685
|
+
await writeFile15(tmp, bytes);
|
|
17455
17686
|
await rename9(tmp, final);
|
|
17456
17687
|
} finally {
|
|
17457
17688
|
await rm10(tmp, { force: true });
|
|
@@ -17494,7 +17725,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17494
17725
|
".json": "application/json; charset=utf-8"
|
|
17495
17726
|
};
|
|
17496
17727
|
function editorContentType(name) {
|
|
17497
|
-
return EDITOR_CONTENT_TYPES[
|
|
17728
|
+
return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
|
|
17498
17729
|
}
|
|
17499
17730
|
|
|
17500
17731
|
// src/gallery/editor.ts
|
|
@@ -17570,7 +17801,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17570
17801
|
if (!status.installed) return null;
|
|
17571
17802
|
let bytes;
|
|
17572
17803
|
try {
|
|
17573
|
-
bytes = await readFile23(
|
|
17804
|
+
bytes = await readFile23(path33.join(dir, name));
|
|
17574
17805
|
} catch {
|
|
17575
17806
|
verified = null;
|
|
17576
17807
|
return null;
|
|
@@ -17610,13 +17841,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17610
17841
|
};
|
|
17611
17842
|
const loadProject = async (project) => {
|
|
17612
17843
|
const ctx = await access2.loadProject(project);
|
|
17613
|
-
const dir =
|
|
17844
|
+
const dir = path34.dirname(ctx.loaded.path);
|
|
17614
17845
|
const declared = readEnvFiles(dir);
|
|
17615
17846
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
17616
17847
|
for (const [name, value] of Object.entries(declared)) {
|
|
17617
17848
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
17618
17849
|
throw new Error(
|
|
17619
|
-
`${
|
|
17850
|
+
`${path34.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.`
|
|
17620
17851
|
);
|
|
17621
17852
|
}
|
|
17622
17853
|
}
|
|
@@ -17644,6 +17875,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17644
17875
|
),
|
|
17645
17876
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
17646
17877
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
17878
|
+
...args.edit ? { price: createGalleryPriceHandler({ manifestFor: access2.manifestFor }) } : {},
|
|
17647
17879
|
// Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
|
|
17648
17880
|
...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
|
|
17649
17881
|
// The editor exists to write hand edits back, so it follows the write gate.
|
|
@@ -17663,8 +17895,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17663
17895
|
}
|
|
17664
17896
|
async function runGallery(args) {
|
|
17665
17897
|
if (args.workspace) {
|
|
17666
|
-
const workspacePath =
|
|
17667
|
-
if (!
|
|
17898
|
+
const workspacePath = path34.resolve(args.workspace);
|
|
17899
|
+
if (!existsSync26(workspacePath)) {
|
|
17668
17900
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
17669
17901
|
}
|
|
17670
17902
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -17684,7 +17916,7 @@ async function runGallery(args) {
|
|
|
17684
17916
|
const registered = async (projectId) => {
|
|
17685
17917
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
17686
17918
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
17687
|
-
return resolveProject(
|
|
17919
|
+
return resolveProject(path34.dirname(workspacePath), project);
|
|
17688
17920
|
};
|
|
17689
17921
|
await serveUntilStopped(initial, build2, args, {
|
|
17690
17922
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -17709,7 +17941,7 @@ async function runGallery(args) {
|
|
|
17709
17941
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
17710
17942
|
};
|
|
17711
17943
|
await serveUntilStopped(await build(), reload, args, {
|
|
17712
|
-
manifestFor: () =>
|
|
17944
|
+
manifestFor: () => path34.resolve(args.manifest),
|
|
17713
17945
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
17714
17946
|
});
|
|
17715
17947
|
}
|
|
@@ -18018,18 +18250,18 @@ async function runRestoreGeneration(args) {
|
|
|
18018
18250
|
}
|
|
18019
18251
|
|
|
18020
18252
|
// src/cli/commands/housekeeping.ts
|
|
18021
|
-
import { existsSync as
|
|
18253
|
+
import { existsSync as existsSync28 } from "fs";
|
|
18022
18254
|
|
|
18023
18255
|
// src/pipeline/cache-health.ts
|
|
18024
|
-
import { existsSync as
|
|
18256
|
+
import { existsSync as existsSync27 } from "fs";
|
|
18025
18257
|
import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
|
|
18026
|
-
import
|
|
18258
|
+
import path35 from "path";
|
|
18027
18259
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18028
|
-
if (options.prune && !
|
|
18029
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
18260
|
+
if (options.prune && !existsSync27(lockPath)) {
|
|
18261
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path35.resolve(lockPath)}`);
|
|
18030
18262
|
}
|
|
18031
|
-
const contentDir =
|
|
18032
|
-
const remotePath =
|
|
18263
|
+
const contentDir = path35.resolve(path35.dirname(lockPath), ".pixelkiln", "cache");
|
|
18264
|
+
const remotePath = path35.resolve(cachePathFor(lockPath));
|
|
18033
18265
|
const referenced = referencedHashes(lock);
|
|
18034
18266
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18035
18267
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18041,7 +18273,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18041
18273
|
]);
|
|
18042
18274
|
for (const name of names) {
|
|
18043
18275
|
try {
|
|
18044
|
-
await rm11(
|
|
18276
|
+
await rm11(path35.join(contentDir, name), { force: true });
|
|
18045
18277
|
removed.contentFiles++;
|
|
18046
18278
|
} catch {
|
|
18047
18279
|
}
|
|
@@ -18068,7 +18300,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18068
18300
|
async function inspectContentCache(contentDir, referenced) {
|
|
18069
18301
|
const report = {
|
|
18070
18302
|
path: contentDir,
|
|
18071
|
-
exists:
|
|
18303
|
+
exists: existsSync27(contentDir),
|
|
18072
18304
|
files: 0,
|
|
18073
18305
|
bytes: 0,
|
|
18074
18306
|
valid: 0,
|
|
@@ -18089,7 +18321,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18089
18321
|
continue;
|
|
18090
18322
|
}
|
|
18091
18323
|
report.files++;
|
|
18092
|
-
const file =
|
|
18324
|
+
const file = path35.join(contentDir, entry.name);
|
|
18093
18325
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
18094
18326
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
18095
18327
|
let bytes;
|
|
@@ -18132,7 +18364,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18132
18364
|
async function inspectRemoteHashCache(remotePath) {
|
|
18133
18365
|
const report = {
|
|
18134
18366
|
path: remotePath,
|
|
18135
|
-
exists:
|
|
18367
|
+
exists: existsSync27(remotePath),
|
|
18136
18368
|
entries: 0,
|
|
18137
18369
|
valid: 0,
|
|
18138
18370
|
invalidIds: [],
|
|
@@ -18205,7 +18437,7 @@ async function runAccept(args) {
|
|
|
18205
18437
|
let intact = true;
|
|
18206
18438
|
for (const output of entry.outputs) {
|
|
18207
18439
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
18208
|
-
if (!
|
|
18440
|
+
if (!existsSync28(file) || await sha256File(file) !== output.sha256) {
|
|
18209
18441
|
intact = false;
|
|
18210
18442
|
break;
|
|
18211
18443
|
}
|
|
@@ -18260,42 +18492,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
18260
18492
|
}
|
|
18261
18493
|
|
|
18262
18494
|
// src/cli/commands/init.ts
|
|
18263
|
-
import
|
|
18264
|
-
import { existsSync as
|
|
18495
|
+
import path36 from "path";
|
|
18496
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18265
18497
|
async function runInit(args) {
|
|
18266
18498
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
18267
|
-
const root =
|
|
18268
|
-
if (!
|
|
18499
|
+
const root = path36.resolve(args.from);
|
|
18500
|
+
if (!existsSync29(root)) throw new Error(`No directory at ${root}`);
|
|
18269
18501
|
const generator = args.generator ?? "map";
|
|
18270
18502
|
if (generator !== "1dir" && generator !== "map") {
|
|
18271
18503
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
18272
18504
|
}
|
|
18273
|
-
const target =
|
|
18505
|
+
const target = path36.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
18274
18506
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18275
18507
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18276
18508
|
const manifest = buildManifest(
|
|
18277
|
-
args.name ??
|
|
18509
|
+
args.name ?? path36.basename(path36.dirname(target)),
|
|
18278
18510
|
args.styles[0] ?? "base",
|
|
18279
18511
|
generator,
|
|
18280
|
-
|
|
18512
|
+
path36.relative(path36.dirname(target), root) || ".",
|
|
18281
18513
|
assets
|
|
18282
18514
|
);
|
|
18283
18515
|
await writeManifestFile(target, manifest);
|
|
18284
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
18516
|
+
log(` scanned ${assets.length} PNG(s) under ${path36.relative(process.cwd(), root)}`);
|
|
18285
18517
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18286
|
-
log(` wrote ${
|
|
18518
|
+
log(` wrote ${path36.relative(process.cwd(), target)}`);
|
|
18287
18519
|
log(`
|
|
18288
18520
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18289
18521
|
log(` PixelLab account instead of inventing them:`);
|
|
18290
18522
|
log(`
|
|
18291
|
-
pixelkiln adopt --manifest ${
|
|
18523
|
+
pixelkiln adopt --manifest ${path36.relative(process.cwd(), target)} --write-prompts
|
|
18292
18524
|
`);
|
|
18293
18525
|
}
|
|
18294
18526
|
|
|
18295
18527
|
// src/pipeline/doctor.ts
|
|
18296
|
-
import { constants, existsSync as
|
|
18528
|
+
import { constants, existsSync as existsSync30 } from "fs";
|
|
18297
18529
|
import { access, stat as stat4 } from "fs/promises";
|
|
18298
|
-
import
|
|
18530
|
+
import path37 from "path";
|
|
18299
18531
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18300
18532
|
const checks = [];
|
|
18301
18533
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18306,8 +18538,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18306
18538
|
);
|
|
18307
18539
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18308
18540
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18309
|
-
|
|
18310
|
-
...spec.quality ? [
|
|
18541
|
+
path37.dirname(spec.outFile),
|
|
18542
|
+
...spec.quality ? [path37.dirname(spec.quality.outFile)] : []
|
|
18311
18543
|
]))];
|
|
18312
18544
|
const unwritable = [];
|
|
18313
18545
|
for (const dir of dirs) {
|
|
@@ -18330,7 +18562,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18330
18562
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18331
18563
|
const output = entry.outputs[index];
|
|
18332
18564
|
const spec = specByKey.get(key);
|
|
18333
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
18565
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path37.dirname(lockPath));
|
|
18334
18566
|
const owner = outputOwners.get(absolute);
|
|
18335
18567
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18336
18568
|
outputOwners.set(absolute, key);
|
|
@@ -18345,7 +18577,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18345
18577
|
} else {
|
|
18346
18578
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18347
18579
|
}
|
|
18348
|
-
const cacheDir =
|
|
18580
|
+
const cacheDir = path37.join(path37.dirname(lockPath), ".pixelkiln", "cache");
|
|
18349
18581
|
const stranded = [];
|
|
18350
18582
|
const invalidState = [];
|
|
18351
18583
|
const staleMapUrls = [];
|
|
@@ -18361,11 +18593,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18361
18593
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18362
18594
|
let hasCache = false;
|
|
18363
18595
|
for (const output of entry.outputs) {
|
|
18364
|
-
const cached =
|
|
18596
|
+
const cached = path37.join(
|
|
18365
18597
|
cacheDir,
|
|
18366
18598
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18367
18599
|
);
|
|
18368
|
-
if (
|
|
18600
|
+
if (existsSync30(cached) && await sha256File(cached) === output.sha256) {
|
|
18369
18601
|
hasCache = true;
|
|
18370
18602
|
break;
|
|
18371
18603
|
}
|
|
@@ -18461,7 +18693,7 @@ async function nearestExistingDirectory(start) {
|
|
|
18461
18693
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18462
18694
|
} catch {
|
|
18463
18695
|
}
|
|
18464
|
-
const parent =
|
|
18696
|
+
const parent = path37.dirname(current);
|
|
18465
18697
|
if (parent === current) return current;
|
|
18466
18698
|
current = parent;
|
|
18467
18699
|
}
|
|
@@ -18469,8 +18701,8 @@ async function nearestExistingDirectory(start) {
|
|
|
18469
18701
|
|
|
18470
18702
|
// src/pipeline/audit.ts
|
|
18471
18703
|
import { readFile as readFile25 } from "fs/promises";
|
|
18472
|
-
import { existsSync as
|
|
18473
|
-
import
|
|
18704
|
+
import { existsSync as existsSync31 } from "fs";
|
|
18705
|
+
import path38 from "path";
|
|
18474
18706
|
function paletteDistance(asset, reference) {
|
|
18475
18707
|
if (!asset.length || !reference.length) return 0;
|
|
18476
18708
|
let total = 0;
|
|
@@ -18506,7 +18738,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18506
18738
|
const unreadable = [];
|
|
18507
18739
|
for (const spec of mine) {
|
|
18508
18740
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18509
|
-
if (!
|
|
18741
|
+
if (!existsSync31(output.absolutePath)) {
|
|
18510
18742
|
missing.push(output.id);
|
|
18511
18743
|
continue;
|
|
18512
18744
|
}
|
|
@@ -18535,8 +18767,8 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18535
18767
|
let referenceFromStyleImages = false;
|
|
18536
18768
|
const refPalettes = [];
|
|
18537
18769
|
for (const img of style.styleImages) {
|
|
18538
|
-
const abs =
|
|
18539
|
-
if (!
|
|
18770
|
+
const abs = path38.resolve(loaded.root, img.path);
|
|
18771
|
+
if (!existsSync31(abs)) continue;
|
|
18540
18772
|
try {
|
|
18541
18773
|
refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
|
|
18542
18774
|
} catch {
|
|
@@ -18766,24 +18998,24 @@ async function runAudit(args) {
|
|
|
18766
18998
|
}
|
|
18767
18999
|
|
|
18768
19000
|
// src/cli/commands/pack.ts
|
|
18769
|
-
import
|
|
19001
|
+
import path40 from "path";
|
|
18770
19002
|
import { readFile as readFile26 } from "fs/promises";
|
|
18771
19003
|
|
|
18772
19004
|
// src/pipeline/pack.ts
|
|
18773
19005
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18774
|
-
import
|
|
19006
|
+
import path39 from "path";
|
|
18775
19007
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
18776
19008
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
18777
19009
|
if (!Array.isArray(raw) || !raw.length) {
|
|
18778
19010
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
18779
19011
|
}
|
|
18780
|
-
const baseDir =
|
|
19012
|
+
const baseDir = path39.dirname(path39.resolve(inputsFilePath));
|
|
18781
19013
|
return raw.map((entry, i) => {
|
|
18782
19014
|
const e = entry;
|
|
18783
19015
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
18784
19016
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
18785
19017
|
}
|
|
18786
|
-
return { id: e.id, path:
|
|
19018
|
+
return { id: e.id, path: path39.resolve(baseDir, e.path) };
|
|
18787
19019
|
});
|
|
18788
19020
|
}
|
|
18789
19021
|
function packSprites(inputs, options = {}) {
|
|
@@ -18867,7 +19099,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18867
19099
|
}
|
|
18868
19100
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
18869
19101
|
id,
|
|
18870
|
-
path:
|
|
19102
|
+
path: path39.resolve(source)
|
|
18871
19103
|
}));
|
|
18872
19104
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
18873
19105
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -18891,7 +19123,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18891
19123
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
18892
19124
|
const source = sources[id];
|
|
18893
19125
|
if (source && outputs.length <= 1) {
|
|
18894
|
-
inputs.push({ id, path:
|
|
19126
|
+
inputs.push({ id, path: path39.resolve(manifestDir, source) });
|
|
18895
19127
|
continue;
|
|
18896
19128
|
}
|
|
18897
19129
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -18925,7 +19157,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18925
19157
|
}
|
|
18926
19158
|
}
|
|
18927
19159
|
for (const [id, source] of Object.entries(sources)) {
|
|
18928
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
19160
|
+
if (!locked.has(id)) inputs.push({ id, path: path39.resolve(manifestDir, source) });
|
|
18929
19161
|
}
|
|
18930
19162
|
if (!inputs.length) {
|
|
18931
19163
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19033,7 +19265,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19033
19265
|
const source = sources[id];
|
|
19034
19266
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19035
19267
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19036
|
-
placements.push({ id, path:
|
|
19268
|
+
placements.push({ id, path: path39.resolve(manifestDir, source), cell });
|
|
19037
19269
|
continue;
|
|
19038
19270
|
}
|
|
19039
19271
|
if (source && entry) {
|
|
@@ -19066,7 +19298,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19066
19298
|
`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}`
|
|
19067
19299
|
);
|
|
19068
19300
|
}
|
|
19069
|
-
const basePng = mount.base ? readFileSync5(
|
|
19301
|
+
const basePng = mount.base ? readFileSync5(path39.resolve(manifestDir, mount.base)) : void 0;
|
|
19070
19302
|
const mounted = mountSprites(placements, {
|
|
19071
19303
|
cellWidth: mount.cellWidth,
|
|
19072
19304
|
cellHeight: mount.cellHeight,
|
|
@@ -19076,7 +19308,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19076
19308
|
...mounted,
|
|
19077
19309
|
sources: mount.base ? [{
|
|
19078
19310
|
id: "$base",
|
|
19079
|
-
path:
|
|
19311
|
+
path: path39.resolve(manifestDir, mount.base),
|
|
19080
19312
|
sha256: sha256(basePng),
|
|
19081
19313
|
included: true
|
|
19082
19314
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19480,12 +19712,12 @@ async function runPack(args) {
|
|
|
19480
19712
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19481
19713
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19482
19714
|
}
|
|
19483
|
-
const raw = JSON.parse(await readFile26(
|
|
19715
|
+
const raw = JSON.parse(await readFile26(path40.resolve(args.inputs), "utf8"));
|
|
19484
19716
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19485
19717
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19486
19718
|
const format = sheetFormat(args);
|
|
19487
|
-
const base =
|
|
19488
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19719
|
+
const base = path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
19720
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
|
|
19489
19721
|
const outputs = [
|
|
19490
19722
|
{ path: `${base}.png`, data: png },
|
|
19491
19723
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19498,7 +19730,7 @@ async function runPack(args) {
|
|
|
19498
19730
|
log(
|
|
19499
19731
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
19500
19732
|
);
|
|
19501
|
-
log(` ${
|
|
19733
|
+
log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19502
19734
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19503
19735
|
return;
|
|
19504
19736
|
}
|
|
@@ -19506,7 +19738,7 @@ async function runPack(args) {
|
|
|
19506
19738
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19507
19739
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19508
19740
|
}
|
|
19509
|
-
const manifestDir =
|
|
19741
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19510
19742
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19511
19743
|
for (const styleId of styleIds) {
|
|
19512
19744
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19521,9 +19753,9 @@ async function runPack(args) {
|
|
|
19521
19753
|
sourceOverrides: qualitySources,
|
|
19522
19754
|
sources: manifestSources(loaded.manifest, styleId)
|
|
19523
19755
|
});
|
|
19524
|
-
const base = args.out ?
|
|
19756
|
+
const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path40.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
19525
19757
|
const format = sheetFormat(args);
|
|
19526
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19758
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
|
|
19527
19759
|
const outputs = [
|
|
19528
19760
|
{ path: `${base}.png`, data: png },
|
|
19529
19761
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19555,13 +19787,13 @@ async function runPack(args) {
|
|
|
19555
19787
|
log(
|
|
19556
19788
|
` ${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})`
|
|
19557
19789
|
);
|
|
19558
|
-
log(` ${
|
|
19790
|
+
log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19559
19791
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19560
19792
|
}
|
|
19561
19793
|
}
|
|
19562
19794
|
async function runMount(args) {
|
|
19563
19795
|
const { loaded, lock } = await openProject2(args);
|
|
19564
|
-
const manifestDir =
|
|
19796
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19565
19797
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19566
19798
|
for (const styleId of styleIds) {
|
|
19567
19799
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19600,9 +19832,9 @@ async function runMount(args) {
|
|
|
19600
19832
|
sources,
|
|
19601
19833
|
outputRoles
|
|
19602
19834
|
);
|
|
19603
|
-
const out =
|
|
19835
|
+
const out = path40.resolve(manifestDir, style.mount.out);
|
|
19604
19836
|
const format = sheetFormat(args);
|
|
19605
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19837
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(out) });
|
|
19606
19838
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
19607
19839
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
19608
19840
|
const outputs = [
|
|
@@ -19622,7 +19854,7 @@ async function runMount(args) {
|
|
|
19622
19854
|
await provenanceFile("$lock", args.lock),
|
|
19623
19855
|
...qualityRecords,
|
|
19624
19856
|
...artifactSources.filter(
|
|
19625
|
-
(source) => source.id !== "$base" ||
|
|
19857
|
+
(source) => source.id !== "$base" || path40.resolve(source.path) !== out
|
|
19626
19858
|
)
|
|
19627
19859
|
],
|
|
19628
19860
|
options: {
|
|
@@ -19636,14 +19868,14 @@ async function runMount(args) {
|
|
|
19636
19868
|
log(
|
|
19637
19869
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
19638
19870
|
);
|
|
19639
|
-
log(` ${
|
|
19871
|
+
log(` ${path40.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
19640
19872
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19641
19873
|
}
|
|
19642
19874
|
}
|
|
19643
19875
|
async function runExport(args) {
|
|
19644
19876
|
const { loaded, specs, lock } = await openProject2(args);
|
|
19645
19877
|
const format = args.format ?? "generic";
|
|
19646
|
-
const manifestDir =
|
|
19878
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19647
19879
|
const selected = specs.filter((spec) => {
|
|
19648
19880
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
19649
19881
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -19659,12 +19891,12 @@ async function runExport(args) {
|
|
|
19659
19891
|
for (const spec of selected) {
|
|
19660
19892
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
19661
19893
|
const style = loaded.manifest.styles[spec.styleId];
|
|
19662
|
-
const defaultBase =
|
|
19663
|
-
const base = args.out ?
|
|
19894
|
+
const defaultBase = path40.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
19895
|
+
const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
19664
19896
|
const result = exportTileset(entry, spec, {
|
|
19665
19897
|
format,
|
|
19666
19898
|
manifestDir,
|
|
19667
|
-
imageName:
|
|
19899
|
+
imageName: path40.basename(`${base}.png`),
|
|
19668
19900
|
columns: args.columns
|
|
19669
19901
|
});
|
|
19670
19902
|
const outputs = [
|
|
@@ -19682,7 +19914,7 @@ async function runExport(args) {
|
|
|
19682
19914
|
asset: spec.assetId,
|
|
19683
19915
|
columns: args.columns ?? null,
|
|
19684
19916
|
format,
|
|
19685
|
-
image:
|
|
19917
|
+
image: path40.basename(`${base}.png`),
|
|
19686
19918
|
providerRules: result.generic.providerRules,
|
|
19687
19919
|
style: spec.styleId,
|
|
19688
19920
|
tileType: spec.tileType ?? null
|
|
@@ -19692,7 +19924,7 @@ async function runExport(args) {
|
|
|
19692
19924
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
19693
19925
|
);
|
|
19694
19926
|
log(
|
|
19695
|
-
` ${
|
|
19927
|
+
` ${path40.relative(process.cwd(), base)}.png + ${path40.basename(base)}${result.extension} + .pixelkiln.json`
|
|
19696
19928
|
);
|
|
19697
19929
|
}
|
|
19698
19930
|
}
|
|
@@ -19705,22 +19937,22 @@ function sheetFormat(args) {
|
|
|
19705
19937
|
}
|
|
19706
19938
|
|
|
19707
19939
|
// src/cli/commands/pixellab-utils.ts
|
|
19708
|
-
import
|
|
19940
|
+
import path42 from "path";
|
|
19709
19941
|
|
|
19710
19942
|
// src/pixellab-utils.ts
|
|
19711
|
-
import { existsSync as
|
|
19712
|
-
import { mkdir as
|
|
19713
|
-
import
|
|
19943
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19944
|
+
import { mkdir as mkdir13, readFile as readFile27, writeFile as writeFile16 } from "fs/promises";
|
|
19945
|
+
import path41 from "path";
|
|
19714
19946
|
var UNZOOM_MIN_SIDE = 256;
|
|
19715
19947
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
19716
19948
|
function defaultUnzoomOut(input) {
|
|
19717
|
-
const ext =
|
|
19949
|
+
const ext = path41.extname(input);
|
|
19718
19950
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
19719
19951
|
}
|
|
19720
19952
|
async function unzoomFile(client2, input, opts = {}) {
|
|
19721
|
-
const out =
|
|
19722
|
-
if (
|
|
19723
|
-
if (
|
|
19953
|
+
const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
19954
|
+
if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
19955
|
+
if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
19724
19956
|
const bytes = await readFile27(input);
|
|
19725
19957
|
const metadata = imageMetadata(bytes);
|
|
19726
19958
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
@@ -19734,22 +19966,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
19734
19966
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
19735
19967
|
}
|
|
19736
19968
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
19737
|
-
await
|
|
19738
|
-
await
|
|
19969
|
+
await mkdir13(path41.dirname(out), { recursive: true });
|
|
19970
|
+
await writeFile16(out, res.png);
|
|
19739
19971
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
19740
19972
|
}
|
|
19741
19973
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
19742
19974
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
19743
19975
|
var FONT_COST_GENERATIONS = 25;
|
|
19744
19976
|
function fontOutputPaths(out) {
|
|
19745
|
-
const base =
|
|
19977
|
+
const base = path41.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
19746
19978
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
19747
19979
|
}
|
|
19748
19980
|
async function generateFont(client2, opts) {
|
|
19749
19981
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
19750
19982
|
if (!opts.force) {
|
|
19751
19983
|
for (const file of [ttf, atlas]) {
|
|
19752
|
-
if (
|
|
19984
|
+
if (existsSync32(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
19753
19985
|
}
|
|
19754
19986
|
}
|
|
19755
19987
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -19776,9 +20008,9 @@ async function generateFont(client2, opts) {
|
|
|
19776
20008
|
client2.download(job.download_ttf_url),
|
|
19777
20009
|
client2.download(job.download_atlas_url)
|
|
19778
20010
|
]);
|
|
19779
|
-
await
|
|
19780
|
-
await
|
|
19781
|
-
await
|
|
20011
|
+
await mkdir13(path41.dirname(ttf), { recursive: true });
|
|
20012
|
+
await writeFile16(ttf, ttfBytes);
|
|
20013
|
+
await writeFile16(atlas, atlasBytes);
|
|
19782
20014
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
19783
20015
|
}
|
|
19784
20016
|
if (Date.now() - started > timeout) {
|
|
@@ -19792,7 +20024,7 @@ async function generateFont(client2, opts) {
|
|
|
19792
20024
|
|
|
19793
20025
|
// src/cli/commands/pixellab-utils.ts
|
|
19794
20026
|
function loadKeys(args) {
|
|
19795
|
-
loadEnvFiles(
|
|
20027
|
+
loadEnvFiles(path42.dirname(path42.resolve(args.manifest)));
|
|
19796
20028
|
loadEnvFiles(process.cwd());
|
|
19797
20029
|
}
|
|
19798
20030
|
async function runUnzoom(args) {
|
|
@@ -19858,19 +20090,19 @@ async function runFont(args) {
|
|
|
19858
20090
|
}
|
|
19859
20091
|
|
|
19860
20092
|
// src/cli/commands/quality.ts
|
|
19861
|
-
import
|
|
20093
|
+
import path44 from "path";
|
|
19862
20094
|
import { readFile as readFile29 } from "fs/promises";
|
|
19863
20095
|
|
|
19864
20096
|
// src/pipeline/quality-regression.ts
|
|
19865
|
-
import
|
|
19866
|
-
import { existsSync as
|
|
20097
|
+
import path43 from "path";
|
|
20098
|
+
import { existsSync as existsSync33 } from "fs";
|
|
19867
20099
|
import { readFile as readFile28 } from "fs/promises";
|
|
19868
|
-
import { z as
|
|
20100
|
+
import { z as z12 } from "zod";
|
|
19869
20101
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19870
20102
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
19871
20103
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
19872
|
-
var QualityPathSchema =
|
|
19873
|
-
(value) => !
|
|
20104
|
+
var QualityPathSchema = z12.string().min(1).refine(
|
|
20105
|
+
(value) => !path43.posix.isAbsolute(value) && !value.includes("\\"),
|
|
19874
20106
|
"expected a portable relative path"
|
|
19875
20107
|
);
|
|
19876
20108
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -19883,53 +20115,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
19883
20115
|
maxEdgeContrastDrop: 0.03,
|
|
19884
20116
|
maxIsolatedPixelIncrease: 5e-3
|
|
19885
20117
|
};
|
|
19886
|
-
var QualityToleranceSchema =
|
|
19887
|
-
requireExactHash:
|
|
19888
|
-
maxColorCountIncrease:
|
|
19889
|
-
maxNewColors:
|
|
19890
|
-
maxTransparencyDelta:
|
|
19891
|
-
maxPartialAlphaIncrease:
|
|
19892
|
-
maxEdgeDensityDelta:
|
|
19893
|
-
maxEdgeContrastDrop:
|
|
19894
|
-
maxIsolatedPixelIncrease:
|
|
20118
|
+
var QualityToleranceSchema = z12.object({
|
|
20119
|
+
requireExactHash: z12.boolean(),
|
|
20120
|
+
maxColorCountIncrease: z12.number().int().min(0),
|
|
20121
|
+
maxNewColors: z12.number().int().min(0),
|
|
20122
|
+
maxTransparencyDelta: z12.number().min(0).max(1),
|
|
20123
|
+
maxPartialAlphaIncrease: z12.number().min(0).max(1),
|
|
20124
|
+
maxEdgeDensityDelta: z12.number().min(0).max(1),
|
|
20125
|
+
maxEdgeContrastDrop: z12.number().min(0).max(1),
|
|
20126
|
+
maxIsolatedPixelIncrease: z12.number().min(0).max(1)
|
|
19895
20127
|
}).strict();
|
|
19896
|
-
var ImageQualityMetricsSchema =
|
|
19897
|
-
sha256:
|
|
19898
|
-
width:
|
|
19899
|
-
height:
|
|
19900
|
-
colorCount:
|
|
19901
|
-
colors:
|
|
19902
|
-
transparency:
|
|
19903
|
-
partialAlpha:
|
|
19904
|
-
edgeDensity:
|
|
19905
|
-
meanEdgeContrast:
|
|
19906
|
-
isolatedPixelRatio:
|
|
20128
|
+
var ImageQualityMetricsSchema = z12.object({
|
|
20129
|
+
sha256: z12.string().regex(SHA256_RE),
|
|
20130
|
+
width: z12.number().int().min(1),
|
|
20131
|
+
height: z12.number().int().min(1),
|
|
20132
|
+
colorCount: z12.number().int().min(0),
|
|
20133
|
+
colors: z12.array(z12.string().regex(HEX_RE)),
|
|
20134
|
+
transparency: z12.number().min(0).max(1),
|
|
20135
|
+
partialAlpha: z12.number().min(0).max(1),
|
|
20136
|
+
edgeDensity: z12.number().min(0).max(1),
|
|
20137
|
+
meanEdgeContrast: z12.number().min(0).max(1),
|
|
20138
|
+
isolatedPixelRatio: z12.number().min(0).max(1)
|
|
19907
20139
|
}).strict();
|
|
19908
|
-
var QualityBaselineCaseSchema =
|
|
19909
|
-
id:
|
|
20140
|
+
var QualityBaselineCaseSchema = z12.object({
|
|
20141
|
+
id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19910
20142
|
file: QualityPathSchema,
|
|
19911
|
-
record:
|
|
20143
|
+
record: z12.object({
|
|
19912
20144
|
path: QualityPathSchema,
|
|
19913
|
-
sha256:
|
|
20145
|
+
sha256: z12.string().regex(SHA256_RE)
|
|
19914
20146
|
}).strict().optional(),
|
|
19915
20147
|
expected: ImageQualityMetricsSchema,
|
|
19916
20148
|
tolerances: QualityToleranceSchema
|
|
19917
20149
|
}).strict();
|
|
19918
|
-
var QualityBaselineSchema =
|
|
19919
|
-
$schema:
|
|
19920
|
-
format:
|
|
19921
|
-
schemaVersion:
|
|
19922
|
-
cases:
|
|
20150
|
+
var QualityBaselineSchema = z12.object({
|
|
20151
|
+
$schema: z12.string().url().optional(),
|
|
20152
|
+
format: z12.literal("pixelkiln-quality-baseline"),
|
|
20153
|
+
schemaVersion: z12.literal(1),
|
|
20154
|
+
cases: z12.array(QualityBaselineCaseSchema).min(1)
|
|
19923
20155
|
}).strict().superRefine((baseline, context) => {
|
|
19924
20156
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
19925
20157
|
if (new Set(ids).size !== ids.length) {
|
|
19926
|
-
context.addIssue({ code:
|
|
20158
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
19927
20159
|
}
|
|
19928
20160
|
});
|
|
19929
|
-
var QualityInputSchema =
|
|
19930
|
-
id:
|
|
19931
|
-
path:
|
|
19932
|
-
record:
|
|
20161
|
+
var QualityInputSchema = z12.object({
|
|
20162
|
+
id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20163
|
+
path: z12.string().min(1),
|
|
20164
|
+
record: z12.string().min(1).optional(),
|
|
19933
20165
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
19934
20166
|
}).strict();
|
|
19935
20167
|
function message4(error) {
|
|
@@ -20017,7 +20249,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20017
20249
|
};
|
|
20018
20250
|
}
|
|
20019
20251
|
async function measureImageQuality(file) {
|
|
20020
|
-
const absolute =
|
|
20252
|
+
const absolute = path43.resolve(file);
|
|
20021
20253
|
let bytes;
|
|
20022
20254
|
try {
|
|
20023
20255
|
bytes = await readFile28(absolute);
|
|
@@ -20031,7 +20263,7 @@ async function measureImageQuality(file) {
|
|
|
20031
20263
|
}
|
|
20032
20264
|
}
|
|
20033
20265
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
20034
|
-
const parsed =
|
|
20266
|
+
const parsed = z12.array(QualityInputSchema).min(1).safeParse(raw);
|
|
20035
20267
|
if (!parsed.success) {
|
|
20036
20268
|
throw new Error(
|
|
20037
20269
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -20039,11 +20271,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20039
20271
|
}
|
|
20040
20272
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20041
20273
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20042
|
-
const root =
|
|
20274
|
+
const root = path43.dirname(path43.resolve(inputsFilePath));
|
|
20043
20275
|
return parsed.data.map((entry) => ({
|
|
20044
20276
|
id: entry.id,
|
|
20045
|
-
file:
|
|
20046
|
-
...entry.record ? { record:
|
|
20277
|
+
file: path43.resolve(root, entry.path),
|
|
20278
|
+
...entry.record ? { record: path43.resolve(root, entry.record) } : {},
|
|
20047
20279
|
tolerances: QualityToleranceSchema.parse({
|
|
20048
20280
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20049
20281
|
...entry.tolerances
|
|
@@ -20051,7 +20283,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20051
20283
|
}));
|
|
20052
20284
|
}
|
|
20053
20285
|
function portableRelative2(from, to) {
|
|
20054
|
-
return
|
|
20286
|
+
return path43.relative(from, path43.resolve(to)).split(path43.sep).join("/") || ".";
|
|
20055
20287
|
}
|
|
20056
20288
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20057
20289
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20066,20 +20298,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20066
20298
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20067
20299
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20068
20300
|
}
|
|
20069
|
-
const outputs = record.outputs.map((output) =>
|
|
20070
|
-
if (!outputs.includes(
|
|
20301
|
+
const outputs = record.outputs.map((output) => path43.resolve(path43.dirname(recordPath), output.path));
|
|
20302
|
+
if (!outputs.includes(path43.resolve(imagePath))) {
|
|
20071
20303
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20072
20304
|
}
|
|
20073
20305
|
}
|
|
20074
20306
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
20075
20307
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
20076
|
-
const absolute =
|
|
20308
|
+
const absolute = path43.resolve(baselinePath);
|
|
20077
20309
|
for (const input of inputs) {
|
|
20078
|
-
if (
|
|
20310
|
+
if (path43.resolve(input.file) === absolute || input.record && path43.resolve(input.record) === absolute) {
|
|
20079
20311
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
20080
20312
|
}
|
|
20081
20313
|
}
|
|
20082
|
-
const root =
|
|
20314
|
+
const root = path43.dirname(absolute);
|
|
20083
20315
|
const cases = [];
|
|
20084
20316
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
20085
20317
|
const expected = await measureImageQuality(input.file);
|
|
@@ -20106,7 +20338,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20106
20338
|
cases
|
|
20107
20339
|
});
|
|
20108
20340
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
20109
|
-
if (
|
|
20341
|
+
if (existsSync33(absolute) && !options.force) {
|
|
20110
20342
|
const current = await readFile28(absolute);
|
|
20111
20343
|
if (!current.equals(data)) {
|
|
20112
20344
|
throw new OverwriteRefusedError(
|
|
@@ -20119,7 +20351,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20119
20351
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
20120
20352
|
}
|
|
20121
20353
|
async function readQualityBaseline(baselinePath) {
|
|
20122
|
-
const absolute =
|
|
20354
|
+
const absolute = path43.resolve(baselinePath);
|
|
20123
20355
|
let raw;
|
|
20124
20356
|
try {
|
|
20125
20357
|
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
@@ -20192,12 +20424,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
20192
20424
|
return { violations, warnings };
|
|
20193
20425
|
}
|
|
20194
20426
|
async function checkQualityBaseline(baselinePath) {
|
|
20195
|
-
const absolute =
|
|
20427
|
+
const absolute = path43.resolve(baselinePath);
|
|
20196
20428
|
const baseline = await readQualityBaseline(absolute);
|
|
20197
|
-
const root =
|
|
20429
|
+
const root = path43.dirname(absolute);
|
|
20198
20430
|
const cases = [];
|
|
20199
20431
|
for (const entry of baseline.cases) {
|
|
20200
|
-
const file =
|
|
20432
|
+
const file = path43.resolve(root, entry.file);
|
|
20201
20433
|
let actual = null;
|
|
20202
20434
|
const violations = [];
|
|
20203
20435
|
const warnings = [];
|
|
@@ -20210,7 +20442,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
20210
20442
|
violations.push(message4(error));
|
|
20211
20443
|
}
|
|
20212
20444
|
if (entry.record) {
|
|
20213
|
-
const recordPath =
|
|
20445
|
+
const recordPath = path43.resolve(root, entry.record.path);
|
|
20214
20446
|
try {
|
|
20215
20447
|
const recordHash = await sha256File(recordPath);
|
|
20216
20448
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -20252,15 +20484,15 @@ async function runQuality(args) {
|
|
|
20252
20484
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
20253
20485
|
let raw;
|
|
20254
20486
|
try {
|
|
20255
|
-
raw = JSON.parse(await readFile29(
|
|
20487
|
+
raw = JSON.parse(await readFile29(path44.resolve(args.inputs), "utf8"));
|
|
20256
20488
|
} catch (error) {
|
|
20257
20489
|
throw new Error(
|
|
20258
|
-
`Could not read quality inputs ${
|
|
20490
|
+
`Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
20259
20491
|
{ cause: error }
|
|
20260
20492
|
);
|
|
20261
20493
|
}
|
|
20262
20494
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
20263
|
-
if (
|
|
20495
|
+
if (path44.resolve(args.inputs) === path44.resolve(args.out)) {
|
|
20264
20496
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
20265
20497
|
}
|
|
20266
20498
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -20273,7 +20505,7 @@ async function runQuality(args) {
|
|
|
20273
20505
|
}, null, 2));
|
|
20274
20506
|
} else {
|
|
20275
20507
|
log(
|
|
20276
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
20508
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path44.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20277
20509
|
);
|
|
20278
20510
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20279
20511
|
}
|
|
@@ -20299,54 +20531,54 @@ async function runQuality(args) {
|
|
|
20299
20531
|
}
|
|
20300
20532
|
|
|
20301
20533
|
// src/cli/commands/recipe.ts
|
|
20302
|
-
import
|
|
20534
|
+
import path46 from "path";
|
|
20303
20535
|
|
|
20304
20536
|
// src/recipes.ts
|
|
20305
|
-
import
|
|
20306
|
-
import { existsSync as
|
|
20537
|
+
import path45 from "path";
|
|
20538
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20307
20539
|
import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
|
|
20308
20540
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20309
|
-
import { z as
|
|
20541
|
+
import { z as z13 } from "zod";
|
|
20310
20542
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20311
20543
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20312
20544
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20313
20545
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20314
20546
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20315
|
-
var PortablePathSchema =
|
|
20316
|
-
if (value.includes("\\") ||
|
|
20547
|
+
var PortablePathSchema = z13.string().min(1).refine((value) => {
|
|
20548
|
+
if (value.includes("\\") || path45.posix.isAbsolute(value)) return false;
|
|
20317
20549
|
const parts = value.split("/");
|
|
20318
20550
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20319
20551
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20320
|
-
var RecipeBindingSchema =
|
|
20321
|
-
nodeId:
|
|
20322
|
-
input:
|
|
20552
|
+
var RecipeBindingSchema = z13.object({
|
|
20553
|
+
nodeId: z13.string().min(1),
|
|
20554
|
+
input: z13.string().min(1)
|
|
20323
20555
|
}).strict();
|
|
20324
|
-
var RecipeSchema =
|
|
20325
|
-
$schema:
|
|
20326
|
-
format:
|
|
20327
|
-
schemaVersion:
|
|
20328
|
-
id:
|
|
20329
|
-
version:
|
|
20330
|
-
provider:
|
|
20331
|
-
summary:
|
|
20332
|
-
files:
|
|
20556
|
+
var RecipeSchema = z13.object({
|
|
20557
|
+
$schema: z13.string().url().optional(),
|
|
20558
|
+
format: z13.literal("pixelkiln-recipe"),
|
|
20559
|
+
schemaVersion: z13.literal(1),
|
|
20560
|
+
id: z13.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
20561
|
+
version: z13.string().regex(VERSION_RE, "expected x.y.z"),
|
|
20562
|
+
provider: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20563
|
+
summary: z13.string().min(1).max(240),
|
|
20564
|
+
files: z13.array(z13.object({
|
|
20333
20565
|
path: PortablePathSchema,
|
|
20334
|
-
role:
|
|
20335
|
-
sha256:
|
|
20566
|
+
role: z13.enum(["workflow", "reference"]),
|
|
20567
|
+
sha256: z13.string().regex(SHA256_RE2)
|
|
20336
20568
|
}).strict()).default([]),
|
|
20337
|
-
models:
|
|
20569
|
+
models: z13.array(z13.object({
|
|
20338
20570
|
path: PortablePathSchema,
|
|
20339
|
-
sha256:
|
|
20340
|
-
source:
|
|
20341
|
-
license:
|
|
20571
|
+
sha256: z13.string().regex(SHA256_RE2),
|
|
20572
|
+
source: z13.string().url(),
|
|
20573
|
+
license: z13.string().min(1)
|
|
20342
20574
|
}).strict()).default([]),
|
|
20343
|
-
styleId:
|
|
20575
|
+
styleId: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20344
20576
|
style: StyleSchema,
|
|
20345
|
-
workflow:
|
|
20577
|
+
workflow: z13.object({
|
|
20346
20578
|
path: PortablePathSchema,
|
|
20347
|
-
outputNodeId:
|
|
20348
|
-
numImages:
|
|
20349
|
-
bindings:
|
|
20579
|
+
outputNodeId: z13.string().min(1),
|
|
20580
|
+
numImages: z13.number().int().min(1).max(16),
|
|
20581
|
+
bindings: z13.object({
|
|
20350
20582
|
prompt: RecipeBindingSchema,
|
|
20351
20583
|
width: RecipeBindingSchema.optional(),
|
|
20352
20584
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20357,67 +20589,67 @@ var RecipeSchema = z12.object({
|
|
|
20357
20589
|
strength: RecipeBindingSchema.optional()
|
|
20358
20590
|
}).strict()
|
|
20359
20591
|
}).strict().optional(),
|
|
20360
|
-
quality:
|
|
20361
|
-
stage:
|
|
20362
|
-
workingCanvas:
|
|
20363
|
-
width:
|
|
20364
|
-
height:
|
|
20592
|
+
quality: z13.object({
|
|
20593
|
+
stage: z13.enum(["composition-source", "production-candidate"]),
|
|
20594
|
+
workingCanvas: z13.object({
|
|
20595
|
+
width: z13.number().int().min(16),
|
|
20596
|
+
height: z13.number().int().min(16)
|
|
20365
20597
|
}).strict(),
|
|
20366
|
-
recommendedNativeSize:
|
|
20367
|
-
min:
|
|
20368
|
-
max:
|
|
20598
|
+
recommendedNativeSize: z13.object({
|
|
20599
|
+
min: z13.number().int().min(1),
|
|
20600
|
+
max: z13.number().int().min(1)
|
|
20369
20601
|
}).strict(),
|
|
20370
|
-
paletteColors:
|
|
20371
|
-
min:
|
|
20372
|
-
max:
|
|
20602
|
+
paletteColors: z13.object({
|
|
20603
|
+
min: z13.number().int().min(1),
|
|
20604
|
+
max: z13.number().int().min(1)
|
|
20373
20605
|
}).strict(),
|
|
20374
|
-
background:
|
|
20375
|
-
checks:
|
|
20606
|
+
background: z13.enum(["opaque", "transparent", "full-bleed"]),
|
|
20607
|
+
checks: z13.array(z13.enum([
|
|
20376
20608
|
"prompt-coverage",
|
|
20377
20609
|
"native-grid",
|
|
20378
20610
|
"final-palette",
|
|
20379
20611
|
"alpha-edge",
|
|
20380
20612
|
"human-1x"
|
|
20381
20613
|
])).min(1),
|
|
20382
|
-
notes:
|
|
20614
|
+
notes: z13.array(z13.string().min(1)).default([])
|
|
20383
20615
|
}).strict(),
|
|
20384
|
-
integrity:
|
|
20385
|
-
algorithm:
|
|
20386
|
-
digest:
|
|
20616
|
+
integrity: z13.object({
|
|
20617
|
+
algorithm: z13.literal("sha256"),
|
|
20618
|
+
digest: z13.string().regex(SHA256_RE2)
|
|
20387
20619
|
}).strict()
|
|
20388
20620
|
}).strict().superRefine((recipe, context) => {
|
|
20389
20621
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20390
20622
|
context.addIssue({
|
|
20391
|
-
code:
|
|
20623
|
+
code: z13.ZodIssueCode.custom,
|
|
20392
20624
|
path: ["style", "provider"],
|
|
20393
20625
|
message: "must match the recipe provider"
|
|
20394
20626
|
});
|
|
20395
20627
|
}
|
|
20396
20628
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20397
20629
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20398
|
-
context.addIssue({ code:
|
|
20630
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20399
20631
|
}
|
|
20400
20632
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20401
20633
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20402
|
-
context.addIssue({ code:
|
|
20634
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20403
20635
|
}
|
|
20404
20636
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20405
20637
|
context.addIssue({
|
|
20406
|
-
code:
|
|
20638
|
+
code: z13.ZodIssueCode.custom,
|
|
20407
20639
|
path: ["files"],
|
|
20408
20640
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20409
20641
|
});
|
|
20410
20642
|
}
|
|
20411
20643
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20412
20644
|
context.addIssue({
|
|
20413
|
-
code:
|
|
20645
|
+
code: z13.ZodIssueCode.custom,
|
|
20414
20646
|
path: ["quality", "recommendedNativeSize"],
|
|
20415
20647
|
message: "min cannot exceed max"
|
|
20416
20648
|
});
|
|
20417
20649
|
}
|
|
20418
20650
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20419
20651
|
context.addIssue({
|
|
20420
|
-
code:
|
|
20652
|
+
code: z13.ZodIssueCode.custom,
|
|
20421
20653
|
path: ["quality", "paletteColors"],
|
|
20422
20654
|
message: "min cannot exceed max"
|
|
20423
20655
|
});
|
|
@@ -20426,7 +20658,7 @@ var RecipeSchema = z12.object({
|
|
|
20426
20658
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20427
20659
|
)) {
|
|
20428
20660
|
context.addIssue({
|
|
20429
|
-
code:
|
|
20661
|
+
code: z13.ZodIssueCode.custom,
|
|
20430
20662
|
path: ["workflow", "path"],
|
|
20431
20663
|
message: "must name a file whose role is workflow"
|
|
20432
20664
|
});
|
|
@@ -20434,7 +20666,7 @@ var RecipeSchema = z12.object({
|
|
|
20434
20666
|
if (recipe.provider === "comfyui") {
|
|
20435
20667
|
if (!recipe.workflow) {
|
|
20436
20668
|
context.addIssue({
|
|
20437
|
-
code:
|
|
20669
|
+
code: z13.ZodIssueCode.custom,
|
|
20438
20670
|
path: ["workflow"],
|
|
20439
20671
|
message: "ComfyUI recipes require workflow metadata"
|
|
20440
20672
|
});
|
|
@@ -20443,7 +20675,7 @@ var RecipeSchema = z12.object({
|
|
|
20443
20675
|
const options = recipe.style.providerOptions.comfyui;
|
|
20444
20676
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20445
20677
|
context.addIssue({
|
|
20446
|
-
code:
|
|
20678
|
+
code: z13.ZodIssueCode.custom,
|
|
20447
20679
|
path: ["style", "providerOptions", "comfyui"],
|
|
20448
20680
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20449
20681
|
});
|
|
@@ -20457,7 +20689,7 @@ var RecipeSchema = z12.object({
|
|
|
20457
20689
|
};
|
|
20458
20690
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20459
20691
|
context.addIssue({
|
|
20460
|
-
code:
|
|
20692
|
+
code: z13.ZodIssueCode.custom,
|
|
20461
20693
|
path: ["style", "providerOptions", "comfyui"],
|
|
20462
20694
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20463
20695
|
});
|
|
@@ -20489,7 +20721,7 @@ function message5(error) {
|
|
|
20489
20721
|
return error instanceof Error ? error.message : String(error);
|
|
20490
20722
|
}
|
|
20491
20723
|
async function readRecipeFile(recipePath, bundled) {
|
|
20492
|
-
const absolute =
|
|
20724
|
+
const absolute = path45.resolve(recipePath);
|
|
20493
20725
|
let raw;
|
|
20494
20726
|
try {
|
|
20495
20727
|
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
@@ -20503,13 +20735,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20503
20735
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20504
20736
|
);
|
|
20505
20737
|
}
|
|
20506
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
20738
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path45.dirname(absolute), bundled };
|
|
20507
20739
|
}
|
|
20508
20740
|
async function walkRecipeFiles(root) {
|
|
20509
|
-
if (!
|
|
20741
|
+
if (!existsSync34(root)) return [];
|
|
20510
20742
|
const out = [];
|
|
20511
20743
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20512
|
-
const resolved =
|
|
20744
|
+
const resolved = path45.join(root, entry.name);
|
|
20513
20745
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20514
20746
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20515
20747
|
}
|
|
@@ -20520,7 +20752,7 @@ function bundledRecipeRoot() {
|
|
|
20520
20752
|
}
|
|
20521
20753
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
20522
20754
|
const recipes = await Promise.all(
|
|
20523
|
-
(await walkRecipeFiles(
|
|
20755
|
+
(await walkRecipeFiles(path45.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
20524
20756
|
);
|
|
20525
20757
|
return recipes.sort(
|
|
20526
20758
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -20543,9 +20775,9 @@ function parseSelector(selector) {
|
|
|
20543
20775
|
return { id: match[1], version: match[2] };
|
|
20544
20776
|
}
|
|
20545
20777
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
20546
|
-
const local =
|
|
20547
|
-
if (
|
|
20548
|
-
const recipePath =
|
|
20778
|
+
const local = path45.resolve(target);
|
|
20779
|
+
if (existsSync34(local)) {
|
|
20780
|
+
const recipePath = path45.basename(local) === RECIPE_FILE ? local : path45.join(local, RECIPE_FILE);
|
|
20549
20781
|
return readRecipeFile(recipePath, false);
|
|
20550
20782
|
}
|
|
20551
20783
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -20559,8 +20791,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
20559
20791
|
return matches[0];
|
|
20560
20792
|
}
|
|
20561
20793
|
async function verifyFile(root, file) {
|
|
20562
|
-
const absolute =
|
|
20563
|
-
if (!
|
|
20794
|
+
const absolute = path45.resolve(root, ...file.path.split("/"));
|
|
20795
|
+
if (!existsSync34(absolute)) {
|
|
20564
20796
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
20565
20797
|
}
|
|
20566
20798
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -20575,7 +20807,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20575
20807
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20576
20808
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
20577
20809
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
20578
|
-
const modelRoot = options.modelRoot ?
|
|
20810
|
+
const modelRoot = options.modelRoot ? path45.resolve(options.modelRoot) : null;
|
|
20579
20811
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
20580
20812
|
...await verifyFile(modelRoot, model),
|
|
20581
20813
|
source: model.source,
|
|
@@ -20609,7 +20841,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20609
20841
|
};
|
|
20610
20842
|
}
|
|
20611
20843
|
function renderStyle(style, destination, cwd) {
|
|
20612
|
-
const portableDir =
|
|
20844
|
+
const portableDir = path45.relative(cwd, destination).split(path45.sep).join("/") || ".";
|
|
20613
20845
|
const replace = (value) => {
|
|
20614
20846
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
20615
20847
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -20621,24 +20853,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
20621
20853
|
return replace(style);
|
|
20622
20854
|
}
|
|
20623
20855
|
async function installRecipe(target, options = {}) {
|
|
20624
|
-
const cwd =
|
|
20856
|
+
const cwd = path45.resolve(options.cwd ?? process.cwd());
|
|
20625
20857
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20626
20858
|
const verification = await verifyRecipe(loaded.path);
|
|
20627
20859
|
if (!verification.ok) {
|
|
20628
20860
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
20629
20861
|
}
|
|
20630
|
-
const destination = options.out ?
|
|
20862
|
+
const destination = options.out ? path45.resolve(cwd, options.out) : path45.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
20631
20863
|
const sources = [
|
|
20632
20864
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
20633
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
20865
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path45.join(loaded.dir, ...file.path.split("/")) }))
|
|
20634
20866
|
];
|
|
20635
20867
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
20636
|
-
path:
|
|
20868
|
+
path: path45.join(destination, ...file.path.split("/")),
|
|
20637
20869
|
data: await readFile30(file.source)
|
|
20638
20870
|
})));
|
|
20639
20871
|
if (!options.force) {
|
|
20640
20872
|
for (const file of files) {
|
|
20641
|
-
if (!
|
|
20873
|
+
if (!existsSync34(file.path)) continue;
|
|
20642
20874
|
const current = await readFile30(file.path);
|
|
20643
20875
|
if (!current.equals(file.data)) {
|
|
20644
20876
|
throw new OverwriteRefusedError(
|
|
@@ -20733,14 +20965,14 @@ async function runRecipe(args) {
|
|
|
20733
20965
|
}, null, 2));
|
|
20734
20966
|
} else {
|
|
20735
20967
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
20736
|
-
log(` ${
|
|
20968
|
+
log(` ${path46.relative(process.cwd(), result.destination) || "."}`);
|
|
20737
20969
|
log(`
|
|
20738
20970
|
Add this entry under your manifest's styles object:`);
|
|
20739
20971
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
20740
20972
|
if (result.recipe.models.length) {
|
|
20741
20973
|
log(`
|
|
20742
20974
|
Models are not downloaded automatically. Verify them with:`);
|
|
20743
|
-
log(` pixelkiln recipe verify ${
|
|
20975
|
+
log(` pixelkiln recipe verify ${path46.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
20744
20976
|
}
|
|
20745
20977
|
}
|
|
20746
20978
|
return;
|
|
@@ -20748,7 +20980,7 @@ async function runRecipe(args) {
|
|
|
20748
20980
|
}
|
|
20749
20981
|
|
|
20750
20982
|
// src/cli/commands/refine.ts
|
|
20751
|
-
import
|
|
20983
|
+
import path47 from "path";
|
|
20752
20984
|
async function runRefine(args) {
|
|
20753
20985
|
if (args.from || args.subcommand === "approve") {
|
|
20754
20986
|
if (!args.from) {
|
|
@@ -20778,12 +21010,12 @@ async function runRefine(args) {
|
|
|
20778
21010
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
20779
21011
|
);
|
|
20780
21012
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
20781
|
-
log(` wrote ${
|
|
20782
|
-
log(` quality record: ${
|
|
21013
|
+
log(` wrote ${path47.relative(process.cwd(), result3.output)}`);
|
|
21014
|
+
log(` quality record: ${path47.relative(process.cwd(), result3.record)}`);
|
|
20783
21015
|
log(`
|
|
20784
21016
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
20785
21017
|
log(
|
|
20786
|
-
` pixelkiln refine approve --from ${
|
|
21018
|
+
` pixelkiln refine approve --from ${path47.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
20787
21019
|
);
|
|
20788
21020
|
}
|
|
20789
21021
|
return;
|
|
@@ -20811,7 +21043,7 @@ async function runRefine(args) {
|
|
|
20811
21043
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
20812
21044
|
else {
|
|
20813
21045
|
log(
|
|
20814
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
21046
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path47.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path47.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
20815
21047
|
);
|
|
20816
21048
|
}
|
|
20817
21049
|
return;
|
|
@@ -20820,7 +21052,7 @@ async function runRefine(args) {
|
|
|
20820
21052
|
if (args.json) {
|
|
20821
21053
|
log(JSON.stringify(result2, null, 2));
|
|
20822
21054
|
} else {
|
|
20823
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
21055
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path47.relative(process.cwd(), result2.output || result2.record)}`);
|
|
20824
21056
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
20825
21057
|
}
|
|
20826
21058
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -20866,7 +21098,7 @@ async function runRefine(args) {
|
|
|
20866
21098
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
20867
21099
|
if (item.state === "needs-approval") {
|
|
20868
21100
|
log(
|
|
20869
|
-
` pixelkiln refine approve --from ${
|
|
21101
|
+
` pixelkiln refine approve --from ${path47.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
20870
21102
|
);
|
|
20871
21103
|
}
|
|
20872
21104
|
}
|
|
@@ -20877,9 +21109,9 @@ async function runRefine(args) {
|
|
|
20877
21109
|
}
|
|
20878
21110
|
|
|
20879
21111
|
// src/cli/commands/skeleton.ts
|
|
20880
|
-
import
|
|
20881
|
-
import { existsSync as
|
|
20882
|
-
import { readFile as readFile31, writeFile as
|
|
21112
|
+
import path48 from "path";
|
|
21113
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21114
|
+
import { readFile as readFile31, writeFile as writeFile17 } from "fs/promises";
|
|
20883
21115
|
|
|
20884
21116
|
// src/skeleton-preview.ts
|
|
20885
21117
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -20990,12 +21222,12 @@ var Canvas = class {
|
|
|
20990
21222
|
async function runEstimateSkeleton(args) {
|
|
20991
21223
|
const image = args.target;
|
|
20992
21224
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
20993
|
-
if (args.out &&
|
|
21225
|
+
if (args.out && existsSync35(path48.resolve(args.out)) && !args.force) {
|
|
20994
21226
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
20995
21227
|
}
|
|
20996
|
-
loadEnvFiles(
|
|
21228
|
+
loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
|
|
20997
21229
|
loadEnvFiles(process.cwd());
|
|
20998
|
-
const bytes = await readFile31(
|
|
21230
|
+
const bytes = await readFile31(path48.resolve(image));
|
|
20999
21231
|
const metadata = imageMetadata(bytes);
|
|
21000
21232
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21001
21233
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21007,7 +21239,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21007
21239
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21008
21240
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21009
21241
|
if (args.out) {
|
|
21010
|
-
await
|
|
21242
|
+
await writeFile17(path48.resolve(args.out), json + "\n");
|
|
21011
21243
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21012
21244
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21013
21245
|
} else {
|
|
@@ -21020,8 +21252,8 @@ async function runSkeletonPreview(args) {
|
|
|
21020
21252
|
let sourceFile;
|
|
21021
21253
|
let label;
|
|
21022
21254
|
if (target.endsWith(".json")) {
|
|
21023
|
-
keypointsFile =
|
|
21024
|
-
sourceFile = args.from ?
|
|
21255
|
+
keypointsFile = path48.resolve(target);
|
|
21256
|
+
sourceFile = args.from ? path48.resolve(args.from) : void 0;
|
|
21025
21257
|
label = target;
|
|
21026
21258
|
} else {
|
|
21027
21259
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21033,10 +21265,10 @@ async function runSkeletonPreview(args) {
|
|
|
21033
21265
|
);
|
|
21034
21266
|
}
|
|
21035
21267
|
keypointsFile = spec.revision.keypointsFile;
|
|
21036
|
-
sourceFile = args.from ?
|
|
21268
|
+
sourceFile = args.from ? path48.resolve(args.from) : spec.revision.sourceFile;
|
|
21037
21269
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21038
21270
|
}
|
|
21039
|
-
if (!
|
|
21271
|
+
if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21040
21272
|
let json;
|
|
21041
21273
|
try {
|
|
21042
21274
|
json = JSON.parse(await readFile31(keypointsFile, "utf8"));
|
|
@@ -21046,7 +21278,7 @@ async function runSkeletonPreview(args) {
|
|
|
21046
21278
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21047
21279
|
let source;
|
|
21048
21280
|
if (sourceFile) {
|
|
21049
|
-
if (!
|
|
21281
|
+
if (!existsSync35(sourceFile)) {
|
|
21050
21282
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21051
21283
|
} else {
|
|
21052
21284
|
const bytes = await readFile31(sourceFile);
|
|
@@ -21054,10 +21286,10 @@ async function runSkeletonPreview(args) {
|
|
|
21054
21286
|
source = decodePng(bytes);
|
|
21055
21287
|
}
|
|
21056
21288
|
}
|
|
21057
|
-
const out =
|
|
21058
|
-
await
|
|
21289
|
+
const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21290
|
+
await writeFile17(out, renderSkeletonSheet(set, { source }));
|
|
21059
21291
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21060
|
-
const shown =
|
|
21292
|
+
const shown = path48.relative(process.cwd(), out);
|
|
21061
21293
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21062
21294
|
}
|
|
21063
21295
|
|
|
@@ -21103,15 +21335,15 @@ async function runTools(args) {
|
|
|
21103
21335
|
}
|
|
21104
21336
|
|
|
21105
21337
|
// src/cli/commands/workspace.ts
|
|
21106
|
-
import
|
|
21107
|
-
import { existsSync as
|
|
21338
|
+
import path49 from "path";
|
|
21339
|
+
import { existsSync as existsSync36 } from "fs";
|
|
21108
21340
|
async function runWorkspace(args) {
|
|
21109
|
-
const workspacePath =
|
|
21110
|
-
const dir =
|
|
21341
|
+
const workspacePath = path49.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21342
|
+
const dir = path49.dirname(workspacePath);
|
|
21111
21343
|
if (args.subcommand === "add") {
|
|
21112
|
-
const manifestPath =
|
|
21344
|
+
const manifestPath = path49.resolve(args.target);
|
|
21113
21345
|
const loadedTarget = await loadManifest(manifestPath);
|
|
21114
|
-
const lockPath = args.explicitLock ?
|
|
21346
|
+
const lockPath = args.explicitLock ? path49.resolve(args.explicitLock) : path49.join(path49.dirname(manifestPath), "pixelkiln.lock.json");
|
|
21115
21347
|
const ws = await loadWorkspace(workspacePath);
|
|
21116
21348
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
21117
21349
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -21131,10 +21363,10 @@ async function runWorkspace(args) {
|
|
|
21131
21363
|
...args.account ? { account: args.account } : {}
|
|
21132
21364
|
};
|
|
21133
21365
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
21134
|
-
log(` registered "${id}" in ${
|
|
21366
|
+
log(` registered "${id}" in ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21135
21367
|
log(` manifest: ${project.manifest}`);
|
|
21136
21368
|
log(` lock: ${project.lock}`);
|
|
21137
|
-
if (!
|
|
21369
|
+
if (!existsSync36(lockPath)) {
|
|
21138
21370
|
log(
|
|
21139
21371
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
21140
21372
|
);
|
|
@@ -21143,7 +21375,7 @@ async function runWorkspace(args) {
|
|
|
21143
21375
|
}
|
|
21144
21376
|
if (args.subcommand === "remove") {
|
|
21145
21377
|
const ws = await loadWorkspace(workspacePath);
|
|
21146
|
-
const resolvedTarget =
|
|
21378
|
+
const resolvedTarget = path49.resolve(args.target);
|
|
21147
21379
|
const match = ws.projects.find(
|
|
21148
21380
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
21149
21381
|
);
|
|
@@ -21154,11 +21386,11 @@ async function runWorkspace(args) {
|
|
|
21154
21386
|
version: 1,
|
|
21155
21387
|
projects: ws.projects.filter((p) => p !== match)
|
|
21156
21388
|
});
|
|
21157
|
-
log(` removed "${match.id}" from ${
|
|
21389
|
+
log(` removed "${match.id}" from ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21158
21390
|
return;
|
|
21159
21391
|
}
|
|
21160
21392
|
if (args.subcommand === "list") {
|
|
21161
|
-
if (!
|
|
21393
|
+
if (!existsSync36(workspacePath)) {
|
|
21162
21394
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21163
21395
|
}
|
|
21164
21396
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21166,9 +21398,9 @@ async function runWorkspace(args) {
|
|
|
21166
21398
|
if (args.json) {
|
|
21167
21399
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
21168
21400
|
} else if (!ws.projects.length) {
|
|
21169
|
-
log(` no projects registered in ${
|
|
21401
|
+
log(` no projects registered in ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21170
21402
|
} else {
|
|
21171
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21403
|
+
log(` ${ws.projects.length} project(s) in ${path49.relative(process.cwd(), workspacePath)}:`);
|
|
21172
21404
|
for (const p of ws.projects) {
|
|
21173
21405
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
21174
21406
|
}
|
|
@@ -21178,7 +21410,7 @@ async function runWorkspace(args) {
|
|
|
21178
21410
|
return;
|
|
21179
21411
|
}
|
|
21180
21412
|
if (args.subcommand === "status") {
|
|
21181
|
-
if (!
|
|
21413
|
+
if (!existsSync36(workspacePath)) {
|
|
21182
21414
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21183
21415
|
}
|
|
21184
21416
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21186,7 +21418,7 @@ async function runWorkspace(args) {
|
|
|
21186
21418
|
if (args.json) {
|
|
21187
21419
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
21188
21420
|
} else {
|
|
21189
|
-
log(` workspace: ${
|
|
21421
|
+
log(` workspace: ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21190
21422
|
for (const p of report.projects) {
|
|
21191
21423
|
if (p.error) {
|
|
21192
21424
|
log(`
|
|
@@ -21240,7 +21472,7 @@ async function runHelp() {
|
|
|
21240
21472
|
async function runVersion() {
|
|
21241
21473
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
21242
21474
|
const url = new URL(up, import.meta.url);
|
|
21243
|
-
if (!
|
|
21475
|
+
if (!existsSync37(url)) continue;
|
|
21244
21476
|
const pkg = JSON.parse(await readFile32(url, "utf8"));
|
|
21245
21477
|
if (pkg.name === "pixelkiln") {
|
|
21246
21478
|
log(`${pkg.name} ${pkg.version}`);
|