pixelkiln 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +773 -516
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +16 -0
- package/dist/client/gallery.js +452 -0
- package/dist/index.cjs +476 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1876 -259
- package/dist/index.d.ts +1876 -259
- package/dist/index.js +459 -249
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +12 -0
- package/docs/CLI.md +4 -1
- package/docs/GENERATORS.md +1 -1
- package/docs/MANIFEST.md +1 -1
- package/package.json +1 -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")));
|
|
@@ -1810,7 +1810,7 @@ var AssetSchema = z.object({
|
|
|
1810
1810
|
path: ["mirror"]
|
|
1811
1811
|
});
|
|
1812
1812
|
}
|
|
1813
|
-
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) {
|
|
1814
1814
|
context.addIssue({
|
|
1815
1815
|
code: z.ZodIssueCode.custom,
|
|
1816
1816
|
message: "Required",
|
|
@@ -6167,9 +6167,9 @@ var PixelLabClient = class {
|
|
|
6167
6167
|
apiKey;
|
|
6168
6168
|
http;
|
|
6169
6169
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6170
|
-
async request(
|
|
6170
|
+
async request(path50, init) {
|
|
6171
6171
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6172
|
-
const res = await this.http(`${BASE}${
|
|
6172
|
+
const res = await this.http(`${BASE}${path50}`, {
|
|
6173
6173
|
...init,
|
|
6174
6174
|
headers: {
|
|
6175
6175
|
Authorization: auth,
|
|
@@ -6179,13 +6179,13 @@ var PixelLabClient = class {
|
|
|
6179
6179
|
});
|
|
6180
6180
|
const text = await res.text();
|
|
6181
6181
|
if (!res.ok) {
|
|
6182
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6182
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path50} \u2192 ${res.status}`, res.status, text);
|
|
6183
6183
|
}
|
|
6184
6184
|
if (!text) return {};
|
|
6185
6185
|
try {
|
|
6186
6186
|
return JSON.parse(text);
|
|
6187
6187
|
} catch {
|
|
6188
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6188
|
+
throw new Error(`${init?.method ?? "GET"} ${path50} returned invalid JSON`);
|
|
6189
6189
|
}
|
|
6190
6190
|
}
|
|
6191
6191
|
async balance() {
|
|
@@ -6577,10 +6577,10 @@ var PixelLabClient = class {
|
|
|
6577
6577
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6578
6578
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6579
6579
|
const south = args.reference?.south;
|
|
6580
|
-
let
|
|
6580
|
+
let path50;
|
|
6581
6581
|
let body;
|
|
6582
6582
|
if (args.mode === "v3") {
|
|
6583
|
-
|
|
6583
|
+
path50 = "/create-character-v3";
|
|
6584
6584
|
body = {
|
|
6585
6585
|
description: args.description,
|
|
6586
6586
|
image_size: imageSize,
|
|
@@ -6594,7 +6594,7 @@ var PixelLabClient = class {
|
|
|
6594
6594
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6595
6595
|
};
|
|
6596
6596
|
} else if (args.mode === "pro-flash") {
|
|
6597
|
-
|
|
6597
|
+
path50 = "/create-character-pro-flash";
|
|
6598
6598
|
const traits = args.styleTraits;
|
|
6599
6599
|
body = {
|
|
6600
6600
|
description: args.description,
|
|
@@ -6618,7 +6618,7 @@ var PixelLabClient = class {
|
|
|
6618
6618
|
} : {}
|
|
6619
6619
|
};
|
|
6620
6620
|
} else if (args.mode === "pro") {
|
|
6621
|
-
|
|
6621
|
+
path50 = "/create-character-pro";
|
|
6622
6622
|
body = {
|
|
6623
6623
|
description: args.description,
|
|
6624
6624
|
image_size: imageSize,
|
|
@@ -6632,7 +6632,7 @@ var PixelLabClient = class {
|
|
|
6632
6632
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6633
6633
|
};
|
|
6634
6634
|
} else {
|
|
6635
|
-
|
|
6635
|
+
path50 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6636
6636
|
body = {
|
|
6637
6637
|
description: args.description,
|
|
6638
6638
|
image_size: imageSize,
|
|
@@ -6649,8 +6649,8 @@ var PixelLabClient = class {
|
|
|
6649
6649
|
...palette
|
|
6650
6650
|
};
|
|
6651
6651
|
}
|
|
6652
|
-
const raw = await this.request(
|
|
6653
|
-
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));
|
|
6654
6654
|
}
|
|
6655
6655
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6656
6656
|
async createCharacterState(args) {
|
|
@@ -7312,7 +7312,11 @@ function characterCost(spec) {
|
|
|
7312
7312
|
return 20;
|
|
7313
7313
|
}
|
|
7314
7314
|
if (character.mode === "standard") return 1;
|
|
7315
|
-
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
|
+
}
|
|
7316
7320
|
if (character.mode === "pro-flash") {
|
|
7317
7321
|
const south = character.reference?.south;
|
|
7318
7322
|
return south ? proFlashCharacterCost(south.width, south.height, true) : proFlashCharacterCost(spec.width, spec.height, false);
|
|
@@ -9506,9 +9510,9 @@ var RetroDiffusionClient = class {
|
|
|
9506
9510
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9507
9511
|
return Buffer.from(await response.arrayBuffer());
|
|
9508
9512
|
}
|
|
9509
|
-
async call(
|
|
9513
|
+
async call(path50, init = {}) {
|
|
9510
9514
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9511
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9515
|
+
const response = await this.request(`${this.baseUrl}${path50}`, {
|
|
9512
9516
|
...init,
|
|
9513
9517
|
headers: {
|
|
9514
9518
|
"Content-Type": "application/json",
|
|
@@ -11813,10 +11817,10 @@ function isSha256Hash(value) {
|
|
|
11813
11817
|
function parseCache(value) {
|
|
11814
11818
|
return HashCacheSchema.parse(value);
|
|
11815
11819
|
}
|
|
11816
|
-
async function loadCache(
|
|
11817
|
-
if (!existsSync10(
|
|
11820
|
+
async function loadCache(path50) {
|
|
11821
|
+
if (!existsSync10(path50)) return { version: 1, hashes: {} };
|
|
11818
11822
|
try {
|
|
11819
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
11823
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path50, "utf8")));
|
|
11820
11824
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11821
11825
|
return {
|
|
11822
11826
|
version: 1,
|
|
@@ -11828,18 +11832,18 @@ async function loadCache(path49) {
|
|
|
11828
11832
|
return { version: 1, hashes: {} };
|
|
11829
11833
|
}
|
|
11830
11834
|
}
|
|
11831
|
-
async function saveCache(
|
|
11835
|
+
async function saveCache(path50, cache) {
|
|
11832
11836
|
const sorted = {};
|
|
11833
11837
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11834
11838
|
const hash = cache.hashes[key];
|
|
11835
11839
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11836
11840
|
sorted[key] = hash;
|
|
11837
11841
|
}
|
|
11838
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11839
|
-
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`;
|
|
11840
11844
|
try {
|
|
11841
11845
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11842
|
-
await rename5(tmp,
|
|
11846
|
+
await rename5(tmp, path50);
|
|
11843
11847
|
} finally {
|
|
11844
11848
|
await rm5(tmp, { force: true });
|
|
11845
11849
|
}
|
|
@@ -13826,13 +13830,16 @@ var NewAnimationSchema = z6.object({
|
|
|
13826
13830
|
direction: CharacterDirectionSchema.optional(),
|
|
13827
13831
|
frames: z6.number().int().min(4).max(16).optional(),
|
|
13828
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(),
|
|
13829
13835
|
mode: CharacterAnimationModeSchema.optional(),
|
|
13830
13836
|
/** What is being animated, when the parent's own description would mislead the model (`character` only). */
|
|
13831
13837
|
subject: z6.string().min(1).optional(),
|
|
13832
13838
|
enhancePrompt: z6.boolean().optional()
|
|
13833
13839
|
}).strict();
|
|
13834
13840
|
var NewAssetSchema = z6.object({
|
|
13835
|
-
|
|
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(),
|
|
13836
13843
|
category: z6.string().optional(),
|
|
13837
13844
|
width: z6.number().int().optional(),
|
|
13838
13845
|
height: z6.number().int().optional(),
|
|
@@ -13867,7 +13874,24 @@ var NewAssetSchema = z6.object({
|
|
|
13867
13874
|
/** A pose or outfit of an existing `character`/`objectPro` base or state. */
|
|
13868
13875
|
state: NewStateSchema.optional(),
|
|
13869
13876
|
/** A loop of an existing `character`/`objectPro` base or state. */
|
|
13870
|
-
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()
|
|
13871
13895
|
}).strict().refine((asset) => !(asset.revision && (asset.state || asset.animation)), {
|
|
13872
13896
|
message: "an asset is a revision, a state, or an animation \u2014 not more than one"
|
|
13873
13897
|
}).refine((asset) => !(asset.state && asset.animation), {
|
|
@@ -13878,59 +13902,95 @@ var CANDIDATE_OPTION = {
|
|
|
13878
13902
|
comfyui: "numImages",
|
|
13879
13903
|
scenario: "numOutputs"
|
|
13880
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]);
|
|
13881
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
|
+
*/
|
|
13882
13990
|
z6.object({
|
|
13883
|
-
action: z6.literal("
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
assetId: z6.string().min(1),
|
|
13887
|
-
/** Manifest bytes the page last saw; a mismatch refuses the write. */
|
|
13888
|
-
expectedSha256: HexSha,
|
|
13889
|
-
patch: AssetPatchSchema
|
|
13890
|
-
}).strict(),
|
|
13891
|
-
z6.object({
|
|
13892
|
-
action: z6.literal("add-asset"),
|
|
13893
|
-
project: z6.string().min(1).optional(),
|
|
13894
|
-
assetId: z6.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
13895
|
-
expectedSha256: HexSha,
|
|
13896
|
-
asset: NewAssetSchema
|
|
13897
|
-
}).strict(),
|
|
13898
|
-
z6.object({
|
|
13899
|
-
action: z6.literal("set-source"),
|
|
13900
|
-
project: z6.string().min(1).optional(),
|
|
13901
|
-
assetId: z6.string().min(1),
|
|
13902
|
-
styleId: z6.string().min(1),
|
|
13903
|
-
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
13904
|
-
source: z6.string().min(1),
|
|
13905
|
-
expectedSha256: HexSha
|
|
13906
|
-
}).strict(),
|
|
13907
|
-
z6.object({
|
|
13908
|
-
action: z6.literal("clear-source"),
|
|
13909
|
-
project: z6.string().min(1).optional(),
|
|
13910
|
-
assetId: z6.string().min(1),
|
|
13911
|
-
styleId: z6.string().min(1),
|
|
13912
|
-
expectedSha256: HexSha
|
|
13913
|
-
}).strict(),
|
|
13914
|
-
z6.object({
|
|
13915
|
-
action: z6.literal("patch-style"),
|
|
13916
|
-
project: z6.string().min(1).optional(),
|
|
13917
|
-
styleId: z6.string().min(1),
|
|
13918
|
-
expectedSha256: HexSha,
|
|
13919
|
-
patch: z6.object({
|
|
13920
|
-
/** Candidates per generation, written to the provider's own option. */
|
|
13921
|
-
candidates: z6.number().int().min(1).max(64).optional(),
|
|
13922
|
-
/** Empty clears the style's own value (inherit, or the default). */
|
|
13923
|
-
promptPrefix: z6.string().optional(),
|
|
13924
|
-
promptSuffix: z6.string().optional(),
|
|
13925
|
-
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
13926
|
-
palette: z6.array(z6.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
13927
|
-
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
13928
|
-
enforcePalette: z6.boolean().optional(),
|
|
13929
|
-
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
13930
|
-
view: z6.string().max(64).nullable().optional(),
|
|
13931
|
-
/** pixflux only; null clears the style's own value. */
|
|
13932
|
-
noBackground: z6.boolean().nullable().optional()
|
|
13933
|
-
}).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)
|
|
13934
13994
|
}).strict()
|
|
13935
13995
|
]);
|
|
13936
13996
|
var ManifestDriftError = class extends Error {
|
|
@@ -13979,6 +14039,18 @@ function undeclaredAsset(raw, assetId) {
|
|
|
13979
14039
|
}
|
|
13980
14040
|
function applyEdit(raw, edit) {
|
|
13981
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
|
+
}
|
|
13982
14054
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
13983
14055
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
13984
14056
|
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
@@ -14056,6 +14128,15 @@ function applyEdit(raw, edit) {
|
|
|
14056
14128
|
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
14057
14129
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
14058
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
|
+
}
|
|
14059
14140
|
raw.assets[edit.assetId] = asset2;
|
|
14060
14141
|
return;
|
|
14061
14142
|
}
|
|
@@ -14083,11 +14164,10 @@ function applyEdit(raw, edit) {
|
|
|
14083
14164
|
else delete asset.tags;
|
|
14084
14165
|
}
|
|
14085
14166
|
}
|
|
14086
|
-
async function
|
|
14167
|
+
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
14087
14168
|
const absolute = path23.resolve(manifestPath);
|
|
14088
14169
|
const original = await readFile15(absolute, "utf8");
|
|
14089
|
-
|
|
14090
|
-
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14170
|
+
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14091
14171
|
let raw;
|
|
14092
14172
|
try {
|
|
14093
14173
|
raw = JSON.parse(original);
|
|
@@ -14098,25 +14178,64 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
14098
14178
|
}
|
|
14099
14179
|
applyEdit(raw, edit);
|
|
14100
14180
|
const next = serializeLike(original, raw);
|
|
14101
|
-
if (next === original) return
|
|
14181
|
+
if (next === original) return unchanged(absolute, original);
|
|
14102
14182
|
const tmp = path23.join(
|
|
14103
14183
|
path23.dirname(absolute),
|
|
14104
14184
|
`.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
14105
14185
|
);
|
|
14106
14186
|
try {
|
|
14107
14187
|
await writeFile10(tmp, next);
|
|
14188
|
+
let specs;
|
|
14108
14189
|
try {
|
|
14109
|
-
|
|
14110
|
-
await resolveSpecs(loaded);
|
|
14190
|
+
specs = await resolveSpecs(await loadManifest(tmp));
|
|
14111
14191
|
} catch (error) {
|
|
14112
14192
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
14113
14193
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
14114
14194
|
}
|
|
14115
|
-
await
|
|
14195
|
+
return await use({ absolute, original, next, tmp, specs });
|
|
14116
14196
|
} finally {
|
|
14117
14197
|
await rm7(tmp, { force: true });
|
|
14118
14198
|
}
|
|
14119
|
-
|
|
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
|
+
provider: spec.provider,
|
|
14229
|
+
cost: spec.cost,
|
|
14230
|
+
costUnit: spec.costUnit,
|
|
14231
|
+
candidates: spec.candidates
|
|
14232
|
+
});
|
|
14233
|
+
totals[spec.costUnit] = (totals[spec.costUnit] ?? 0) + spec.cost;
|
|
14234
|
+
}
|
|
14235
|
+
return { items, totals };
|
|
14236
|
+
},
|
|
14237
|
+
() => ({ items: [], totals: {} })
|
|
14238
|
+
);
|
|
14120
14239
|
}
|
|
14121
14240
|
|
|
14122
14241
|
// src/pipeline/hand-edit.ts
|
|
@@ -14388,63 +14507,104 @@ async function runEdit(args) {
|
|
|
14388
14507
|
}
|
|
14389
14508
|
|
|
14390
14509
|
// src/cli/commands/gallery.ts
|
|
14391
|
-
import
|
|
14392
|
-
import { existsSync as
|
|
14510
|
+
import path34 from "path";
|
|
14511
|
+
import { existsSync as existsSync26 } from "fs";
|
|
14393
14512
|
|
|
14394
14513
|
// src/gallery/snapshot.ts
|
|
14395
14514
|
import { createHash as createHash3 } from "crypto";
|
|
14396
|
-
import { existsSync as
|
|
14515
|
+
import { existsSync as existsSync21 } from "fs";
|
|
14397
14516
|
import { readFile as readFile18, stat as stat2 } from "fs/promises";
|
|
14398
|
-
import
|
|
14517
|
+
import path30 from "path";
|
|
14399
14518
|
|
|
14400
14519
|
// src/gallery/edit.ts
|
|
14401
|
-
import
|
|
14402
|
-
import { z as
|
|
14520
|
+
import path28 from "path";
|
|
14521
|
+
import { z as z10 } from "zod";
|
|
14403
14522
|
|
|
14404
|
-
// src/gallery/
|
|
14523
|
+
// src/gallery/upload.ts
|
|
14405
14524
|
import { existsSync as existsSync18 } from "fs";
|
|
14406
|
-
import { mkdir as mkdir10,
|
|
14525
|
+
import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
|
|
14407
14526
|
import path26 from "path";
|
|
14408
14527
|
import { z as z8 } from "zod";
|
|
14409
|
-
var
|
|
14528
|
+
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
14529
|
+
var UploadImageSchema = z8.object({
|
|
14530
|
+
action: z8.literal("upload-image"),
|
|
14410
14531
|
project: z8.string().min(1).optional(),
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14532
|
+
/** Manifest-relative destination, ending .png, .jpg, or .jpeg. */
|
|
14533
|
+
path: z8.string().min(1),
|
|
14534
|
+
base64: z8.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_UPLOAD_BYTES / 3) * 4),
|
|
14535
|
+
/** Replace a file already at `path`. */
|
|
14536
|
+
overwrite: z8.boolean().optional()
|
|
14414
14537
|
}).strict();
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14538
|
+
async function saveProjectImage(root, request) {
|
|
14539
|
+
const file = path26.resolve(root, request.path);
|
|
14540
|
+
const relative = path26.relative(root, file);
|
|
14541
|
+
if (relative.startsWith("..") || path26.isAbsolute(relative)) {
|
|
14542
|
+
throw new ManifestEditError("an upload must stay inside the project");
|
|
14543
|
+
}
|
|
14544
|
+
const extension = path26.extname(file).toLowerCase();
|
|
14545
|
+
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
14546
|
+
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
14547
|
+
const bytes = Buffer.from(request.base64, "base64");
|
|
14548
|
+
if (!bytes.length || bytes.length > MAX_UPLOAD_BYTES) {
|
|
14549
|
+
throw new ManifestEditError(`an upload must be between 1 byte and ${MAX_UPLOAD_BYTES / 1024 / 1024} MB`);
|
|
14550
|
+
}
|
|
14551
|
+
const metadata = imageMetadata(bytes);
|
|
14552
|
+
if (!metadata) throw new ManifestEditError("the upload is not a readable PNG or JPEG");
|
|
14553
|
+
if (metadata.format !== expected) {
|
|
14554
|
+
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
14555
|
+
}
|
|
14556
|
+
if (existsSync18(file) && !request.overwrite) {
|
|
14557
|
+
throw new ManifestEditError(`${relative.split(path26.sep).join("/")} already exists; choose another name, or replace it`);
|
|
14558
|
+
}
|
|
14559
|
+
await mkdir10(path26.dirname(file), { recursive: true });
|
|
14560
|
+
await writeFile12(file, bytes);
|
|
14561
|
+
return relative.split(path26.sep).join("/");
|
|
14562
|
+
}
|
|
14563
|
+
|
|
14564
|
+
// src/gallery/skeleton.ts
|
|
14565
|
+
import { existsSync as existsSync19 } from "fs";
|
|
14566
|
+
import { mkdir as mkdir11, readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
|
|
14567
|
+
import path27 from "path";
|
|
14568
|
+
import { z as z9 } from "zod";
|
|
14569
|
+
var SkeletonEstimateRequestSchema = z9.object({
|
|
14570
|
+
project: z9.string().min(1).optional(),
|
|
14571
|
+
styleId: z9.string().min(1),
|
|
14572
|
+
assetId: z9.string().min(1),
|
|
14573
|
+
frames: z9.number().int().min(3).max(15).optional()
|
|
14574
|
+
}).strict();
|
|
14575
|
+
var SkeletonAnimationRequestSchema = z9.object({
|
|
14576
|
+
action: z9.literal("create-skeleton-animation"),
|
|
14577
|
+
project: z9.string().min(1).optional(),
|
|
14578
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14579
|
+
styleId: z9.string().min(1),
|
|
14420
14580
|
/** The sprite the animation poses. */
|
|
14421
|
-
from:
|
|
14422
|
-
assetId:
|
|
14581
|
+
from: z9.string().min(1),
|
|
14582
|
+
assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14423
14583
|
/** The motion, sent as PixelLab's `action`. */
|
|
14424
|
-
prompt:
|
|
14584
|
+
prompt: z9.string().min(1),
|
|
14425
14585
|
direction: CharacterDirectionSchema,
|
|
14426
|
-
description:
|
|
14427
|
-
skeletonTemplate:
|
|
14586
|
+
description: z9.string().min(1).optional(),
|
|
14587
|
+
skeletonTemplate: z9.string().min(1).optional(),
|
|
14428
14588
|
/** Manifest-relative JSON path inside the project. */
|
|
14429
|
-
keypointsFile:
|
|
14589
|
+
keypointsFile: z9.string().min(1),
|
|
14430
14590
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14431
14591
|
set: SkeletonSetSchema.optional(),
|
|
14432
14592
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
14433
|
-
overwrite:
|
|
14593
|
+
overwrite: z9.boolean().optional()
|
|
14434
14594
|
}).strict();
|
|
14435
|
-
var SkeletonKeypointsUpdateSchema =
|
|
14436
|
-
action:
|
|
14437
|
-
project:
|
|
14438
|
-
styleId:
|
|
14439
|
-
assetId:
|
|
14595
|
+
var SkeletonKeypointsUpdateSchema = z9.object({
|
|
14596
|
+
action: z9.literal("update-skeleton-keypoints"),
|
|
14597
|
+
project: z9.string().min(1).optional(),
|
|
14598
|
+
styleId: z9.string().min(1),
|
|
14599
|
+
assetId: z9.string().min(1),
|
|
14440
14600
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
14441
|
-
expectedSha256:
|
|
14601
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14442
14602
|
set: SkeletonSetSchema
|
|
14443
14603
|
}).strict();
|
|
14444
14604
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14445
14605
|
function skeletonSourceFile(spec) {
|
|
14446
|
-
if (spec.source) return
|
|
14447
|
-
if (spec.quality &&
|
|
14606
|
+
if (spec.source) return path27.resolve(spec.root, spec.source);
|
|
14607
|
+
if (spec.quality && existsSync19(spec.quality.outFile)) return spec.quality.outFile;
|
|
14448
14608
|
return spec.outFile;
|
|
14449
14609
|
}
|
|
14450
14610
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -14465,10 +14625,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14465
14625
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14466
14626
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14467
14627
|
const file = skeletonSourceFile(spec);
|
|
14468
|
-
if (!
|
|
14628
|
+
if (!existsSync19(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
14469
14629
|
const bytes = await readFile17(file);
|
|
14470
14630
|
const metadata = imageMetadata(bytes);
|
|
14471
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
14631
|
+
if (!metadata) throw new ManifestEditError(`${path27.basename(file)} is not a readable PNG or JPEG`);
|
|
14472
14632
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14473
14633
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14474
14634
|
if (used >= limit) {
|
|
@@ -14485,7 +14645,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14485
14645
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14486
14646
|
return {
|
|
14487
14647
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14488
|
-
source:
|
|
14648
|
+
source: path27.relative(ctx.loaded.root, file).split(path27.sep).join("/"),
|
|
14489
14649
|
session: status()
|
|
14490
14650
|
};
|
|
14491
14651
|
}
|
|
@@ -14499,21 +14659,21 @@ function billedAmount(usage) {
|
|
|
14499
14659
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14500
14660
|
}
|
|
14501
14661
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
14502
|
-
const root =
|
|
14503
|
-
const file =
|
|
14504
|
-
const relative =
|
|
14505
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
14662
|
+
const root = path27.dirname(path27.resolve(manifestPath));
|
|
14663
|
+
const file = path27.resolve(root, request.keypointsFile);
|
|
14664
|
+
const relative = path27.relative(root, file);
|
|
14665
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path27.isAbsolute(relative)) {
|
|
14506
14666
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14507
14667
|
}
|
|
14508
|
-
const existed =
|
|
14668
|
+
const existed = existsSync19(file);
|
|
14509
14669
|
let previous = null;
|
|
14510
14670
|
if (request.set) {
|
|
14511
14671
|
if (existed && !request.overwrite) {
|
|
14512
14672
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14513
14673
|
}
|
|
14514
14674
|
if (existed) previous = await readFile17(file);
|
|
14515
|
-
await
|
|
14516
|
-
await
|
|
14675
|
+
await mkdir11(path27.dirname(file), { recursive: true });
|
|
14676
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14517
14677
|
} else if (!existed) {
|
|
14518
14678
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14519
14679
|
}
|
|
@@ -14528,7 +14688,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14528
14688
|
revision: {
|
|
14529
14689
|
mode: "animate-skeleton",
|
|
14530
14690
|
from: request.from,
|
|
14531
|
-
keypointsFile: relative.split(
|
|
14691
|
+
keypointsFile: relative.split(path27.sep).join("/"),
|
|
14532
14692
|
direction: request.direction,
|
|
14533
14693
|
...request.description ? { description: request.description } : {},
|
|
14534
14694
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -14537,7 +14697,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14537
14697
|
});
|
|
14538
14698
|
} catch (error) {
|
|
14539
14699
|
if (request.set) {
|
|
14540
|
-
if (previous) await
|
|
14700
|
+
if (previous) await writeFile13(file, previous);
|
|
14541
14701
|
else await rm9(file, { force: true });
|
|
14542
14702
|
}
|
|
14543
14703
|
throw error;
|
|
@@ -14549,56 +14709,56 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
14549
14709
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14550
14710
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
14551
14711
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
14552
|
-
const relative =
|
|
14553
|
-
if (relative.startsWith("..") ||
|
|
14712
|
+
const relative = path27.relative(ctx.loaded.root, file);
|
|
14713
|
+
if (relative.startsWith("..") || path27.isAbsolute(relative)) {
|
|
14554
14714
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
14555
14715
|
}
|
|
14556
|
-
const current =
|
|
14716
|
+
const current = existsSync19(file) ? sha256(await readFile17(file)) : null;
|
|
14557
14717
|
if (current !== request.expectedSha256) {
|
|
14558
14718
|
throw Object.assign(
|
|
14559
|
-
new Error(`${relative.split(
|
|
14719
|
+
new Error(`${relative.split(path27.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
14560
14720
|
{ status: 409 }
|
|
14561
14721
|
);
|
|
14562
14722
|
}
|
|
14563
|
-
await
|
|
14564
|
-
return relative.split(
|
|
14723
|
+
await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14724
|
+
return relative.split(path27.sep).join("/");
|
|
14565
14725
|
}
|
|
14566
14726
|
|
|
14567
14727
|
// src/gallery/edit.ts
|
|
14568
|
-
var Base64 =
|
|
14569
|
-
var HandEditRequestSchema =
|
|
14570
|
-
|
|
14571
|
-
action:
|
|
14572
|
-
project:
|
|
14573
|
-
assetId:
|
|
14574
|
-
styleId:
|
|
14575
|
-
expectedSha256:
|
|
14728
|
+
var Base64 = z10.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
14729
|
+
var HandEditRequestSchema = z10.discriminatedUnion("action", [
|
|
14730
|
+
z10.object({
|
|
14731
|
+
action: z10.literal("start-edit"),
|
|
14732
|
+
project: z10.string().min(1).optional(),
|
|
14733
|
+
assetId: z10.string().min(1),
|
|
14734
|
+
styleId: z10.string().min(1),
|
|
14735
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14576
14736
|
/** Also launch the author's editor on the file. */
|
|
14577
|
-
open:
|
|
14737
|
+
open: z10.boolean().optional()
|
|
14578
14738
|
}).strict(),
|
|
14579
|
-
|
|
14580
|
-
action:
|
|
14581
|
-
project:
|
|
14582
|
-
assetId:
|
|
14583
|
-
styleId:
|
|
14584
|
-
expectedSha256:
|
|
14739
|
+
z10.object({
|
|
14740
|
+
action: z10.literal("detach-edit"),
|
|
14741
|
+
project: z10.string().min(1).optional(),
|
|
14742
|
+
assetId: z10.string().min(1),
|
|
14743
|
+
styleId: z10.string().min(1),
|
|
14744
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
|
|
14585
14745
|
}).strict(),
|
|
14586
14746
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14587
|
-
|
|
14588
|
-
action:
|
|
14589
|
-
project:
|
|
14590
|
-
assetId:
|
|
14591
|
-
styleId:
|
|
14592
|
-
expectedSha256:
|
|
14747
|
+
z10.object({
|
|
14748
|
+
action: z10.literal("save-edit"),
|
|
14749
|
+
project: z10.string().min(1).optional(),
|
|
14750
|
+
assetId: z10.string().min(1),
|
|
14751
|
+
styleId: z10.string().min(1),
|
|
14752
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14593
14753
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14594
14754
|
png: Base64.min(1).optional(),
|
|
14595
14755
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14596
|
-
frames:
|
|
14756
|
+
frames: z10.array(z10.object({ role: z10.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14597
14757
|
/** The editor's layered project file, kept beside the edit. */
|
|
14598
14758
|
pxo: Base64.optional(),
|
|
14599
14759
|
/** Editor identity from the bridge's ready message. */
|
|
14600
|
-
editor:
|
|
14601
|
-
protocol:
|
|
14760
|
+
editor: z10.string().min(1).max(200),
|
|
14761
|
+
protocol: z10.number().int().positive()
|
|
14602
14762
|
}).strict()
|
|
14603
14763
|
]);
|
|
14604
14764
|
function createGalleryEditHandler(opts) {
|
|
@@ -14639,6 +14799,21 @@ function createGalleryEditHandler(opts) {
|
|
|
14639
14799
|
}
|
|
14640
14800
|
return opts.reload();
|
|
14641
14801
|
}
|
|
14802
|
+
const upload = UploadImageSchema.safeParse(body);
|
|
14803
|
+
if (upload.success) {
|
|
14804
|
+
let manifestPath2;
|
|
14805
|
+
try {
|
|
14806
|
+
manifestPath2 = await opts.manifestFor(upload.data.project);
|
|
14807
|
+
} catch (error) {
|
|
14808
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14809
|
+
}
|
|
14810
|
+
const written = await saveProjectImage(path28.dirname(path28.resolve(manifestPath2)), upload.data);
|
|
14811
|
+
log2(` image added: ${written}`);
|
|
14812
|
+
return opts.reload();
|
|
14813
|
+
}
|
|
14814
|
+
if (body?.action === "upload-image") {
|
|
14815
|
+
throw new ManifestEditError("invalid upload: " + upload.error.issues.slice(0, 2).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
14816
|
+
}
|
|
14642
14817
|
const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
|
|
14643
14818
|
if (poses.success) {
|
|
14644
14819
|
if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
|
|
@@ -14682,18 +14857,51 @@ function createGalleryEditHandler(opts) {
|
|
|
14682
14857
|
}
|
|
14683
14858
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14684
14859
|
if (result.changed) {
|
|
14685
|
-
const relative =
|
|
14860
|
+
const relative = path28.relative(process.cwd(), result.manifestPath);
|
|
14686
14861
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14687
|
-
|
|
14688
|
-
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
14862
|
+
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
14689
14863
|
}
|
|
14690
14864
|
return opts.reload();
|
|
14691
14865
|
};
|
|
14692
14866
|
}
|
|
14867
|
+
function describeEdit(edit) {
|
|
14868
|
+
switch (edit.action) {
|
|
14869
|
+
case "batch":
|
|
14870
|
+
return `${edit.edits.length} changes (${edit.edits.map(describeEdit).join("; ")})`;
|
|
14871
|
+
case "add-style":
|
|
14872
|
+
return `added style ${edit.styleId}`;
|
|
14873
|
+
case "patch-style":
|
|
14874
|
+
return `changed style ${edit.styleId}`;
|
|
14875
|
+
case "set-source":
|
|
14876
|
+
case "clear-source":
|
|
14877
|
+
return `changed ${edit.styleId}/${edit.assetId} source`;
|
|
14878
|
+
case "add-asset":
|
|
14879
|
+
return `added ${edit.assetId}`;
|
|
14880
|
+
case "patch-asset":
|
|
14881
|
+
return `changed ${edit.assetId}`;
|
|
14882
|
+
}
|
|
14883
|
+
}
|
|
14884
|
+
function createGalleryPriceHandler(opts) {
|
|
14885
|
+
return async (body) => {
|
|
14886
|
+
const parsed = ManifestEditSchema.safeParse(body);
|
|
14887
|
+
if (!parsed.success) {
|
|
14888
|
+
throw new ManifestEditError(
|
|
14889
|
+
"invalid edit: " + parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
14890
|
+
);
|
|
14891
|
+
}
|
|
14892
|
+
let manifestPath;
|
|
14893
|
+
try {
|
|
14894
|
+
manifestPath = await opts.manifestFor(parsed.data.project);
|
|
14895
|
+
} catch (error) {
|
|
14896
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14897
|
+
}
|
|
14898
|
+
return priceManifestEdit(manifestPath, parsed.data);
|
|
14899
|
+
};
|
|
14900
|
+
}
|
|
14693
14901
|
|
|
14694
14902
|
// src/pipeline/history.ts
|
|
14695
|
-
import { existsSync as
|
|
14696
|
-
import
|
|
14903
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14904
|
+
import path29 from "path";
|
|
14697
14905
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
14698
14906
|
function historyLimit(manifest, env = process.env) {
|
|
14699
14907
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -14780,10 +14988,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14780
14988
|
}
|
|
14781
14989
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
14782
14990
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
14783
|
-
if (!
|
|
14991
|
+
if (!existsSync20(file)) continue;
|
|
14784
14992
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
14785
14993
|
throw new OverwriteRefusedError(
|
|
14786
|
-
`${
|
|
14994
|
+
`${path29.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
14787
14995
|
[file]
|
|
14788
14996
|
);
|
|
14789
14997
|
}
|
|
@@ -14836,7 +15044,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
14836
15044
|
// src/gallery/snapshot.ts
|
|
14837
15045
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14838
15046
|
function galleryMediaId(absolutePath) {
|
|
14839
|
-
return createHash3("sha256").update(
|
|
15047
|
+
return createHash3("sha256").update(path30.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14840
15048
|
}
|
|
14841
15049
|
function galleryMediaRoute(id) {
|
|
14842
15050
|
return `/media/${id}`;
|
|
@@ -14855,9 +15063,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14855
15063
|
return (entry.history ?? []).map((generation, i) => {
|
|
14856
15064
|
const outputs = generation.outputs.map((output) => {
|
|
14857
15065
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14858
|
-
const file =
|
|
15066
|
+
const file = path30.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14859
15067
|
let url = null;
|
|
14860
|
-
if (
|
|
15068
|
+
if (existsSync21(file)) {
|
|
14861
15069
|
const id = galleryMediaId(file);
|
|
14862
15070
|
media.set(id, { path: file, contentType: mediaType });
|
|
14863
15071
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14987,10 +15195,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14987
15195
|
async function describeQuality(media, root, inspection) {
|
|
14988
15196
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14989
15197
|
const outputs = await Promise.all(
|
|
14990
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
15198
|
+
outputPaths.map((file) => describeOutput(media, root, path30.resolve(root, file)))
|
|
14991
15199
|
);
|
|
14992
|
-
const recordPath =
|
|
14993
|
-
const recordExists =
|
|
15200
|
+
const recordPath = path30.resolve(root, inspection.record);
|
|
15201
|
+
const recordExists = existsSync21(recordPath);
|
|
14994
15202
|
let options = null;
|
|
14995
15203
|
let check = null;
|
|
14996
15204
|
if (recordExists) {
|
|
@@ -15009,7 +15217,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15009
15217
|
return {
|
|
15010
15218
|
state: inspection.state,
|
|
15011
15219
|
reason: inspection.reason,
|
|
15012
|
-
output: portableOutputPath(
|
|
15220
|
+
output: portableOutputPath(path30.resolve(root, inspection.output), root),
|
|
15013
15221
|
record: portableOutputPath(recordPath, root),
|
|
15014
15222
|
recordExists,
|
|
15015
15223
|
outputs,
|
|
@@ -15049,7 +15257,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15049
15257
|
const items = [];
|
|
15050
15258
|
const manifestText = await readFile18(loaded.path, "utf8");
|
|
15051
15259
|
const manifestSha256 = sha256(manifestText);
|
|
15052
|
-
const cacheDir =
|
|
15260
|
+
const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15053
15261
|
let rawStyles = {};
|
|
15054
15262
|
try {
|
|
15055
15263
|
const raw = JSON.parse(manifestText);
|
|
@@ -15073,12 +15281,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15073
15281
|
)
|
|
15074
15282
|
);
|
|
15075
15283
|
if (spec.source) {
|
|
15076
|
-
const editPath =
|
|
15284
|
+
const editPath = path30.resolve(root, spec.source);
|
|
15077
15285
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15078
15286
|
const editShas = await Promise.all(
|
|
15079
15287
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15080
15288
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15081
|
-
return { file, sha256:
|
|
15289
|
+
return { file, sha256: existsSync21(file) ? await sha256File(file) : null };
|
|
15082
15290
|
})
|
|
15083
15291
|
);
|
|
15084
15292
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15086,7 +15294,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15086
15294
|
const companion = await readHandEditCompanion(editPath);
|
|
15087
15295
|
if (companion) {
|
|
15088
15296
|
const projectPath = handEditProjectPath(editPath);
|
|
15089
|
-
const project = companion.project &&
|
|
15297
|
+
const project = companion.project && existsSync21(projectPath) ? await fileInfo(projectPath) : null;
|
|
15090
15298
|
let projectUrl = null;
|
|
15091
15299
|
if (project) {
|
|
15092
15300
|
const id = galleryMediaId(projectPath);
|
|
@@ -15122,8 +15330,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15122
15330
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15123
15331
|
}
|
|
15124
15332
|
} else if (spec.source) {
|
|
15125
|
-
outputs = [await describeOutput(media, root,
|
|
15126
|
-
} else if (!entry &&
|
|
15333
|
+
outputs = [await describeOutput(media, root, path30.resolve(root, spec.source))];
|
|
15334
|
+
} else if (!entry && existsSync21(spec.outFile)) {
|
|
15127
15335
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15128
15336
|
} else {
|
|
15129
15337
|
outputs = [];
|
|
@@ -15324,7 +15532,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15324
15532
|
id: loaded.manifest.name,
|
|
15325
15533
|
name: loaded.manifest.name,
|
|
15326
15534
|
manifest: loaded.path,
|
|
15327
|
-
lock:
|
|
15535
|
+
lock: path30.resolve(opts.lockPath),
|
|
15328
15536
|
root,
|
|
15329
15537
|
provider: loaded.manifest.provider,
|
|
15330
15538
|
account: null,
|
|
@@ -15350,7 +15558,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15350
15558
|
return { snapshot, media };
|
|
15351
15559
|
}
|
|
15352
15560
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15353
|
-
const dir =
|
|
15561
|
+
const dir = path30.dirname(path30.resolve(opts.workspacePath));
|
|
15354
15562
|
const media = /* @__PURE__ */ new Map();
|
|
15355
15563
|
const projects = [];
|
|
15356
15564
|
const items = [];
|
|
@@ -15393,7 +15601,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15393
15601
|
id: project.id,
|
|
15394
15602
|
name: loaded.manifest.name,
|
|
15395
15603
|
manifest: manifestPath,
|
|
15396
|
-
lock:
|
|
15604
|
+
lock: path30.resolve(lockPath),
|
|
15397
15605
|
root: loaded.root,
|
|
15398
15606
|
provider: loaded.manifest.provider,
|
|
15399
15607
|
account: project.account ?? null,
|
|
@@ -15409,8 +15617,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15409
15617
|
id: project.id,
|
|
15410
15618
|
name: project.id,
|
|
15411
15619
|
manifest: manifestPath,
|
|
15412
|
-
lock:
|
|
15413
|
-
root:
|
|
15620
|
+
lock: path30.resolve(lockPath),
|
|
15621
|
+
root: path30.dirname(manifestPath),
|
|
15414
15622
|
provider: project.provider,
|
|
15415
15623
|
account: project.account ?? null,
|
|
15416
15624
|
manifestSha256: null,
|
|
@@ -15437,7 +15645,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15437
15645
|
version: 1,
|
|
15438
15646
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15439
15647
|
project: null,
|
|
15440
|
-
workspace: { path:
|
|
15648
|
+
workspace: { path: path30.resolve(opts.workspacePath), dir, projects },
|
|
15441
15649
|
filter,
|
|
15442
15650
|
totals: {
|
|
15443
15651
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15459,7 +15667,7 @@ import { createServer as createServer2 } from "http";
|
|
|
15459
15667
|
import { readFile as readFile19 } from "fs/promises";
|
|
15460
15668
|
|
|
15461
15669
|
// src/gallery/page.ts
|
|
15462
|
-
import { existsSync as
|
|
15670
|
+
import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
|
|
15463
15671
|
|
|
15464
15672
|
// src/gallery/client-bundle.ts
|
|
15465
15673
|
import { createRequire } from "module";
|
|
@@ -15500,7 +15708,7 @@ function clientAssets() {
|
|
|
15500
15708
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15501
15709
|
client ??= {
|
|
15502
15710
|
css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
15503
|
-
js:
|
|
15711
|
+
js: existsSync22(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
|
|
15504
15712
|
};
|
|
15505
15713
|
return client;
|
|
15506
15714
|
}
|
|
@@ -15542,6 +15750,7 @@ ${css}</style>
|
|
|
15542
15750
|
<option value="none">Group: none</option>
|
|
15543
15751
|
</select>
|
|
15544
15752
|
<span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
|
|
15753
|
+
<button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
|
|
15545
15754
|
<button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
|
|
15546
15755
|
<label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
|
|
15547
15756
|
</div>
|
|
@@ -15602,7 +15811,7 @@ async function readJsonBody(req) {
|
|
|
15602
15811
|
async function serveGallery(opts) {
|
|
15603
15812
|
const log2 = opts.onProgress ?? (() => {
|
|
15604
15813
|
});
|
|
15605
|
-
const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
|
|
15814
|
+
const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? randomBytes(16).toString("hex") : null;
|
|
15606
15815
|
let media = /* @__PURE__ */ new Map();
|
|
15607
15816
|
let loading = null;
|
|
15608
15817
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -15677,6 +15886,17 @@ async function serveGallery(opts) {
|
|
|
15677
15886
|
}
|
|
15678
15887
|
return;
|
|
15679
15888
|
}
|
|
15889
|
+
if (req.method === "POST" && url2.pathname === "/api/price") {
|
|
15890
|
+
if (!opts.price || !session) return fail(405, "pricing an edit needs the gallery started with --edit");
|
|
15891
|
+
try {
|
|
15892
|
+
const body = await guardedBody("price requests");
|
|
15893
|
+
if (body === void 0) return;
|
|
15894
|
+
json(200, await opts.price(body));
|
|
15895
|
+
} catch (err) {
|
|
15896
|
+
reportError("price", err);
|
|
15897
|
+
}
|
|
15898
|
+
return;
|
|
15899
|
+
}
|
|
15680
15900
|
if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
|
|
15681
15901
|
if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
|
|
15682
15902
|
try {
|
|
@@ -15871,7 +16091,7 @@ async function serveGallery(opts) {
|
|
|
15871
16091
|
|
|
15872
16092
|
// src/gallery/generate.ts
|
|
15873
16093
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15874
|
-
import { z as
|
|
16094
|
+
import { z as z11 } from "zod";
|
|
15875
16095
|
|
|
15876
16096
|
// src/pipeline/poll.ts
|
|
15877
16097
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -16282,13 +16502,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
16282
16502
|
}
|
|
16283
16503
|
|
|
16284
16504
|
// src/pick/server.ts
|
|
16285
|
-
import { existsSync as
|
|
16505
|
+
import { existsSync as existsSync24 } from "fs";
|
|
16286
16506
|
import { readFile as readFile21 } from "fs/promises";
|
|
16287
16507
|
|
|
16288
16508
|
// src/pipeline/init.ts
|
|
16289
|
-
import { readdir, readFile as readFile20, writeFile as
|
|
16290
|
-
import { existsSync as
|
|
16291
|
-
import
|
|
16509
|
+
import { readdir, readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
|
|
16510
|
+
import { existsSync as existsSync23 } from "fs";
|
|
16511
|
+
import path31 from "path";
|
|
16292
16512
|
function pngSize(buf) {
|
|
16293
16513
|
if (buf.length < 24) return null;
|
|
16294
16514
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16302,7 +16522,7 @@ function slugify(value) {
|
|
|
16302
16522
|
async function walk(dir, exclude) {
|
|
16303
16523
|
const out = [];
|
|
16304
16524
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16305
|
-
const full =
|
|
16525
|
+
const full = path31.join(dir, entry.name);
|
|
16306
16526
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16307
16527
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16308
16528
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16321,9 +16541,9 @@ async function scanAssets(root, opts = {}) {
|
|
|
16321
16541
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16322
16542
|
continue;
|
|
16323
16543
|
}
|
|
16324
|
-
const rel =
|
|
16325
|
-
const category =
|
|
16326
|
-
let id = slugify(
|
|
16544
|
+
const rel = path31.relative(root, file);
|
|
16545
|
+
const category = path31.dirname(rel) === "." ? "" : path31.dirname(rel);
|
|
16546
|
+
let id = slugify(path31.basename(rel));
|
|
16327
16547
|
if (seen.has(id)) {
|
|
16328
16548
|
const n = seen.get(id) + 1;
|
|
16329
16549
|
seen.set(id, n);
|
|
@@ -16381,10 +16601,10 @@ function mode(values) {
|
|
|
16381
16601
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16382
16602
|
}
|
|
16383
16603
|
async function writeManifestFile(target, manifest) {
|
|
16384
|
-
if (
|
|
16604
|
+
if (existsSync23(target)) {
|
|
16385
16605
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16386
16606
|
}
|
|
16387
|
-
await
|
|
16607
|
+
await writeFile14(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16388
16608
|
}
|
|
16389
16609
|
|
|
16390
16610
|
// src/pick/sheet.ts
|
|
@@ -16797,7 +17017,7 @@ async function currentArt(entry, spec) {
|
|
|
16797
17017
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
16798
17018
|
if (!previous) return null;
|
|
16799
17019
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
16800
|
-
if (!
|
|
17020
|
+
if (!existsSync24(file)) return null;
|
|
16801
17021
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
16802
17022
|
if (!contentType) return null;
|
|
16803
17023
|
let width = entry.width;
|
|
@@ -16971,26 +17191,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
16971
17191
|
}
|
|
16972
17192
|
|
|
16973
17193
|
// src/gallery/generate.ts
|
|
16974
|
-
var GenerateRequestSchema =
|
|
16975
|
-
project:
|
|
16976
|
-
keys:
|
|
17194
|
+
var GenerateRequestSchema = z11.object({
|
|
17195
|
+
project: z11.string().min(1).optional(),
|
|
17196
|
+
keys: z11.array(z11.string().min(1)).min(1).max(500),
|
|
16977
17197
|
/**
|
|
16978
17198
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
16979
17199
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
16980
17200
|
* the recorded bytes back over a modified file.
|
|
16981
17201
|
*/
|
|
16982
|
-
force:
|
|
17202
|
+
force: z11.boolean().optional(),
|
|
16983
17203
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
16984
|
-
resume:
|
|
17204
|
+
resume: z11.boolean().optional(),
|
|
16985
17205
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
16986
|
-
refresh:
|
|
17206
|
+
refresh: z11.boolean().optional(),
|
|
16987
17207
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
16988
|
-
restore:
|
|
17208
|
+
restore: z11.boolean().optional(),
|
|
16989
17209
|
/**
|
|
16990
17210
|
* Bring a replaced generation back: 1-based position in the record's
|
|
16991
17211
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
16992
17212
|
*/
|
|
16993
|
-
revert:
|
|
17213
|
+
revert: z11.string().min(1).max(64).optional()
|
|
16994
17214
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
16995
17215
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
16996
17216
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17114,6 +17334,70 @@ function createGenerateHandlers(opts) {
|
|
|
17114
17334
|
job.phase = "done";
|
|
17115
17335
|
job.finishedAt = now().toISOString();
|
|
17116
17336
|
}
|
|
17337
|
+
async function submitGroups(job, ctx, groups) {
|
|
17338
|
+
job.phase = "submitting";
|
|
17339
|
+
for (const group of groups) {
|
|
17340
|
+
const provider = opts.providerFor(job.project ?? void 0, group.provider);
|
|
17341
|
+
const ceiling = remainingFor(group, groups.length, opts.budget, spent);
|
|
17342
|
+
if (provider.balance) {
|
|
17343
|
+
const balance = await provider.balance();
|
|
17344
|
+
say(job, `balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`);
|
|
17345
|
+
if (balance.unit !== group.costUnit) {
|
|
17346
|
+
throw new Error(
|
|
17347
|
+
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
17348
|
+
);
|
|
17349
|
+
}
|
|
17350
|
+
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
17351
|
+
throw new Error(
|
|
17352
|
+
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
17353
|
+
);
|
|
17354
|
+
}
|
|
17355
|
+
}
|
|
17356
|
+
const res = await submit(provider, ctx.loaded, group.actionable, ctx.lock, ctx.lockPath, {
|
|
17357
|
+
budget: ceiling,
|
|
17358
|
+
onProgress: (line) => say(job, line),
|
|
17359
|
+
spacingMs: opts.submitSpacingMs
|
|
17360
|
+
});
|
|
17361
|
+
spent[group.provider] = (spent[group.provider] ?? 0) + res.spent;
|
|
17362
|
+
units[group.provider] = res.unit;
|
|
17363
|
+
job.spent[group.provider] = (job.spent[group.provider] ?? 0) + res.spent;
|
|
17364
|
+
job.counts.submitted += res.submitted;
|
|
17365
|
+
job.counts.failed += res.failed;
|
|
17366
|
+
say(job, `${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`);
|
|
17367
|
+
}
|
|
17368
|
+
}
|
|
17369
|
+
async function generateInWaves(job, ctx, specs, groups) {
|
|
17370
|
+
for (let wave = 1; ; wave++) {
|
|
17371
|
+
const submittedBefore = job.counts.submitted;
|
|
17372
|
+
await submitGroups(job, ctx, groups);
|
|
17373
|
+
await settle(job, ctx, specs);
|
|
17374
|
+
if (job.phase !== "done" || job.counts.submitted === submittedBefore) return;
|
|
17375
|
+
const finishedAt = job.finishedAt;
|
|
17376
|
+
const finish = () => {
|
|
17377
|
+
job.phase = "done";
|
|
17378
|
+
job.finishedAt = finishedAt;
|
|
17379
|
+
};
|
|
17380
|
+
job.phase = "queued";
|
|
17381
|
+
job.finishedAt = null;
|
|
17382
|
+
const next = await opts.loadProject(job.project ?? void 0);
|
|
17383
|
+
const byKey = new Map(next.specs.map((spec) => [lockKey(spec.styleId, spec.assetId), spec]));
|
|
17384
|
+
const nextSpecs = job.keys.map((key) => byKey.get(key)).filter((spec) => Boolean(spec));
|
|
17385
|
+
const plan = await buildPlan(nextSpecs, next.lock);
|
|
17386
|
+
if (!plan.actionable.length) return finish();
|
|
17387
|
+
for (const group of plan.groups) {
|
|
17388
|
+
const remaining = remainingFor(group, plan.groups.length, opts.budget, spent);
|
|
17389
|
+
if (group.cost > remaining) {
|
|
17390
|
+
say(job, `wave ${wave + 1} would spend ${formatCost(group.costUnit, group.cost)} on ${group.provider} but only ${formatCost(group.costUnit, remaining)} of this session's budget remain; stopping here`);
|
|
17391
|
+
return finish();
|
|
17392
|
+
}
|
|
17393
|
+
}
|
|
17394
|
+
say(job, `wave ${wave + 1}: ${plan.actionable.length} asset(s) whose parents are now on disk: ` + plan.groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
17395
|
+
contexts.set(job.id, next);
|
|
17396
|
+
ctx = next;
|
|
17397
|
+
specs = nextSpecs;
|
|
17398
|
+
groups = plan.groups;
|
|
17399
|
+
}
|
|
17400
|
+
}
|
|
17117
17401
|
async function run(job, ctx, specs, groups) {
|
|
17118
17402
|
try {
|
|
17119
17403
|
if (job.mode === "refresh") {
|
|
@@ -17141,36 +17425,8 @@ function createGenerateHandlers(opts) {
|
|
|
17141
17425
|
return;
|
|
17142
17426
|
}
|
|
17143
17427
|
if (job.mode === "generate") {
|
|
17144
|
-
job
|
|
17145
|
-
|
|
17146
|
-
const provider = opts.providerFor(job.project ?? void 0, group.provider);
|
|
17147
|
-
const ceiling = remainingFor(group, groups.length, opts.budget, spent);
|
|
17148
|
-
if (provider.balance) {
|
|
17149
|
-
const balance = await provider.balance();
|
|
17150
|
-
say(job, `balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`);
|
|
17151
|
-
if (balance.unit !== group.costUnit) {
|
|
17152
|
-
throw new Error(
|
|
17153
|
-
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
17154
|
-
);
|
|
17155
|
-
}
|
|
17156
|
-
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
17157
|
-
throw new Error(
|
|
17158
|
-
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
17159
|
-
);
|
|
17160
|
-
}
|
|
17161
|
-
}
|
|
17162
|
-
const res = await submit(provider, ctx.loaded, group.actionable, ctx.lock, ctx.lockPath, {
|
|
17163
|
-
budget: ceiling,
|
|
17164
|
-
onProgress: (line) => say(job, line),
|
|
17165
|
-
spacingMs: opts.submitSpacingMs
|
|
17166
|
-
});
|
|
17167
|
-
spent[group.provider] = (spent[group.provider] ?? 0) + res.spent;
|
|
17168
|
-
units[group.provider] = res.unit;
|
|
17169
|
-
job.spent[group.provider] = (job.spent[group.provider] ?? 0) + res.spent;
|
|
17170
|
-
job.counts.submitted += res.submitted;
|
|
17171
|
-
job.counts.failed += res.failed;
|
|
17172
|
-
say(job, `${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`);
|
|
17173
|
-
}
|
|
17428
|
+
await generateInWaves(job, ctx, specs, groups);
|
|
17429
|
+
return;
|
|
17174
17430
|
}
|
|
17175
17431
|
await settle(job, ctx, specs);
|
|
17176
17432
|
} catch (error) {
|
|
@@ -17301,14 +17557,14 @@ function createGenerateHandlers(opts) {
|
|
|
17301
17557
|
|
|
17302
17558
|
// src/gallery/editor.ts
|
|
17303
17559
|
import { readFile as readFile23 } from "fs/promises";
|
|
17304
|
-
import
|
|
17560
|
+
import path33 from "path";
|
|
17305
17561
|
|
|
17306
17562
|
// src/editor/install.ts
|
|
17307
17563
|
import { createHash as createHash4 } from "crypto";
|
|
17308
|
-
import { existsSync as
|
|
17309
|
-
import { mkdir as
|
|
17564
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17565
|
+
import { mkdir as mkdir12, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile15 } from "fs/promises";
|
|
17310
17566
|
import os2 from "os";
|
|
17311
|
-
import
|
|
17567
|
+
import path32 from "path";
|
|
17312
17568
|
|
|
17313
17569
|
// tools/pixelorama-bridge/pin.json
|
|
17314
17570
|
var pin_default = {
|
|
@@ -17372,15 +17628,15 @@ var EditorInstallError = class extends Error {
|
|
|
17372
17628
|
};
|
|
17373
17629
|
function editorToolsRoot() {
|
|
17374
17630
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17375
|
-
if (override) return
|
|
17376
|
-
if (process.platform === "darwin") return
|
|
17631
|
+
if (override) return path32.resolve(override);
|
|
17632
|
+
if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17377
17633
|
if (process.platform === "win32") {
|
|
17378
|
-
return
|
|
17634
|
+
return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17379
17635
|
}
|
|
17380
|
-
return
|
|
17636
|
+
return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17381
17637
|
}
|
|
17382
17638
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17383
|
-
return
|
|
17639
|
+
return path32.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17384
17640
|
}
|
|
17385
17641
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17386
17642
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17403,8 +17659,8 @@ async function editorStatus(opts = {}) {
|
|
|
17403
17659
|
let totalBytes = 0;
|
|
17404
17660
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17405
17661
|
totalBytes += expected.bytes;
|
|
17406
|
-
const file =
|
|
17407
|
-
const info =
|
|
17662
|
+
const file = path32.join(dir, name);
|
|
17663
|
+
const info = existsSync25(file) ? await stat3(file).catch(() => null) : null;
|
|
17408
17664
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17409
17665
|
installedBytes += expected.bytes;
|
|
17410
17666
|
} else {
|
|
@@ -17433,7 +17689,7 @@ async function installEditor(opts = {}) {
|
|
|
17433
17689
|
);
|
|
17434
17690
|
}
|
|
17435
17691
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
17436
|
-
await
|
|
17692
|
+
await mkdir12(dir, { recursive: true });
|
|
17437
17693
|
const downloaded = [];
|
|
17438
17694
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
17439
17695
|
let fetchedBytes = before.installedBytes;
|
|
@@ -17461,10 +17717,10 @@ async function installEditor(opts = {}) {
|
|
|
17461
17717
|
if (digest2 !== expected.sha256) {
|
|
17462
17718
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17463
17719
|
}
|
|
17464
|
-
const final =
|
|
17720
|
+
const final = path32.join(dir, name);
|
|
17465
17721
|
const tmp = `${final}.${process.pid}.part`;
|
|
17466
17722
|
try {
|
|
17467
|
-
await
|
|
17723
|
+
await writeFile15(tmp, bytes);
|
|
17468
17724
|
await rename9(tmp, final);
|
|
17469
17725
|
} finally {
|
|
17470
17726
|
await rm10(tmp, { force: true });
|
|
@@ -17507,7 +17763,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17507
17763
|
".json": "application/json; charset=utf-8"
|
|
17508
17764
|
};
|
|
17509
17765
|
function editorContentType(name) {
|
|
17510
|
-
return EDITOR_CONTENT_TYPES[
|
|
17766
|
+
return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
|
|
17511
17767
|
}
|
|
17512
17768
|
|
|
17513
17769
|
// src/gallery/editor.ts
|
|
@@ -17583,7 +17839,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17583
17839
|
if (!status.installed) return null;
|
|
17584
17840
|
let bytes;
|
|
17585
17841
|
try {
|
|
17586
|
-
bytes = await readFile23(
|
|
17842
|
+
bytes = await readFile23(path33.join(dir, name));
|
|
17587
17843
|
} catch {
|
|
17588
17844
|
verified = null;
|
|
17589
17845
|
return null;
|
|
@@ -17623,13 +17879,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17623
17879
|
};
|
|
17624
17880
|
const loadProject = async (project) => {
|
|
17625
17881
|
const ctx = await access2.loadProject(project);
|
|
17626
|
-
const dir =
|
|
17882
|
+
const dir = path34.dirname(ctx.loaded.path);
|
|
17627
17883
|
const declared = readEnvFiles(dir);
|
|
17628
17884
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
17629
17885
|
for (const [name, value] of Object.entries(declared)) {
|
|
17630
17886
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
17631
17887
|
throw new Error(
|
|
17632
|
-
`${
|
|
17888
|
+
`${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.`
|
|
17633
17889
|
);
|
|
17634
17890
|
}
|
|
17635
17891
|
}
|
|
@@ -17657,6 +17913,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17657
17913
|
),
|
|
17658
17914
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
17659
17915
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
17916
|
+
...args.edit ? { price: createGalleryPriceHandler({ manifestFor: access2.manifestFor }) } : {},
|
|
17660
17917
|
// Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
|
|
17661
17918
|
...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
|
|
17662
17919
|
// The editor exists to write hand edits back, so it follows the write gate.
|
|
@@ -17676,8 +17933,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
17676
17933
|
}
|
|
17677
17934
|
async function runGallery(args) {
|
|
17678
17935
|
if (args.workspace) {
|
|
17679
|
-
const workspacePath =
|
|
17680
|
-
if (!
|
|
17936
|
+
const workspacePath = path34.resolve(args.workspace);
|
|
17937
|
+
if (!existsSync26(workspacePath)) {
|
|
17681
17938
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
17682
17939
|
}
|
|
17683
17940
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -17697,7 +17954,7 @@ async function runGallery(args) {
|
|
|
17697
17954
|
const registered = async (projectId) => {
|
|
17698
17955
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
17699
17956
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
17700
|
-
return resolveProject(
|
|
17957
|
+
return resolveProject(path34.dirname(workspacePath), project);
|
|
17701
17958
|
};
|
|
17702
17959
|
await serveUntilStopped(initial, build2, args, {
|
|
17703
17960
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -17722,7 +17979,7 @@ async function runGallery(args) {
|
|
|
17722
17979
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
17723
17980
|
};
|
|
17724
17981
|
await serveUntilStopped(await build(), reload, args, {
|
|
17725
|
-
manifestFor: () =>
|
|
17982
|
+
manifestFor: () => path34.resolve(args.manifest),
|
|
17726
17983
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
17727
17984
|
});
|
|
17728
17985
|
}
|
|
@@ -18031,18 +18288,18 @@ async function runRestoreGeneration(args) {
|
|
|
18031
18288
|
}
|
|
18032
18289
|
|
|
18033
18290
|
// src/cli/commands/housekeeping.ts
|
|
18034
|
-
import { existsSync as
|
|
18291
|
+
import { existsSync as existsSync28 } from "fs";
|
|
18035
18292
|
|
|
18036
18293
|
// src/pipeline/cache-health.ts
|
|
18037
|
-
import { existsSync as
|
|
18294
|
+
import { existsSync as existsSync27 } from "fs";
|
|
18038
18295
|
import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
|
|
18039
|
-
import
|
|
18296
|
+
import path35 from "path";
|
|
18040
18297
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18041
|
-
if (options.prune && !
|
|
18042
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
18298
|
+
if (options.prune && !existsSync27(lockPath)) {
|
|
18299
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path35.resolve(lockPath)}`);
|
|
18043
18300
|
}
|
|
18044
|
-
const contentDir =
|
|
18045
|
-
const remotePath =
|
|
18301
|
+
const contentDir = path35.resolve(path35.dirname(lockPath), ".pixelkiln", "cache");
|
|
18302
|
+
const remotePath = path35.resolve(cachePathFor(lockPath));
|
|
18046
18303
|
const referenced = referencedHashes(lock);
|
|
18047
18304
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18048
18305
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18054,7 +18311,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18054
18311
|
]);
|
|
18055
18312
|
for (const name of names) {
|
|
18056
18313
|
try {
|
|
18057
|
-
await rm11(
|
|
18314
|
+
await rm11(path35.join(contentDir, name), { force: true });
|
|
18058
18315
|
removed.contentFiles++;
|
|
18059
18316
|
} catch {
|
|
18060
18317
|
}
|
|
@@ -18081,7 +18338,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18081
18338
|
async function inspectContentCache(contentDir, referenced) {
|
|
18082
18339
|
const report = {
|
|
18083
18340
|
path: contentDir,
|
|
18084
|
-
exists:
|
|
18341
|
+
exists: existsSync27(contentDir),
|
|
18085
18342
|
files: 0,
|
|
18086
18343
|
bytes: 0,
|
|
18087
18344
|
valid: 0,
|
|
@@ -18102,7 +18359,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18102
18359
|
continue;
|
|
18103
18360
|
}
|
|
18104
18361
|
report.files++;
|
|
18105
|
-
const file =
|
|
18362
|
+
const file = path35.join(contentDir, entry.name);
|
|
18106
18363
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
18107
18364
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
18108
18365
|
let bytes;
|
|
@@ -18145,7 +18402,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18145
18402
|
async function inspectRemoteHashCache(remotePath) {
|
|
18146
18403
|
const report = {
|
|
18147
18404
|
path: remotePath,
|
|
18148
|
-
exists:
|
|
18405
|
+
exists: existsSync27(remotePath),
|
|
18149
18406
|
entries: 0,
|
|
18150
18407
|
valid: 0,
|
|
18151
18408
|
invalidIds: [],
|
|
@@ -18218,7 +18475,7 @@ async function runAccept(args) {
|
|
|
18218
18475
|
let intact = true;
|
|
18219
18476
|
for (const output of entry.outputs) {
|
|
18220
18477
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
18221
|
-
if (!
|
|
18478
|
+
if (!existsSync28(file) || await sha256File(file) !== output.sha256) {
|
|
18222
18479
|
intact = false;
|
|
18223
18480
|
break;
|
|
18224
18481
|
}
|
|
@@ -18273,42 +18530,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
18273
18530
|
}
|
|
18274
18531
|
|
|
18275
18532
|
// src/cli/commands/init.ts
|
|
18276
|
-
import
|
|
18277
|
-
import { existsSync as
|
|
18533
|
+
import path36 from "path";
|
|
18534
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18278
18535
|
async function runInit(args) {
|
|
18279
18536
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
18280
|
-
const root =
|
|
18281
|
-
if (!
|
|
18537
|
+
const root = path36.resolve(args.from);
|
|
18538
|
+
if (!existsSync29(root)) throw new Error(`No directory at ${root}`);
|
|
18282
18539
|
const generator = args.generator ?? "map";
|
|
18283
18540
|
if (generator !== "1dir" && generator !== "map") {
|
|
18284
18541
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
18285
18542
|
}
|
|
18286
|
-
const target =
|
|
18543
|
+
const target = path36.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
18287
18544
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18288
18545
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18289
18546
|
const manifest = buildManifest(
|
|
18290
|
-
args.name ??
|
|
18547
|
+
args.name ?? path36.basename(path36.dirname(target)),
|
|
18291
18548
|
args.styles[0] ?? "base",
|
|
18292
18549
|
generator,
|
|
18293
|
-
|
|
18550
|
+
path36.relative(path36.dirname(target), root) || ".",
|
|
18294
18551
|
assets
|
|
18295
18552
|
);
|
|
18296
18553
|
await writeManifestFile(target, manifest);
|
|
18297
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
18554
|
+
log(` scanned ${assets.length} PNG(s) under ${path36.relative(process.cwd(), root)}`);
|
|
18298
18555
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18299
|
-
log(` wrote ${
|
|
18556
|
+
log(` wrote ${path36.relative(process.cwd(), target)}`);
|
|
18300
18557
|
log(`
|
|
18301
18558
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18302
18559
|
log(` PixelLab account instead of inventing them:`);
|
|
18303
18560
|
log(`
|
|
18304
|
-
pixelkiln adopt --manifest ${
|
|
18561
|
+
pixelkiln adopt --manifest ${path36.relative(process.cwd(), target)} --write-prompts
|
|
18305
18562
|
`);
|
|
18306
18563
|
}
|
|
18307
18564
|
|
|
18308
18565
|
// src/pipeline/doctor.ts
|
|
18309
|
-
import { constants, existsSync as
|
|
18566
|
+
import { constants, existsSync as existsSync30 } from "fs";
|
|
18310
18567
|
import { access, stat as stat4 } from "fs/promises";
|
|
18311
|
-
import
|
|
18568
|
+
import path37 from "path";
|
|
18312
18569
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18313
18570
|
const checks = [];
|
|
18314
18571
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18319,8 +18576,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18319
18576
|
);
|
|
18320
18577
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18321
18578
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18322
|
-
|
|
18323
|
-
...spec.quality ? [
|
|
18579
|
+
path37.dirname(spec.outFile),
|
|
18580
|
+
...spec.quality ? [path37.dirname(spec.quality.outFile)] : []
|
|
18324
18581
|
]))];
|
|
18325
18582
|
const unwritable = [];
|
|
18326
18583
|
for (const dir of dirs) {
|
|
@@ -18343,7 +18600,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18343
18600
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18344
18601
|
const output = entry.outputs[index];
|
|
18345
18602
|
const spec = specByKey.get(key);
|
|
18346
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
18603
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path37.dirname(lockPath));
|
|
18347
18604
|
const owner = outputOwners.get(absolute);
|
|
18348
18605
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18349
18606
|
outputOwners.set(absolute, key);
|
|
@@ -18358,7 +18615,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18358
18615
|
} else {
|
|
18359
18616
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18360
18617
|
}
|
|
18361
|
-
const cacheDir =
|
|
18618
|
+
const cacheDir = path37.join(path37.dirname(lockPath), ".pixelkiln", "cache");
|
|
18362
18619
|
const stranded = [];
|
|
18363
18620
|
const invalidState = [];
|
|
18364
18621
|
const staleMapUrls = [];
|
|
@@ -18374,11 +18631,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18374
18631
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18375
18632
|
let hasCache = false;
|
|
18376
18633
|
for (const output of entry.outputs) {
|
|
18377
|
-
const cached =
|
|
18634
|
+
const cached = path37.join(
|
|
18378
18635
|
cacheDir,
|
|
18379
18636
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18380
18637
|
);
|
|
18381
|
-
if (
|
|
18638
|
+
if (existsSync30(cached) && await sha256File(cached) === output.sha256) {
|
|
18382
18639
|
hasCache = true;
|
|
18383
18640
|
break;
|
|
18384
18641
|
}
|
|
@@ -18474,7 +18731,7 @@ async function nearestExistingDirectory(start) {
|
|
|
18474
18731
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18475
18732
|
} catch {
|
|
18476
18733
|
}
|
|
18477
|
-
const parent =
|
|
18734
|
+
const parent = path37.dirname(current);
|
|
18478
18735
|
if (parent === current) return current;
|
|
18479
18736
|
current = parent;
|
|
18480
18737
|
}
|
|
@@ -18482,8 +18739,8 @@ async function nearestExistingDirectory(start) {
|
|
|
18482
18739
|
|
|
18483
18740
|
// src/pipeline/audit.ts
|
|
18484
18741
|
import { readFile as readFile25 } from "fs/promises";
|
|
18485
|
-
import { existsSync as
|
|
18486
|
-
import
|
|
18742
|
+
import { existsSync as existsSync31 } from "fs";
|
|
18743
|
+
import path38 from "path";
|
|
18487
18744
|
function paletteDistance(asset, reference) {
|
|
18488
18745
|
if (!asset.length || !reference.length) return 0;
|
|
18489
18746
|
let total = 0;
|
|
@@ -18519,7 +18776,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18519
18776
|
const unreadable = [];
|
|
18520
18777
|
for (const spec of mine) {
|
|
18521
18778
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18522
|
-
if (!
|
|
18779
|
+
if (!existsSync31(output.absolutePath)) {
|
|
18523
18780
|
missing.push(output.id);
|
|
18524
18781
|
continue;
|
|
18525
18782
|
}
|
|
@@ -18548,8 +18805,8 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18548
18805
|
let referenceFromStyleImages = false;
|
|
18549
18806
|
const refPalettes = [];
|
|
18550
18807
|
for (const img of style.styleImages) {
|
|
18551
|
-
const abs =
|
|
18552
|
-
if (!
|
|
18808
|
+
const abs = path38.resolve(loaded.root, img.path);
|
|
18809
|
+
if (!existsSync31(abs)) continue;
|
|
18553
18810
|
try {
|
|
18554
18811
|
refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
|
|
18555
18812
|
} catch {
|
|
@@ -18779,24 +19036,24 @@ async function runAudit(args) {
|
|
|
18779
19036
|
}
|
|
18780
19037
|
|
|
18781
19038
|
// src/cli/commands/pack.ts
|
|
18782
|
-
import
|
|
19039
|
+
import path40 from "path";
|
|
18783
19040
|
import { readFile as readFile26 } from "fs/promises";
|
|
18784
19041
|
|
|
18785
19042
|
// src/pipeline/pack.ts
|
|
18786
19043
|
import { readFileSync as readFileSync5 } from "fs";
|
|
18787
|
-
import
|
|
19044
|
+
import path39 from "path";
|
|
18788
19045
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
18789
19046
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
18790
19047
|
if (!Array.isArray(raw) || !raw.length) {
|
|
18791
19048
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
18792
19049
|
}
|
|
18793
|
-
const baseDir =
|
|
19050
|
+
const baseDir = path39.dirname(path39.resolve(inputsFilePath));
|
|
18794
19051
|
return raw.map((entry, i) => {
|
|
18795
19052
|
const e = entry;
|
|
18796
19053
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
18797
19054
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
18798
19055
|
}
|
|
18799
|
-
return { id: e.id, path:
|
|
19056
|
+
return { id: e.id, path: path39.resolve(baseDir, e.path) };
|
|
18800
19057
|
});
|
|
18801
19058
|
}
|
|
18802
19059
|
function packSprites(inputs, options = {}) {
|
|
@@ -18880,7 +19137,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18880
19137
|
}
|
|
18881
19138
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
18882
19139
|
id,
|
|
18883
|
-
path:
|
|
19140
|
+
path: path39.resolve(source)
|
|
18884
19141
|
}));
|
|
18885
19142
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
18886
19143
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -18904,7 +19161,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18904
19161
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
18905
19162
|
const source = sources[id];
|
|
18906
19163
|
if (source && outputs.length <= 1) {
|
|
18907
|
-
inputs.push({ id, path:
|
|
19164
|
+
inputs.push({ id, path: path39.resolve(manifestDir, source) });
|
|
18908
19165
|
continue;
|
|
18909
19166
|
}
|
|
18910
19167
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -18938,7 +19195,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
18938
19195
|
}
|
|
18939
19196
|
}
|
|
18940
19197
|
for (const [id, source] of Object.entries(sources)) {
|
|
18941
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
19198
|
+
if (!locked.has(id)) inputs.push({ id, path: path39.resolve(manifestDir, source) });
|
|
18942
19199
|
}
|
|
18943
19200
|
if (!inputs.length) {
|
|
18944
19201
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19046,7 +19303,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19046
19303
|
const source = sources[id];
|
|
19047
19304
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19048
19305
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19049
|
-
placements.push({ id, path:
|
|
19306
|
+
placements.push({ id, path: path39.resolve(manifestDir, source), cell });
|
|
19050
19307
|
continue;
|
|
19051
19308
|
}
|
|
19052
19309
|
if (source && entry) {
|
|
@@ -19079,7 +19336,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19079
19336
|
`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}`
|
|
19080
19337
|
);
|
|
19081
19338
|
}
|
|
19082
|
-
const basePng = mount.base ? readFileSync5(
|
|
19339
|
+
const basePng = mount.base ? readFileSync5(path39.resolve(manifestDir, mount.base)) : void 0;
|
|
19083
19340
|
const mounted = mountSprites(placements, {
|
|
19084
19341
|
cellWidth: mount.cellWidth,
|
|
19085
19342
|
cellHeight: mount.cellHeight,
|
|
@@ -19089,7 +19346,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19089
19346
|
...mounted,
|
|
19090
19347
|
sources: mount.base ? [{
|
|
19091
19348
|
id: "$base",
|
|
19092
|
-
path:
|
|
19349
|
+
path: path39.resolve(manifestDir, mount.base),
|
|
19093
19350
|
sha256: sha256(basePng),
|
|
19094
19351
|
included: true
|
|
19095
19352
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19493,12 +19750,12 @@ async function runPack(args) {
|
|
|
19493
19750
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19494
19751
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19495
19752
|
}
|
|
19496
|
-
const raw = JSON.parse(await readFile26(
|
|
19753
|
+
const raw = JSON.parse(await readFile26(path40.resolve(args.inputs), "utf8"));
|
|
19497
19754
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19498
19755
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19499
19756
|
const format = sheetFormat(args);
|
|
19500
|
-
const base =
|
|
19501
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19757
|
+
const base = path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
19758
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
|
|
19502
19759
|
const outputs = [
|
|
19503
19760
|
{ path: `${base}.png`, data: png },
|
|
19504
19761
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19511,7 +19768,7 @@ async function runPack(args) {
|
|
|
19511
19768
|
log(
|
|
19512
19769
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
19513
19770
|
);
|
|
19514
|
-
log(` ${
|
|
19771
|
+
log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19515
19772
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19516
19773
|
return;
|
|
19517
19774
|
}
|
|
@@ -19519,7 +19776,7 @@ async function runPack(args) {
|
|
|
19519
19776
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19520
19777
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19521
19778
|
}
|
|
19522
|
-
const manifestDir =
|
|
19779
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19523
19780
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19524
19781
|
for (const styleId of styleIds) {
|
|
19525
19782
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19534,9 +19791,9 @@ async function runPack(args) {
|
|
|
19534
19791
|
sourceOverrides: qualitySources,
|
|
19535
19792
|
sources: manifestSources(loaded.manifest, styleId)
|
|
19536
19793
|
});
|
|
19537
|
-
const base = args.out ?
|
|
19794
|
+
const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path40.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
19538
19795
|
const format = sheetFormat(args);
|
|
19539
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19796
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
|
|
19540
19797
|
const outputs = [
|
|
19541
19798
|
{ path: `${base}.png`, data: png },
|
|
19542
19799
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19568,13 +19825,13 @@ async function runPack(args) {
|
|
|
19568
19825
|
log(
|
|
19569
19826
|
` ${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})`
|
|
19570
19827
|
);
|
|
19571
|
-
log(` ${
|
|
19828
|
+
log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19572
19829
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19573
19830
|
}
|
|
19574
19831
|
}
|
|
19575
19832
|
async function runMount(args) {
|
|
19576
19833
|
const { loaded, lock } = await openProject2(args);
|
|
19577
|
-
const manifestDir =
|
|
19834
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19578
19835
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19579
19836
|
for (const styleId of styleIds) {
|
|
19580
19837
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19613,9 +19870,9 @@ async function runMount(args) {
|
|
|
19613
19870
|
sources,
|
|
19614
19871
|
outputRoles
|
|
19615
19872
|
);
|
|
19616
|
-
const out =
|
|
19873
|
+
const out = path40.resolve(manifestDir, style.mount.out);
|
|
19617
19874
|
const format = sheetFormat(args);
|
|
19618
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
19875
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(out) });
|
|
19619
19876
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
19620
19877
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
19621
19878
|
const outputs = [
|
|
@@ -19635,7 +19892,7 @@ async function runMount(args) {
|
|
|
19635
19892
|
await provenanceFile("$lock", args.lock),
|
|
19636
19893
|
...qualityRecords,
|
|
19637
19894
|
...artifactSources.filter(
|
|
19638
|
-
(source) => source.id !== "$base" ||
|
|
19895
|
+
(source) => source.id !== "$base" || path40.resolve(source.path) !== out
|
|
19639
19896
|
)
|
|
19640
19897
|
],
|
|
19641
19898
|
options: {
|
|
@@ -19649,14 +19906,14 @@ async function runMount(args) {
|
|
|
19649
19906
|
log(
|
|
19650
19907
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
19651
19908
|
);
|
|
19652
|
-
log(` ${
|
|
19909
|
+
log(` ${path40.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
19653
19910
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19654
19911
|
}
|
|
19655
19912
|
}
|
|
19656
19913
|
async function runExport(args) {
|
|
19657
19914
|
const { loaded, specs, lock } = await openProject2(args);
|
|
19658
19915
|
const format = args.format ?? "generic";
|
|
19659
|
-
const manifestDir =
|
|
19916
|
+
const manifestDir = path40.dirname(path40.resolve(args.manifest));
|
|
19660
19917
|
const selected = specs.filter((spec) => {
|
|
19661
19918
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
19662
19919
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -19672,12 +19929,12 @@ async function runExport(args) {
|
|
|
19672
19929
|
for (const spec of selected) {
|
|
19673
19930
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
19674
19931
|
const style = loaded.manifest.styles[spec.styleId];
|
|
19675
|
-
const defaultBase =
|
|
19676
|
-
const base = args.out ?
|
|
19932
|
+
const defaultBase = path40.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
19933
|
+
const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
19677
19934
|
const result = exportTileset(entry, spec, {
|
|
19678
19935
|
format,
|
|
19679
19936
|
manifestDir,
|
|
19680
|
-
imageName:
|
|
19937
|
+
imageName: path40.basename(`${base}.png`),
|
|
19681
19938
|
columns: args.columns
|
|
19682
19939
|
});
|
|
19683
19940
|
const outputs = [
|
|
@@ -19695,7 +19952,7 @@ async function runExport(args) {
|
|
|
19695
19952
|
asset: spec.assetId,
|
|
19696
19953
|
columns: args.columns ?? null,
|
|
19697
19954
|
format,
|
|
19698
|
-
image:
|
|
19955
|
+
image: path40.basename(`${base}.png`),
|
|
19699
19956
|
providerRules: result.generic.providerRules,
|
|
19700
19957
|
style: spec.styleId,
|
|
19701
19958
|
tileType: spec.tileType ?? null
|
|
@@ -19705,7 +19962,7 @@ async function runExport(args) {
|
|
|
19705
19962
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
19706
19963
|
);
|
|
19707
19964
|
log(
|
|
19708
|
-
` ${
|
|
19965
|
+
` ${path40.relative(process.cwd(), base)}.png + ${path40.basename(base)}${result.extension} + .pixelkiln.json`
|
|
19709
19966
|
);
|
|
19710
19967
|
}
|
|
19711
19968
|
}
|
|
@@ -19718,22 +19975,22 @@ function sheetFormat(args) {
|
|
|
19718
19975
|
}
|
|
19719
19976
|
|
|
19720
19977
|
// src/cli/commands/pixellab-utils.ts
|
|
19721
|
-
import
|
|
19978
|
+
import path42 from "path";
|
|
19722
19979
|
|
|
19723
19980
|
// src/pixellab-utils.ts
|
|
19724
|
-
import { existsSync as
|
|
19725
|
-
import { mkdir as
|
|
19726
|
-
import
|
|
19981
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19982
|
+
import { mkdir as mkdir13, readFile as readFile27, writeFile as writeFile16 } from "fs/promises";
|
|
19983
|
+
import path41 from "path";
|
|
19727
19984
|
var UNZOOM_MIN_SIDE = 256;
|
|
19728
19985
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
19729
19986
|
function defaultUnzoomOut(input) {
|
|
19730
|
-
const ext =
|
|
19987
|
+
const ext = path41.extname(input);
|
|
19731
19988
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
19732
19989
|
}
|
|
19733
19990
|
async function unzoomFile(client2, input, opts = {}) {
|
|
19734
|
-
const out =
|
|
19735
|
-
if (
|
|
19736
|
-
if (
|
|
19991
|
+
const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
19992
|
+
if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
19993
|
+
if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
19737
19994
|
const bytes = await readFile27(input);
|
|
19738
19995
|
const metadata = imageMetadata(bytes);
|
|
19739
19996
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
@@ -19747,22 +20004,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
19747
20004
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
19748
20005
|
}
|
|
19749
20006
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
19750
|
-
await
|
|
19751
|
-
await
|
|
20007
|
+
await mkdir13(path41.dirname(out), { recursive: true });
|
|
20008
|
+
await writeFile16(out, res.png);
|
|
19752
20009
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
19753
20010
|
}
|
|
19754
20011
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
19755
20012
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
19756
20013
|
var FONT_COST_GENERATIONS = 25;
|
|
19757
20014
|
function fontOutputPaths(out) {
|
|
19758
|
-
const base =
|
|
20015
|
+
const base = path41.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
19759
20016
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
19760
20017
|
}
|
|
19761
20018
|
async function generateFont(client2, opts) {
|
|
19762
20019
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
19763
20020
|
if (!opts.force) {
|
|
19764
20021
|
for (const file of [ttf, atlas]) {
|
|
19765
|
-
if (
|
|
20022
|
+
if (existsSync32(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
19766
20023
|
}
|
|
19767
20024
|
}
|
|
19768
20025
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -19789,9 +20046,9 @@ async function generateFont(client2, opts) {
|
|
|
19789
20046
|
client2.download(job.download_ttf_url),
|
|
19790
20047
|
client2.download(job.download_atlas_url)
|
|
19791
20048
|
]);
|
|
19792
|
-
await
|
|
19793
|
-
await
|
|
19794
|
-
await
|
|
20049
|
+
await mkdir13(path41.dirname(ttf), { recursive: true });
|
|
20050
|
+
await writeFile16(ttf, ttfBytes);
|
|
20051
|
+
await writeFile16(atlas, atlasBytes);
|
|
19795
20052
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
19796
20053
|
}
|
|
19797
20054
|
if (Date.now() - started > timeout) {
|
|
@@ -19805,7 +20062,7 @@ async function generateFont(client2, opts) {
|
|
|
19805
20062
|
|
|
19806
20063
|
// src/cli/commands/pixellab-utils.ts
|
|
19807
20064
|
function loadKeys(args) {
|
|
19808
|
-
loadEnvFiles(
|
|
20065
|
+
loadEnvFiles(path42.dirname(path42.resolve(args.manifest)));
|
|
19809
20066
|
loadEnvFiles(process.cwd());
|
|
19810
20067
|
}
|
|
19811
20068
|
async function runUnzoom(args) {
|
|
@@ -19871,19 +20128,19 @@ async function runFont(args) {
|
|
|
19871
20128
|
}
|
|
19872
20129
|
|
|
19873
20130
|
// src/cli/commands/quality.ts
|
|
19874
|
-
import
|
|
20131
|
+
import path44 from "path";
|
|
19875
20132
|
import { readFile as readFile29 } from "fs/promises";
|
|
19876
20133
|
|
|
19877
20134
|
// src/pipeline/quality-regression.ts
|
|
19878
|
-
import
|
|
19879
|
-
import { existsSync as
|
|
20135
|
+
import path43 from "path";
|
|
20136
|
+
import { existsSync as existsSync33 } from "fs";
|
|
19880
20137
|
import { readFile as readFile28 } from "fs/promises";
|
|
19881
|
-
import { z as
|
|
20138
|
+
import { z as z12 } from "zod";
|
|
19882
20139
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19883
20140
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
19884
20141
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
19885
|
-
var QualityPathSchema =
|
|
19886
|
-
(value) => !
|
|
20142
|
+
var QualityPathSchema = z12.string().min(1).refine(
|
|
20143
|
+
(value) => !path43.posix.isAbsolute(value) && !value.includes("\\"),
|
|
19887
20144
|
"expected a portable relative path"
|
|
19888
20145
|
);
|
|
19889
20146
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -19896,53 +20153,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
19896
20153
|
maxEdgeContrastDrop: 0.03,
|
|
19897
20154
|
maxIsolatedPixelIncrease: 5e-3
|
|
19898
20155
|
};
|
|
19899
|
-
var QualityToleranceSchema =
|
|
19900
|
-
requireExactHash:
|
|
19901
|
-
maxColorCountIncrease:
|
|
19902
|
-
maxNewColors:
|
|
19903
|
-
maxTransparencyDelta:
|
|
19904
|
-
maxPartialAlphaIncrease:
|
|
19905
|
-
maxEdgeDensityDelta:
|
|
19906
|
-
maxEdgeContrastDrop:
|
|
19907
|
-
maxIsolatedPixelIncrease:
|
|
20156
|
+
var QualityToleranceSchema = z12.object({
|
|
20157
|
+
requireExactHash: z12.boolean(),
|
|
20158
|
+
maxColorCountIncrease: z12.number().int().min(0),
|
|
20159
|
+
maxNewColors: z12.number().int().min(0),
|
|
20160
|
+
maxTransparencyDelta: z12.number().min(0).max(1),
|
|
20161
|
+
maxPartialAlphaIncrease: z12.number().min(0).max(1),
|
|
20162
|
+
maxEdgeDensityDelta: z12.number().min(0).max(1),
|
|
20163
|
+
maxEdgeContrastDrop: z12.number().min(0).max(1),
|
|
20164
|
+
maxIsolatedPixelIncrease: z12.number().min(0).max(1)
|
|
19908
20165
|
}).strict();
|
|
19909
|
-
var ImageQualityMetricsSchema =
|
|
19910
|
-
sha256:
|
|
19911
|
-
width:
|
|
19912
|
-
height:
|
|
19913
|
-
colorCount:
|
|
19914
|
-
colors:
|
|
19915
|
-
transparency:
|
|
19916
|
-
partialAlpha:
|
|
19917
|
-
edgeDensity:
|
|
19918
|
-
meanEdgeContrast:
|
|
19919
|
-
isolatedPixelRatio:
|
|
20166
|
+
var ImageQualityMetricsSchema = z12.object({
|
|
20167
|
+
sha256: z12.string().regex(SHA256_RE),
|
|
20168
|
+
width: z12.number().int().min(1),
|
|
20169
|
+
height: z12.number().int().min(1),
|
|
20170
|
+
colorCount: z12.number().int().min(0),
|
|
20171
|
+
colors: z12.array(z12.string().regex(HEX_RE)),
|
|
20172
|
+
transparency: z12.number().min(0).max(1),
|
|
20173
|
+
partialAlpha: z12.number().min(0).max(1),
|
|
20174
|
+
edgeDensity: z12.number().min(0).max(1),
|
|
20175
|
+
meanEdgeContrast: z12.number().min(0).max(1),
|
|
20176
|
+
isolatedPixelRatio: z12.number().min(0).max(1)
|
|
19920
20177
|
}).strict();
|
|
19921
|
-
var QualityBaselineCaseSchema =
|
|
19922
|
-
id:
|
|
20178
|
+
var QualityBaselineCaseSchema = z12.object({
|
|
20179
|
+
id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
19923
20180
|
file: QualityPathSchema,
|
|
19924
|
-
record:
|
|
20181
|
+
record: z12.object({
|
|
19925
20182
|
path: QualityPathSchema,
|
|
19926
|
-
sha256:
|
|
20183
|
+
sha256: z12.string().regex(SHA256_RE)
|
|
19927
20184
|
}).strict().optional(),
|
|
19928
20185
|
expected: ImageQualityMetricsSchema,
|
|
19929
20186
|
tolerances: QualityToleranceSchema
|
|
19930
20187
|
}).strict();
|
|
19931
|
-
var QualityBaselineSchema =
|
|
19932
|
-
$schema:
|
|
19933
|
-
format:
|
|
19934
|
-
schemaVersion:
|
|
19935
|
-
cases:
|
|
20188
|
+
var QualityBaselineSchema = z12.object({
|
|
20189
|
+
$schema: z12.string().url().optional(),
|
|
20190
|
+
format: z12.literal("pixelkiln-quality-baseline"),
|
|
20191
|
+
schemaVersion: z12.literal(1),
|
|
20192
|
+
cases: z12.array(QualityBaselineCaseSchema).min(1)
|
|
19936
20193
|
}).strict().superRefine((baseline, context) => {
|
|
19937
20194
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
19938
20195
|
if (new Set(ids).size !== ids.length) {
|
|
19939
|
-
context.addIssue({ code:
|
|
20196
|
+
context.addIssue({ code: z12.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
19940
20197
|
}
|
|
19941
20198
|
});
|
|
19942
|
-
var QualityInputSchema =
|
|
19943
|
-
id:
|
|
19944
|
-
path:
|
|
19945
|
-
record:
|
|
20199
|
+
var QualityInputSchema = z12.object({
|
|
20200
|
+
id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20201
|
+
path: z12.string().min(1),
|
|
20202
|
+
record: z12.string().min(1).optional(),
|
|
19946
20203
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
19947
20204
|
}).strict();
|
|
19948
20205
|
function message4(error) {
|
|
@@ -20030,7 +20287,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20030
20287
|
};
|
|
20031
20288
|
}
|
|
20032
20289
|
async function measureImageQuality(file) {
|
|
20033
|
-
const absolute =
|
|
20290
|
+
const absolute = path43.resolve(file);
|
|
20034
20291
|
let bytes;
|
|
20035
20292
|
try {
|
|
20036
20293
|
bytes = await readFile28(absolute);
|
|
@@ -20044,7 +20301,7 @@ async function measureImageQuality(file) {
|
|
|
20044
20301
|
}
|
|
20045
20302
|
}
|
|
20046
20303
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
20047
|
-
const parsed =
|
|
20304
|
+
const parsed = z12.array(QualityInputSchema).min(1).safeParse(raw);
|
|
20048
20305
|
if (!parsed.success) {
|
|
20049
20306
|
throw new Error(
|
|
20050
20307
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -20052,11 +20309,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20052
20309
|
}
|
|
20053
20310
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20054
20311
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20055
|
-
const root =
|
|
20312
|
+
const root = path43.dirname(path43.resolve(inputsFilePath));
|
|
20056
20313
|
return parsed.data.map((entry) => ({
|
|
20057
20314
|
id: entry.id,
|
|
20058
|
-
file:
|
|
20059
|
-
...entry.record ? { record:
|
|
20315
|
+
file: path43.resolve(root, entry.path),
|
|
20316
|
+
...entry.record ? { record: path43.resolve(root, entry.record) } : {},
|
|
20060
20317
|
tolerances: QualityToleranceSchema.parse({
|
|
20061
20318
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20062
20319
|
...entry.tolerances
|
|
@@ -20064,7 +20321,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20064
20321
|
}));
|
|
20065
20322
|
}
|
|
20066
20323
|
function portableRelative2(from, to) {
|
|
20067
|
-
return
|
|
20324
|
+
return path43.relative(from, path43.resolve(to)).split(path43.sep).join("/") || ".";
|
|
20068
20325
|
}
|
|
20069
20326
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20070
20327
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20079,20 +20336,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20079
20336
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20080
20337
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20081
20338
|
}
|
|
20082
|
-
const outputs = record.outputs.map((output) =>
|
|
20083
|
-
if (!outputs.includes(
|
|
20339
|
+
const outputs = record.outputs.map((output) => path43.resolve(path43.dirname(recordPath), output.path));
|
|
20340
|
+
if (!outputs.includes(path43.resolve(imagePath))) {
|
|
20084
20341
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20085
20342
|
}
|
|
20086
20343
|
}
|
|
20087
20344
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
20088
20345
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
20089
|
-
const absolute =
|
|
20346
|
+
const absolute = path43.resolve(baselinePath);
|
|
20090
20347
|
for (const input of inputs) {
|
|
20091
|
-
if (
|
|
20348
|
+
if (path43.resolve(input.file) === absolute || input.record && path43.resolve(input.record) === absolute) {
|
|
20092
20349
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
20093
20350
|
}
|
|
20094
20351
|
}
|
|
20095
|
-
const root =
|
|
20352
|
+
const root = path43.dirname(absolute);
|
|
20096
20353
|
const cases = [];
|
|
20097
20354
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
20098
20355
|
const expected = await measureImageQuality(input.file);
|
|
@@ -20119,7 +20376,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20119
20376
|
cases
|
|
20120
20377
|
});
|
|
20121
20378
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
20122
|
-
if (
|
|
20379
|
+
if (existsSync33(absolute) && !options.force) {
|
|
20123
20380
|
const current = await readFile28(absolute);
|
|
20124
20381
|
if (!current.equals(data)) {
|
|
20125
20382
|
throw new OverwriteRefusedError(
|
|
@@ -20132,7 +20389,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20132
20389
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
20133
20390
|
}
|
|
20134
20391
|
async function readQualityBaseline(baselinePath) {
|
|
20135
|
-
const absolute =
|
|
20392
|
+
const absolute = path43.resolve(baselinePath);
|
|
20136
20393
|
let raw;
|
|
20137
20394
|
try {
|
|
20138
20395
|
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
@@ -20205,12 +20462,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
20205
20462
|
return { violations, warnings };
|
|
20206
20463
|
}
|
|
20207
20464
|
async function checkQualityBaseline(baselinePath) {
|
|
20208
|
-
const absolute =
|
|
20465
|
+
const absolute = path43.resolve(baselinePath);
|
|
20209
20466
|
const baseline = await readQualityBaseline(absolute);
|
|
20210
|
-
const root =
|
|
20467
|
+
const root = path43.dirname(absolute);
|
|
20211
20468
|
const cases = [];
|
|
20212
20469
|
for (const entry of baseline.cases) {
|
|
20213
|
-
const file =
|
|
20470
|
+
const file = path43.resolve(root, entry.file);
|
|
20214
20471
|
let actual = null;
|
|
20215
20472
|
const violations = [];
|
|
20216
20473
|
const warnings = [];
|
|
@@ -20223,7 +20480,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
20223
20480
|
violations.push(message4(error));
|
|
20224
20481
|
}
|
|
20225
20482
|
if (entry.record) {
|
|
20226
|
-
const recordPath =
|
|
20483
|
+
const recordPath = path43.resolve(root, entry.record.path);
|
|
20227
20484
|
try {
|
|
20228
20485
|
const recordHash = await sha256File(recordPath);
|
|
20229
20486
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -20265,15 +20522,15 @@ async function runQuality(args) {
|
|
|
20265
20522
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
20266
20523
|
let raw;
|
|
20267
20524
|
try {
|
|
20268
|
-
raw = JSON.parse(await readFile29(
|
|
20525
|
+
raw = JSON.parse(await readFile29(path44.resolve(args.inputs), "utf8"));
|
|
20269
20526
|
} catch (error) {
|
|
20270
20527
|
throw new Error(
|
|
20271
|
-
`Could not read quality inputs ${
|
|
20528
|
+
`Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
20272
20529
|
{ cause: error }
|
|
20273
20530
|
);
|
|
20274
20531
|
}
|
|
20275
20532
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
20276
|
-
if (
|
|
20533
|
+
if (path44.resolve(args.inputs) === path44.resolve(args.out)) {
|
|
20277
20534
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
20278
20535
|
}
|
|
20279
20536
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -20286,7 +20543,7 @@ async function runQuality(args) {
|
|
|
20286
20543
|
}, null, 2));
|
|
20287
20544
|
} else {
|
|
20288
20545
|
log(
|
|
20289
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
20546
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path44.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20290
20547
|
);
|
|
20291
20548
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20292
20549
|
}
|
|
@@ -20312,54 +20569,54 @@ async function runQuality(args) {
|
|
|
20312
20569
|
}
|
|
20313
20570
|
|
|
20314
20571
|
// src/cli/commands/recipe.ts
|
|
20315
|
-
import
|
|
20572
|
+
import path46 from "path";
|
|
20316
20573
|
|
|
20317
20574
|
// src/recipes.ts
|
|
20318
|
-
import
|
|
20319
|
-
import { existsSync as
|
|
20575
|
+
import path45 from "path";
|
|
20576
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20320
20577
|
import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
|
|
20321
20578
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20322
|
-
import { z as
|
|
20579
|
+
import { z as z13 } from "zod";
|
|
20323
20580
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20324
20581
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20325
20582
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20326
20583
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20327
20584
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20328
|
-
var PortablePathSchema =
|
|
20329
|
-
if (value.includes("\\") ||
|
|
20585
|
+
var PortablePathSchema = z13.string().min(1).refine((value) => {
|
|
20586
|
+
if (value.includes("\\") || path45.posix.isAbsolute(value)) return false;
|
|
20330
20587
|
const parts = value.split("/");
|
|
20331
20588
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20332
20589
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20333
|
-
var RecipeBindingSchema =
|
|
20334
|
-
nodeId:
|
|
20335
|
-
input:
|
|
20590
|
+
var RecipeBindingSchema = z13.object({
|
|
20591
|
+
nodeId: z13.string().min(1),
|
|
20592
|
+
input: z13.string().min(1)
|
|
20336
20593
|
}).strict();
|
|
20337
|
-
var RecipeSchema =
|
|
20338
|
-
$schema:
|
|
20339
|
-
format:
|
|
20340
|
-
schemaVersion:
|
|
20341
|
-
id:
|
|
20342
|
-
version:
|
|
20343
|
-
provider:
|
|
20344
|
-
summary:
|
|
20345
|
-
files:
|
|
20594
|
+
var RecipeSchema = z13.object({
|
|
20595
|
+
$schema: z13.string().url().optional(),
|
|
20596
|
+
format: z13.literal("pixelkiln-recipe"),
|
|
20597
|
+
schemaVersion: z13.literal(1),
|
|
20598
|
+
id: z13.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
20599
|
+
version: z13.string().regex(VERSION_RE, "expected x.y.z"),
|
|
20600
|
+
provider: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20601
|
+
summary: z13.string().min(1).max(240),
|
|
20602
|
+
files: z13.array(z13.object({
|
|
20346
20603
|
path: PortablePathSchema,
|
|
20347
|
-
role:
|
|
20348
|
-
sha256:
|
|
20604
|
+
role: z13.enum(["workflow", "reference"]),
|
|
20605
|
+
sha256: z13.string().regex(SHA256_RE2)
|
|
20349
20606
|
}).strict()).default([]),
|
|
20350
|
-
models:
|
|
20607
|
+
models: z13.array(z13.object({
|
|
20351
20608
|
path: PortablePathSchema,
|
|
20352
|
-
sha256:
|
|
20353
|
-
source:
|
|
20354
|
-
license:
|
|
20609
|
+
sha256: z13.string().regex(SHA256_RE2),
|
|
20610
|
+
source: z13.string().url(),
|
|
20611
|
+
license: z13.string().min(1)
|
|
20355
20612
|
}).strict()).default([]),
|
|
20356
|
-
styleId:
|
|
20613
|
+
styleId: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20357
20614
|
style: StyleSchema,
|
|
20358
|
-
workflow:
|
|
20615
|
+
workflow: z13.object({
|
|
20359
20616
|
path: PortablePathSchema,
|
|
20360
|
-
outputNodeId:
|
|
20361
|
-
numImages:
|
|
20362
|
-
bindings:
|
|
20617
|
+
outputNodeId: z13.string().min(1),
|
|
20618
|
+
numImages: z13.number().int().min(1).max(16),
|
|
20619
|
+
bindings: z13.object({
|
|
20363
20620
|
prompt: RecipeBindingSchema,
|
|
20364
20621
|
width: RecipeBindingSchema.optional(),
|
|
20365
20622
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20370,67 +20627,67 @@ var RecipeSchema = z12.object({
|
|
|
20370
20627
|
strength: RecipeBindingSchema.optional()
|
|
20371
20628
|
}).strict()
|
|
20372
20629
|
}).strict().optional(),
|
|
20373
|
-
quality:
|
|
20374
|
-
stage:
|
|
20375
|
-
workingCanvas:
|
|
20376
|
-
width:
|
|
20377
|
-
height:
|
|
20630
|
+
quality: z13.object({
|
|
20631
|
+
stage: z13.enum(["composition-source", "production-candidate"]),
|
|
20632
|
+
workingCanvas: z13.object({
|
|
20633
|
+
width: z13.number().int().min(16),
|
|
20634
|
+
height: z13.number().int().min(16)
|
|
20378
20635
|
}).strict(),
|
|
20379
|
-
recommendedNativeSize:
|
|
20380
|
-
min:
|
|
20381
|
-
max:
|
|
20636
|
+
recommendedNativeSize: z13.object({
|
|
20637
|
+
min: z13.number().int().min(1),
|
|
20638
|
+
max: z13.number().int().min(1)
|
|
20382
20639
|
}).strict(),
|
|
20383
|
-
paletteColors:
|
|
20384
|
-
min:
|
|
20385
|
-
max:
|
|
20640
|
+
paletteColors: z13.object({
|
|
20641
|
+
min: z13.number().int().min(1),
|
|
20642
|
+
max: z13.number().int().min(1)
|
|
20386
20643
|
}).strict(),
|
|
20387
|
-
background:
|
|
20388
|
-
checks:
|
|
20644
|
+
background: z13.enum(["opaque", "transparent", "full-bleed"]),
|
|
20645
|
+
checks: z13.array(z13.enum([
|
|
20389
20646
|
"prompt-coverage",
|
|
20390
20647
|
"native-grid",
|
|
20391
20648
|
"final-palette",
|
|
20392
20649
|
"alpha-edge",
|
|
20393
20650
|
"human-1x"
|
|
20394
20651
|
])).min(1),
|
|
20395
|
-
notes:
|
|
20652
|
+
notes: z13.array(z13.string().min(1)).default([])
|
|
20396
20653
|
}).strict(),
|
|
20397
|
-
integrity:
|
|
20398
|
-
algorithm:
|
|
20399
|
-
digest:
|
|
20654
|
+
integrity: z13.object({
|
|
20655
|
+
algorithm: z13.literal("sha256"),
|
|
20656
|
+
digest: z13.string().regex(SHA256_RE2)
|
|
20400
20657
|
}).strict()
|
|
20401
20658
|
}).strict().superRefine((recipe, context) => {
|
|
20402
20659
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20403
20660
|
context.addIssue({
|
|
20404
|
-
code:
|
|
20661
|
+
code: z13.ZodIssueCode.custom,
|
|
20405
20662
|
path: ["style", "provider"],
|
|
20406
20663
|
message: "must match the recipe provider"
|
|
20407
20664
|
});
|
|
20408
20665
|
}
|
|
20409
20666
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20410
20667
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20411
|
-
context.addIssue({ code:
|
|
20668
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20412
20669
|
}
|
|
20413
20670
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20414
20671
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20415
|
-
context.addIssue({ code:
|
|
20672
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20416
20673
|
}
|
|
20417
20674
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20418
20675
|
context.addIssue({
|
|
20419
|
-
code:
|
|
20676
|
+
code: z13.ZodIssueCode.custom,
|
|
20420
20677
|
path: ["files"],
|
|
20421
20678
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20422
20679
|
});
|
|
20423
20680
|
}
|
|
20424
20681
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20425
20682
|
context.addIssue({
|
|
20426
|
-
code:
|
|
20683
|
+
code: z13.ZodIssueCode.custom,
|
|
20427
20684
|
path: ["quality", "recommendedNativeSize"],
|
|
20428
20685
|
message: "min cannot exceed max"
|
|
20429
20686
|
});
|
|
20430
20687
|
}
|
|
20431
20688
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20432
20689
|
context.addIssue({
|
|
20433
|
-
code:
|
|
20690
|
+
code: z13.ZodIssueCode.custom,
|
|
20434
20691
|
path: ["quality", "paletteColors"],
|
|
20435
20692
|
message: "min cannot exceed max"
|
|
20436
20693
|
});
|
|
@@ -20439,7 +20696,7 @@ var RecipeSchema = z12.object({
|
|
|
20439
20696
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20440
20697
|
)) {
|
|
20441
20698
|
context.addIssue({
|
|
20442
|
-
code:
|
|
20699
|
+
code: z13.ZodIssueCode.custom,
|
|
20443
20700
|
path: ["workflow", "path"],
|
|
20444
20701
|
message: "must name a file whose role is workflow"
|
|
20445
20702
|
});
|
|
@@ -20447,7 +20704,7 @@ var RecipeSchema = z12.object({
|
|
|
20447
20704
|
if (recipe.provider === "comfyui") {
|
|
20448
20705
|
if (!recipe.workflow) {
|
|
20449
20706
|
context.addIssue({
|
|
20450
|
-
code:
|
|
20707
|
+
code: z13.ZodIssueCode.custom,
|
|
20451
20708
|
path: ["workflow"],
|
|
20452
20709
|
message: "ComfyUI recipes require workflow metadata"
|
|
20453
20710
|
});
|
|
@@ -20456,7 +20713,7 @@ var RecipeSchema = z12.object({
|
|
|
20456
20713
|
const options = recipe.style.providerOptions.comfyui;
|
|
20457
20714
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20458
20715
|
context.addIssue({
|
|
20459
|
-
code:
|
|
20716
|
+
code: z13.ZodIssueCode.custom,
|
|
20460
20717
|
path: ["style", "providerOptions", "comfyui"],
|
|
20461
20718
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20462
20719
|
});
|
|
@@ -20470,7 +20727,7 @@ var RecipeSchema = z12.object({
|
|
|
20470
20727
|
};
|
|
20471
20728
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20472
20729
|
context.addIssue({
|
|
20473
|
-
code:
|
|
20730
|
+
code: z13.ZodIssueCode.custom,
|
|
20474
20731
|
path: ["style", "providerOptions", "comfyui"],
|
|
20475
20732
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20476
20733
|
});
|
|
@@ -20502,7 +20759,7 @@ function message5(error) {
|
|
|
20502
20759
|
return error instanceof Error ? error.message : String(error);
|
|
20503
20760
|
}
|
|
20504
20761
|
async function readRecipeFile(recipePath, bundled) {
|
|
20505
|
-
const absolute =
|
|
20762
|
+
const absolute = path45.resolve(recipePath);
|
|
20506
20763
|
let raw;
|
|
20507
20764
|
try {
|
|
20508
20765
|
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
@@ -20516,13 +20773,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20516
20773
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20517
20774
|
);
|
|
20518
20775
|
}
|
|
20519
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
20776
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path45.dirname(absolute), bundled };
|
|
20520
20777
|
}
|
|
20521
20778
|
async function walkRecipeFiles(root) {
|
|
20522
|
-
if (!
|
|
20779
|
+
if (!existsSync34(root)) return [];
|
|
20523
20780
|
const out = [];
|
|
20524
20781
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20525
|
-
const resolved =
|
|
20782
|
+
const resolved = path45.join(root, entry.name);
|
|
20526
20783
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20527
20784
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20528
20785
|
}
|
|
@@ -20533,7 +20790,7 @@ function bundledRecipeRoot() {
|
|
|
20533
20790
|
}
|
|
20534
20791
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
20535
20792
|
const recipes = await Promise.all(
|
|
20536
|
-
(await walkRecipeFiles(
|
|
20793
|
+
(await walkRecipeFiles(path45.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
20537
20794
|
);
|
|
20538
20795
|
return recipes.sort(
|
|
20539
20796
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -20556,9 +20813,9 @@ function parseSelector(selector) {
|
|
|
20556
20813
|
return { id: match[1], version: match[2] };
|
|
20557
20814
|
}
|
|
20558
20815
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
20559
|
-
const local =
|
|
20560
|
-
if (
|
|
20561
|
-
const recipePath =
|
|
20816
|
+
const local = path45.resolve(target);
|
|
20817
|
+
if (existsSync34(local)) {
|
|
20818
|
+
const recipePath = path45.basename(local) === RECIPE_FILE ? local : path45.join(local, RECIPE_FILE);
|
|
20562
20819
|
return readRecipeFile(recipePath, false);
|
|
20563
20820
|
}
|
|
20564
20821
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -20572,8 +20829,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
20572
20829
|
return matches[0];
|
|
20573
20830
|
}
|
|
20574
20831
|
async function verifyFile(root, file) {
|
|
20575
|
-
const absolute =
|
|
20576
|
-
if (!
|
|
20832
|
+
const absolute = path45.resolve(root, ...file.path.split("/"));
|
|
20833
|
+
if (!existsSync34(absolute)) {
|
|
20577
20834
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
20578
20835
|
}
|
|
20579
20836
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -20588,7 +20845,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20588
20845
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20589
20846
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
20590
20847
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
20591
|
-
const modelRoot = options.modelRoot ?
|
|
20848
|
+
const modelRoot = options.modelRoot ? path45.resolve(options.modelRoot) : null;
|
|
20592
20849
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
20593
20850
|
...await verifyFile(modelRoot, model),
|
|
20594
20851
|
source: model.source,
|
|
@@ -20622,7 +20879,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
20622
20879
|
};
|
|
20623
20880
|
}
|
|
20624
20881
|
function renderStyle(style, destination, cwd) {
|
|
20625
|
-
const portableDir =
|
|
20882
|
+
const portableDir = path45.relative(cwd, destination).split(path45.sep).join("/") || ".";
|
|
20626
20883
|
const replace = (value) => {
|
|
20627
20884
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
20628
20885
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -20634,24 +20891,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
20634
20891
|
return replace(style);
|
|
20635
20892
|
}
|
|
20636
20893
|
async function installRecipe(target, options = {}) {
|
|
20637
|
-
const cwd =
|
|
20894
|
+
const cwd = path45.resolve(options.cwd ?? process.cwd());
|
|
20638
20895
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
20639
20896
|
const verification = await verifyRecipe(loaded.path);
|
|
20640
20897
|
if (!verification.ok) {
|
|
20641
20898
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
20642
20899
|
}
|
|
20643
|
-
const destination = options.out ?
|
|
20900
|
+
const destination = options.out ? path45.resolve(cwd, options.out) : path45.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
20644
20901
|
const sources = [
|
|
20645
20902
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
20646
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
20903
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path45.join(loaded.dir, ...file.path.split("/")) }))
|
|
20647
20904
|
];
|
|
20648
20905
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
20649
|
-
path:
|
|
20906
|
+
path: path45.join(destination, ...file.path.split("/")),
|
|
20650
20907
|
data: await readFile30(file.source)
|
|
20651
20908
|
})));
|
|
20652
20909
|
if (!options.force) {
|
|
20653
20910
|
for (const file of files) {
|
|
20654
|
-
if (!
|
|
20911
|
+
if (!existsSync34(file.path)) continue;
|
|
20655
20912
|
const current = await readFile30(file.path);
|
|
20656
20913
|
if (!current.equals(file.data)) {
|
|
20657
20914
|
throw new OverwriteRefusedError(
|
|
@@ -20746,14 +21003,14 @@ async function runRecipe(args) {
|
|
|
20746
21003
|
}, null, 2));
|
|
20747
21004
|
} else {
|
|
20748
21005
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
20749
|
-
log(` ${
|
|
21006
|
+
log(` ${path46.relative(process.cwd(), result.destination) || "."}`);
|
|
20750
21007
|
log(`
|
|
20751
21008
|
Add this entry under your manifest's styles object:`);
|
|
20752
21009
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
20753
21010
|
if (result.recipe.models.length) {
|
|
20754
21011
|
log(`
|
|
20755
21012
|
Models are not downloaded automatically. Verify them with:`);
|
|
20756
|
-
log(` pixelkiln recipe verify ${
|
|
21013
|
+
log(` pixelkiln recipe verify ${path46.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
20757
21014
|
}
|
|
20758
21015
|
}
|
|
20759
21016
|
return;
|
|
@@ -20761,7 +21018,7 @@ async function runRecipe(args) {
|
|
|
20761
21018
|
}
|
|
20762
21019
|
|
|
20763
21020
|
// src/cli/commands/refine.ts
|
|
20764
|
-
import
|
|
21021
|
+
import path47 from "path";
|
|
20765
21022
|
async function runRefine(args) {
|
|
20766
21023
|
if (args.from || args.subcommand === "approve") {
|
|
20767
21024
|
if (!args.from) {
|
|
@@ -20791,12 +21048,12 @@ async function runRefine(args) {
|
|
|
20791
21048
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
20792
21049
|
);
|
|
20793
21050
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
20794
|
-
log(` wrote ${
|
|
20795
|
-
log(` quality record: ${
|
|
21051
|
+
log(` wrote ${path47.relative(process.cwd(), result3.output)}`);
|
|
21052
|
+
log(` quality record: ${path47.relative(process.cwd(), result3.record)}`);
|
|
20796
21053
|
log(`
|
|
20797
21054
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
20798
21055
|
log(
|
|
20799
|
-
` pixelkiln refine approve --from ${
|
|
21056
|
+
` pixelkiln refine approve --from ${path47.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
20800
21057
|
);
|
|
20801
21058
|
}
|
|
20802
21059
|
return;
|
|
@@ -20824,7 +21081,7 @@ async function runRefine(args) {
|
|
|
20824
21081
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
20825
21082
|
else {
|
|
20826
21083
|
log(
|
|
20827
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
21084
|
+
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()}`
|
|
20828
21085
|
);
|
|
20829
21086
|
}
|
|
20830
21087
|
return;
|
|
@@ -20833,7 +21090,7 @@ async function runRefine(args) {
|
|
|
20833
21090
|
if (args.json) {
|
|
20834
21091
|
log(JSON.stringify(result2, null, 2));
|
|
20835
21092
|
} else {
|
|
20836
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
21093
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path47.relative(process.cwd(), result2.output || result2.record)}`);
|
|
20837
21094
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
20838
21095
|
}
|
|
20839
21096
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -20879,7 +21136,7 @@ async function runRefine(args) {
|
|
|
20879
21136
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
20880
21137
|
if (item.state === "needs-approval") {
|
|
20881
21138
|
log(
|
|
20882
|
-
` pixelkiln refine approve --from ${
|
|
21139
|
+
` pixelkiln refine approve --from ${path47.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
20883
21140
|
);
|
|
20884
21141
|
}
|
|
20885
21142
|
}
|
|
@@ -20890,9 +21147,9 @@ async function runRefine(args) {
|
|
|
20890
21147
|
}
|
|
20891
21148
|
|
|
20892
21149
|
// src/cli/commands/skeleton.ts
|
|
20893
|
-
import
|
|
20894
|
-
import { existsSync as
|
|
20895
|
-
import { readFile as readFile31, writeFile as
|
|
21150
|
+
import path48 from "path";
|
|
21151
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21152
|
+
import { readFile as readFile31, writeFile as writeFile17 } from "fs/promises";
|
|
20896
21153
|
|
|
20897
21154
|
// src/skeleton-preview.ts
|
|
20898
21155
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -21003,12 +21260,12 @@ var Canvas = class {
|
|
|
21003
21260
|
async function runEstimateSkeleton(args) {
|
|
21004
21261
|
const image = args.target;
|
|
21005
21262
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
21006
|
-
if (args.out &&
|
|
21263
|
+
if (args.out && existsSync35(path48.resolve(args.out)) && !args.force) {
|
|
21007
21264
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
21008
21265
|
}
|
|
21009
|
-
loadEnvFiles(
|
|
21266
|
+
loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
|
|
21010
21267
|
loadEnvFiles(process.cwd());
|
|
21011
|
-
const bytes = await readFile31(
|
|
21268
|
+
const bytes = await readFile31(path48.resolve(image));
|
|
21012
21269
|
const metadata = imageMetadata(bytes);
|
|
21013
21270
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21014
21271
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21020,7 +21277,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21020
21277
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21021
21278
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21022
21279
|
if (args.out) {
|
|
21023
|
-
await
|
|
21280
|
+
await writeFile17(path48.resolve(args.out), json + "\n");
|
|
21024
21281
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21025
21282
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21026
21283
|
} else {
|
|
@@ -21033,8 +21290,8 @@ async function runSkeletonPreview(args) {
|
|
|
21033
21290
|
let sourceFile;
|
|
21034
21291
|
let label;
|
|
21035
21292
|
if (target.endsWith(".json")) {
|
|
21036
|
-
keypointsFile =
|
|
21037
|
-
sourceFile = args.from ?
|
|
21293
|
+
keypointsFile = path48.resolve(target);
|
|
21294
|
+
sourceFile = args.from ? path48.resolve(args.from) : void 0;
|
|
21038
21295
|
label = target;
|
|
21039
21296
|
} else {
|
|
21040
21297
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21046,10 +21303,10 @@ async function runSkeletonPreview(args) {
|
|
|
21046
21303
|
);
|
|
21047
21304
|
}
|
|
21048
21305
|
keypointsFile = spec.revision.keypointsFile;
|
|
21049
|
-
sourceFile = args.from ?
|
|
21306
|
+
sourceFile = args.from ? path48.resolve(args.from) : spec.revision.sourceFile;
|
|
21050
21307
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21051
21308
|
}
|
|
21052
|
-
if (!
|
|
21309
|
+
if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21053
21310
|
let json;
|
|
21054
21311
|
try {
|
|
21055
21312
|
json = JSON.parse(await readFile31(keypointsFile, "utf8"));
|
|
@@ -21059,7 +21316,7 @@ async function runSkeletonPreview(args) {
|
|
|
21059
21316
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21060
21317
|
let source;
|
|
21061
21318
|
if (sourceFile) {
|
|
21062
|
-
if (!
|
|
21319
|
+
if (!existsSync35(sourceFile)) {
|
|
21063
21320
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21064
21321
|
} else {
|
|
21065
21322
|
const bytes = await readFile31(sourceFile);
|
|
@@ -21067,10 +21324,10 @@ async function runSkeletonPreview(args) {
|
|
|
21067
21324
|
source = decodePng(bytes);
|
|
21068
21325
|
}
|
|
21069
21326
|
}
|
|
21070
|
-
const out =
|
|
21071
|
-
await
|
|
21327
|
+
const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21328
|
+
await writeFile17(out, renderSkeletonSheet(set, { source }));
|
|
21072
21329
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21073
|
-
const shown =
|
|
21330
|
+
const shown = path48.relative(process.cwd(), out);
|
|
21074
21331
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21075
21332
|
}
|
|
21076
21333
|
|
|
@@ -21116,15 +21373,15 @@ async function runTools(args) {
|
|
|
21116
21373
|
}
|
|
21117
21374
|
|
|
21118
21375
|
// src/cli/commands/workspace.ts
|
|
21119
|
-
import
|
|
21120
|
-
import { existsSync as
|
|
21376
|
+
import path49 from "path";
|
|
21377
|
+
import { existsSync as existsSync36 } from "fs";
|
|
21121
21378
|
async function runWorkspace(args) {
|
|
21122
|
-
const workspacePath =
|
|
21123
|
-
const dir =
|
|
21379
|
+
const workspacePath = path49.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21380
|
+
const dir = path49.dirname(workspacePath);
|
|
21124
21381
|
if (args.subcommand === "add") {
|
|
21125
|
-
const manifestPath =
|
|
21382
|
+
const manifestPath = path49.resolve(args.target);
|
|
21126
21383
|
const loadedTarget = await loadManifest(manifestPath);
|
|
21127
|
-
const lockPath = args.explicitLock ?
|
|
21384
|
+
const lockPath = args.explicitLock ? path49.resolve(args.explicitLock) : path49.join(path49.dirname(manifestPath), "pixelkiln.lock.json");
|
|
21128
21385
|
const ws = await loadWorkspace(workspacePath);
|
|
21129
21386
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
21130
21387
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -21144,10 +21401,10 @@ async function runWorkspace(args) {
|
|
|
21144
21401
|
...args.account ? { account: args.account } : {}
|
|
21145
21402
|
};
|
|
21146
21403
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
21147
|
-
log(` registered "${id}" in ${
|
|
21404
|
+
log(` registered "${id}" in ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21148
21405
|
log(` manifest: ${project.manifest}`);
|
|
21149
21406
|
log(` lock: ${project.lock}`);
|
|
21150
|
-
if (!
|
|
21407
|
+
if (!existsSync36(lockPath)) {
|
|
21151
21408
|
log(
|
|
21152
21409
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
21153
21410
|
);
|
|
@@ -21156,7 +21413,7 @@ async function runWorkspace(args) {
|
|
|
21156
21413
|
}
|
|
21157
21414
|
if (args.subcommand === "remove") {
|
|
21158
21415
|
const ws = await loadWorkspace(workspacePath);
|
|
21159
|
-
const resolvedTarget =
|
|
21416
|
+
const resolvedTarget = path49.resolve(args.target);
|
|
21160
21417
|
const match = ws.projects.find(
|
|
21161
21418
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
21162
21419
|
);
|
|
@@ -21167,11 +21424,11 @@ async function runWorkspace(args) {
|
|
|
21167
21424
|
version: 1,
|
|
21168
21425
|
projects: ws.projects.filter((p) => p !== match)
|
|
21169
21426
|
});
|
|
21170
|
-
log(` removed "${match.id}" from ${
|
|
21427
|
+
log(` removed "${match.id}" from ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21171
21428
|
return;
|
|
21172
21429
|
}
|
|
21173
21430
|
if (args.subcommand === "list") {
|
|
21174
|
-
if (!
|
|
21431
|
+
if (!existsSync36(workspacePath)) {
|
|
21175
21432
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21176
21433
|
}
|
|
21177
21434
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21179,9 +21436,9 @@ async function runWorkspace(args) {
|
|
|
21179
21436
|
if (args.json) {
|
|
21180
21437
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
21181
21438
|
} else if (!ws.projects.length) {
|
|
21182
|
-
log(` no projects registered in ${
|
|
21439
|
+
log(` no projects registered in ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21183
21440
|
} else {
|
|
21184
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21441
|
+
log(` ${ws.projects.length} project(s) in ${path49.relative(process.cwd(), workspacePath)}:`);
|
|
21185
21442
|
for (const p of ws.projects) {
|
|
21186
21443
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
21187
21444
|
}
|
|
@@ -21191,7 +21448,7 @@ async function runWorkspace(args) {
|
|
|
21191
21448
|
return;
|
|
21192
21449
|
}
|
|
21193
21450
|
if (args.subcommand === "status") {
|
|
21194
|
-
if (!
|
|
21451
|
+
if (!existsSync36(workspacePath)) {
|
|
21195
21452
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21196
21453
|
}
|
|
21197
21454
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21199,7 +21456,7 @@ async function runWorkspace(args) {
|
|
|
21199
21456
|
if (args.json) {
|
|
21200
21457
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
21201
21458
|
} else {
|
|
21202
|
-
log(` workspace: ${
|
|
21459
|
+
log(` workspace: ${path49.relative(process.cwd(), workspacePath)}`);
|
|
21203
21460
|
for (const p of report.projects) {
|
|
21204
21461
|
if (p.error) {
|
|
21205
21462
|
log(`
|
|
@@ -21253,7 +21510,7 @@ async function runHelp() {
|
|
|
21253
21510
|
async function runVersion() {
|
|
21254
21511
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
21255
21512
|
const url = new URL(up, import.meta.url);
|
|
21256
|
-
if (!
|
|
21513
|
+
if (!existsSync37(url)) continue;
|
|
21257
21514
|
const pkg = JSON.parse(await readFile32(url, "utf8"));
|
|
21258
21515
|
if (pkg.name === "pixelkiln") {
|
|
21259
21516
|
log(`${pkg.name} ${pkg.version}`);
|