pixelkiln 0.86.3 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROVIDERS.md +12 -11
- package/README.md +7 -7
- package/dist/cli.js +1382 -602
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +4 -4
- package/dist/index.cjs +1361 -654
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +897 -124
- package/dist/index.d.ts +897 -124
- package/dist/index.js +1259 -581
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +2 -2
- package/docs/CLI.md +102 -7
- package/docs/ENDPOINTS.md +142 -11
- package/docs/GENERATORS.md +84 -14
- package/docs/MANIFEST.md +4 -3
- package/docs/PIXELLAB.md +7 -6
- package/docs/REVISIONS.md +2 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +47 -2
- package/schema/recipe.schema.json +2 -1
- package/skills/pixelkiln/SKILL.md +11 -3
- package/skills/pixelkiln/references/pixellab-roadmap.md +31 -4
- package/skills/pixelkiln/references/pixellab.md +26 -13
- package/skills/pixelkiln/references/revisions.md +2 -2
package/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
71
|
-
import { readFile as
|
|
70
|
+
import { existsSync as existsSync39 } from "fs";
|
|
71
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -109,7 +109,13 @@ var VALUE_FLAGS = [
|
|
|
109
109
|
"--weight",
|
|
110
110
|
"--glyph-px",
|
|
111
111
|
"--frames",
|
|
112
|
-
"--estimate-limit"
|
|
112
|
+
"--estimate-limit",
|
|
113
|
+
"--preset",
|
|
114
|
+
"--recipe",
|
|
115
|
+
"--effect",
|
|
116
|
+
"--part",
|
|
117
|
+
"--sprite",
|
|
118
|
+
"--fps"
|
|
113
119
|
];
|
|
114
120
|
var BOOL_FLAGS = [
|
|
115
121
|
"--force",
|
|
@@ -126,7 +132,8 @@ var BOOL_FLAGS = [
|
|
|
126
132
|
"--prune",
|
|
127
133
|
"--edit",
|
|
128
134
|
"--refresh",
|
|
129
|
-
"--no-editor"
|
|
135
|
+
"--no-editor",
|
|
136
|
+
"--game-pack"
|
|
130
137
|
];
|
|
131
138
|
var COMMANDS = [
|
|
132
139
|
"init",
|
|
@@ -162,6 +169,8 @@ var COMMANDS = [
|
|
|
162
169
|
"estimate-skeleton",
|
|
163
170
|
"skeleton-preview",
|
|
164
171
|
"unzoom",
|
|
172
|
+
"pixelate",
|
|
173
|
+
"vfx",
|
|
165
174
|
"font",
|
|
166
175
|
"help",
|
|
167
176
|
"--help",
|
|
@@ -176,6 +185,7 @@ var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
|
176
185
|
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
177
186
|
var TOOLS_SUBCOMMANDS = ["status", "install"];
|
|
178
187
|
var TOOLS = ["editor"];
|
|
188
|
+
var VFX_SUBCOMMANDS = ["make", "presets", "parts"];
|
|
179
189
|
function parseArgs(argv) {
|
|
180
190
|
const [command = "help"] = argv;
|
|
181
191
|
if (!COMMANDS.includes(command)) {
|
|
@@ -265,6 +275,12 @@ function parseArgs(argv) {
|
|
|
265
275
|
throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
|
|
266
276
|
}
|
|
267
277
|
rest = rest.slice(1);
|
|
278
|
+
} else if (command === "vfx") {
|
|
279
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "make" : rest[0];
|
|
280
|
+
if (!VFX_SUBCOMMANDS.includes(subcommand)) {
|
|
281
|
+
throw new UsageError(`Unknown vfx subcommand "${subcommand}". Known: ${VFX_SUBCOMMANDS.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
268
284
|
} else if (command === "refine") {
|
|
269
285
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
270
286
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -432,7 +448,14 @@ function parseArgs(argv) {
|
|
|
432
448
|
weight: get("--weight"),
|
|
433
449
|
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
|
|
434
450
|
frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
|
|
435
|
-
estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true })
|
|
451
|
+
estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true }),
|
|
452
|
+
preset: get("--preset"),
|
|
453
|
+
recipe: get("--recipe"),
|
|
454
|
+
effect: get("--effect"),
|
|
455
|
+
parts: list("--part"),
|
|
456
|
+
sprite: get("--sprite"),
|
|
457
|
+
fps: numberOption("--fps", { min: 1, max: 60, integer: true }),
|
|
458
|
+
gamePack: rest.includes("--game-pack")
|
|
436
459
|
};
|
|
437
460
|
}
|
|
438
461
|
|
|
@@ -499,8 +522,16 @@ Commands
|
|
|
499
522
|
add/remove/list/status/claims. Offline.
|
|
500
523
|
unzoom PixelLab: shrink an upscaled image (--from) back to its native
|
|
501
524
|
pixel grid before using it as a style or reference image.
|
|
525
|
+
pixelate PixelLab: convert a photo, painting, or render (--from) into
|
|
526
|
+
pixel art with Image to Pixel Art Pro Flash; PixelLab picks the
|
|
527
|
+
output size. Optional --description steers the style. 6
|
|
528
|
+
generations; asks first.
|
|
529
|
+
vfx PixelLab: build an animated pixel-art effect with the VFX
|
|
530
|
+
workbench (free) from a --preset, a --recipe file, a saved
|
|
531
|
+
--effect, and/or --part; writes <out>.png/.gif/.json and a
|
|
532
|
+
.vfx.json recipe record. vfx presets / vfx parts list options.
|
|
502
533
|
font PixelLab: generate a pixel font (--description, --out); writes a
|
|
503
|
-
.ttf and a glyph atlas PNG.
|
|
534
|
+
.ttf and a glyph atlas PNG. 15 generations; asks first.
|
|
504
535
|
|
|
505
536
|
Options
|
|
506
537
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
@@ -523,8 +554,16 @@ Options
|
|
|
523
554
|
--model-root <dir> recipe verify: also hash required local model files
|
|
524
555
|
--generation <n|hash> restore: the previous generation to bring back (1 = newest)
|
|
525
556
|
--quantize <n> unzoom: 0 auto palette (default), -1 keep all colors, 2-256 exact
|
|
526
|
-
--description <text> font: the style to draw, e.g. "warm orange arcade font"
|
|
557
|
+
--description <text> font: the style to draw, e.g. "warm orange arcade font";
|
|
558
|
+
pixelate: an optional style steer, e.g. "light dithering"
|
|
527
559
|
--weight <w> font: Bold or Regular (default)
|
|
560
|
+
--preset <name> vfx: start from a whole-effect preset (vfx presets lists them)
|
|
561
|
+
--recipe <file> vfx: render a recipe or an earlier run's .vfx.json record
|
|
562
|
+
--effect <id> vfx: start from a saved My effects id and export over it
|
|
563
|
+
--part <name> vfx: add a part (repeatable; vfx parts lists them)
|
|
564
|
+
--sprite <ref> vfx: build around a sprite: a file, a URL, or character:<id>:<dir>
|
|
565
|
+
--fps <n> vfx: frames per second, 1-60
|
|
566
|
+
--game-pack vfx: also download the game-pack ZIP
|
|
528
567
|
--glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
|
|
529
568
|
--prune cache: remove invalid/unreferenced local cache data
|
|
530
569
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
@@ -583,6 +622,8 @@ Examples
|
|
|
583
622
|
pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
|
|
584
623
|
pixelkiln quality check --from pixelkiln.quality.json
|
|
585
624
|
pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
|
|
625
|
+
pixelkiln pixelate --from renders/lodge.png --out art/lodge.png
|
|
626
|
+
pixelkiln vfx --preset explosion --part scorch_mark --out fx/explosion --game-pack
|
|
586
627
|
pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
|
|
587
628
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
588
629
|
pixelkiln workspace status --json
|
|
@@ -665,16 +706,17 @@ import { createReadStream } from "fs";
|
|
|
665
706
|
function sha256(data) {
|
|
666
707
|
return createHash("sha256").update(data).digest("hex");
|
|
667
708
|
}
|
|
668
|
-
async function sha256File(
|
|
709
|
+
async function sha256File(path53) {
|
|
669
710
|
return new Promise((resolve, reject) => {
|
|
670
711
|
const hash = createHash("sha256");
|
|
671
|
-
const stream = createReadStream(
|
|
712
|
+
const stream = createReadStream(path53);
|
|
672
713
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
673
714
|
stream.on("error", reject);
|
|
674
715
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
675
716
|
});
|
|
676
717
|
}
|
|
677
718
|
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
|
|
719
|
+
const effect = Boolean(spec.vfx);
|
|
678
720
|
return sha256(
|
|
679
721
|
JSON.stringify({
|
|
680
722
|
// Preserve every existing PixelLab hash while making a provider switch
|
|
@@ -683,15 +725,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
683
725
|
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
684
726
|
providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
|
|
685
727
|
generator: spec.generator,
|
|
686
|
-
prompt: spec.prompt,
|
|
687
|
-
width: spec.width,
|
|
688
|
-
height: spec.height,
|
|
689
|
-
view: spec.view,
|
|
690
|
-
outline: spec.outline ?? null,
|
|
691
|
-
shading: spec.shading ?? null,
|
|
692
|
-
detail: spec.detail ?? null,
|
|
693
|
-
seed: spec.seed ?? null,
|
|
694
|
-
palette: spec.palette,
|
|
728
|
+
prompt: effect ? void 0 : spec.prompt,
|
|
729
|
+
width: effect ? void 0 : spec.width,
|
|
730
|
+
height: effect ? void 0 : spec.height,
|
|
731
|
+
view: effect ? void 0 : spec.view,
|
|
732
|
+
outline: effect ? void 0 : spec.outline ?? null,
|
|
733
|
+
shading: effect ? void 0 : spec.shading ?? null,
|
|
734
|
+
detail: effect ? void 0 : spec.detail ?? null,
|
|
735
|
+
seed: effect ? void 0 : spec.seed ?? null,
|
|
736
|
+
palette: effect ? void 0 : spec.palette,
|
|
695
737
|
// `noBackground` only reaches the wire for pixflux and uiAsset; the
|
|
696
738
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
697
739
|
// intentionally absent; width/height are derived from it, so it is
|
|
@@ -766,6 +808,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
766
808
|
styleTraits: spec.objectPro.styleTraits,
|
|
767
809
|
reference: spec.objectPro.reference?.sha256
|
|
768
810
|
} : void 0,
|
|
811
|
+
// A recipe and a local sprite enter as their contents' hashes, never
|
|
812
|
+
// their paths; the My effects name is not identity.
|
|
813
|
+
vfx: spec.vfx ? {
|
|
814
|
+
preset: spec.vfx.preset,
|
|
815
|
+
recipe: spec.vfx.recipe?.sha256,
|
|
816
|
+
parts: spec.vfx.parts,
|
|
817
|
+
fps: spec.vfx.fps,
|
|
818
|
+
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
|
|
819
|
+
} : void 0,
|
|
769
820
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
770
821
|
// compares those directly, so only the choice of source is identity.
|
|
771
822
|
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
@@ -815,7 +866,7 @@ import path2 from "path";
|
|
|
815
866
|
// src/types.ts
|
|
816
867
|
import { z } from "zod";
|
|
817
868
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
818
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
|
|
869
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash", "vfx"]);
|
|
819
870
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
820
871
|
var RevisionModeSchema = z.enum([
|
|
821
872
|
"image-to-image",
|
|
@@ -848,18 +899,26 @@ function candidateCount(size) {
|
|
|
848
899
|
return 1;
|
|
849
900
|
}
|
|
850
901
|
function generationCost(width, height, generator = "map") {
|
|
902
|
+
if (generator === "vfx") return 0;
|
|
851
903
|
if (generator === "map" || generator === "pixflux") return 1;
|
|
852
|
-
if (generator === "imagePro") return
|
|
904
|
+
if (generator === "imagePro") return proTierCost(width, height);
|
|
853
905
|
const px = width * height;
|
|
854
|
-
if (px <= 1024) return
|
|
855
|
-
if (px <= 2048) return
|
|
856
|
-
return
|
|
906
|
+
if (px <= 1024) return PRO_TIER_PRICES[0];
|
|
907
|
+
if (px <= 2048) return PRO_TIER_PRICES[1];
|
|
908
|
+
return PRO_TIER_PRICES[2];
|
|
909
|
+
}
|
|
910
|
+
var PRO_TIER_PRICES = [10, 15, 25];
|
|
911
|
+
function proTierCost(width, height) {
|
|
912
|
+
const px = width * height;
|
|
913
|
+
if (px <= 65536) return PRO_TIER_PRICES[0];
|
|
914
|
+
if (px <= 102400) return PRO_TIER_PRICES[1];
|
|
915
|
+
return PRO_TIER_PRICES[2];
|
|
857
916
|
}
|
|
858
917
|
function tilesCost(tileSize, variations) {
|
|
859
918
|
const px = tileSize * tileSize * Math.max(1, variations);
|
|
860
|
-
if (px <= 1024) return
|
|
861
|
-
if (px <= 2048) return
|
|
862
|
-
return
|
|
919
|
+
if (px <= 1024) return PRO_TIER_PRICES[0];
|
|
920
|
+
if (px <= 2048) return PRO_TIER_PRICES[1];
|
|
921
|
+
return PRO_TIER_PRICES[2];
|
|
863
922
|
}
|
|
864
923
|
function isometricTileCost() {
|
|
865
924
|
return 1;
|
|
@@ -1025,6 +1084,29 @@ var MapSceneSchema = z.object({
|
|
|
1025
1084
|
image: z.string().min(1),
|
|
1026
1085
|
placement: MapScenePlacementSchema.default({ oval: 0.3 })
|
|
1027
1086
|
}).strict();
|
|
1087
|
+
var VfxPartSchema = z.object({ name: z.string().min(1) }).catchall(z.unknown());
|
|
1088
|
+
var AssetVfxSchema = z.object({
|
|
1089
|
+
preset: z.string().min(1).optional(),
|
|
1090
|
+
recipe: z.string().min(1).optional(),
|
|
1091
|
+
parts: z.array(VfxPartSchema).optional(),
|
|
1092
|
+
sprite: z.string().min(1).optional(),
|
|
1093
|
+
fps: z.number().int().min(1).max(60).optional(),
|
|
1094
|
+
name: z.string().min(1).optional()
|
|
1095
|
+
}).strict().superRefine((vfx, context) => {
|
|
1096
|
+
if (vfx.preset && vfx.recipe) {
|
|
1097
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
|
|
1098
|
+
}
|
|
1099
|
+
if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
1100
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
|
|
1101
|
+
}
|
|
1102
|
+
if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
|
|
1103
|
+
context.addIssue({
|
|
1104
|
+
code: z.ZodIssueCode.custom,
|
|
1105
|
+
message: "a vfx sprite is a manifest-relative image or a character:/object:/gallery: reference; a URL has no content to track",
|
|
1106
|
+
path: ["sprite"]
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1028
1110
|
var CharacterOutfitSchema = z.object({
|
|
1029
1111
|
/** The loop (a character animation asset) to re-clothe, in the same style. */
|
|
1030
1112
|
of: z.string().min(1),
|
|
@@ -1375,7 +1457,7 @@ var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-anim
|
|
|
1375
1457
|
var StyleObjectSchema = z.object({
|
|
1376
1458
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
1377
1459
|
provider: z.string().min(1).optional(),
|
|
1378
|
-
// `map` is the default because it is
|
|
1460
|
+
// `map` is the default because it is 10-25x cheaper and correct for any
|
|
1379
1461
|
// asset that is not going to be rotated or animated.
|
|
1380
1462
|
generator: GeneratorSchema.default("map"),
|
|
1381
1463
|
/** Square edge length. The active provider owns its exact limit. */
|
|
@@ -1658,7 +1740,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
1658
1740
|
message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
|
|
1659
1741
|
path: ["tileFeature"]
|
|
1660
1742
|
}).superRefine((style, ctx) => {
|
|
1661
|
-
if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
|
|
1743
|
+
if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation" || style.generator === "vfx")) {
|
|
1662
1744
|
ctx.addIssue({
|
|
1663
1745
|
code: z.ZodIssueCode.custom,
|
|
1664
1746
|
message: "quality profiles currently support single-image generators only",
|
|
@@ -1788,6 +1870,8 @@ var AssetSchema = z.object({
|
|
|
1788
1870
|
outfit: CharacterOutfitSchema.optional(),
|
|
1789
1871
|
/** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
|
|
1790
1872
|
scene: MapSceneSchema.optional(),
|
|
1873
|
+
/** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
|
|
1874
|
+
vfx: AssetVfxSchema.optional(),
|
|
1791
1875
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1792
1876
|
proportions: CharacterProportionsSchema.optional(),
|
|
1793
1877
|
/**
|
|
@@ -1864,7 +1948,7 @@ var AssetSchema = z.object({
|
|
|
1864
1948
|
path: ["revision"]
|
|
1865
1949
|
});
|
|
1866
1950
|
}
|
|
1867
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene"].filter(Boolean);
|
|
1951
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene", asset.vfx && "vfx"].filter(Boolean);
|
|
1868
1952
|
if (shapes.length > 1) {
|
|
1869
1953
|
context.addIssue({
|
|
1870
1954
|
code: z.ZodIssueCode.custom,
|
|
@@ -1879,7 +1963,7 @@ var AssetSchema = z.object({
|
|
|
1879
1963
|
path: ["mirror"]
|
|
1880
1964
|
});
|
|
1881
1965
|
}
|
|
1882
|
-
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
|
|
1966
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template && !asset.vfx) {
|
|
1883
1967
|
context.addIssue({
|
|
1884
1968
|
code: z.ZodIssueCode.custom,
|
|
1885
1969
|
message: "Required",
|
|
@@ -2904,6 +2988,11 @@ function frameSetFps(entry) {
|
|
|
2904
2988
|
}
|
|
2905
2989
|
return null;
|
|
2906
2990
|
}
|
|
2991
|
+
function frameSetLoop(entry) {
|
|
2992
|
+
const frameSet = entry.providerMetadata?.[entry.provider]?.frameSet;
|
|
2993
|
+
const loop = frameSet && typeof frameSet === "object" ? frameSet.loop : void 0;
|
|
2994
|
+
return typeof loop === "boolean" ? loop : null;
|
|
2995
|
+
}
|
|
2907
2996
|
|
|
2908
2997
|
// src/palette.ts
|
|
2909
2998
|
function normalizePalette(values) {
|
|
@@ -4325,6 +4414,7 @@ async function inspectRevisionReadiness(spec, lock) {
|
|
|
4325
4414
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
4326
4415
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
4327
4416
|
if (spec.scene) return inspectScene(spec);
|
|
4417
|
+
if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
|
|
4328
4418
|
if (!spec.revision) return null;
|
|
4329
4419
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
4330
4420
|
}
|
|
@@ -4352,6 +4442,12 @@ async function inspectScene(spec) {
|
|
|
4352
4442
|
}
|
|
4353
4443
|
return { ready: true, reason: "scene is current" };
|
|
4354
4444
|
}
|
|
4445
|
+
async function inspectVfxSprite(spec) {
|
|
4446
|
+
const sprite = spec.vfx.sprite;
|
|
4447
|
+
if (!sprite.sha256 || !existsSync4(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
|
|
4448
|
+
if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
|
|
4449
|
+
return { ready: true, reason: "vfx sprite is current" };
|
|
4450
|
+
}
|
|
4355
4451
|
async function inspectMirror(spec, lock, seen) {
|
|
4356
4452
|
const source = spec.mirror.sourceSpec;
|
|
4357
4453
|
const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
|
|
@@ -4588,7 +4684,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
4588
4684
|
} else if (opts.force) {
|
|
4589
4685
|
state = "missing";
|
|
4590
4686
|
reason = "--force";
|
|
4591
|
-
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
4687
|
+
} else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
|
|
4592
4688
|
if (existsSync5(path9.resolve(spec.root, spec.source))) {
|
|
4593
4689
|
state = "ok";
|
|
4594
4690
|
reason = `placed from ${spec.source}; not generated`;
|
|
@@ -4828,8 +4924,8 @@ async function confirm(question, auto) {
|
|
|
4828
4924
|
}
|
|
4829
4925
|
|
|
4830
4926
|
// src/cli/commands/account.ts
|
|
4831
|
-
import
|
|
4832
|
-
import { existsSync as
|
|
4927
|
+
import path24 from "path";
|
|
4928
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4833
4929
|
|
|
4834
4930
|
// src/env.ts
|
|
4835
4931
|
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
@@ -5841,10 +5937,10 @@ function canonical2(value) {
|
|
|
5841
5937
|
}
|
|
5842
5938
|
|
|
5843
5939
|
// src/providers/pixellab.ts
|
|
5844
|
-
import { mkdirSync, existsSync as
|
|
5940
|
+
import { mkdirSync, existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
5845
5941
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5846
5942
|
import os from "os";
|
|
5847
|
-
import
|
|
5943
|
+
import path13 from "path";
|
|
5848
5944
|
|
|
5849
5945
|
// src/client.ts
|
|
5850
5946
|
import { z as z3 } from "zod";
|
|
@@ -6261,9 +6357,9 @@ var PixelLabClient = class {
|
|
|
6261
6357
|
apiKey;
|
|
6262
6358
|
http;
|
|
6263
6359
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6264
|
-
async request(
|
|
6360
|
+
async request(path53, init) {
|
|
6265
6361
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6266
|
-
const res = await this.http(`${BASE}${
|
|
6362
|
+
const res = await this.http(`${BASE}${path53}`, {
|
|
6267
6363
|
...init,
|
|
6268
6364
|
headers: {
|
|
6269
6365
|
Authorization: auth,
|
|
@@ -6273,13 +6369,13 @@ var PixelLabClient = class {
|
|
|
6273
6369
|
});
|
|
6274
6370
|
const text = await res.text();
|
|
6275
6371
|
if (!res.ok) {
|
|
6276
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6372
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path53} \u2192 ${res.status}`, res.status, text);
|
|
6277
6373
|
}
|
|
6278
6374
|
if (!text) return {};
|
|
6279
6375
|
try {
|
|
6280
6376
|
return JSON.parse(text);
|
|
6281
6377
|
} catch {
|
|
6282
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6378
|
+
throw new Error(`${init?.method ?? "GET"} ${path53} returned invalid JSON`);
|
|
6283
6379
|
}
|
|
6284
6380
|
}
|
|
6285
6381
|
async balance() {
|
|
@@ -6684,10 +6780,10 @@ var PixelLabClient = class {
|
|
|
6684
6780
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6685
6781
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6686
6782
|
const south = args.reference?.south;
|
|
6687
|
-
let
|
|
6783
|
+
let path53;
|
|
6688
6784
|
let body;
|
|
6689
6785
|
if (args.mode === "v3") {
|
|
6690
|
-
|
|
6786
|
+
path53 = "/create-character-v3";
|
|
6691
6787
|
body = {
|
|
6692
6788
|
description: args.description,
|
|
6693
6789
|
image_size: imageSize,
|
|
@@ -6701,7 +6797,7 @@ var PixelLabClient = class {
|
|
|
6701
6797
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6702
6798
|
};
|
|
6703
6799
|
} else if (args.mode === "pro-flash") {
|
|
6704
|
-
|
|
6800
|
+
path53 = "/create-character-pro-flash";
|
|
6705
6801
|
const traits = args.styleTraits;
|
|
6706
6802
|
body = {
|
|
6707
6803
|
description: args.description,
|
|
@@ -6725,7 +6821,7 @@ var PixelLabClient = class {
|
|
|
6725
6821
|
} : {}
|
|
6726
6822
|
};
|
|
6727
6823
|
} else if (args.mode === "pro") {
|
|
6728
|
-
|
|
6824
|
+
path53 = "/create-character-pro";
|
|
6729
6825
|
body = {
|
|
6730
6826
|
description: args.description,
|
|
6731
6827
|
image_size: imageSize,
|
|
@@ -6739,7 +6835,7 @@ var PixelLabClient = class {
|
|
|
6739
6835
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6740
6836
|
};
|
|
6741
6837
|
} else {
|
|
6742
|
-
|
|
6838
|
+
path53 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6743
6839
|
body = {
|
|
6744
6840
|
description: args.description,
|
|
6745
6841
|
image_size: imageSize,
|
|
@@ -6756,8 +6852,8 @@ var PixelLabClient = class {
|
|
|
6756
6852
|
...palette
|
|
6757
6853
|
};
|
|
6758
6854
|
}
|
|
6759
|
-
const raw = await this.request(
|
|
6760
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6855
|
+
const raw = await this.request(path53, { method: "POST", body: JSON.stringify(body) });
|
|
6856
|
+
return validateResponse(CharacterSubmitSchema, raw, path53.slice(1));
|
|
6761
6857
|
}
|
|
6762
6858
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6763
6859
|
async createCharacterState(args) {
|
|
@@ -6820,7 +6916,8 @@ var PixelLabClient = class {
|
|
|
6820
6916
|
* A full-body sprite in, a bust portrait out (or the reverse, unused
|
|
6821
6917
|
* today). Job-based like a state or `1dir`, not the couple-generations
|
|
6822
6918
|
* "conversion" the name suggests: the smallest `resultSize` (16px) billed
|
|
6823
|
-
* 20 generations live (docs/ENDPOINTS.md, "Characters, measured")
|
|
6919
|
+
* 20 generations live (docs/ENDPOINTS.md, "Characters, measured"), the
|
|
6920
|
+
* Pro floor before PixelLab's October 2026 cut put it at 10.
|
|
6824
6921
|
*/
|
|
6825
6922
|
async createPortraitCharacterPro(args) {
|
|
6826
6923
|
const raw = await this.request("/portrait-character-pro", {
|
|
@@ -6859,7 +6956,8 @@ var PixelLabClient = class {
|
|
|
6859
6956
|
* everything else here, but the completed job returns the result frames
|
|
6860
6957
|
* inline as base64 (`last_response.quantized_images`), not as storage
|
|
6861
6958
|
* URLs — the one PixelLab character tool that works this way. A live
|
|
6862
|
-
* 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md)
|
|
6959
|
+
* 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md), before
|
|
6960
|
+
* PixelLab's October 2026 Pro cut halved that floor to 10.
|
|
6863
6961
|
*/
|
|
6864
6962
|
async transferOutfitV2(args) {
|
|
6865
6963
|
const image = (img) => ({
|
|
@@ -6909,7 +7007,9 @@ var PixelLabClient = class {
|
|
|
6909
7007
|
* far higher than this tiering assumes; 352x352 (123904px²), 384x384
|
|
6910
7008
|
* (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
|
|
6911
7009
|
* 40. Both breakpoints are now tightly bracketed: 20->25 in
|
|
6912
|
-
* (65536px², 82944px²], 25->40 in (102400px², 123904px²].
|
|
7010
|
+
* (65536px², 82944px²], 25->40 in (102400px², 123904px²]. Those prices
|
|
7011
|
+
* predate PixelLab's October 2026 Pro cut to 10/15/25; `proTierCost`
|
|
7012
|
+
* keeps the breakpoints with the new prices.
|
|
6913
7013
|
* `editImagesV2` below returns the same fields under `images`, plural
|
|
6914
7014
|
* and array-wrapped, not `image` — do not assume the two endpoints share
|
|
6915
7015
|
* one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
|
|
@@ -6962,8 +7062,9 @@ var PixelLabClient = class {
|
|
|
6962
7062
|
);
|
|
6963
7063
|
}
|
|
6964
7064
|
/**
|
|
6965
|
-
* PixelLab's Pro image tier, `/generate-image-v2`:
|
|
6966
|
-
*
|
|
7065
|
+
* PixelLab's Pro image tier, `/generate-image-v2`: 10 to 25 generations by
|
|
7066
|
+
* canvas since PixelLab's October 2026 Pro cut, a flat 40 before
|
|
7067
|
+
* (docs/ENDPOINTS.md, "Single-image generators, measured"), for real style
|
|
6967
7068
|
* transfer and non-square canvases up to 792x688, where `pixflux` is
|
|
6968
7069
|
* limited to 400x400 and no style reference. Like a revision, this hands
|
|
6969
7070
|
* back a plain background job with no resource of its own; unlike a
|
|
@@ -7049,6 +7150,24 @@ var PixelLabClient = class {
|
|
|
7049
7150
|
"inpaint-image-pro-flash"
|
|
7050
7151
|
);
|
|
7051
7152
|
}
|
|
7153
|
+
/**
|
|
7154
|
+
* `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
|
|
7155
|
+
* art out, with no size to pass: PixelLab detects the native pixel scale
|
|
7156
|
+
* and picks the output size (a 256x256 render came back 108x109). The
|
|
7157
|
+
* result is opaque, on a near-white background. A flat 6 generations, billed exactly that live
|
|
7158
|
+
* in October 2026. Sources over 2048px on the longer side are scaled down
|
|
7159
|
+
* upstream. The finished job carries the image at `last_response.images[0]`.
|
|
7160
|
+
*/
|
|
7161
|
+
async imageToPixelartProFlash(args) {
|
|
7162
|
+
const body = { image: args.image };
|
|
7163
|
+
if (args.description) body.description = args.description;
|
|
7164
|
+
if (args.seed != null) body.seed = args.seed;
|
|
7165
|
+
return validateResponse(
|
|
7166
|
+
ProFlashImageSubmitSchema,
|
|
7167
|
+
await this.request("/image-to-pixelart-pro-flash", { method: "POST", body: JSON.stringify(body) }),
|
|
7168
|
+
"image-to-pixelart-pro-flash"
|
|
7169
|
+
);
|
|
7170
|
+
}
|
|
7052
7171
|
/**
|
|
7053
7172
|
* `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
|
|
7054
7173
|
* health bar, an inventory slot, a dialogue box) from a description,
|
|
@@ -7406,6 +7525,337 @@ function clientFromEnv() {
|
|
|
7406
7525
|
return new PixelLabClient(key);
|
|
7407
7526
|
}
|
|
7408
7527
|
|
|
7528
|
+
// src/pixellab-mcp.ts
|
|
7529
|
+
var PIXELLAB_MCP_URL = "https://api.pixellab.ai/mcp";
|
|
7530
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
7531
|
+
function parseMcpBody(body) {
|
|
7532
|
+
const trimmed = body.trim();
|
|
7533
|
+
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
|
|
7534
|
+
let last;
|
|
7535
|
+
for (const line of body.split(/\r?\n/)) {
|
|
7536
|
+
if (!line.startsWith("data:")) continue;
|
|
7537
|
+
const payload = line.slice(5).trim();
|
|
7538
|
+
if (!payload) continue;
|
|
7539
|
+
const message6 = JSON.parse(payload);
|
|
7540
|
+
if ("id" in message6) last = message6;
|
|
7541
|
+
}
|
|
7542
|
+
if (!last) throw new Error(`PixelLab MCP reply had no JSON-RPC response: ${body.slice(0, 200)}`);
|
|
7543
|
+
return last;
|
|
7544
|
+
}
|
|
7545
|
+
function isBusyReply(text) {
|
|
7546
|
+
return /\bbusy\b.*\bretry\b/i.test(text);
|
|
7547
|
+
}
|
|
7548
|
+
var PixelLabMcpClient = class {
|
|
7549
|
+
constructor(apiKey, opts = {}) {
|
|
7550
|
+
this.apiKey = apiKey;
|
|
7551
|
+
if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
|
|
7552
|
+
this.url = opts.url ?? PIXELLAB_MCP_URL;
|
|
7553
|
+
this.http = fetchWithRetry(opts.fetch, { timeoutMs: opts.timeoutMs ?? 18e4 });
|
|
7554
|
+
this.busyRetries = opts.busyRetries ?? 4;
|
|
7555
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
7556
|
+
}
|
|
7557
|
+
apiKey;
|
|
7558
|
+
http;
|
|
7559
|
+
url;
|
|
7560
|
+
busyRetries;
|
|
7561
|
+
sleep;
|
|
7562
|
+
nextId = 1;
|
|
7563
|
+
/**
|
|
7564
|
+
* Call one tool and return its text and images. A tool-level error
|
|
7565
|
+
* (`isError`) throws with the tool's own message, after a few spaced
|
|
7566
|
+
* retries when the tool only said it was busy.
|
|
7567
|
+
*/
|
|
7568
|
+
async callTool(name, args) {
|
|
7569
|
+
for (let attempt = 0; ; attempt++) {
|
|
7570
|
+
const reply = await this.callOnce(name, args);
|
|
7571
|
+
if (!reply.isError) return { text: reply.text, images: reply.images };
|
|
7572
|
+
if (attempt < this.busyRetries && isBusyReply(reply.text)) {
|
|
7573
|
+
await this.sleep(2e3 * 2 ** attempt);
|
|
7574
|
+
continue;
|
|
7575
|
+
}
|
|
7576
|
+
throw new Error(`PixelLab ${name}: ${reply.text || "the tool reported an error"}`);
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
async callOnce(name, args) {
|
|
7580
|
+
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
7581
|
+
const id = this.nextId++;
|
|
7582
|
+
const res = await this.http(this.url, {
|
|
7583
|
+
method: "POST",
|
|
7584
|
+
headers: {
|
|
7585
|
+
Authorization: auth,
|
|
7586
|
+
"Content-Type": "application/json",
|
|
7587
|
+
Accept: "application/json, text/event-stream",
|
|
7588
|
+
"MCP-Protocol-Version": PROTOCOL_VERSION
|
|
7589
|
+
},
|
|
7590
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } })
|
|
7591
|
+
});
|
|
7592
|
+
const body = await res.text();
|
|
7593
|
+
if (!res.ok) throw new PixelLabError(`MCP ${name} \u2192 ${res.status}`, res.status, body);
|
|
7594
|
+
const message6 = parseMcpBody(body);
|
|
7595
|
+
const error = message6.error;
|
|
7596
|
+
if (error) throw new Error(`PixelLab MCP ${name} failed: ${error.message ?? JSON.stringify(error)}`);
|
|
7597
|
+
const result = message6.result ?? {};
|
|
7598
|
+
const text = (result.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
7599
|
+
const images = (result.content ?? []).filter((block) => block.type === "image" && typeof block.data === "string").map((block) => ({ mimeType: String(block.mimeType ?? "image/png"), data: block.data }));
|
|
7600
|
+
return { text, images, isError: Boolean(result.isError) };
|
|
7601
|
+
}
|
|
7602
|
+
};
|
|
7603
|
+
function mcpClientFromEnv() {
|
|
7604
|
+
const key = process.env.PIXELLAB_API_KEY;
|
|
7605
|
+
if (!key) {
|
|
7606
|
+
throw new Error(
|
|
7607
|
+
"PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
|
|
7608
|
+
);
|
|
7609
|
+
}
|
|
7610
|
+
return new PixelLabMcpClient(key);
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
// src/pixellab-vfx.ts
|
|
7614
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7615
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
7616
|
+
import path12 from "path";
|
|
7617
|
+
var VFX_TOOL = "vfx_workbench";
|
|
7618
|
+
var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
7619
|
+
function parseVfxStep(text) {
|
|
7620
|
+
const match = new RegExp(`effect step: (${UUID})`).exec(text);
|
|
7621
|
+
if (!match) throw new Error(`PixelLab vfx_workbench returned no step id:
|
|
7622
|
+
${text.slice(0, 600)}`);
|
|
7623
|
+
return match[1];
|
|
7624
|
+
}
|
|
7625
|
+
function parseVfxRecipe(text) {
|
|
7626
|
+
const lines = text.split(/\r?\n/);
|
|
7627
|
+
const at = lines.findIndex((line) => line.trim() === "working recipe:");
|
|
7628
|
+
const json = at >= 0 ? lines[at + 1]?.trim() : void 0;
|
|
7629
|
+
if (!json?.startsWith("{")) return null;
|
|
7630
|
+
return JSON.parse(json);
|
|
7631
|
+
}
|
|
7632
|
+
function parseVfxExport(text) {
|
|
7633
|
+
const effectId = new RegExp(`effect id (${UUID})`).exec(text)?.[1];
|
|
7634
|
+
const download = /download (https:\/\/\S+\/): (.+)$/m.exec(text);
|
|
7635
|
+
const size = /(\d+)x(\d+), (\d+) frames @ ([\d.]+) fps, ([^;\n]+)/.exec(text);
|
|
7636
|
+
if (!effectId || !download || !size) {
|
|
7637
|
+
throw new Error(`PixelLab vfx_workbench export reply was not understood:
|
|
7638
|
+
${text.slice(0, 600)}`);
|
|
7639
|
+
}
|
|
7640
|
+
const anchor = /anchor[^\n]*? at (-?\d+),\s*(-?\d+)/.exec(text);
|
|
7641
|
+
return {
|
|
7642
|
+
effectId,
|
|
7643
|
+
editUrl: /(https:\/\/pixellab\.ai\/create-vfx\?effect=[0-9a-f-]+)/.exec(text)?.[1] ?? null,
|
|
7644
|
+
width: Number(size[1]),
|
|
7645
|
+
height: Number(size[2]),
|
|
7646
|
+
frames: Number(size[3]),
|
|
7647
|
+
fps: Number(size[4]),
|
|
7648
|
+
loop: !/plays once/.test(size[5]),
|
|
7649
|
+
anchor: anchor ? { x: Number(anchor[1]), y: Number(anchor[2]) } : null,
|
|
7650
|
+
downloadBase: download[1],
|
|
7651
|
+
files: download[2].split(",").map((file) => file.trim()).filter(Boolean),
|
|
7652
|
+
imageId: new RegExp(`image id: (${UUID})`).exec(text)?.[1] ?? null
|
|
7653
|
+
};
|
|
7654
|
+
}
|
|
7655
|
+
async function describeVfx(client2, topic) {
|
|
7656
|
+
return (await client2.callTool(VFX_TOOL, { argv: ["describe", topic] })).text;
|
|
7657
|
+
}
|
|
7658
|
+
function vfxOutputPaths(out) {
|
|
7659
|
+
const base = path12.resolve(out.replace(/\.(png|gif|json|zip)$/i, "").replace(/\.vfx$/i, ""));
|
|
7660
|
+
return {
|
|
7661
|
+
sheet: `${base}.png`,
|
|
7662
|
+
gif: `${base}.gif`,
|
|
7663
|
+
atlas: `${base}.json`,
|
|
7664
|
+
provenance: `${base}.vfx.json`,
|
|
7665
|
+
gamePack: `${base}-game.zip`
|
|
7666
|
+
};
|
|
7667
|
+
}
|
|
7668
|
+
function isVfxReference(sprite) {
|
|
7669
|
+
return /^(character|object|gallery):/.test(sprite) || /^(https?|data):/.test(sprite);
|
|
7670
|
+
}
|
|
7671
|
+
async function resolveVfxSprite(sprite) {
|
|
7672
|
+
if (isVfxReference(sprite)) return sprite;
|
|
7673
|
+
const bytes = await readFile6(sprite);
|
|
7674
|
+
const metadata = imageMetadata(bytes);
|
|
7675
|
+
if (!metadata) throw new Error(`${sprite} is not a readable PNG or JPEG`);
|
|
7676
|
+
return `data:image/${metadata.format};base64,${bytes.toString("base64")}`;
|
|
7677
|
+
}
|
|
7678
|
+
async function readVfxRecipe(file) {
|
|
7679
|
+
const parsed = JSON.parse(await readFile6(file, "utf8"));
|
|
7680
|
+
const record = parsed.tool === VFX_TOOL && parsed.recipe && typeof parsed.recipe === "object";
|
|
7681
|
+
const recipe = record ? parsed.recipe : parsed;
|
|
7682
|
+
if (!Array.isArray(recipe.layers)) throw new Error(`${file} is not a VFX recipe: it has no "layers" list`);
|
|
7683
|
+
const recorded = record ? parsed.source?.sprite : void 0;
|
|
7684
|
+
if (typeof recorded !== "string") return { recipe };
|
|
7685
|
+
const sprite = isVfxReference(recorded) ? recorded : path12.resolve(path12.dirname(path12.resolve(file)), recorded);
|
|
7686
|
+
return { recipe, sprite };
|
|
7687
|
+
}
|
|
7688
|
+
async function defaultDownload(url) {
|
|
7689
|
+
const http = fetchWithRetry();
|
|
7690
|
+
const res = await http(url);
|
|
7691
|
+
if (!res.ok) throw new Error(`download ${url} \u2192 ${res.status}`);
|
|
7692
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
7693
|
+
if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
7694
|
+
return buf;
|
|
7695
|
+
}
|
|
7696
|
+
function localPathFor(file, paths) {
|
|
7697
|
+
if (/-game\.zip$/i.test(file)) return paths.gamePack;
|
|
7698
|
+
if (/\.gif$/i.test(file)) return paths.gif;
|
|
7699
|
+
if (/\.json$/i.test(file)) return paths.atlas;
|
|
7700
|
+
if (/\.png$/i.test(file)) return paths.sheet;
|
|
7701
|
+
return null;
|
|
7702
|
+
}
|
|
7703
|
+
async function runVfxWorkbench(client2, opts) {
|
|
7704
|
+
const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
|
|
7705
|
+
if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
|
|
7706
|
+
if (starts === 0 && !opts.parts?.length) {
|
|
7707
|
+
throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
|
|
7708
|
+
}
|
|
7709
|
+
const progress = opts.onProgress ?? (() => {
|
|
7710
|
+
});
|
|
7711
|
+
const call = async (argv) => (await client2.callTool(VFX_TOOL, { argv, ...opts.notes ? { notes: opts.notes } : {} })).text;
|
|
7712
|
+
const startArgv = ["start"];
|
|
7713
|
+
if (opts.sprite) startArgv.push("--sprite", opts.sprite);
|
|
7714
|
+
if (opts.preset) startArgv.push("--preset", opts.preset);
|
|
7715
|
+
if (opts.effect) startArgv.push("--effect", opts.effect);
|
|
7716
|
+
progress(opts.preset ? `starting from preset ${opts.preset}` : opts.effect ? `starting from saved effect ${opts.effect}` : "starting");
|
|
7717
|
+
const started = await call(startArgv);
|
|
7718
|
+
let step = parseVfxStep(started);
|
|
7719
|
+
const savedName = opts.effect ? parseVfxRecipe(started)?.name : opts.recipe?.name;
|
|
7720
|
+
const name = opts.name ?? (typeof savedName === "string" ? savedName : void 0) ?? opts.defaultName;
|
|
7721
|
+
if (opts.recipe) {
|
|
7722
|
+
progress("rendering the recipe");
|
|
7723
|
+
step = parseVfxStep(await call([step, "render", JSON.stringify({ recipe: opts.recipe })]));
|
|
7724
|
+
}
|
|
7725
|
+
if (opts.parts?.length) {
|
|
7726
|
+
progress(`adding ${opts.parts.map((part) => part.name).join(", ")}`);
|
|
7727
|
+
step = parseVfxStep(await call([step, "add_part", JSON.stringify({ parts: opts.parts })]));
|
|
7728
|
+
}
|
|
7729
|
+
if (opts.fps !== void 0) {
|
|
7730
|
+
progress(`setting ${opts.fps} fps`);
|
|
7731
|
+
step = parseVfxStep(await call([step, "render", JSON.stringify({ patch: [{ op: "set", path: "fps", value: opts.fps }] })]));
|
|
7732
|
+
}
|
|
7733
|
+
const exportArgv = [step, "export", "--name", name];
|
|
7734
|
+
if (opts.replace) exportArgv.push("--replace", opts.replace);
|
|
7735
|
+
if (opts.gamePack) exportArgv.push("--game-pack");
|
|
7736
|
+
progress(opts.gamePack ? "exporting with a game pack" : "exporting");
|
|
7737
|
+
const exported = parseVfxExport(await call(exportArgv));
|
|
7738
|
+
await opts.onExported?.(exported, name);
|
|
7739
|
+
progress("reading the saved recipe");
|
|
7740
|
+
const recipe = parseVfxRecipe(await call(["start", "--effect", exported.effectId]));
|
|
7741
|
+
return { exported, recipe, name };
|
|
7742
|
+
}
|
|
7743
|
+
async function readSavedVfxRecipe(client2, effectId) {
|
|
7744
|
+
try {
|
|
7745
|
+
return parseVfxRecipe((await client2.callTool(VFX_TOOL, { argv: ["start", "--effect", effectId] })).text);
|
|
7746
|
+
} catch (error) {
|
|
7747
|
+
if (error instanceof Error && /no saved effect/i.test(error.message)) return null;
|
|
7748
|
+
throw error;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
function vfxRecipeSha256(recipe) {
|
|
7752
|
+
const { name: _name, notes: _notes, ...rest } = recipe;
|
|
7753
|
+
return sha256(JSON.stringify(rest));
|
|
7754
|
+
}
|
|
7755
|
+
function sliceVfxSheet(sheet, facts, atlas) {
|
|
7756
|
+
const image = decodePng(sheet);
|
|
7757
|
+
const listed = atlas?.frames;
|
|
7758
|
+
const rects = Array.isArray(listed) && listed.length === facts.frames ? listed.map((entry) => entry.frame) : Array.from({ length: facts.frames }, (_, index) => {
|
|
7759
|
+
const columns = Math.max(1, Math.floor(image.width / facts.width));
|
|
7760
|
+
return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
|
|
7761
|
+
});
|
|
7762
|
+
return rects.map((rect, index) => {
|
|
7763
|
+
if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > image.width || rect.y + rect.h > image.height) {
|
|
7764
|
+
throw new Error(
|
|
7765
|
+
`VFX frame ${index + 1} (${rect.w}x${rect.h} at ${rect.x},${rect.y}) falls outside the ${image.width}x${image.height} sheet`
|
|
7766
|
+
);
|
|
7767
|
+
}
|
|
7768
|
+
const out = Buffer.alloc(rect.w * rect.h * 4);
|
|
7769
|
+
for (let y = 0; y < rect.h; y++) {
|
|
7770
|
+
const from = ((rect.y + y) * image.width + rect.x) * 4;
|
|
7771
|
+
image.pixels.copy(out, y * rect.w * 4, from, from + rect.w * 4);
|
|
7772
|
+
}
|
|
7773
|
+
return encodeRgbaPng(rect.w, rect.h, out);
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
async function makeVfx(client2, opts) {
|
|
7777
|
+
const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
|
|
7778
|
+
if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
|
|
7779
|
+
if (starts === 0 && !opts.parts?.length) {
|
|
7780
|
+
throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
|
|
7781
|
+
}
|
|
7782
|
+
const paths = vfxOutputPaths(opts.out);
|
|
7783
|
+
const wanted = [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []];
|
|
7784
|
+
if (!opts.force) {
|
|
7785
|
+
for (const file of wanted) {
|
|
7786
|
+
if (existsSync7(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
7787
|
+
}
|
|
7788
|
+
}
|
|
7789
|
+
const progress = opts.onProgress ?? (() => {
|
|
7790
|
+
});
|
|
7791
|
+
const { exported, recipe, name } = await runVfxWorkbench(client2, {
|
|
7792
|
+
preset: opts.preset,
|
|
7793
|
+
recipe: opts.recipe,
|
|
7794
|
+
effect: opts.effect,
|
|
7795
|
+
parts: opts.parts,
|
|
7796
|
+
sprite: opts.sprite ? await resolveVfxSprite(opts.sprite) : void 0,
|
|
7797
|
+
fps: opts.fps,
|
|
7798
|
+
name: opts.name,
|
|
7799
|
+
defaultName: opts.preset ?? path12.basename(paths.sheet, ".png"),
|
|
7800
|
+
replace: opts.replace ?? opts.effect,
|
|
7801
|
+
gamePack: opts.gamePack,
|
|
7802
|
+
notes: "pixelkiln vfx",
|
|
7803
|
+
onProgress: progress
|
|
7804
|
+
});
|
|
7805
|
+
const download = opts.download ?? defaultDownload;
|
|
7806
|
+
const written = { sheet: paths.sheet, gif: paths.gif, atlas: paths.atlas, provenance: paths.provenance };
|
|
7807
|
+
await mkdir4(path12.dirname(paths.sheet), { recursive: true });
|
|
7808
|
+
for (const file of exported.files) {
|
|
7809
|
+
const target = localPathFor(file, paths);
|
|
7810
|
+
if (!target) continue;
|
|
7811
|
+
let bytes = await download(`${exported.downloadBase}${encodeURIComponent(file)}`);
|
|
7812
|
+
if (target === paths.atlas) {
|
|
7813
|
+
const atlas = JSON.parse(bytes.toString("utf8"));
|
|
7814
|
+
if (atlas.meta) atlas.meta.image = path12.basename(paths.sheet);
|
|
7815
|
+
bytes = Buffer.from(`${JSON.stringify(atlas, null, 2)}
|
|
7816
|
+
`);
|
|
7817
|
+
}
|
|
7818
|
+
await writeFile4(target, bytes);
|
|
7819
|
+
if (target === paths.gamePack) written.gamePack = target;
|
|
7820
|
+
}
|
|
7821
|
+
for (const file of [paths.sheet, paths.gif, paths.atlas]) {
|
|
7822
|
+
if (!existsSync7(file)) throw new Error(`PixelLab's export listed no file for ${path12.basename(file)}: ${exported.files.join(", ")}`);
|
|
7823
|
+
}
|
|
7824
|
+
const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : path12.relative(path12.dirname(paths.provenance), path12.resolve(opts.sprite)).split(path12.sep).join("/");
|
|
7825
|
+
const provenance = {
|
|
7826
|
+
version: 1,
|
|
7827
|
+
provider: "pixellab",
|
|
7828
|
+
tool: VFX_TOOL,
|
|
7829
|
+
name,
|
|
7830
|
+
effectId: exported.effectId,
|
|
7831
|
+
editUrl: exported.editUrl,
|
|
7832
|
+
source: {
|
|
7833
|
+
...opts.preset ? { preset: opts.preset } : {},
|
|
7834
|
+
...opts.effect ? { effect: opts.effect } : {},
|
|
7835
|
+
...opts.recipe ? { recipeFile: true } : {},
|
|
7836
|
+
...opts.parts?.length ? { parts: opts.parts } : {},
|
|
7837
|
+
...spriteRecord ? { sprite: spriteRecord } : {}
|
|
7838
|
+
},
|
|
7839
|
+
width: exported.width,
|
|
7840
|
+
height: exported.height,
|
|
7841
|
+
frames: exported.frames,
|
|
7842
|
+
fps: exported.fps,
|
|
7843
|
+
loop: exported.loop,
|
|
7844
|
+
anchor: exported.anchor,
|
|
7845
|
+
files: {
|
|
7846
|
+
sheet: path12.basename(paths.sheet),
|
|
7847
|
+
gif: path12.basename(paths.gif),
|
|
7848
|
+
atlas: path12.basename(paths.atlas),
|
|
7849
|
+
...written.gamePack ? { gamePack: path12.basename(written.gamePack) } : {}
|
|
7850
|
+
},
|
|
7851
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7852
|
+
recipe
|
|
7853
|
+
};
|
|
7854
|
+
await writeFile4(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
7855
|
+
`);
|
|
7856
|
+
return { ...exported, paths: written, recipe };
|
|
7857
|
+
}
|
|
7858
|
+
|
|
7409
7859
|
// src/providers/pixellab.ts
|
|
7410
7860
|
function pixelLabObjectUrl(generator, objectId) {
|
|
7411
7861
|
if (!objectId) return null;
|
|
@@ -7420,6 +7870,7 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
7420
7870
|
const characterId = objectId.split("#")[0];
|
|
7421
7871
|
return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
|
|
7422
7872
|
}
|
|
7873
|
+
if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
|
|
7423
7874
|
return null;
|
|
7424
7875
|
}
|
|
7425
7876
|
function billedFromUsage(usage) {
|
|
@@ -7443,7 +7894,7 @@ function characterCost(spec) {
|
|
|
7443
7894
|
}
|
|
7444
7895
|
if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
|
|
7445
7896
|
if (character.kind === "outfit") {
|
|
7446
|
-
return
|
|
7897
|
+
return PRO_TIER_PRICES[0];
|
|
7447
7898
|
}
|
|
7448
7899
|
if (character.mode === "standard") return 1;
|
|
7449
7900
|
if (character.mode === "v3") {
|
|
@@ -7595,13 +8046,20 @@ function decodeObjectAnimationJob(jobId) {
|
|
|
7595
8046
|
return null;
|
|
7596
8047
|
}
|
|
7597
8048
|
var PixelLabProvider = class _PixelLabProvider {
|
|
7598
|
-
|
|
8049
|
+
/**
|
|
8050
|
+
* `mcp` reaches the tools PixelLab offers only over MCP (the VFX
|
|
8051
|
+
* workbench). Offline and download-only providers have none, and a `vfx`
|
|
8052
|
+
* submission through one says so.
|
|
8053
|
+
*/
|
|
8054
|
+
constructor(client2, mcp) {
|
|
7599
8055
|
this.client = client2;
|
|
8056
|
+
this.mcp = mcp;
|
|
7600
8057
|
}
|
|
7601
8058
|
client;
|
|
8059
|
+
mcp;
|
|
7602
8060
|
id = "pixellab";
|
|
7603
8061
|
static fromEnv() {
|
|
7604
|
-
return new _PixelLabProvider(clientFromEnv());
|
|
8062
|
+
return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
|
|
7605
8063
|
}
|
|
7606
8064
|
/** Public storage URLs and the local content cache do not require API auth. */
|
|
7607
8065
|
static forDownloads() {
|
|
@@ -7612,7 +8070,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7612
8070
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
7613
8071
|
}
|
|
7614
8072
|
supports(generator) {
|
|
7615
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash";
|
|
8073
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash" || generator === "vfx";
|
|
7616
8074
|
}
|
|
7617
8075
|
/** `/map-objects` style matching: `background_image` plus `inpainting`. */
|
|
7618
8076
|
supportsMapScene = true;
|
|
@@ -7654,11 +8112,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7654
8112
|
* is a file read.
|
|
7655
8113
|
*/
|
|
7656
8114
|
static cacheDir(namespace = "pixflux") {
|
|
7657
|
-
const dir =
|
|
8115
|
+
const dir = path13.join(os.tmpdir(), `pixelkiln-${namespace}`);
|
|
7658
8116
|
mkdirSync(dir, { recursive: true });
|
|
7659
8117
|
return dir;
|
|
7660
8118
|
}
|
|
7661
8119
|
estimate(spec) {
|
|
8120
|
+
if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
|
|
7662
8121
|
if (spec.batch?.role === "member") {
|
|
7663
8122
|
return { unit: "generations", amount: 0, candidates: 1 };
|
|
7664
8123
|
}
|
|
@@ -7695,7 +8154,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7695
8154
|
if (spec.revision) {
|
|
7696
8155
|
const width = spec.revision.sourceWidth ?? spec.width;
|
|
7697
8156
|
const height = spec.revision.sourceHeight ?? spec.height;
|
|
7698
|
-
return { unit: "generations", amount:
|
|
8157
|
+
return { unit: "generations", amount: proTierCost(width, height), candidates: 1 };
|
|
7699
8158
|
}
|
|
7700
8159
|
if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
|
|
7701
8160
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
@@ -7716,6 +8175,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7716
8175
|
};
|
|
7717
8176
|
}
|
|
7718
8177
|
validate(spec, styleImages) {
|
|
8178
|
+
if (spec.generator === "vfx" && styleImages.length) {
|
|
8179
|
+
throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
|
|
8180
|
+
}
|
|
7719
8181
|
const members = spec.revision?.sourceMembers;
|
|
7720
8182
|
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
7721
8183
|
throw new Error(
|
|
@@ -8000,7 +8462,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8000
8462
|
}
|
|
8001
8463
|
}
|
|
8002
8464
|
if (endFrame) {
|
|
8003
|
-
const start = startFrame ?? (character.parentFile &&
|
|
8465
|
+
const start = startFrame ?? (character.parentFile && existsSync8(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
|
|
8004
8466
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
8005
8467
|
throw new Error(
|
|
8006
8468
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -8158,7 +8620,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8158
8620
|
}
|
|
8159
8621
|
}
|
|
8160
8622
|
if (endFrame) {
|
|
8161
|
-
const start = startFrame ?? (object.parentFile &&
|
|
8623
|
+
const start = startFrame ?? (object.parentFile && existsSync8(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
|
|
8162
8624
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
8163
8625
|
throw new Error(
|
|
8164
8626
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -8468,7 +8930,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8468
8930
|
const sources = images.map((image, index) => {
|
|
8469
8931
|
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
8470
8932
|
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
8471
|
-
const file =
|
|
8933
|
+
const file = path13.join(dir, `${jobId}-${index}.png`);
|
|
8472
8934
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
8473
8935
|
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
8474
8936
|
});
|
|
@@ -8620,6 +9082,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8620
9082
|
async submit(spec, styleImages, context) {
|
|
8621
9083
|
this.validate(spec, styleImages);
|
|
8622
9084
|
if (spec.revision) return this.submitRevision(spec);
|
|
9085
|
+
if (spec.vfx) return this.submitVfx(spec, context);
|
|
8623
9086
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
8624
9087
|
if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
|
|
8625
9088
|
if (spec.generator === "uiAsset") {
|
|
@@ -8673,7 +9136,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8673
9136
|
seed: spec.seed
|
|
8674
9137
|
});
|
|
8675
9138
|
const jobId = randomUUID3();
|
|
8676
|
-
writeFileSync(
|
|
9139
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
8677
9140
|
return { jobId };
|
|
8678
9141
|
}
|
|
8679
9142
|
if (spec.generator === "tiles") {
|
|
@@ -9002,11 +9465,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9002
9465
|
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
9003
9466
|
const jobId = randomUUID3();
|
|
9004
9467
|
if (!roles) {
|
|
9005
|
-
writeFileSync(
|
|
9468
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
9006
9469
|
return { jobId, metadata: { revisionUsage: usage } };
|
|
9007
9470
|
}
|
|
9008
9471
|
for (const [index, png] of pngs.entries()) {
|
|
9009
|
-
writeFileSync(
|
|
9472
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
9010
9473
|
}
|
|
9011
9474
|
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
9012
9475
|
}
|
|
@@ -9034,10 +9497,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9034
9497
|
return this.pollAnimateRevision(jobId, context);
|
|
9035
9498
|
}
|
|
9036
9499
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
9500
|
+
if (generator === "vfx") return this.pollVfx(jobId, context);
|
|
9037
9501
|
if (generator === "imageProFlash") return this.pollRevision(jobId);
|
|
9038
9502
|
if (generator === "pixflux") {
|
|
9039
|
-
const file =
|
|
9040
|
-
if (
|
|
9503
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9504
|
+
if (existsSync8(file)) {
|
|
9041
9505
|
const sourceUrl = `file://${file}`;
|
|
9042
9506
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
9043
9507
|
}
|
|
@@ -9080,6 +9544,120 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9080
9544
|
* background job to ask afterwards the way `billedForJob` does for async
|
|
9081
9545
|
* work.
|
|
9082
9546
|
*/
|
|
9547
|
+
/**
|
|
9548
|
+
* Render a `vfx` asset with the VFX workbench, which is synchronous and
|
|
9549
|
+
* free. The frames are cut from the exported sheet and parked in the
|
|
9550
|
+
* cache like pixflux's bytes; the export's download folder is recorded so
|
|
9551
|
+
* `poll` can cut them again if the cache is cleared.
|
|
9552
|
+
*
|
|
9553
|
+
* A regeneration exports over the effect this asset rendered last time, so
|
|
9554
|
+
* My effects keeps one entry per asset, unless that effect has changed on
|
|
9555
|
+
* pixellab.ai since (someone moved its sliders) or is gone. Then a new
|
|
9556
|
+
* effect is saved and the edited one is left alone.
|
|
9557
|
+
*/
|
|
9558
|
+
async submitVfx(spec, context) {
|
|
9559
|
+
const vfx = spec.vfx;
|
|
9560
|
+
if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
|
|
9561
|
+
let sprite;
|
|
9562
|
+
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
9563
|
+
sprite = vfx.sprite.reference;
|
|
9564
|
+
} else if (vfx.sprite) {
|
|
9565
|
+
const { file } = vfx.sprite;
|
|
9566
|
+
if (!existsSync8(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
9567
|
+
const bytes = readFileSync2(file);
|
|
9568
|
+
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
9569
|
+
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
9570
|
+
}
|
|
9571
|
+
const format = imageMetadata(bytes)?.format ?? "png";
|
|
9572
|
+
sprite = `data:image/${format};base64,${bytes.toString("base64")}`;
|
|
9573
|
+
}
|
|
9574
|
+
const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
|
|
9575
|
+
let replace;
|
|
9576
|
+
let newEffect;
|
|
9577
|
+
if (previous?.effectId) {
|
|
9578
|
+
const upstream = await readSavedVfxRecipe(this.mcp, previous.effectId);
|
|
9579
|
+
if (!upstream) newEffect = `effect ${previous.effectId} is no longer in My effects`;
|
|
9580
|
+
else if (!previous.recipe || vfxRecipeSha256(upstream) !== vfxRecipeSha256(previous.recipe)) {
|
|
9581
|
+
newEffect = `effect ${previous.effectId} changed on pixellab.ai since it was rendered; it was kept`;
|
|
9582
|
+
} else replace = previous.effectId;
|
|
9583
|
+
}
|
|
9584
|
+
const jobId = randomUUID3();
|
|
9585
|
+
const facts = (exported2, name2) => ({
|
|
9586
|
+
effectId: exported2.effectId,
|
|
9587
|
+
editUrl: exported2.editUrl,
|
|
9588
|
+
name: name2,
|
|
9589
|
+
imageId: exported2.imageId,
|
|
9590
|
+
width: exported2.width,
|
|
9591
|
+
height: exported2.height,
|
|
9592
|
+
frames: exported2.frames,
|
|
9593
|
+
fps: exported2.fps,
|
|
9594
|
+
loop: exported2.loop,
|
|
9595
|
+
anchor: exported2.anchor,
|
|
9596
|
+
downloadBase: exported2.downloadBase,
|
|
9597
|
+
files: exported2.files,
|
|
9598
|
+
replaced: replace ?? null,
|
|
9599
|
+
...newEffect ? { newEffect } : {}
|
|
9600
|
+
});
|
|
9601
|
+
const { exported, recipe, name } = await runVfxWorkbench(this.mcp, {
|
|
9602
|
+
preset: vfx.preset,
|
|
9603
|
+
recipe: vfx.recipe?.recipe,
|
|
9604
|
+
parts: vfx.parts,
|
|
9605
|
+
sprite,
|
|
9606
|
+
fps: vfx.fps,
|
|
9607
|
+
name: vfx.name,
|
|
9608
|
+
defaultName: vfx.name,
|
|
9609
|
+
replace,
|
|
9610
|
+
notes: `pixelkiln ${spec.styleId}/${spec.assetId}`,
|
|
9611
|
+
// The effect exists from here on; record it before anything else can fail.
|
|
9612
|
+
onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
|
|
9613
|
+
});
|
|
9614
|
+
await this.cacheVfxFrames(jobId, exported);
|
|
9615
|
+
return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
|
|
9616
|
+
}
|
|
9617
|
+
/** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
|
|
9618
|
+
async cacheVfxFrames(jobId, exported) {
|
|
9619
|
+
const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
|
|
9620
|
+
const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
|
|
9621
|
+
if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
|
|
9622
|
+
const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
|
|
9623
|
+
const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
|
|
9624
|
+
const frames = sliceVfxSheet(sheet, exported, atlas);
|
|
9625
|
+
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
9626
|
+
frames.forEach((frame, index) => writeFileSync(path13.join(dir, `${jobId}-${index}.png`), frame));
|
|
9627
|
+
writeFileSync(
|
|
9628
|
+
path13.join(dir, `${jobId}.json`),
|
|
9629
|
+
JSON.stringify({ effectId: exported.effectId, count: frames.length, fps: exported.fps, loop: exported.loop })
|
|
9630
|
+
);
|
|
9631
|
+
}
|
|
9632
|
+
async pollVfx(jobId, context) {
|
|
9633
|
+
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
9634
|
+
const sidecar = path13.join(dir, `${jobId}.json`);
|
|
9635
|
+
const cached = () => {
|
|
9636
|
+
if (!existsSync8(sidecar)) return null;
|
|
9637
|
+
const facts2 = JSON.parse(readFileSync2(sidecar, "utf8"));
|
|
9638
|
+
const files2 = Array.from({ length: facts2.count }, (_, index) => path13.join(dir, `${jobId}-${index}.png`));
|
|
9639
|
+
return files2.every((file) => existsSync8(file)) ? { facts: facts2, files: files2 } : null;
|
|
9640
|
+
};
|
|
9641
|
+
let found = cached();
|
|
9642
|
+
const recorded = context?.metadata?.vfx;
|
|
9643
|
+
if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
|
|
9644
|
+
await this.cacheVfxFrames(jobId, recorded);
|
|
9645
|
+
found = cached();
|
|
9646
|
+
}
|
|
9647
|
+
if (!found) {
|
|
9648
|
+
return { status: "failed", error: "vfx frames are no longer cached locally; re-run submit for this asset (free)" };
|
|
9649
|
+
}
|
|
9650
|
+
const { facts, files } = found;
|
|
9651
|
+
const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
9652
|
+
return {
|
|
9653
|
+
status: "ready",
|
|
9654
|
+
objectId: facts.effectId,
|
|
9655
|
+
sourceUrl: sources[0].url,
|
|
9656
|
+
sources,
|
|
9657
|
+
...files.length > 1 ? { metadata: { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } } : {},
|
|
9658
|
+
billed: null
|
|
9659
|
+
};
|
|
9660
|
+
}
|
|
9083
9661
|
async pollCachedRevision(jobId, context) {
|
|
9084
9662
|
const usage = context?.metadata?.revisionUsage;
|
|
9085
9663
|
const roles = context?.metadata?.revisionMembers;
|
|
@@ -9089,10 +9667,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9089
9667
|
};
|
|
9090
9668
|
if (Array.isArray(roles) && roles.length) {
|
|
9091
9669
|
const sources = roles.map((role, index) => ({
|
|
9092
|
-
url: `file://${
|
|
9670
|
+
url: `file://${path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
9093
9671
|
role: String(role)
|
|
9094
9672
|
}));
|
|
9095
|
-
if (sources.some((source) => !
|
|
9673
|
+
if (sources.some((source) => !existsSync8(source.url.slice("file://".length)))) return gone;
|
|
9096
9674
|
const fps = context?.metadata?.revisionFps;
|
|
9097
9675
|
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
9098
9676
|
return {
|
|
@@ -9104,8 +9682,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9104
9682
|
billed: billedFromUsage(usage)
|
|
9105
9683
|
};
|
|
9106
9684
|
}
|
|
9107
|
-
const file =
|
|
9108
|
-
if (!
|
|
9685
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9686
|
+
if (!existsSync8(file)) return gone;
|
|
9109
9687
|
const sourceUrl = `file://${file}`;
|
|
9110
9688
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
9111
9689
|
}
|
|
@@ -9161,7 +9739,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9161
9739
|
}
|
|
9162
9740
|
const base64 = extractBase64(item);
|
|
9163
9741
|
if (base64) {
|
|
9164
|
-
const file =
|
|
9742
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9165
9743
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9166
9744
|
urls.push(`file://${file}`);
|
|
9167
9745
|
continue;
|
|
@@ -9212,7 +9790,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9212
9790
|
const candidate = done[key];
|
|
9213
9791
|
const base64 = extractBase64(candidate);
|
|
9214
9792
|
if (base64) {
|
|
9215
|
-
const file =
|
|
9793
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9216
9794
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9217
9795
|
const sourceUrl = `file://${file}`;
|
|
9218
9796
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -9221,7 +9799,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9221
9799
|
if (Array.isArray(done.images) && done.images.length) {
|
|
9222
9800
|
const base64 = extractBase64(done.images[0]);
|
|
9223
9801
|
if (base64) {
|
|
9224
|
-
const file =
|
|
9802
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9225
9803
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9226
9804
|
const sourceUrl = `file://${file}`;
|
|
9227
9805
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -9255,7 +9833,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9255
9833
|
for (const [index, candidate] of done.images.entries()) {
|
|
9256
9834
|
const base64 = extractBase64(candidate);
|
|
9257
9835
|
if (!base64) continue;
|
|
9258
|
-
const file =
|
|
9836
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9259
9837
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9260
9838
|
urls.push(`file://${file}`);
|
|
9261
9839
|
}
|
|
@@ -9285,7 +9863,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9285
9863
|
for (const [index, candidate] of images.entries()) {
|
|
9286
9864
|
const base64 = extractBase64(candidate);
|
|
9287
9865
|
if (!base64) continue;
|
|
9288
|
-
const file =
|
|
9866
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9289
9867
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9290
9868
|
urls.push(`file://${file}`);
|
|
9291
9869
|
}
|
|
@@ -9383,7 +9961,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9383
9961
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
9384
9962
|
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
9385
9963
|
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
9386
|
-
const file =
|
|
9964
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
9387
9965
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
9388
9966
|
sources.push({ url: `file://${file}`, role });
|
|
9389
9967
|
terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
|
|
@@ -9413,7 +9991,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9413
9991
|
const { image, usage } = await this.client.getIsometricTile(tileId);
|
|
9414
9992
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
9415
9993
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
9416
|
-
const file =
|
|
9994
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
|
|
9417
9995
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
9418
9996
|
return {
|
|
9419
9997
|
status: "ready",
|
|
@@ -9457,8 +10035,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9457
10035
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
9458
10036
|
}
|
|
9459
10037
|
if (generator === "imagePro" || generator === "uiElement") {
|
|
9460
|
-
const file =
|
|
9461
|
-
if (!
|
|
10038
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
10039
|
+
if (!existsSync8(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
9462
10040
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
9463
10041
|
}
|
|
9464
10042
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -9735,9 +10313,9 @@ var RetroDiffusionClient = class {
|
|
|
9735
10313
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9736
10314
|
return Buffer.from(await response.arrayBuffer());
|
|
9737
10315
|
}
|
|
9738
|
-
async call(
|
|
10316
|
+
async call(path53, init = {}) {
|
|
9739
10317
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9740
|
-
const response = await this.request(`${this.baseUrl}${
|
|
10318
|
+
const response = await this.request(`${this.baseUrl}${path53}`, {
|
|
9741
10319
|
...init,
|
|
9742
10320
|
headers: {
|
|
9743
10321
|
"Content-Type": "application/json",
|
|
@@ -10049,7 +10627,7 @@ function retroError(value) {
|
|
|
10049
10627
|
import { createHash as createHash3 } from "crypto";
|
|
10050
10628
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
10051
10629
|
import os2 from "os";
|
|
10052
|
-
import
|
|
10630
|
+
import path14 from "path";
|
|
10053
10631
|
var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
|
|
10054
10632
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
10055
10633
|
var SUBMIT_RETRIES = 8;
|
|
@@ -10432,7 +11010,7 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10432
11010
|
}
|
|
10433
11011
|
};
|
|
10434
11012
|
function uploadCachePath() {
|
|
10435
|
-
return process.env.SCENARIO_UPLOAD_CACHE ??
|
|
11013
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? path14.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
10436
11014
|
}
|
|
10437
11015
|
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
10438
11016
|
return createHash3("sha256").update(`${baseUrl}
|
|
@@ -10450,7 +11028,7 @@ function readUploadCache() {
|
|
|
10450
11028
|
function writeUploadCache(key, assetId) {
|
|
10451
11029
|
try {
|
|
10452
11030
|
const file = uploadCachePath();
|
|
10453
|
-
mkdirSync2(
|
|
11031
|
+
mkdirSync2(path14.dirname(file), { recursive: true });
|
|
10454
11032
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10455
11033
|
writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
10456
11034
|
renameSync(tmp, file);
|
|
@@ -10710,9 +11288,9 @@ registerProvider({
|
|
|
10710
11288
|
});
|
|
10711
11289
|
|
|
10712
11290
|
// src/manifest.ts
|
|
10713
|
-
import { readFile as
|
|
10714
|
-
import { existsSync as
|
|
10715
|
-
import
|
|
11291
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
11292
|
+
import { existsSync as existsSync9 } from "fs";
|
|
11293
|
+
import path15 from "path";
|
|
10716
11294
|
|
|
10717
11295
|
// src/loop-directions.ts
|
|
10718
11296
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -10801,9 +11379,9 @@ function isObject2(value) {
|
|
|
10801
11379
|
|
|
10802
11380
|
// src/manifest.ts
|
|
10803
11381
|
async function loadManifest(manifestPath) {
|
|
10804
|
-
const abs =
|
|
10805
|
-
if (!
|
|
10806
|
-
const expansion = expandLoopDirections(JSON.parse(await
|
|
11382
|
+
const abs = path15.resolve(manifestPath);
|
|
11383
|
+
if (!existsSync9(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
11384
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile7(abs, "utf8")));
|
|
10807
11385
|
if (expansion.issues.length) {
|
|
10808
11386
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
10809
11387
|
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
@@ -10858,7 +11436,7 @@ ${issues}`);
|
|
|
10858
11436
|
if (fromStyle) {
|
|
10859
11437
|
if (!styleIds.has(fromStyle)) {
|
|
10860
11438
|
unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
|
|
10861
|
-
} else if (parent && parent
|
|
11439
|
+
} else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
|
|
10862
11440
|
unknownReferences.push(
|
|
10863
11441
|
`assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
|
|
10864
11442
|
);
|
|
@@ -10905,7 +11483,11 @@ ${issues}`);
|
|
|
10905
11483
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
10906
11484
|
}
|
|
10907
11485
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
10908
|
-
return { manifest: parsed.data, root:
|
|
11486
|
+
return { manifest: parsed.data, root: path15.dirname(abs), path: abs, ...loopFamilies };
|
|
11487
|
+
}
|
|
11488
|
+
function assetInStyle(asset, styleId, style) {
|
|
11489
|
+
if (asset.styles.length) return asset.styles.includes(styleId);
|
|
11490
|
+
return style?.generator === "vfx" === Boolean(asset.vfx);
|
|
10909
11491
|
}
|
|
10910
11492
|
function parentAssetId(asset) {
|
|
10911
11493
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
@@ -11009,8 +11591,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11009
11591
|
const needed = /* @__PURE__ */ new Set();
|
|
11010
11592
|
for (const assetId of resolutionAssetIds) {
|
|
11011
11593
|
const asset = manifest.assets[assetId];
|
|
11012
|
-
if (asset
|
|
11013
|
-
const fromStyle = asset
|
|
11594
|
+
if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
|
|
11595
|
+
const fromStyle = asset.revision?.fromStyle;
|
|
11014
11596
|
if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
|
|
11015
11597
|
}
|
|
11016
11598
|
styleDependencies.set(styleId, needed);
|
|
@@ -11039,11 +11621,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11039
11621
|
const styleFinalizers = /* @__PURE__ */ new Map();
|
|
11040
11622
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
11041
11623
|
async function loadStyleImage(rel, what = "Style image") {
|
|
11042
|
-
const abs =
|
|
11624
|
+
const abs = path15.resolve(root, rel);
|
|
11043
11625
|
let hit = styleImageCache.get(abs);
|
|
11044
11626
|
if (!hit) {
|
|
11045
|
-
if (!
|
|
11046
|
-
const buf = await
|
|
11627
|
+
if (!existsSync9(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
11628
|
+
const buf = await readFile7(abs);
|
|
11047
11629
|
const metadata = imageMetadata(buf);
|
|
11048
11630
|
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
11049
11631
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -11069,14 +11651,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11069
11651
|
styleImageDimensions.push(loadedImage);
|
|
11070
11652
|
}
|
|
11071
11653
|
const resolvedImages = style.styleImages.map((image) => {
|
|
11072
|
-
const hit = styleImageCache.get(
|
|
11654
|
+
const hit = styleImageCache.get(path15.resolve(root, image.path));
|
|
11073
11655
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
11074
11656
|
});
|
|
11075
11657
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
11076
11658
|
const providerInputIdentities = /* @__PURE__ */ new Map();
|
|
11077
11659
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
11078
11660
|
if (!resolutionAssetIds.has(assetId)) continue;
|
|
11079
|
-
if (asset
|
|
11661
|
+
if (!assetInStyle(asset, styleId, style)) continue;
|
|
11080
11662
|
const generator = style.generator;
|
|
11081
11663
|
if (!activeProvider.supports(generator)) {
|
|
11082
11664
|
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
@@ -11096,6 +11678,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11096
11678
|
const providerInputs = inputResolution.inputs;
|
|
11097
11679
|
const providerInputIdentity = inputResolution.identity ?? providerInputs;
|
|
11098
11680
|
providerInputIdentities.set(assetId, providerInputIdentity);
|
|
11681
|
+
if (asset.vfx && generator !== "vfx") {
|
|
11682
|
+
throw new Error(`assets.${assetId}: vfx needs a vfx style; "${styleId}" generates ${generator}`);
|
|
11683
|
+
}
|
|
11684
|
+
if (generator === "vfx" && !asset.vfx && !asset.mirror && !asset.revision) {
|
|
11685
|
+
throw new Error(
|
|
11686
|
+
`assets.${assetId}: style "${styleId}" renders effects; give the asset a vfx block, or make it a mirror or revision of one`
|
|
11687
|
+
);
|
|
11688
|
+
}
|
|
11689
|
+
const vfx = generator === "vfx" && asset.vfx ? await resolveVfxShape(root, assetId, asset.vfx, `pixelkiln:${styleId}/${assetId}`) : void 0;
|
|
11099
11690
|
let width;
|
|
11100
11691
|
let height;
|
|
11101
11692
|
let size;
|
|
@@ -11123,6 +11714,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11123
11714
|
size = asset.size ?? style.size ?? 64;
|
|
11124
11715
|
width = size;
|
|
11125
11716
|
height = size;
|
|
11717
|
+
} else if (generator === "vfx") {
|
|
11718
|
+
const canvas = vfx?.recipe?.recipe.canvas;
|
|
11719
|
+
width = typeof canvas?.w === "number" ? canvas.w : asset.width ?? style.size ?? 64;
|
|
11720
|
+
height = typeof canvas?.h === "number" ? canvas.h : asset.height ?? style.size ?? 64;
|
|
11721
|
+
size = Math.max(width, height);
|
|
11126
11722
|
} else if (generator === "uiAsset" || generator === "uiElement") {
|
|
11127
11723
|
width = asset.width ?? style.size ?? 256;
|
|
11128
11724
|
height = asset.height ?? style.size ?? 256;
|
|
@@ -11165,14 +11761,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11165
11761
|
terrainUpperDescription = wrap(parsed.upper);
|
|
11166
11762
|
terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
|
|
11167
11763
|
prompt = subject.trim();
|
|
11168
|
-
} else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
|
|
11764
|
+
} else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
|
|
11169
11765
|
prompt = subject.trim();
|
|
11170
11766
|
} else {
|
|
11171
11767
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
11172
11768
|
}
|
|
11173
|
-
const relFile = asset.file ??
|
|
11174
|
-
const outFile =
|
|
11175
|
-
const qualityOutFile = style.quality ?
|
|
11769
|
+
const relFile = asset.file ?? path15.join(asset.category ?? "", `${assetId}.png`);
|
|
11770
|
+
const outFile = path15.resolve(root, style.outDir, relFile);
|
|
11771
|
+
const qualityOutFile = style.quality ? path15.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
11176
11772
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
11177
11773
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
11178
11774
|
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
@@ -11243,6 +11839,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11243
11839
|
// A portrait needs a character style (refused above), so an
|
|
11244
11840
|
// objectPro asset is only ever a base, state, or animation.
|
|
11245
11841
|
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
11842
|
+
...vfx ? { vfx } : {},
|
|
11246
11843
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
11247
11844
|
costUnit: "generations",
|
|
11248
11845
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -11267,7 +11864,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11267
11864
|
minGridConfidence: style.quality.minGridConfidence,
|
|
11268
11865
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
11269
11866
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
11270
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
11867
|
+
...style.quality.fixerPython ? { fixerPython: path15.resolve(root, style.quality.fixerPython) } : {},
|
|
11271
11868
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
11272
11869
|
}
|
|
11273
11870
|
} : {},
|
|
@@ -11350,7 +11947,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11350
11947
|
const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
|
|
11351
11948
|
resolved.character = {
|
|
11352
11949
|
...resolved.character,
|
|
11353
|
-
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256:
|
|
11950
|
+
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync9(file) ? await sha256File(file) : null }
|
|
11354
11951
|
};
|
|
11355
11952
|
}
|
|
11356
11953
|
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
@@ -11363,7 +11960,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11363
11960
|
parentAssetId: characterParent,
|
|
11364
11961
|
parentSpec,
|
|
11365
11962
|
parentFile,
|
|
11366
|
-
parentSha256:
|
|
11963
|
+
parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
|
|
11367
11964
|
// A state and its animations keep the engine and rotations of the base.
|
|
11368
11965
|
mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
|
|
11369
11966
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
@@ -11381,7 +11978,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11381
11978
|
parentAssetId: characterParent,
|
|
11382
11979
|
parentSpec,
|
|
11383
11980
|
parentFile,
|
|
11384
|
-
parentSha256:
|
|
11981
|
+
parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
|
|
11385
11982
|
// A state and its animations keep the base's own rotation count.
|
|
11386
11983
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
11387
11984
|
};
|
|
@@ -11403,7 +12000,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11403
12000
|
animation: sourceSpec.character.animation,
|
|
11404
12001
|
outfit: {
|
|
11405
12002
|
reference: {
|
|
11406
|
-
path:
|
|
12003
|
+
path: path15.resolve(root, asset.outfit.reference),
|
|
11407
12004
|
sha256: referenceImage.hash,
|
|
11408
12005
|
width: referenceImage.width,
|
|
11409
12006
|
height: referenceImage.height,
|
|
@@ -11426,23 +12023,23 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11426
12023
|
}
|
|
11427
12024
|
const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
|
|
11428
12025
|
const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
|
|
11429
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
12026
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path15.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
11430
12027
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
11431
12028
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
11432
12029
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
11433
12030
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
11434
|
-
const maskFile = asset.revision.mask ?
|
|
12031
|
+
const maskFile = asset.revision.mask ? path15.resolve(root, asset.revision.mask) : void 0;
|
|
11435
12032
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
11436
12033
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
11437
12034
|
throw new Error(
|
|
11438
12035
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
11439
12036
|
);
|
|
11440
12037
|
}
|
|
11441
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
12038
|
+
const paletteImageFile = asset.revision.paletteImage ? path15.resolve(root, asset.revision.paletteImage) : void 0;
|
|
11442
12039
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
11443
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
12040
|
+
const lastFrameFile = asset.revision.lastFrame ? path15.resolve(root, asset.revision.lastFrame) : void 0;
|
|
11444
12041
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
11445
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
12042
|
+
const keypointsFile = asset.revision.keypointsFile ? path15.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
11446
12043
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
11447
12044
|
resolved.revision = {
|
|
11448
12045
|
mode: asset.revision.mode,
|
|
@@ -11612,7 +12209,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11612
12209
|
return specs;
|
|
11613
12210
|
}
|
|
11614
12211
|
function pngPath(file) {
|
|
11615
|
-
const extension =
|
|
12212
|
+
const extension = path15.extname(file);
|
|
11616
12213
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
11617
12214
|
}
|
|
11618
12215
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -11622,12 +12219,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
11622
12219
|
if (!provider.supportsMapScene) {
|
|
11623
12220
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
11624
12221
|
}
|
|
11625
|
-
const file =
|
|
12222
|
+
const file = path15.resolve(root, scene.image);
|
|
11626
12223
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
11627
12224
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
11628
12225
|
const placement = scene.placement;
|
|
11629
12226
|
if ("mask" in placement) {
|
|
11630
|
-
const maskFile =
|
|
12227
|
+
const maskFile = path15.resolve(root, placement.mask);
|
|
11631
12228
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
11632
12229
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
11633
12230
|
throw new Error(
|
|
@@ -11641,9 +12238,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
11641
12238
|
}
|
|
11642
12239
|
return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
|
|
11643
12240
|
}
|
|
12241
|
+
async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
12242
|
+
let recipe;
|
|
12243
|
+
let recordedSprite;
|
|
12244
|
+
if (vfx.recipe) {
|
|
12245
|
+
const file = path15.resolve(root, vfx.recipe);
|
|
12246
|
+
if (!existsSync9(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
|
|
12247
|
+
const read = await readVfxRecipe(file).catch((error) => {
|
|
12248
|
+
throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
|
|
12249
|
+
});
|
|
12250
|
+
recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
|
|
12251
|
+
recordedSprite = read.sprite;
|
|
12252
|
+
}
|
|
12253
|
+
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : path15.resolve(root, vfx.sprite) : recordedSprite;
|
|
12254
|
+
let sprite;
|
|
12255
|
+
if (spriteRef && isVfxReference(spriteRef)) {
|
|
12256
|
+
sprite = { reference: spriteRef };
|
|
12257
|
+
} else if (spriteRef) {
|
|
12258
|
+
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`);
|
|
12259
|
+
sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
|
|
12260
|
+
}
|
|
12261
|
+
return {
|
|
12262
|
+
...vfx.preset ? { preset: vfx.preset } : {},
|
|
12263
|
+
...recipe ? { recipe } : {},
|
|
12264
|
+
...vfx.parts?.length ? { parts: vfx.parts } : {},
|
|
12265
|
+
...sprite ? { sprite } : {},
|
|
12266
|
+
...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
|
|
12267
|
+
name: vfx.name ?? defaultName
|
|
12268
|
+
};
|
|
12269
|
+
}
|
|
11644
12270
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
11645
|
-
if (!
|
|
11646
|
-
const bytes = await
|
|
12271
|
+
if (!existsSync9(file)) return null;
|
|
12272
|
+
const bytes = await readFile7(file);
|
|
11647
12273
|
const metadata = imageMetadata(bytes);
|
|
11648
12274
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
11649
12275
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -11652,8 +12278,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
11652
12278
|
return { hash: sha256(bytes), ...metadata };
|
|
11653
12279
|
}
|
|
11654
12280
|
async function optionalSkeletonSet(file) {
|
|
11655
|
-
if (!
|
|
11656
|
-
const bytes = await
|
|
12281
|
+
if (!existsSync9(file)) return null;
|
|
12282
|
+
const bytes = await readFile7(file);
|
|
11657
12283
|
let json;
|
|
11658
12284
|
try {
|
|
11659
12285
|
json = JSON.parse(bytes.toString("utf8"));
|
|
@@ -11663,17 +12289,17 @@ async function optionalSkeletonSet(file) {
|
|
|
11663
12289
|
return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
|
|
11664
12290
|
}
|
|
11665
12291
|
async function revisionSourceMembers(stem, label) {
|
|
11666
|
-
if (
|
|
12292
|
+
if (existsSync9(stem)) return null;
|
|
11667
12293
|
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
11668
12294
|
let roles = [];
|
|
11669
12295
|
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
11670
|
-
if (set.every((direction) =>
|
|
12296
|
+
if (set.every((direction) => existsSync9(at(direction)))) {
|
|
11671
12297
|
roles = [...set];
|
|
11672
12298
|
break;
|
|
11673
12299
|
}
|
|
11674
12300
|
}
|
|
11675
12301
|
if (!roles.length) {
|
|
11676
|
-
for (let index = 0;
|
|
12302
|
+
for (let index = 0; existsSync9(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
11677
12303
|
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
11678
12304
|
}
|
|
11679
12305
|
}
|
|
@@ -11699,8 +12325,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
11699
12325
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
11700
12326
|
const out = [];
|
|
11701
12327
|
for (const img of style.styleImages) {
|
|
11702
|
-
const file =
|
|
11703
|
-
const buf = await
|
|
12328
|
+
const file = path15.resolve(loaded.root, img.path);
|
|
12329
|
+
const buf = await readFile7(file);
|
|
11704
12330
|
const metadata = imageMetadata(buf);
|
|
11705
12331
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
11706
12332
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -11727,7 +12353,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11727
12353
|
};
|
|
11728
12354
|
if (asset.concept) {
|
|
11729
12355
|
const image = await files.load(asset.concept, "Concept image");
|
|
11730
|
-
shape.concept = { path:
|
|
12356
|
+
shape.concept = { path: path15.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11731
12357
|
}
|
|
11732
12358
|
if (asset.reference) {
|
|
11733
12359
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -11735,7 +12361,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11735
12361
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
11736
12362
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
11737
12363
|
shape.reference[direction] = {
|
|
11738
|
-
path:
|
|
12364
|
+
path: path15.resolve(files.root, rel),
|
|
11739
12365
|
sha256: image.hash,
|
|
11740
12366
|
width: image.width,
|
|
11741
12367
|
height: image.height,
|
|
@@ -11756,7 +12382,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11756
12382
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
11757
12383
|
const pose = async (rel, what) => {
|
|
11758
12384
|
const image = await files.load(rel, what);
|
|
11759
|
-
return { path:
|
|
12385
|
+
return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11760
12386
|
};
|
|
11761
12387
|
shape.animation = {
|
|
11762
12388
|
mode: animationMode,
|
|
@@ -11787,7 +12413,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11787
12413
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
11788
12414
|
if (south) {
|
|
11789
12415
|
const image = await files.load(south, "Reference sprite");
|
|
11790
|
-
shape.reference = { path:
|
|
12416
|
+
shape.reference = { path: path15.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11791
12417
|
}
|
|
11792
12418
|
}
|
|
11793
12419
|
if (asset.state) {
|
|
@@ -11815,7 +12441,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11815
12441
|
}
|
|
11816
12442
|
const pose = async (rel, what) => {
|
|
11817
12443
|
const image = await files.load(rel, what);
|
|
11818
|
-
return { path:
|
|
12444
|
+
return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11819
12445
|
};
|
|
11820
12446
|
shape.animation = {
|
|
11821
12447
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -11832,9 +12458,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11832
12458
|
}
|
|
11833
12459
|
|
|
11834
12460
|
// src/pipeline/fetch.ts
|
|
11835
|
-
import { existsSync as
|
|
11836
|
-
import { mkdir as
|
|
11837
|
-
import
|
|
12461
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
12462
|
+
import { mkdir as mkdir5, readFile as readFile8, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
12463
|
+
import path16 from "path";
|
|
11838
12464
|
|
|
11839
12465
|
// src/source-url.ts
|
|
11840
12466
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -11880,7 +12506,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11880
12506
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
11881
12507
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
11882
12508
|
normalizeLockOutputPaths(lock, specs);
|
|
11883
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
12509
|
+
const cacheDir = opts.cacheDir === false ? null : path16.resolve(opts.cacheDir ?? path16.join(path16.dirname(lockPath), ".pixelkiln", "cache"));
|
|
11884
12510
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
11885
12511
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
11886
12512
|
if (specByKey.get(key)?.mirror) return false;
|
|
@@ -11891,14 +12517,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11891
12517
|
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
11892
12518
|
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
11893
12519
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11894
|
-
return
|
|
12520
|
+
return existsSync10(file) && sha256(readFileSync4(file)) === output.sha256;
|
|
11895
12521
|
});
|
|
11896
12522
|
}
|
|
11897
12523
|
if (!opts.repair) return false;
|
|
11898
12524
|
if (e.outputs.length === 0 || !spec) return true;
|
|
11899
12525
|
return e.outputs.some((output) => {
|
|
11900
12526
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11901
|
-
if (!
|
|
12527
|
+
if (!existsSync10(file)) return true;
|
|
11902
12528
|
const current = sha256(readFileSync4(file));
|
|
11903
12529
|
if (current === output.sha256) return false;
|
|
11904
12530
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
@@ -11982,21 +12608,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11982
12608
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
11983
12609
|
fresh = await provider.download(source.url);
|
|
11984
12610
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
11985
|
-
log2(` current ${
|
|
12611
|
+
log2(` current ${path16.relative(process.cwd(), target)}`);
|
|
11986
12612
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
11987
12613
|
continue;
|
|
11988
12614
|
}
|
|
11989
12615
|
}
|
|
11990
12616
|
let raw = null;
|
|
11991
12617
|
let final = null;
|
|
11992
|
-
if (
|
|
12618
|
+
if (existsSync10(target)) {
|
|
11993
12619
|
const currentHash = await sha256File(target);
|
|
11994
12620
|
if (recorded && currentHash === recorded.sha256) {
|
|
11995
12621
|
if (!fresh && !reapply) {
|
|
11996
12622
|
if (cacheDir) {
|
|
11997
12623
|
await cacheMedia(
|
|
11998
12624
|
cacheDir,
|
|
11999
|
-
await
|
|
12625
|
+
await readFile8(target),
|
|
12000
12626
|
recorded.mediaType ?? MediaType.PNG,
|
|
12001
12627
|
recorded.sha256
|
|
12002
12628
|
);
|
|
@@ -12005,10 +12631,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12005
12631
|
continue;
|
|
12006
12632
|
}
|
|
12007
12633
|
if (reapply && !fresh) {
|
|
12008
|
-
if (!recorded.raw) raw = await
|
|
12009
|
-
log2(` reapply ${
|
|
12634
|
+
if (!recorded.raw) raw = await readFile8(target);
|
|
12635
|
+
log2(` reapply ${path16.relative(process.cwd(), target)}`);
|
|
12010
12636
|
} else {
|
|
12011
|
-
log2(` changed ${
|
|
12637
|
+
log2(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
|
|
12012
12638
|
}
|
|
12013
12639
|
} else {
|
|
12014
12640
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -12029,14 +12655,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12029
12655
|
}
|
|
12030
12656
|
if (outgoing && cacheDir) {
|
|
12031
12657
|
try {
|
|
12032
|
-
const oldBytes = await
|
|
12658
|
+
const oldBytes = await readFile8(target);
|
|
12033
12659
|
const oldMediaType = detectMediaType(oldBytes);
|
|
12034
12660
|
if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
|
|
12035
12661
|
} catch {
|
|
12036
12662
|
}
|
|
12037
12663
|
}
|
|
12038
12664
|
log2(
|
|
12039
|
-
` replace ${
|
|
12665
|
+
` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
12040
12666
|
);
|
|
12041
12667
|
}
|
|
12042
12668
|
}
|
|
@@ -12045,7 +12671,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12045
12671
|
} else if (!raw && recorded && cacheDir) {
|
|
12046
12672
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
12047
12673
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
12048
|
-
if (final || raw) log2(` cached ${
|
|
12674
|
+
if (final || raw) log2(` cached ${path16.relative(process.cwd(), target)}`);
|
|
12049
12675
|
}
|
|
12050
12676
|
if (!final && !raw) {
|
|
12051
12677
|
if (!source.url) {
|
|
@@ -12072,13 +12698,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12072
12698
|
buf = processed.bytes;
|
|
12073
12699
|
if (processed.changed) {
|
|
12074
12700
|
rawHash = sha256(raw);
|
|
12075
|
-
log2(` snapped ${
|
|
12701
|
+
log2(` snapped ${path16.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
12076
12702
|
}
|
|
12077
12703
|
}
|
|
12078
12704
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
12079
|
-
await
|
|
12705
|
+
await mkdir5(path16.dirname(target), { recursive: true });
|
|
12080
12706
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
12081
|
-
await
|
|
12707
|
+
await writeFile5(tmp, buf);
|
|
12082
12708
|
outputs.push({
|
|
12083
12709
|
path: portableOutputPath(target, spec.root),
|
|
12084
12710
|
sha256: sha256(buf),
|
|
@@ -12090,7 +12716,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12090
12716
|
await saveLock(lockPath, lock);
|
|
12091
12717
|
await rename4(tmp, target);
|
|
12092
12718
|
wrote = true;
|
|
12093
|
-
log2(` wrote ${
|
|
12719
|
+
log2(` wrote ${path16.relative(process.cwd(), target)}`);
|
|
12094
12720
|
}
|
|
12095
12721
|
if (opts.refresh && !wrote) {
|
|
12096
12722
|
if (rotated) {
|
|
@@ -12134,10 +12760,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12134
12760
|
return result;
|
|
12135
12761
|
}
|
|
12136
12762
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
12137
|
-
const file =
|
|
12138
|
-
if (!
|
|
12763
|
+
const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12764
|
+
if (!existsSync10(file)) return null;
|
|
12139
12765
|
try {
|
|
12140
|
-
const buf = await
|
|
12766
|
+
const buf = await readFile8(file);
|
|
12141
12767
|
if (sha256(buf) !== hash) return null;
|
|
12142
12768
|
validateMedia(buf, mediaType);
|
|
12143
12769
|
return buf;
|
|
@@ -12148,15 +12774,15 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
12148
12774
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
12149
12775
|
const hash = knownHash ?? sha256(buf);
|
|
12150
12776
|
validateMedia(buf, mediaType);
|
|
12151
|
-
const file =
|
|
12777
|
+
const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12152
12778
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
12153
|
-
await
|
|
12779
|
+
await mkdir5(cacheDir, { recursive: true });
|
|
12154
12780
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12155
|
-
await
|
|
12781
|
+
await writeFile5(tmp, buf);
|
|
12156
12782
|
try {
|
|
12157
12783
|
await rename4(tmp, file);
|
|
12158
12784
|
} catch (err) {
|
|
12159
|
-
if (!
|
|
12785
|
+
if (!existsSync10(file)) throw err;
|
|
12160
12786
|
} finally {
|
|
12161
12787
|
await rm4(tmp, { force: true });
|
|
12162
12788
|
}
|
|
@@ -12176,7 +12802,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
12176
12802
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
12177
12803
|
const spec = specByKey.get(key);
|
|
12178
12804
|
if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
|
|
12179
|
-
if (entry.generator === "pixflux") continue;
|
|
12805
|
+
if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
|
|
12180
12806
|
if (!provider.setTags) return tagged;
|
|
12181
12807
|
try {
|
|
12182
12808
|
await provider.setTags(entry.objectId, spec.tags, entry.generator);
|
|
@@ -12189,13 +12815,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
12189
12815
|
}
|
|
12190
12816
|
|
|
12191
12817
|
// src/pipeline/adopt.ts
|
|
12192
|
-
import { existsSync as
|
|
12193
|
-
import { readFile as
|
|
12194
|
-
import
|
|
12818
|
+
import { existsSync as existsSync12 } from "fs";
|
|
12819
|
+
import { readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
12820
|
+
import path17 from "path";
|
|
12195
12821
|
|
|
12196
12822
|
// src/cache.ts
|
|
12197
|
-
import { mkdir as
|
|
12198
|
-
import { existsSync as
|
|
12823
|
+
import { mkdir as mkdir6, readFile as readFile9, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
12824
|
+
import { existsSync as existsSync11 } from "fs";
|
|
12199
12825
|
import pathModule from "path";
|
|
12200
12826
|
import { z as z4 } from "zod";
|
|
12201
12827
|
var HashCacheSchema = z4.object({
|
|
@@ -12210,10 +12836,10 @@ function isSha256Hash(value) {
|
|
|
12210
12836
|
function parseCache(value) {
|
|
12211
12837
|
return HashCacheSchema.parse(value);
|
|
12212
12838
|
}
|
|
12213
|
-
async function loadCache(
|
|
12214
|
-
if (!
|
|
12839
|
+
async function loadCache(path53) {
|
|
12840
|
+
if (!existsSync11(path53)) return { version: 1, hashes: {} };
|
|
12215
12841
|
try {
|
|
12216
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
12842
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path53, "utf8")));
|
|
12217
12843
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
12218
12844
|
return {
|
|
12219
12845
|
version: 1,
|
|
@@ -12225,18 +12851,18 @@ async function loadCache(path52) {
|
|
|
12225
12851
|
return { version: 1, hashes: {} };
|
|
12226
12852
|
}
|
|
12227
12853
|
}
|
|
12228
|
-
async function saveCache(
|
|
12854
|
+
async function saveCache(path53, cache) {
|
|
12229
12855
|
const sorted = {};
|
|
12230
12856
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
12231
12857
|
const hash = cache.hashes[key];
|
|
12232
12858
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
12233
12859
|
sorted[key] = hash;
|
|
12234
12860
|
}
|
|
12235
|
-
await
|
|
12236
|
-
const tmp = `${
|
|
12861
|
+
await mkdir6(pathModule.dirname(pathModule.resolve(path53)), { recursive: true });
|
|
12862
|
+
const tmp = `${path53}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12237
12863
|
try {
|
|
12238
|
-
await
|
|
12239
|
-
await rename5(tmp,
|
|
12864
|
+
await writeFile6(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
12865
|
+
await rename5(tmp, path53);
|
|
12240
12866
|
} finally {
|
|
12241
12867
|
await rm5(tmp, { force: true });
|
|
12242
12868
|
}
|
|
@@ -12270,11 +12896,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12270
12896
|
const localByHash = /* @__PURE__ */ new Map();
|
|
12271
12897
|
const unmatchedLocal = [];
|
|
12272
12898
|
for (const spec of specs) {
|
|
12273
|
-
if (!
|
|
12899
|
+
if (!existsSync12(spec.outFile)) {
|
|
12274
12900
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
12275
12901
|
continue;
|
|
12276
12902
|
}
|
|
12277
|
-
const bytes = await
|
|
12903
|
+
const bytes = await readFile10(spec.outFile);
|
|
12278
12904
|
try {
|
|
12279
12905
|
decodePng(bytes);
|
|
12280
12906
|
} catch (err) {
|
|
@@ -12394,7 +13020,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
12394
13020
|
const log2 = opts.onProgress ?? (() => {
|
|
12395
13021
|
});
|
|
12396
13022
|
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
12397
|
-
const raw = JSON.parse(await
|
|
13023
|
+
const raw = JSON.parse(await readFile10(manifestPath, "utf8"));
|
|
12398
13024
|
const best = /* @__PURE__ */ new Map();
|
|
12399
13025
|
for (const entry of Object.values(lock.entries)) {
|
|
12400
13026
|
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
@@ -12416,7 +13042,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
12416
13042
|
stillEmpty.push(id);
|
|
12417
13043
|
}
|
|
12418
13044
|
}
|
|
12419
|
-
await
|
|
13045
|
+
await writeFile7(manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12420
13046
|
log2(` filled ${filled} prompt(s) from adopted objects`);
|
|
12421
13047
|
return { filled, stillEmpty };
|
|
12422
13048
|
}
|
|
@@ -12427,9 +13053,9 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
12427
13053
|
}
|
|
12428
13054
|
|
|
12429
13055
|
// src/pipeline/adopt-characters.ts
|
|
12430
|
-
import { existsSync as
|
|
12431
|
-
import { mkdir as
|
|
12432
|
-
import
|
|
13056
|
+
import { existsSync as existsSync13 } from "fs";
|
|
13057
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
|
|
13058
|
+
import path18 from "path";
|
|
12433
13059
|
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
12434
13060
|
const log2 = opts.onProgress ?? (() => {
|
|
12435
13061
|
});
|
|
@@ -12480,12 +13106,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12480
13106
|
const bytes = await download(source.url);
|
|
12481
13107
|
const hash = sha256(bytes);
|
|
12482
13108
|
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
12483
|
-
if (
|
|
12484
|
-
if (sha256(await
|
|
13109
|
+
if (existsSync13(file)) {
|
|
13110
|
+
if (sha256(await readFile11(file)) !== hash) result.differing.push(path18.relative(process.cwd(), file));
|
|
12485
13111
|
} else {
|
|
12486
|
-
await
|
|
12487
|
-
await
|
|
12488
|
-
result.written.push(
|
|
13112
|
+
await mkdir7(path18.dirname(file), { recursive: true });
|
|
13113
|
+
await writeFile8(file, bytes);
|
|
13114
|
+
result.written.push(path18.relative(process.cwd(), file));
|
|
12489
13115
|
}
|
|
12490
13116
|
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
12491
13117
|
}
|
|
@@ -12523,11 +13149,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12523
13149
|
if (!match) throw new Error(`remoteId ${spec.remoteId} is not a character on this account`);
|
|
12524
13150
|
} else {
|
|
12525
13151
|
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
12526
|
-
if (!
|
|
12527
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
13152
|
+
if (!existsSync13(south)) {
|
|
13153
|
+
result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), south)} to match by`);
|
|
12528
13154
|
return false;
|
|
12529
13155
|
}
|
|
12530
|
-
const localHash = sha256(await
|
|
13156
|
+
const localHash = sha256(await readFile11(south));
|
|
12531
13157
|
for (const candidate of remote) {
|
|
12532
13158
|
if (claimed.has(candidate.id)) continue;
|
|
12533
13159
|
if (await southHash(candidate) === localHash) {
|
|
@@ -12536,7 +13162,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12536
13162
|
}
|
|
12537
13163
|
}
|
|
12538
13164
|
if (!match) {
|
|
12539
|
-
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${
|
|
13165
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path18.relative(process.cwd(), south)}`);
|
|
12540
13166
|
return false;
|
|
12541
13167
|
}
|
|
12542
13168
|
}
|
|
@@ -12603,15 +13229,15 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12603
13229
|
if (!match) throw new Error(`remoteId ${spec.remoteId} names no ${animation.direction} animation of character ${parentId}`);
|
|
12604
13230
|
} else {
|
|
12605
13231
|
const first = expectedOutputPath(spec, "frame-00", 0, 2);
|
|
12606
|
-
if (!
|
|
13232
|
+
if (!existsSync13(first)) {
|
|
12607
13233
|
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
12608
13234
|
if (!byName) {
|
|
12609
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
13235
|
+
result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), first)} to match by`);
|
|
12610
13236
|
return false;
|
|
12611
13237
|
}
|
|
12612
13238
|
match = byName;
|
|
12613
13239
|
} else {
|
|
12614
|
-
const localHash = sha256(await
|
|
13240
|
+
const localHash = sha256(await readFile11(first));
|
|
12615
13241
|
for (const candidate of candidates) {
|
|
12616
13242
|
if (sha256(await download(candidate.frames[0])) === localHash) {
|
|
12617
13243
|
match = candidate;
|
|
@@ -12619,7 +13245,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12619
13245
|
}
|
|
12620
13246
|
}
|
|
12621
13247
|
if (!match) {
|
|
12622
|
-
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${
|
|
13248
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path18.relative(process.cwd(), first)}`);
|
|
12623
13249
|
return false;
|
|
12624
13250
|
}
|
|
12625
13251
|
}
|
|
@@ -12679,19 +13305,19 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12679
13305
|
}
|
|
12680
13306
|
|
|
12681
13307
|
// src/pipeline/salvage.ts
|
|
12682
|
-
import { readFile as
|
|
12683
|
-
import { existsSync as
|
|
12684
|
-
import
|
|
13308
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13309
|
+
import { existsSync as existsSync14 } from "fs";
|
|
13310
|
+
import path19 from "path";
|
|
12685
13311
|
function providerClaimId(provider, objectId) {
|
|
12686
13312
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12687
13313
|
}
|
|
12688
13314
|
async function loadClaims(lockPaths, opts = {}) {
|
|
12689
13315
|
const claimed = /* @__PURE__ */ new Set();
|
|
12690
13316
|
for (const p of lockPaths) {
|
|
12691
|
-
if (!
|
|
13317
|
+
if (!existsSync14(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12692
13318
|
let parsed;
|
|
12693
13319
|
try {
|
|
12694
|
-
parsed = parseLock(JSON.parse(await
|
|
13320
|
+
parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
|
|
12695
13321
|
} catch {
|
|
12696
13322
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
12697
13323
|
}
|
|
@@ -12858,20 +13484,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
12858
13484
|
return null;
|
|
12859
13485
|
}
|
|
12860
13486
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12861
|
-
const own =
|
|
13487
|
+
const own = path19.resolve(ownManifestPath);
|
|
12862
13488
|
const siblingManifestPaths = [
|
|
12863
13489
|
.../* @__PURE__ */ new Set([
|
|
12864
13490
|
...workspaceManifestPaths,
|
|
12865
|
-
...claimPaths.map((c) =>
|
|
13491
|
+
...claimPaths.map((c) => path19.join(path19.dirname(path19.resolve(c)), "pixelkiln.manifest.json"))
|
|
12866
13492
|
])
|
|
12867
13493
|
];
|
|
12868
13494
|
const siblings = [];
|
|
12869
13495
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12870
|
-
if (
|
|
12871
|
-
if (!
|
|
13496
|
+
if (path19.resolve(siblingManifestPath) === own) continue;
|
|
13497
|
+
if (!existsSync14(siblingManifestPath)) continue;
|
|
12872
13498
|
try {
|
|
12873
13499
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12874
|
-
siblings.push({ label:
|
|
13500
|
+
siblings.push({ label: path19.basename(path19.dirname(siblingManifestPath)), manifest });
|
|
12875
13501
|
} catch {
|
|
12876
13502
|
}
|
|
12877
13503
|
}
|
|
@@ -12991,9 +13617,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
12991
13617
|
}
|
|
12992
13618
|
|
|
12993
13619
|
// src/workspace.ts
|
|
12994
|
-
import { mkdir as
|
|
12995
|
-
import { existsSync as
|
|
12996
|
-
import
|
|
13620
|
+
import { mkdir as mkdir8, readFile as readFile13, rename as rename6, rm as rm6, writeFile as writeFile9 } from "fs/promises";
|
|
13621
|
+
import { existsSync as existsSync15 } from "fs";
|
|
13622
|
+
import path20 from "path";
|
|
12997
13623
|
import { z as z5 } from "zod";
|
|
12998
13624
|
var WorkspaceProjectSchema = z5.object({
|
|
12999
13625
|
id: z5.string().min(1),
|
|
@@ -13018,10 +13644,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
13018
13644
|
);
|
|
13019
13645
|
}
|
|
13020
13646
|
async function loadWorkspace(workspacePath) {
|
|
13021
|
-
if (!
|
|
13647
|
+
if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
|
|
13022
13648
|
let raw;
|
|
13023
13649
|
try {
|
|
13024
|
-
raw = JSON.parse(await
|
|
13650
|
+
raw = JSON.parse(await readFile13(workspacePath, "utf8"));
|
|
13025
13651
|
} catch (err) {
|
|
13026
13652
|
throw new ProjectError(
|
|
13027
13653
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -13035,22 +13661,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
13035
13661
|
version: 1,
|
|
13036
13662
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
13037
13663
|
};
|
|
13038
|
-
await
|
|
13664
|
+
await mkdir8(path20.dirname(path20.resolve(workspacePath)), { recursive: true });
|
|
13039
13665
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
13040
13666
|
try {
|
|
13041
|
-
await
|
|
13667
|
+
await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
13042
13668
|
await rename6(tmp, workspacePath);
|
|
13043
13669
|
} finally {
|
|
13044
13670
|
await rm6(tmp, { force: true });
|
|
13045
13671
|
}
|
|
13046
13672
|
}
|
|
13047
13673
|
function toPortablePath(dir, absolute) {
|
|
13048
|
-
return
|
|
13674
|
+
return path20.relative(dir, absolute).split(path20.sep).join("/");
|
|
13049
13675
|
}
|
|
13050
13676
|
function resolveProject(dir, project) {
|
|
13051
13677
|
return {
|
|
13052
|
-
manifestPath:
|
|
13053
|
-
lockPath:
|
|
13678
|
+
manifestPath: path20.resolve(dir, project.manifest.split("/").join(path20.sep)),
|
|
13679
|
+
lockPath: path20.resolve(dir, project.lock.split("/").join(path20.sep))
|
|
13054
13680
|
};
|
|
13055
13681
|
}
|
|
13056
13682
|
function validateWorkspace(ws, dir) {
|
|
@@ -13063,21 +13689,21 @@ function validateWorkspace(ws, dir) {
|
|
|
13063
13689
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
13064
13690
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
13065
13691
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
13066
|
-
if (
|
|
13692
|
+
if (path20.isAbsolute(project.manifest) || path20.isAbsolute(project.lock)) {
|
|
13067
13693
|
diagnostics.push({
|
|
13068
13694
|
id: "absolute-path",
|
|
13069
13695
|
level: "warning",
|
|
13070
13696
|
message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
13071
13697
|
});
|
|
13072
13698
|
}
|
|
13073
|
-
if (!
|
|
13699
|
+
if (!existsSync15(manifestPath)) {
|
|
13074
13700
|
diagnostics.push({
|
|
13075
13701
|
id: "missing-manifest",
|
|
13076
13702
|
level: "error",
|
|
13077
13703
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
13078
13704
|
});
|
|
13079
13705
|
}
|
|
13080
|
-
if (!
|
|
13706
|
+
if (!existsSync15(lockPath)) {
|
|
13081
13707
|
diagnostics.push({
|
|
13082
13708
|
id: "missing-lock",
|
|
13083
13709
|
level: "error",
|
|
@@ -13124,8 +13750,8 @@ function validateWorkspace(ws, dir) {
|
|
|
13124
13750
|
}
|
|
13125
13751
|
|
|
13126
13752
|
// src/pick/salvage-server.ts
|
|
13127
|
-
import { mkdir as
|
|
13128
|
-
import
|
|
13753
|
+
import { mkdir as mkdir9, writeFile as writeFile10, readFile as readFile15 } from "fs/promises";
|
|
13754
|
+
import path21 from "path";
|
|
13129
13755
|
|
|
13130
13756
|
// src/pick/salvage-sheet.ts
|
|
13131
13757
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13286,7 +13912,7 @@ refresh();
|
|
|
13286
13912
|
|
|
13287
13913
|
// src/pick/review-server.ts
|
|
13288
13914
|
import { createServer } from "http";
|
|
13289
|
-
import { readFile as
|
|
13915
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
13290
13916
|
|
|
13291
13917
|
// src/open.ts
|
|
13292
13918
|
import { spawn } from "child_process";
|
|
@@ -13313,7 +13939,7 @@ function serveReviewPage(opts) {
|
|
|
13313
13939
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
13314
13940
|
try {
|
|
13315
13941
|
const asset = opts.assets.get(req.url);
|
|
13316
|
-
const bytes = await
|
|
13942
|
+
const bytes = await readFile14(asset.path);
|
|
13317
13943
|
res.writeHead(200, {
|
|
13318
13944
|
"Content-Type": asset.contentType,
|
|
13319
13945
|
"Cache-Control": "no-store",
|
|
@@ -13390,7 +14016,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13390
14016
|
});
|
|
13391
14017
|
const html = renderSalvageSheet(orphans, {
|
|
13392
14018
|
styleId: ctx.styleId,
|
|
13393
|
-
importDir:
|
|
14019
|
+
importDir: path21.relative(process.cwd(), ctx.importDir) || "."
|
|
13394
14020
|
});
|
|
13395
14021
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13396
14022
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13446,10 +14072,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13446
14072
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13447
14073
|
decodePng(buf);
|
|
13448
14074
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13449
|
-
const rel =
|
|
13450
|
-
const outFile =
|
|
13451
|
-
await
|
|
13452
|
-
await
|
|
14075
|
+
const rel = path21.join("_salvaged", `${assetId}.png`);
|
|
14076
|
+
const outFile = path21.resolve(ctx.importDir, rel);
|
|
14077
|
+
await mkdir9(path21.dirname(outFile), { recursive: true });
|
|
14078
|
+
await writeFile10(outFile, buf);
|
|
13453
14079
|
ctx.manifest.assets[assetId] = {
|
|
13454
14080
|
prompt: orphan.prompt,
|
|
13455
14081
|
promptByStyle: {},
|
|
@@ -13481,7 +14107,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13481
14107
|
sourceUrl: durableSource,
|
|
13482
14108
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13483
14109
|
outputs: [{
|
|
13484
|
-
path: portableOutputPath(outFile,
|
|
14110
|
+
path: portableOutputPath(outFile, path21.dirname(ctx.manifestPath)),
|
|
13485
14111
|
sha256: sha256(buf)
|
|
13486
14112
|
}],
|
|
13487
14113
|
submittedAt: orphan.createdAt,
|
|
@@ -13505,9 +14131,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13505
14131
|
}
|
|
13506
14132
|
}
|
|
13507
14133
|
await applyTags(provider, decisions, existingTags, { onProgress: log2, characters: characterMembers });
|
|
13508
|
-
const raw = JSON.parse(await
|
|
14134
|
+
const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
|
|
13509
14135
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
13510
|
-
await
|
|
14136
|
+
await writeFile10(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
13511
14137
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
13512
14138
|
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
13513
14139
|
return result;
|
|
@@ -13516,19 +14142,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13516
14142
|
}
|
|
13517
14143
|
|
|
13518
14144
|
// src/cli/project.ts
|
|
13519
|
-
import
|
|
13520
|
-
import { existsSync as
|
|
14145
|
+
import path23 from "path";
|
|
14146
|
+
import { existsSync as existsSync16 } from "fs";
|
|
13521
14147
|
|
|
13522
14148
|
// src/project.ts
|
|
13523
|
-
import
|
|
14149
|
+
import path22 from "path";
|
|
13524
14150
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
13525
|
-
const absolute =
|
|
13526
|
-
const root =
|
|
14151
|
+
const absolute = path22.resolve(manifestPath);
|
|
14152
|
+
const root = path22.dirname(absolute);
|
|
13527
14153
|
if (opts.env !== false) {
|
|
13528
14154
|
loadEnvFiles(root);
|
|
13529
|
-
if (
|
|
14155
|
+
if (path22.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
13530
14156
|
}
|
|
13531
|
-
const lockPath =
|
|
14157
|
+
const lockPath = path22.resolve(opts.lockPath ?? path22.join(root, "pixelkiln.lock.json"));
|
|
13532
14158
|
const loaded = await loadManifest(absolute);
|
|
13533
14159
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
13534
14160
|
const lock = await loadLock(lockPath);
|
|
@@ -13659,18 +14285,18 @@ async function workspaceStatus(ws, dir) {
|
|
|
13659
14285
|
|
|
13660
14286
|
// src/cli/project.ts
|
|
13661
14287
|
async function provenanceFile(id, file) {
|
|
13662
|
-
const absolute =
|
|
14288
|
+
const absolute = path23.resolve(file);
|
|
13663
14289
|
return {
|
|
13664
14290
|
id,
|
|
13665
14291
|
path: absolute,
|
|
13666
|
-
sha256:
|
|
14292
|
+
sha256: existsSync16(absolute) ? await sha256File(absolute) : null,
|
|
13667
14293
|
included: true
|
|
13668
14294
|
};
|
|
13669
14295
|
}
|
|
13670
14296
|
function manifestSources(manifest, styleId) {
|
|
13671
14297
|
const sources = {};
|
|
13672
14298
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
13673
|
-
if (asset
|
|
14299
|
+
if (!assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
|
|
13674
14300
|
const source = asset.sourceByStyle[styleId] ?? asset.source;
|
|
13675
14301
|
if (source) sources[assetId] = source;
|
|
13676
14302
|
}
|
|
@@ -13735,10 +14361,10 @@ function budgetsForPlan(plan, args) {
|
|
|
13735
14361
|
return out;
|
|
13736
14362
|
}
|
|
13737
14363
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13738
|
-
if (!
|
|
14364
|
+
if (!existsSync16(workspacePath)) {
|
|
13739
14365
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
13740
14366
|
}
|
|
13741
|
-
const dir =
|
|
14367
|
+
const dir = path23.dirname(path23.resolve(workspacePath));
|
|
13742
14368
|
const ws = await loadWorkspace(workspacePath);
|
|
13743
14369
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13744
14370
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13761,9 +14387,9 @@ async function openAccountProject(args) {
|
|
|
13761
14387
|
return project;
|
|
13762
14388
|
}
|
|
13763
14389
|
async function open(args) {
|
|
13764
|
-
if (!
|
|
14390
|
+
if (!existsSync16(path23.resolve(args.manifest))) {
|
|
13765
14391
|
throw new ProjectError(
|
|
13766
|
-
`No manifest at ${
|
|
14392
|
+
`No manifest at ${path23.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
13767
14393
|
);
|
|
13768
14394
|
}
|
|
13769
14395
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -13805,7 +14431,7 @@ function providerCache(mode2) {
|
|
|
13805
14431
|
|
|
13806
14432
|
// src/cli/commands/account.ts
|
|
13807
14433
|
async function runBalance(args) {
|
|
13808
|
-
loadEnvFiles(
|
|
14434
|
+
loadEnvFiles(path24.dirname(path24.resolve(args.manifest)));
|
|
13809
14435
|
loadEnvFiles(process.cwd());
|
|
13810
14436
|
const loaded = await loadManifest(args.manifest);
|
|
13811
14437
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -13878,12 +14504,12 @@ async function runAdopt(args) {
|
|
|
13878
14504
|
tagged ${n} object(s) upstream`);
|
|
13879
14505
|
}
|
|
13880
14506
|
if (args.writePrompts) {
|
|
13881
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
14507
|
+
const { filled, stillEmpty } = await writePromptsBack(path24.resolve(args.manifest), lock, {
|
|
13882
14508
|
onProgress: log,
|
|
13883
14509
|
provider: provider.id,
|
|
13884
14510
|
assetIds: specs.map((spec) => spec.assetId)
|
|
13885
14511
|
});
|
|
13886
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
14512
|
+
log(` recovered ${filled} prompt(s) into ${path24.relative(process.cwd(), args.manifest)}`);
|
|
13887
14513
|
const reloaded = await loadManifest(args.manifest);
|
|
13888
14514
|
const rebased = (await resolveSpecs(reloaded, {
|
|
13889
14515
|
styles: args.styles,
|
|
@@ -13913,12 +14539,12 @@ async function runSalvage2(args) {
|
|
|
13913
14539
|
const provider = providerFor(accountProvider);
|
|
13914
14540
|
const jsonMode = args.dryRun && args.json;
|
|
13915
14541
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
13916
|
-
const ownLock =
|
|
14542
|
+
const ownLock = path24.resolve(args.lock);
|
|
13917
14543
|
let workspaceProjects = [];
|
|
13918
14544
|
let workspaceDir = "";
|
|
13919
14545
|
if (args.workspace) {
|
|
13920
|
-
const workspacePath =
|
|
13921
|
-
workspaceDir =
|
|
14546
|
+
const workspacePath = path24.resolve(args.workspace);
|
|
14547
|
+
workspaceDir = path24.dirname(workspacePath);
|
|
13922
14548
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
13923
14549
|
workspaceProjects = complete.ws.projects;
|
|
13924
14550
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -13927,12 +14553,12 @@ async function runSalvage2(args) {
|
|
|
13927
14553
|
const lockPaths = [
|
|
13928
14554
|
.../* @__PURE__ */ new Set([
|
|
13929
14555
|
...workspaceLockPaths,
|
|
13930
|
-
...
|
|
13931
|
-
...args.claims.map((c) =>
|
|
14556
|
+
...existsSync17(ownLock) ? [ownLock] : [],
|
|
14557
|
+
...args.claims.map((c) => path24.resolve(c))
|
|
13932
14558
|
])
|
|
13933
14559
|
];
|
|
13934
14560
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
13935
|
-
for (const p of lockPaths) diag(` ${
|
|
14561
|
+
for (const p of lockPaths) diag(` ${path24.relative(process.cwd(), p)}`);
|
|
13936
14562
|
if (!args.claims.length && !args.workspace) {
|
|
13937
14563
|
diag(
|
|
13938
14564
|
`
|
|
@@ -13940,7 +14566,7 @@ async function runSalvage2(args) {
|
|
|
13940
14566
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
13941
14567
|
register every project in a workspace catalog and pass --workspace.`
|
|
13942
14568
|
);
|
|
13943
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
14569
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path24.resolve(args.manifest))) {
|
|
13944
14570
|
diag(
|
|
13945
14571
|
`
|
|
13946
14572
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14059,7 +14685,7 @@ async function runSalvage2(args) {
|
|
|
14059
14685
|
manifestPath: loaded.path,
|
|
14060
14686
|
manifest: accountManifest,
|
|
14061
14687
|
styleId,
|
|
14062
|
-
importDir:
|
|
14688
|
+
importDir: path24.resolve(loaded.root, style.outDir),
|
|
14063
14689
|
lock,
|
|
14064
14690
|
lockPath: args.lock
|
|
14065
14691
|
},
|
|
@@ -14186,20 +14812,20 @@ async function runTag(args) {
|
|
|
14186
14812
|
}
|
|
14187
14813
|
|
|
14188
14814
|
// src/cli/commands/edit.ts
|
|
14189
|
-
import
|
|
14815
|
+
import path27 from "path";
|
|
14190
14816
|
|
|
14191
14817
|
// src/pipeline/hand-edit.ts
|
|
14192
|
-
import { existsSync as
|
|
14193
|
-
import { copyFile, mkdir as
|
|
14194
|
-
import
|
|
14818
|
+
import { existsSync as existsSync18 } from "fs";
|
|
14819
|
+
import { copyFile, mkdir as mkdir10, readFile as readFile17, rename as rename8, rm as rm8, writeFile as writeFile12 } from "fs/promises";
|
|
14820
|
+
import path26 from "path";
|
|
14195
14821
|
import { z as z7 } from "zod";
|
|
14196
14822
|
|
|
14197
14823
|
// src/manifest-edit.ts
|
|
14198
|
-
import { readFile as
|
|
14199
|
-
import
|
|
14824
|
+
import { readFile as readFile16, rename as rename7, rm as rm7, writeFile as writeFile11 } from "fs/promises";
|
|
14825
|
+
import path25 from "path";
|
|
14200
14826
|
import { z as z6 } from "zod";
|
|
14201
14827
|
var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
|
|
14202
|
-
var ProjectFileSchema = z6.string().min(1).refine((file) => !
|
|
14828
|
+
var ProjectFileSchema = z6.string().min(1).refine((file) => !path25.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
|
|
14203
14829
|
var AssetPatchSchema = z6.object({
|
|
14204
14830
|
prompt: z6.string().optional(),
|
|
14205
14831
|
/** Per-style override for the style being viewed; null removes it. */
|
|
@@ -14618,8 +15244,8 @@ function applyEdit(raw, edit) {
|
|
|
14618
15244
|
}
|
|
14619
15245
|
}
|
|
14620
15246
|
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
14621
|
-
const absolute =
|
|
14622
|
-
const original = await
|
|
15247
|
+
const absolute = path25.resolve(manifestPath);
|
|
15248
|
+
const original = await readFile16(absolute, "utf8");
|
|
14623
15249
|
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14624
15250
|
let raw;
|
|
14625
15251
|
try {
|
|
@@ -14632,12 +15258,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
|
14632
15258
|
applyEdit(raw, edit);
|
|
14633
15259
|
const next = serializeLike(original, raw);
|
|
14634
15260
|
if (next === original) return unchanged(absolute, original);
|
|
14635
|
-
const tmp =
|
|
14636
|
-
|
|
14637
|
-
`.${
|
|
15261
|
+
const tmp = path25.join(
|
|
15262
|
+
path25.dirname(absolute),
|
|
15263
|
+
`.${path25.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
14638
15264
|
);
|
|
14639
15265
|
try {
|
|
14640
|
-
await
|
|
15266
|
+
await writeFile11(tmp, next);
|
|
14641
15267
|
let specs;
|
|
14642
15268
|
try {
|
|
14643
15269
|
specs = await resolveSpecs(await loadManifest(tmp));
|
|
@@ -14707,11 +15333,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
|
|
|
14707
15333
|
var HAND_EDIT_DIR = "edits";
|
|
14708
15334
|
function handEditPath(loaded, spec) {
|
|
14709
15335
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14710
|
-
const outDir =
|
|
14711
|
-
const relative =
|
|
14712
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
14713
|
-
const file = inside ? relative :
|
|
14714
|
-
return
|
|
15336
|
+
const outDir = path26.resolve(loaded.root, style?.outDir ?? ".");
|
|
15337
|
+
const relative = path26.relative(outDir, spec.outFile);
|
|
15338
|
+
const inside = relative && !relative.startsWith("..") && !path26.isAbsolute(relative);
|
|
15339
|
+
const file = inside ? relative : path26.basename(spec.outFile);
|
|
15340
|
+
return path26.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
14715
15341
|
}
|
|
14716
15342
|
function handEditBases(spec, lock) {
|
|
14717
15343
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -14724,10 +15350,10 @@ function handEditBases(spec, lock) {
|
|
|
14724
15350
|
}));
|
|
14725
15351
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
14726
15352
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
14727
|
-
if (!members.every((member) =>
|
|
15353
|
+
if (!members.every((member) => existsSync18(member.path))) return null;
|
|
14728
15354
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
14729
15355
|
}
|
|
14730
|
-
return
|
|
15356
|
+
return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
14731
15357
|
}
|
|
14732
15358
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
14733
15359
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -14753,18 +15379,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
14753
15379
|
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
14754
15380
|
);
|
|
14755
15381
|
}
|
|
14756
|
-
const existing = spec.source ?
|
|
15382
|
+
const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
|
|
14757
15383
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
14758
15384
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
14759
15385
|
let created = false;
|
|
14760
15386
|
for (const member of members) {
|
|
14761
|
-
if (
|
|
15387
|
+
if (existsSync18(member.path)) continue;
|
|
14762
15388
|
if (!member.base) {
|
|
14763
15389
|
throw new ManifestEditError(
|
|
14764
15390
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
14765
15391
|
);
|
|
14766
15392
|
}
|
|
14767
|
-
await
|
|
15393
|
+
await mkdir10(path26.dirname(member.path), { recursive: true });
|
|
14768
15394
|
await copyFile(member.base, member.path);
|
|
14769
15395
|
created = true;
|
|
14770
15396
|
}
|
|
@@ -14843,7 +15469,7 @@ function handEditProjectPath(editPath) {
|
|
|
14843
15469
|
async function readHandEditCompanion(editPath) {
|
|
14844
15470
|
let text;
|
|
14845
15471
|
try {
|
|
14846
|
-
text = await
|
|
15472
|
+
text = await readFile17(handEditCompanionPath(editPath), "utf8");
|
|
14847
15473
|
} catch {
|
|
14848
15474
|
return null;
|
|
14849
15475
|
}
|
|
@@ -14886,7 +15512,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14886
15512
|
}
|
|
14887
15513
|
});
|
|
14888
15514
|
const bases = handEditBases(spec, lock);
|
|
14889
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
15515
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile17(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
14890
15516
|
for (const [index, image] of decoded.entries()) {
|
|
14891
15517
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
14892
15518
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -14902,7 +15528,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14902
15528
|
editor: input.editor,
|
|
14903
15529
|
protocol: input.protocol,
|
|
14904
15530
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
14905
|
-
project: projectPath ?
|
|
15531
|
+
project: projectPath ? path26.basename(projectPath) : null,
|
|
14906
15532
|
outputs: incoming.map((frame, index) => ({
|
|
14907
15533
|
role: frame.role,
|
|
14908
15534
|
basedOn: basedOnHash(entry, index),
|
|
@@ -14932,7 +15558,7 @@ function basedOnHash(entry, index) {
|
|
|
14932
15558
|
async function replaceFile(file, bytes) {
|
|
14933
15559
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
14934
15560
|
try {
|
|
14935
|
-
await
|
|
15561
|
+
await writeFile12(tmp, bytes);
|
|
14936
15562
|
await rename8(tmp, file);
|
|
14937
15563
|
} finally {
|
|
14938
15564
|
await rm8(tmp, { force: true });
|
|
@@ -14962,7 +15588,7 @@ async function runEdit(args) {
|
|
|
14962
15588
|
const set = started.members.length > 1;
|
|
14963
15589
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
14964
15590
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
14965
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
15591
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path27.relative(process.cwd(), member.path)}`);
|
|
14966
15592
|
if (args.noOpen) {
|
|
14967
15593
|
log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
|
|
14968
15594
|
} else {
|
|
@@ -14972,24 +15598,24 @@ async function runEdit(args) {
|
|
|
14972
15598
|
}
|
|
14973
15599
|
|
|
14974
15600
|
// src/cli/commands/gallery.ts
|
|
14975
|
-
import
|
|
14976
|
-
import { existsSync as
|
|
15601
|
+
import path37 from "path";
|
|
15602
|
+
import { existsSync as existsSync28 } from "fs";
|
|
14977
15603
|
|
|
14978
15604
|
// src/gallery/snapshot.ts
|
|
14979
15605
|
import { createHash as createHash4 } from "crypto";
|
|
14980
|
-
import { existsSync as
|
|
14981
|
-
import { readFile as
|
|
14982
|
-
import
|
|
15606
|
+
import { existsSync as existsSync23 } from "fs";
|
|
15607
|
+
import { readFile as readFile21, stat as stat2 } from "fs/promises";
|
|
15608
|
+
import path33 from "path";
|
|
14983
15609
|
|
|
14984
15610
|
// src/gallery/edit.ts
|
|
14985
|
-
import { readFile as
|
|
14986
|
-
import
|
|
15611
|
+
import { readFile as readFile20, rename as rename9, writeFile as writeFile16 } from "fs/promises";
|
|
15612
|
+
import path31 from "path";
|
|
14987
15613
|
import { z as z11 } from "zod";
|
|
14988
15614
|
|
|
14989
15615
|
// src/gallery/upload.ts
|
|
14990
|
-
import { existsSync as
|
|
14991
|
-
import { mkdir as
|
|
14992
|
-
import
|
|
15616
|
+
import { existsSync as existsSync19 } from "fs";
|
|
15617
|
+
import { mkdir as mkdir11, writeFile as writeFile13 } from "fs/promises";
|
|
15618
|
+
import path28 from "path";
|
|
14993
15619
|
import { z as z8 } from "zod";
|
|
14994
15620
|
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
14995
15621
|
var UploadImageSchema = z8.object({
|
|
@@ -15002,12 +15628,12 @@ var UploadImageSchema = z8.object({
|
|
|
15002
15628
|
overwrite: z8.boolean().optional()
|
|
15003
15629
|
}).strict();
|
|
15004
15630
|
async function saveProjectImage(root, request) {
|
|
15005
|
-
const file =
|
|
15006
|
-
const relative =
|
|
15007
|
-
if (relative.startsWith("..") ||
|
|
15631
|
+
const file = path28.resolve(root, request.path);
|
|
15632
|
+
const relative = path28.relative(root, file);
|
|
15633
|
+
if (relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
15008
15634
|
throw new ManifestEditError("an upload must stay inside the project");
|
|
15009
15635
|
}
|
|
15010
|
-
const extension =
|
|
15636
|
+
const extension = path28.extname(file).toLowerCase();
|
|
15011
15637
|
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
15012
15638
|
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
15013
15639
|
const bytes = Buffer.from(request.base64, "base64");
|
|
@@ -15019,18 +15645,18 @@ async function saveProjectImage(root, request) {
|
|
|
15019
15645
|
if (metadata.format !== expected) {
|
|
15020
15646
|
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
15021
15647
|
}
|
|
15022
|
-
if (
|
|
15023
|
-
throw new ManifestEditError(`${relative.split(
|
|
15648
|
+
if (existsSync19(file) && !request.overwrite) {
|
|
15649
|
+
throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
|
|
15024
15650
|
}
|
|
15025
|
-
await
|
|
15026
|
-
await
|
|
15027
|
-
return relative.split(
|
|
15651
|
+
await mkdir11(path28.dirname(file), { recursive: true });
|
|
15652
|
+
await writeFile13(file, bytes);
|
|
15653
|
+
return relative.split(path28.sep).join("/");
|
|
15028
15654
|
}
|
|
15029
15655
|
|
|
15030
15656
|
// src/gallery/image-revision.ts
|
|
15031
|
-
import { existsSync as
|
|
15032
|
-
import { readFile as
|
|
15033
|
-
import
|
|
15657
|
+
import { existsSync as existsSync20 } from "fs";
|
|
15658
|
+
import { readFile as readFile18, rm as rm9, writeFile as writeFile14 } from "fs/promises";
|
|
15659
|
+
import path29 from "path";
|
|
15034
15660
|
import { z as z9 } from "zod";
|
|
15035
15661
|
var InpaintRevisionRequestSchema = z9.object({
|
|
15036
15662
|
action: z9.literal("create-inpaint-revision"),
|
|
@@ -15049,15 +15675,15 @@ var InpaintRevisionRequestSchema = z9.object({
|
|
|
15049
15675
|
overwrite: z9.boolean().optional()
|
|
15050
15676
|
}).strict();
|
|
15051
15677
|
async function createInpaintRevision(manifestPath, request) {
|
|
15052
|
-
const root =
|
|
15053
|
-
if (
|
|
15678
|
+
const root = path29.dirname(path29.resolve(manifestPath));
|
|
15679
|
+
if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
|
|
15054
15680
|
throw new ManifestEditError("the mask must be a .png path inside the project");
|
|
15055
15681
|
}
|
|
15056
15682
|
const bytes = Buffer.from(request.maskBase64, "base64");
|
|
15057
15683
|
const metadata = imageMetadata(bytes);
|
|
15058
15684
|
if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
|
|
15059
|
-
const file =
|
|
15060
|
-
const previous =
|
|
15685
|
+
const file = path29.resolve(root, request.maskPath);
|
|
15686
|
+
const previous = existsSync20(file) ? await readFile18(file) : null;
|
|
15061
15687
|
const mask = await saveProjectImage(root, {
|
|
15062
15688
|
action: "upload-image",
|
|
15063
15689
|
path: request.maskPath,
|
|
@@ -15076,7 +15702,7 @@ async function createInpaintRevision(manifestPath, request) {
|
|
|
15076
15702
|
}
|
|
15077
15703
|
});
|
|
15078
15704
|
} catch (error) {
|
|
15079
|
-
if (previous) await
|
|
15705
|
+
if (previous) await writeFile14(file, previous);
|
|
15080
15706
|
else await rm9(file, { force: true });
|
|
15081
15707
|
throw error;
|
|
15082
15708
|
}
|
|
@@ -15084,9 +15710,9 @@ async function createInpaintRevision(manifestPath, request) {
|
|
|
15084
15710
|
}
|
|
15085
15711
|
|
|
15086
15712
|
// src/gallery/skeleton.ts
|
|
15087
|
-
import { existsSync as
|
|
15088
|
-
import { mkdir as
|
|
15089
|
-
import
|
|
15713
|
+
import { existsSync as existsSync21 } from "fs";
|
|
15714
|
+
import { mkdir as mkdir12, readFile as readFile19, rm as rm10, writeFile as writeFile15 } from "fs/promises";
|
|
15715
|
+
import path30 from "path";
|
|
15090
15716
|
import { z as z10 } from "zod";
|
|
15091
15717
|
var SkeletonEstimateRequestSchema = z10.object({
|
|
15092
15718
|
project: z10.string().min(1).optional(),
|
|
@@ -15125,8 +15751,8 @@ var SkeletonKeypointsUpdateSchema = z10.object({
|
|
|
15125
15751
|
}).strict();
|
|
15126
15752
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
15127
15753
|
function skeletonSourceFile(spec) {
|
|
15128
|
-
if (spec.source) return
|
|
15129
|
-
if (spec.quality &&
|
|
15754
|
+
if (spec.source) return path30.resolve(spec.root, spec.source);
|
|
15755
|
+
if (spec.quality && existsSync21(spec.quality.outFile)) return spec.quality.outFile;
|
|
15130
15756
|
return spec.outFile;
|
|
15131
15757
|
}
|
|
15132
15758
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -15147,10 +15773,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
15147
15773
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
15148
15774
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15149
15775
|
const file = skeletonSourceFile(spec);
|
|
15150
|
-
if (!
|
|
15151
|
-
const bytes = await
|
|
15776
|
+
if (!existsSync21(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
15777
|
+
const bytes = await readFile19(file);
|
|
15152
15778
|
const metadata = imageMetadata(bytes);
|
|
15153
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
15779
|
+
if (!metadata) throw new ManifestEditError(`${path30.basename(file)} is not a readable PNG or JPEG`);
|
|
15154
15780
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
15155
15781
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
15156
15782
|
if (used >= limit) {
|
|
@@ -15167,7 +15793,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
15167
15793
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
15168
15794
|
return {
|
|
15169
15795
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
15170
|
-
source:
|
|
15796
|
+
source: path30.relative(ctx.loaded.root, file).split(path30.sep).join("/"),
|
|
15171
15797
|
session: status()
|
|
15172
15798
|
};
|
|
15173
15799
|
}
|
|
@@ -15181,21 +15807,21 @@ function billedAmount(usage) {
|
|
|
15181
15807
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
15182
15808
|
}
|
|
15183
15809
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
15184
|
-
const root =
|
|
15185
|
-
const file =
|
|
15186
|
-
const relative =
|
|
15187
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
15810
|
+
const root = path30.dirname(path30.resolve(manifestPath));
|
|
15811
|
+
const file = path30.resolve(root, request.keypointsFile);
|
|
15812
|
+
const relative = path30.relative(root, file);
|
|
15813
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
|
|
15188
15814
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
15189
15815
|
}
|
|
15190
|
-
const existed =
|
|
15816
|
+
const existed = existsSync21(file);
|
|
15191
15817
|
let previous = null;
|
|
15192
15818
|
if (request.set) {
|
|
15193
15819
|
if (existed && !request.overwrite) {
|
|
15194
15820
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
15195
15821
|
}
|
|
15196
|
-
if (existed) previous = await
|
|
15197
|
-
await
|
|
15198
|
-
await
|
|
15822
|
+
if (existed) previous = await readFile19(file);
|
|
15823
|
+
await mkdir12(path30.dirname(file), { recursive: true });
|
|
15824
|
+
await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15199
15825
|
} else if (!existed) {
|
|
15200
15826
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
15201
15827
|
}
|
|
@@ -15210,7 +15836,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15210
15836
|
revision: {
|
|
15211
15837
|
mode: "animate-skeleton",
|
|
15212
15838
|
from: request.from,
|
|
15213
|
-
keypointsFile: relative.split(
|
|
15839
|
+
keypointsFile: relative.split(path30.sep).join("/"),
|
|
15214
15840
|
direction: request.direction,
|
|
15215
15841
|
...request.description ? { description: request.description } : {},
|
|
15216
15842
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -15219,7 +15845,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15219
15845
|
});
|
|
15220
15846
|
} catch (error) {
|
|
15221
15847
|
if (request.set) {
|
|
15222
|
-
if (previous) await
|
|
15848
|
+
if (previous) await writeFile15(file, previous);
|
|
15223
15849
|
else await rm10(file, { force: true });
|
|
15224
15850
|
}
|
|
15225
15851
|
throw error;
|
|
@@ -15231,19 +15857,19 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
15231
15857
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15232
15858
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
15233
15859
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
15234
|
-
const relative =
|
|
15235
|
-
if (relative.startsWith("..") ||
|
|
15860
|
+
const relative = path30.relative(ctx.loaded.root, file);
|
|
15861
|
+
if (relative.startsWith("..") || path30.isAbsolute(relative)) {
|
|
15236
15862
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
15237
15863
|
}
|
|
15238
|
-
const current =
|
|
15864
|
+
const current = existsSync21(file) ? sha256(await readFile19(file)) : null;
|
|
15239
15865
|
if (current !== request.expectedSha256) {
|
|
15240
15866
|
throw Object.assign(
|
|
15241
|
-
new Error(`${relative.split(
|
|
15867
|
+
new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
15242
15868
|
{ status: 409 }
|
|
15243
15869
|
);
|
|
15244
15870
|
}
|
|
15245
|
-
await
|
|
15246
|
-
return relative.split(
|
|
15871
|
+
await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15872
|
+
return relative.split(path30.sep).join("/");
|
|
15247
15873
|
}
|
|
15248
15874
|
|
|
15249
15875
|
// src/gallery/edit.ts
|
|
@@ -15297,7 +15923,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15297
15923
|
const history = /* @__PURE__ */ new Map();
|
|
15298
15924
|
const manifestOf = async (project) => {
|
|
15299
15925
|
try {
|
|
15300
|
-
return
|
|
15926
|
+
return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
15301
15927
|
} catch {
|
|
15302
15928
|
return null;
|
|
15303
15929
|
}
|
|
@@ -15307,7 +15933,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15307
15933
|
if (undo.success) {
|
|
15308
15934
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
15309
15935
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
15310
|
-
const current = await
|
|
15936
|
+
const current = await readFile20(manifestPath2, "utf8");
|
|
15311
15937
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
15312
15938
|
const stack = history.get(manifestPath2) ?? [];
|
|
15313
15939
|
const last = stack[stack.length - 1];
|
|
@@ -15317,17 +15943,17 @@ function createGalleryEditHandler(opts) {
|
|
|
15317
15943
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
15318
15944
|
}
|
|
15319
15945
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
15320
|
-
await
|
|
15946
|
+
await writeFile16(tmp, last.before);
|
|
15321
15947
|
await rename9(tmp, manifestPath2);
|
|
15322
15948
|
stack.pop();
|
|
15323
|
-
log2(` manifest edit undone in ${
|
|
15949
|
+
log2(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
15324
15950
|
return opts.reload();
|
|
15325
15951
|
}
|
|
15326
15952
|
const manifestPath = await manifestOf(body?.project);
|
|
15327
|
-
const before = manifestPath ? await
|
|
15953
|
+
const before = manifestPath ? await readFile20(manifestPath, "utf8").catch(() => null) : null;
|
|
15328
15954
|
const build = await apply(body);
|
|
15329
15955
|
if (manifestPath && before !== null) {
|
|
15330
|
-
const after = await
|
|
15956
|
+
const after = await readFile20(manifestPath, "utf8").catch(() => null);
|
|
15331
15957
|
if (after !== null && after !== before) {
|
|
15332
15958
|
const stack = history.get(manifestPath) ?? [];
|
|
15333
15959
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -15382,7 +16008,7 @@ function createEditApplier(opts, log2) {
|
|
|
15382
16008
|
} catch (error) {
|
|
15383
16009
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15384
16010
|
}
|
|
15385
|
-
const written = await saveProjectImage(
|
|
16011
|
+
const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
|
|
15386
16012
|
log2(` image added: ${written}`);
|
|
15387
16013
|
return opts.reload();
|
|
15388
16014
|
}
|
|
@@ -15449,7 +16075,7 @@ function createEditApplier(opts, log2) {
|
|
|
15449
16075
|
}
|
|
15450
16076
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15451
16077
|
if (result.changed) {
|
|
15452
|
-
const relative =
|
|
16078
|
+
const relative = path31.relative(process.cwd(), result.manifestPath);
|
|
15453
16079
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15454
16080
|
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15455
16081
|
}
|
|
@@ -15500,8 +16126,8 @@ function createGalleryPriceHandler(opts) {
|
|
|
15500
16126
|
}
|
|
15501
16127
|
|
|
15502
16128
|
// src/pipeline/history.ts
|
|
15503
|
-
import { existsSync as
|
|
15504
|
-
import
|
|
16129
|
+
import { existsSync as existsSync22 } from "fs";
|
|
16130
|
+
import path32 from "path";
|
|
15505
16131
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
15506
16132
|
function historyLimit(manifest, env = process.env) {
|
|
15507
16133
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -15588,10 +16214,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15588
16214
|
}
|
|
15589
16215
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
15590
16216
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
15591
|
-
if (!
|
|
16217
|
+
if (!existsSync22(file)) continue;
|
|
15592
16218
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
15593
16219
|
throw new OverwriteRefusedError(
|
|
15594
|
-
`${
|
|
16220
|
+
`${path32.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
15595
16221
|
[file]
|
|
15596
16222
|
);
|
|
15597
16223
|
}
|
|
@@ -15644,15 +16270,20 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15644
16270
|
// src/gallery/snapshot.ts
|
|
15645
16271
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15646
16272
|
function galleryMediaId(absolutePath) {
|
|
15647
|
-
return createHash4("sha256").update(
|
|
16273
|
+
return createHash4("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15648
16274
|
}
|
|
15649
16275
|
function galleryMediaRoute(id) {
|
|
15650
16276
|
return `/media/${id}`;
|
|
15651
16277
|
}
|
|
16278
|
+
function renderedSize(entry) {
|
|
16279
|
+
const vfx = entry?.providerMetadata?.[entry.provider]?.vfx;
|
|
16280
|
+
return typeof vfx?.width === "number" && typeof vfx.height === "number" ? { width: vfx.width, height: vfx.height } : null;
|
|
16281
|
+
}
|
|
15652
16282
|
function isDeclared(loaded, styleId, assetId) {
|
|
15653
16283
|
const asset = loaded.manifest.assets[assetId];
|
|
15654
|
-
|
|
15655
|
-
|
|
16284
|
+
const style = loaded.manifest.styles[styleId];
|
|
16285
|
+
if (!asset || !style) return false;
|
|
16286
|
+
return assetInStyle(asset, styleId, style);
|
|
15656
16287
|
}
|
|
15657
16288
|
function matchesFilter(filter, styleId, assetId) {
|
|
15658
16289
|
if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
|
|
@@ -15663,9 +16294,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15663
16294
|
return (entry.history ?? []).map((generation, i) => {
|
|
15664
16295
|
const outputs = generation.outputs.map((output) => {
|
|
15665
16296
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15666
|
-
const file =
|
|
16297
|
+
const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15667
16298
|
let url = null;
|
|
15668
|
-
if (
|
|
16299
|
+
if (existsSync23(file)) {
|
|
15669
16300
|
const id = galleryMediaId(file);
|
|
15670
16301
|
media.set(id, { path: file, contentType: mediaType });
|
|
15671
16302
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15697,7 +16328,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15697
16328
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
15698
16329
|
async function samePixels(a, b) {
|
|
15699
16330
|
try {
|
|
15700
|
-
const [left, right] = await Promise.all([
|
|
16331
|
+
const [left, right] = await Promise.all([readFile21(a), readFile21(b)]);
|
|
15701
16332
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
15702
16333
|
const x = decodePng(left);
|
|
15703
16334
|
const y = decodePng(right);
|
|
@@ -15795,10 +16426,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15795
16426
|
async function describeQuality(media, root, inspection) {
|
|
15796
16427
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15797
16428
|
const outputs = await Promise.all(
|
|
15798
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
16429
|
+
outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
|
|
15799
16430
|
);
|
|
15800
|
-
const recordPath =
|
|
15801
|
-
const recordExists =
|
|
16431
|
+
const recordPath = path33.resolve(root, inspection.record);
|
|
16432
|
+
const recordExists = existsSync23(recordPath);
|
|
15802
16433
|
let options = null;
|
|
15803
16434
|
let check = null;
|
|
15804
16435
|
if (recordExists) {
|
|
@@ -15817,7 +16448,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15817
16448
|
return {
|
|
15818
16449
|
state: inspection.state,
|
|
15819
16450
|
reason: inspection.reason,
|
|
15820
|
-
output: portableOutputPath(
|
|
16451
|
+
output: portableOutputPath(path33.resolve(root, inspection.output), root),
|
|
15821
16452
|
record: portableOutputPath(recordPath, root),
|
|
15822
16453
|
recordExists,
|
|
15823
16454
|
outputs,
|
|
@@ -15855,9 +16486,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15855
16486
|
const media = /* @__PURE__ */ new Map();
|
|
15856
16487
|
const plan = await buildPlan(specs, lock);
|
|
15857
16488
|
const items = [];
|
|
15858
|
-
const manifestText = await
|
|
16489
|
+
const manifestText = await readFile21(loaded.path, "utf8");
|
|
15859
16490
|
const manifestSha256 = sha256(manifestText);
|
|
15860
|
-
const cacheDir =
|
|
16491
|
+
const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15861
16492
|
let rawStyles = {};
|
|
15862
16493
|
try {
|
|
15863
16494
|
const raw = JSON.parse(manifestText);
|
|
@@ -15881,12 +16512,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15881
16512
|
)
|
|
15882
16513
|
);
|
|
15883
16514
|
if (spec.source) {
|
|
15884
|
-
const editPath =
|
|
16515
|
+
const editPath = path33.resolve(root, spec.source);
|
|
15885
16516
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15886
16517
|
const editShas = await Promise.all(
|
|
15887
16518
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15888
16519
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15889
|
-
return { file, sha256:
|
|
16520
|
+
return { file, sha256: existsSync23(file) ? await sha256File(file) : null };
|
|
15890
16521
|
})
|
|
15891
16522
|
);
|
|
15892
16523
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15894,7 +16525,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15894
16525
|
const companion = await readHandEditCompanion(editPath);
|
|
15895
16526
|
if (companion) {
|
|
15896
16527
|
const projectPath = handEditProjectPath(editPath);
|
|
15897
|
-
const project = companion.project &&
|
|
16528
|
+
const project = companion.project && existsSync23(projectPath) ? await fileInfo(projectPath) : null;
|
|
15898
16529
|
let projectUrl = null;
|
|
15899
16530
|
if (project) {
|
|
15900
16531
|
const id = galleryMediaId(projectPath);
|
|
@@ -15930,8 +16561,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15930
16561
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15931
16562
|
}
|
|
15932
16563
|
} else if (spec.source) {
|
|
15933
|
-
outputs = [await describeOutput(media, root,
|
|
15934
|
-
} else if (!entry &&
|
|
16564
|
+
outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
|
|
16565
|
+
} else if (!entry && existsSync23(spec.outFile)) {
|
|
15935
16566
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15936
16567
|
} else {
|
|
15937
16568
|
outputs = [];
|
|
@@ -15952,8 +16583,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15952
16583
|
generator: entry?.generator ?? spec.generator,
|
|
15953
16584
|
prompt: entry?.prompt ?? spec.prompt,
|
|
15954
16585
|
currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
|
|
15955
|
-
width: entry?.width ?? spec.width,
|
|
15956
|
-
height: entry?.height ?? spec.height,
|
|
16586
|
+
width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
|
|
16587
|
+
height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
|
|
15957
16588
|
tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
|
|
15958
16589
|
fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
|
|
15959
16590
|
cost: entry?.cost ?? 0,
|
|
@@ -16134,7 +16765,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16134
16765
|
id: loaded.manifest.name,
|
|
16135
16766
|
name: loaded.manifest.name,
|
|
16136
16767
|
manifest: loaded.path,
|
|
16137
|
-
lock:
|
|
16768
|
+
lock: path33.resolve(opts.lockPath),
|
|
16138
16769
|
root,
|
|
16139
16770
|
provider: loaded.manifest.provider,
|
|
16140
16771
|
account: null,
|
|
@@ -16160,7 +16791,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16160
16791
|
return { snapshot, media };
|
|
16161
16792
|
}
|
|
16162
16793
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
16163
|
-
const dir =
|
|
16794
|
+
const dir = path33.dirname(path33.resolve(opts.workspacePath));
|
|
16164
16795
|
const media = /* @__PURE__ */ new Map();
|
|
16165
16796
|
const projects = [];
|
|
16166
16797
|
const items = [];
|
|
@@ -16203,11 +16834,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16203
16834
|
id: project.id,
|
|
16204
16835
|
name: loaded.manifest.name,
|
|
16205
16836
|
manifest: manifestPath,
|
|
16206
|
-
lock:
|
|
16837
|
+
lock: path33.resolve(lockPath),
|
|
16207
16838
|
root: loaded.root,
|
|
16208
16839
|
provider: loaded.manifest.provider,
|
|
16209
16840
|
account: project.account ?? null,
|
|
16210
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
16841
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile21(manifestPath, "utf8")),
|
|
16211
16842
|
historyLimit: historyLimit(loaded.manifest),
|
|
16212
16843
|
entries: Object.keys(lock.entries).length,
|
|
16213
16844
|
items: projectItems.length,
|
|
@@ -16219,8 +16850,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16219
16850
|
id: project.id,
|
|
16220
16851
|
name: project.id,
|
|
16221
16852
|
manifest: manifestPath,
|
|
16222
|
-
lock:
|
|
16223
|
-
root:
|
|
16853
|
+
lock: path33.resolve(lockPath),
|
|
16854
|
+
root: path33.dirname(manifestPath),
|
|
16224
16855
|
provider: project.provider,
|
|
16225
16856
|
account: project.account ?? null,
|
|
16226
16857
|
manifestSha256: null,
|
|
@@ -16247,7 +16878,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16247
16878
|
version: 1,
|
|
16248
16879
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
16249
16880
|
project: null,
|
|
16250
|
-
workspace: { path:
|
|
16881
|
+
workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
|
|
16251
16882
|
filter,
|
|
16252
16883
|
totals: {
|
|
16253
16884
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -16266,10 +16897,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16266
16897
|
// src/gallery/server.ts
|
|
16267
16898
|
import { randomBytes } from "crypto";
|
|
16268
16899
|
import { createServer as createServer2 } from "http";
|
|
16269
|
-
import { readFile as
|
|
16900
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
16270
16901
|
|
|
16271
16902
|
// src/gallery/page.ts
|
|
16272
|
-
import { existsSync as
|
|
16903
|
+
import { existsSync as existsSync24, readFileSync as readFileSync5 } from "fs";
|
|
16273
16904
|
|
|
16274
16905
|
// src/gallery/client-bundle.ts
|
|
16275
16906
|
import { createRequire } from "module";
|
|
@@ -16310,7 +16941,7 @@ function clientAssets() {
|
|
|
16310
16941
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
16311
16942
|
client ??= {
|
|
16312
16943
|
css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
16313
|
-
js:
|
|
16944
|
+
js: existsSync24(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
|
|
16314
16945
|
};
|
|
16315
16946
|
return client;
|
|
16316
16947
|
}
|
|
@@ -16581,7 +17212,7 @@ async function serveGallery(opts) {
|
|
|
16581
17212
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
16582
17213
|
if (!asset) return fail(404, "no such review asset");
|
|
16583
17214
|
try {
|
|
16584
|
-
const bytes = await
|
|
17215
|
+
const bytes = await readFile22(asset.path);
|
|
16585
17216
|
res.writeHead(200, {
|
|
16586
17217
|
"Content-Type": asset.contentType,
|
|
16587
17218
|
"Cache-Control": "no-store",
|
|
@@ -16664,7 +17295,7 @@ async function serveGallery(opts) {
|
|
|
16664
17295
|
if (!asset) return fail(404, "no such gallery media");
|
|
16665
17296
|
let bytes;
|
|
16666
17297
|
try {
|
|
16667
|
-
bytes = await
|
|
17298
|
+
bytes = await readFile22(asset.path);
|
|
16668
17299
|
} catch {
|
|
16669
17300
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
16670
17301
|
}
|
|
@@ -17037,6 +17668,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
17037
17668
|
const anchor = spec.character?.styleAnchor;
|
|
17038
17669
|
const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
|
|
17039
17670
|
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
17671
|
+
const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
|
|
17672
|
+
const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
|
|
17040
17673
|
const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
|
|
17041
17674
|
path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
|
|
17042
17675
|
role: output.role
|
|
@@ -17049,6 +17682,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
17049
17682
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
17050
17683
|
...parentOutputs ? { parentOutputs } : {},
|
|
17051
17684
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
17685
|
+
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
17052
17686
|
...previousMetadata ? { previousMetadata } : {},
|
|
17053
17687
|
checkpoint: async (checkpoint) => {
|
|
17054
17688
|
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
@@ -17124,13 +17758,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
17124
17758
|
}
|
|
17125
17759
|
|
|
17126
17760
|
// src/pick/server.ts
|
|
17127
|
-
import { existsSync as
|
|
17128
|
-
import { readFile as
|
|
17761
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17762
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
17129
17763
|
|
|
17130
17764
|
// src/pipeline/init.ts
|
|
17131
|
-
import { readdir, readFile as
|
|
17132
|
-
import { existsSync as
|
|
17133
|
-
import
|
|
17765
|
+
import { readdir, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
|
|
17766
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17767
|
+
import path34 from "path";
|
|
17134
17768
|
function pngSize(buf) {
|
|
17135
17769
|
if (buf.length < 24) return null;
|
|
17136
17770
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -17144,7 +17778,7 @@ function slugify(value) {
|
|
|
17144
17778
|
async function walk(dir, exclude) {
|
|
17145
17779
|
const out = [];
|
|
17146
17780
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
17147
|
-
const full =
|
|
17781
|
+
const full = path34.join(dir, entry.name);
|
|
17148
17782
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
17149
17783
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
17150
17784
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -17158,14 +17792,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
17158
17792
|
const skipped = [];
|
|
17159
17793
|
const seen = /* @__PURE__ */ new Map();
|
|
17160
17794
|
for (const file of files.sort()) {
|
|
17161
|
-
const size = pngSize(await
|
|
17795
|
+
const size = pngSize(await readFile23(file));
|
|
17162
17796
|
if (!size) {
|
|
17163
17797
|
skipped.push(`${file} (not a readable PNG)`);
|
|
17164
17798
|
continue;
|
|
17165
17799
|
}
|
|
17166
|
-
const rel =
|
|
17167
|
-
const category =
|
|
17168
|
-
let id = slugify(
|
|
17800
|
+
const rel = path34.relative(root, file);
|
|
17801
|
+
const category = path34.dirname(rel) === "." ? "" : path34.dirname(rel);
|
|
17802
|
+
let id = slugify(path34.basename(rel));
|
|
17169
17803
|
if (seen.has(id)) {
|
|
17170
17804
|
const n = seen.get(id) + 1;
|
|
17171
17805
|
seen.set(id, n);
|
|
@@ -17223,10 +17857,10 @@ function mode(values) {
|
|
|
17223
17857
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
17224
17858
|
}
|
|
17225
17859
|
async function writeManifestFile(target, manifest) {
|
|
17226
|
-
if (
|
|
17860
|
+
if (existsSync25(target)) {
|
|
17227
17861
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
17228
17862
|
}
|
|
17229
|
-
await
|
|
17863
|
+
await writeFile17(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
17230
17864
|
}
|
|
17231
17865
|
|
|
17232
17866
|
// src/pick/sheet.ts
|
|
@@ -17639,13 +18273,13 @@ async function currentArt(entry, spec) {
|
|
|
17639
18273
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
17640
18274
|
if (!previous) return null;
|
|
17641
18275
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
17642
|
-
if (!
|
|
18276
|
+
if (!existsSync26(file)) return null;
|
|
17643
18277
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
17644
18278
|
if (!contentType) return null;
|
|
17645
18279
|
let width = entry.width;
|
|
17646
18280
|
let height = entry.height;
|
|
17647
18281
|
if (contentType === "image/png") {
|
|
17648
|
-
const size = pngSize(await
|
|
18282
|
+
const size = pngSize(await readFile24(file));
|
|
17649
18283
|
if (size) ({ width, height } = size);
|
|
17650
18284
|
}
|
|
17651
18285
|
return { path: file, contentType, width, height };
|
|
@@ -17653,7 +18287,7 @@ async function currentArt(entry, spec) {
|
|
|
17653
18287
|
async function cachedFrame(url) {
|
|
17654
18288
|
if (!url.startsWith("file://")) return null;
|
|
17655
18289
|
const file = url.slice("file://".length);
|
|
17656
|
-
const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await
|
|
18290
|
+
const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await readFile24(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
|
|
17657
18291
|
return { path: file, contentType };
|
|
17658
18292
|
}
|
|
17659
18293
|
async function prepareReview(provider, lock, opts = {}) {
|
|
@@ -18199,15 +18833,15 @@ function createGenerateHandlers(opts) {
|
|
|
18199
18833
|
}
|
|
18200
18834
|
|
|
18201
18835
|
// src/gallery/editor.ts
|
|
18202
|
-
import { readFile as
|
|
18203
|
-
import
|
|
18836
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
18837
|
+
import path36 from "path";
|
|
18204
18838
|
|
|
18205
18839
|
// src/editor/install.ts
|
|
18206
18840
|
import { createHash as createHash5 } from "crypto";
|
|
18207
|
-
import { existsSync as
|
|
18208
|
-
import { mkdir as
|
|
18841
|
+
import { existsSync as existsSync27 } from "fs";
|
|
18842
|
+
import { mkdir as mkdir13, readFile as readFile25, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile18 } from "fs/promises";
|
|
18209
18843
|
import os3 from "os";
|
|
18210
|
-
import
|
|
18844
|
+
import path35 from "path";
|
|
18211
18845
|
|
|
18212
18846
|
// tools/pixelorama-bridge/pin.json
|
|
18213
18847
|
var pin_default = {
|
|
@@ -18271,15 +18905,15 @@ var EditorInstallError = class extends Error {
|
|
|
18271
18905
|
};
|
|
18272
18906
|
function editorToolsRoot() {
|
|
18273
18907
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
18274
|
-
if (override) return
|
|
18275
|
-
if (process.platform === "darwin") return
|
|
18908
|
+
if (override) return path35.resolve(override);
|
|
18909
|
+
if (process.platform === "darwin") return path35.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
18276
18910
|
if (process.platform === "win32") {
|
|
18277
|
-
return
|
|
18911
|
+
return path35.join(process.env.LOCALAPPDATA ?? path35.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
18278
18912
|
}
|
|
18279
|
-
return
|
|
18913
|
+
return path35.join(process.env.XDG_CACHE_HOME ?? path35.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
|
|
18280
18914
|
}
|
|
18281
18915
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
18282
|
-
return
|
|
18916
|
+
return path35.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
18283
18917
|
}
|
|
18284
18918
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
18285
18919
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -18289,7 +18923,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
18289
18923
|
}
|
|
18290
18924
|
async function sha256Of(file) {
|
|
18291
18925
|
try {
|
|
18292
|
-
return createHash5("sha256").update(await
|
|
18926
|
+
return createHash5("sha256").update(await readFile25(file)).digest("hex");
|
|
18293
18927
|
} catch {
|
|
18294
18928
|
return null;
|
|
18295
18929
|
}
|
|
@@ -18302,8 +18936,8 @@ async function editorStatus(opts = {}) {
|
|
|
18302
18936
|
let totalBytes = 0;
|
|
18303
18937
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
18304
18938
|
totalBytes += expected.bytes;
|
|
18305
|
-
const file =
|
|
18306
|
-
const info =
|
|
18939
|
+
const file = path35.join(dir, name);
|
|
18940
|
+
const info = existsSync27(file) ? await stat3(file).catch(() => null) : null;
|
|
18307
18941
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
18308
18942
|
installedBytes += expected.bytes;
|
|
18309
18943
|
} else {
|
|
@@ -18332,7 +18966,7 @@ async function installEditor(opts = {}) {
|
|
|
18332
18966
|
);
|
|
18333
18967
|
}
|
|
18334
18968
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
18335
|
-
await
|
|
18969
|
+
await mkdir13(dir, { recursive: true });
|
|
18336
18970
|
const downloaded = [];
|
|
18337
18971
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
18338
18972
|
let fetchedBytes = before.installedBytes;
|
|
@@ -18360,10 +18994,10 @@ async function installEditor(opts = {}) {
|
|
|
18360
18994
|
if (digest2 !== expected.sha256) {
|
|
18361
18995
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
18362
18996
|
}
|
|
18363
|
-
const final =
|
|
18997
|
+
const final = path35.join(dir, name);
|
|
18364
18998
|
const tmp = `${final}.${process.pid}.part`;
|
|
18365
18999
|
try {
|
|
18366
|
-
await
|
|
19000
|
+
await writeFile18(tmp, bytes);
|
|
18367
19001
|
await rename10(tmp, final);
|
|
18368
19002
|
} finally {
|
|
18369
19003
|
await rm11(tmp, { force: true });
|
|
@@ -18406,7 +19040,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
18406
19040
|
".json": "application/json; charset=utf-8"
|
|
18407
19041
|
};
|
|
18408
19042
|
function editorContentType(name) {
|
|
18409
|
-
return EDITOR_CONTENT_TYPES[
|
|
19043
|
+
return EDITOR_CONTENT_TYPES[path35.extname(name).toLowerCase()] ?? null;
|
|
18410
19044
|
}
|
|
18411
19045
|
|
|
18412
19046
|
// src/gallery/editor.ts
|
|
@@ -18482,7 +19116,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
18482
19116
|
if (!status.installed) return null;
|
|
18483
19117
|
let bytes;
|
|
18484
19118
|
try {
|
|
18485
|
-
bytes = await
|
|
19119
|
+
bytes = await readFile26(path36.join(dir, name));
|
|
18486
19120
|
} catch {
|
|
18487
19121
|
verified = null;
|
|
18488
19122
|
return null;
|
|
@@ -18522,7 +19156,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18522
19156
|
};
|
|
18523
19157
|
const loadProject = async (project) => {
|
|
18524
19158
|
const ctx = await access2.loadProject(project);
|
|
18525
|
-
loadProjectEnv(
|
|
19159
|
+
loadProjectEnv(path37.dirname(ctx.loaded.path));
|
|
18526
19160
|
return ctx;
|
|
18527
19161
|
};
|
|
18528
19162
|
const loadProjectEnv = (dir) => {
|
|
@@ -18531,7 +19165,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18531
19165
|
for (const [name, value] of Object.entries(declared)) {
|
|
18532
19166
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
18533
19167
|
throw new Error(
|
|
18534
|
-
`${
|
|
19168
|
+
`${path37.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.`
|
|
18535
19169
|
);
|
|
18536
19170
|
}
|
|
18537
19171
|
}
|
|
@@ -18539,7 +19173,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18539
19173
|
};
|
|
18540
19174
|
const quoters = /* @__PURE__ */ new Map();
|
|
18541
19175
|
const quoteFor = async (project) => {
|
|
18542
|
-
loadProjectEnv(
|
|
19176
|
+
loadProjectEnv(path37.dirname(path37.resolve(await access2.manifestFor(project))));
|
|
18543
19177
|
const key = process.env.PIXELLAB_API_KEY;
|
|
18544
19178
|
if (!key) return null;
|
|
18545
19179
|
let quoter = quoters.get(key);
|
|
@@ -18590,8 +19224,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18590
19224
|
}
|
|
18591
19225
|
async function runGallery(args) {
|
|
18592
19226
|
if (args.workspace) {
|
|
18593
|
-
const workspacePath =
|
|
18594
|
-
if (!
|
|
19227
|
+
const workspacePath = path37.resolve(args.workspace);
|
|
19228
|
+
if (!existsSync28(workspacePath)) {
|
|
18595
19229
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18596
19230
|
}
|
|
18597
19231
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -18611,7 +19245,7 @@ async function runGallery(args) {
|
|
|
18611
19245
|
const registered = async (projectId) => {
|
|
18612
19246
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
18613
19247
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
18614
|
-
return resolveProject(
|
|
19248
|
+
return resolveProject(path37.dirname(workspacePath), project);
|
|
18615
19249
|
};
|
|
18616
19250
|
await serveUntilStopped(initial, build2, args, {
|
|
18617
19251
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -18636,7 +19270,7 @@ async function runGallery(args) {
|
|
|
18636
19270
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
18637
19271
|
};
|
|
18638
19272
|
await serveUntilStopped(await build(), reload, args, {
|
|
18639
|
-
manifestFor: () =>
|
|
19273
|
+
manifestFor: () => path37.resolve(args.manifest),
|
|
18640
19274
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
18641
19275
|
});
|
|
18642
19276
|
}
|
|
@@ -18879,7 +19513,7 @@ async function runLifecycle(args, wave, spent) {
|
|
|
18879
19513
|
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
18880
19514
|
}
|
|
18881
19515
|
}
|
|
18882
|
-
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror);
|
|
19516
|
+
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror || spec.scene || spec.vfx?.sprite && "file" in spec.vfx.sprite);
|
|
18883
19517
|
}
|
|
18884
19518
|
return false;
|
|
18885
19519
|
}
|
|
@@ -18945,18 +19579,18 @@ async function runRestoreGeneration(args) {
|
|
|
18945
19579
|
}
|
|
18946
19580
|
|
|
18947
19581
|
// src/cli/commands/housekeeping.ts
|
|
18948
|
-
import { existsSync as
|
|
19582
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18949
19583
|
|
|
18950
19584
|
// src/pipeline/cache-health.ts
|
|
18951
|
-
import { existsSync as
|
|
18952
|
-
import { readFile as
|
|
18953
|
-
import
|
|
19585
|
+
import { existsSync as existsSync29 } from "fs";
|
|
19586
|
+
import { readFile as readFile27, readdir as readdir2, rm as rm12 } from "fs/promises";
|
|
19587
|
+
import path38 from "path";
|
|
18954
19588
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18955
|
-
if (options.prune && !
|
|
18956
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
19589
|
+
if (options.prune && !existsSync29(lockPath)) {
|
|
19590
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path38.resolve(lockPath)}`);
|
|
18957
19591
|
}
|
|
18958
|
-
const contentDir =
|
|
18959
|
-
const remotePath =
|
|
19592
|
+
const contentDir = path38.resolve(path38.dirname(lockPath), ".pixelkiln", "cache");
|
|
19593
|
+
const remotePath = path38.resolve(cachePathFor(lockPath));
|
|
18960
19594
|
const referenced = referencedHashes(lock);
|
|
18961
19595
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18962
19596
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18968,7 +19602,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18968
19602
|
]);
|
|
18969
19603
|
for (const name of names) {
|
|
18970
19604
|
try {
|
|
18971
|
-
await rm12(
|
|
19605
|
+
await rm12(path38.join(contentDir, name), { force: true });
|
|
18972
19606
|
removed.contentFiles++;
|
|
18973
19607
|
} catch {
|
|
18974
19608
|
}
|
|
@@ -18977,7 +19611,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18977
19611
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
18978
19612
|
removed.resetRemoteHashCache = true;
|
|
18979
19613
|
} else if (remoteHashes.invalidIds.length) {
|
|
18980
|
-
const cache = parseCache(JSON.parse(await
|
|
19614
|
+
const cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
|
|
18981
19615
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
18982
19616
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
18983
19617
|
await saveCache(remotePath, cache);
|
|
@@ -18995,7 +19629,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18995
19629
|
async function inspectContentCache(contentDir, referenced) {
|
|
18996
19630
|
const report = {
|
|
18997
19631
|
path: contentDir,
|
|
18998
|
-
exists:
|
|
19632
|
+
exists: existsSync29(contentDir),
|
|
18999
19633
|
files: 0,
|
|
19000
19634
|
bytes: 0,
|
|
19001
19635
|
valid: 0,
|
|
@@ -19016,12 +19650,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
19016
19650
|
continue;
|
|
19017
19651
|
}
|
|
19018
19652
|
report.files++;
|
|
19019
|
-
const file =
|
|
19653
|
+
const file = path38.join(contentDir, entry.name);
|
|
19020
19654
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
19021
19655
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
19022
19656
|
let bytes;
|
|
19023
19657
|
try {
|
|
19024
|
-
bytes = await
|
|
19658
|
+
bytes = await readFile27(file);
|
|
19025
19659
|
report.bytes += bytes.length;
|
|
19026
19660
|
} catch (err) {
|
|
19027
19661
|
report.invalid.push({
|
|
@@ -19059,7 +19693,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
19059
19693
|
async function inspectRemoteHashCache(remotePath) {
|
|
19060
19694
|
const report = {
|
|
19061
19695
|
path: remotePath,
|
|
19062
|
-
exists:
|
|
19696
|
+
exists: existsSync29(remotePath),
|
|
19063
19697
|
entries: 0,
|
|
19064
19698
|
valid: 0,
|
|
19065
19699
|
invalidIds: [],
|
|
@@ -19068,7 +19702,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
19068
19702
|
if (!report.exists) return report;
|
|
19069
19703
|
let cache;
|
|
19070
19704
|
try {
|
|
19071
|
-
cache = parseCache(JSON.parse(await
|
|
19705
|
+
cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
|
|
19072
19706
|
} catch (err) {
|
|
19073
19707
|
report.error = err instanceof Error ? err.message : String(err);
|
|
19074
19708
|
return report;
|
|
@@ -19132,7 +19766,7 @@ async function runAccept(args) {
|
|
|
19132
19766
|
let intact = true;
|
|
19133
19767
|
for (const output of entry.outputs) {
|
|
19134
19768
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
19135
|
-
if (!
|
|
19769
|
+
if (!existsSync30(file) || await sha256File(file) !== output.sha256) {
|
|
19136
19770
|
intact = false;
|
|
19137
19771
|
break;
|
|
19138
19772
|
}
|
|
@@ -19187,42 +19821,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
19187
19821
|
}
|
|
19188
19822
|
|
|
19189
19823
|
// src/cli/commands/init.ts
|
|
19190
|
-
import
|
|
19191
|
-
import { existsSync as
|
|
19824
|
+
import path39 from "path";
|
|
19825
|
+
import { existsSync as existsSync31 } from "fs";
|
|
19192
19826
|
async function runInit(args) {
|
|
19193
19827
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
19194
|
-
const root =
|
|
19195
|
-
if (!
|
|
19828
|
+
const root = path39.resolve(args.from);
|
|
19829
|
+
if (!existsSync31(root)) throw new Error(`No directory at ${root}`);
|
|
19196
19830
|
const generator = args.generator ?? "map";
|
|
19197
19831
|
if (generator !== "1dir" && generator !== "map") {
|
|
19198
19832
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
19199
19833
|
}
|
|
19200
|
-
const target =
|
|
19834
|
+
const target = path39.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
19201
19835
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
19202
19836
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
19203
19837
|
const manifest = buildManifest(
|
|
19204
|
-
args.name ??
|
|
19838
|
+
args.name ?? path39.basename(path39.dirname(target)),
|
|
19205
19839
|
args.styles[0] ?? "base",
|
|
19206
19840
|
generator,
|
|
19207
|
-
|
|
19841
|
+
path39.relative(path39.dirname(target), root) || ".",
|
|
19208
19842
|
assets
|
|
19209
19843
|
);
|
|
19210
19844
|
await writeManifestFile(target, manifest);
|
|
19211
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
19845
|
+
log(` scanned ${assets.length} PNG(s) under ${path39.relative(process.cwd(), root)}`);
|
|
19212
19846
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
19213
|
-
log(` wrote ${
|
|
19847
|
+
log(` wrote ${path39.relative(process.cwd(), target)}`);
|
|
19214
19848
|
log(`
|
|
19215
19849
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
19216
19850
|
log(` PixelLab account instead of inventing them:`);
|
|
19217
19851
|
log(`
|
|
19218
|
-
pixelkiln adopt --manifest ${
|
|
19852
|
+
pixelkiln adopt --manifest ${path39.relative(process.cwd(), target)} --write-prompts
|
|
19219
19853
|
`);
|
|
19220
19854
|
}
|
|
19221
19855
|
|
|
19222
19856
|
// src/pipeline/doctor.ts
|
|
19223
|
-
import { constants, existsSync as
|
|
19857
|
+
import { constants, existsSync as existsSync32 } from "fs";
|
|
19224
19858
|
import { access, stat as stat4 } from "fs/promises";
|
|
19225
|
-
import
|
|
19859
|
+
import path40 from "path";
|
|
19226
19860
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
19227
19861
|
const checks = [];
|
|
19228
19862
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -19233,8 +19867,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19233
19867
|
);
|
|
19234
19868
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
19235
19869
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
19236
|
-
|
|
19237
|
-
...spec.quality ? [
|
|
19870
|
+
path40.dirname(spec.outFile),
|
|
19871
|
+
...spec.quality ? [path40.dirname(spec.quality.outFile)] : []
|
|
19238
19872
|
]))];
|
|
19239
19873
|
const unwritable = [];
|
|
19240
19874
|
for (const dir of dirs) {
|
|
@@ -19257,7 +19891,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19257
19891
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
19258
19892
|
const output = entry.outputs[index];
|
|
19259
19893
|
const spec = specByKey.get(key);
|
|
19260
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
19894
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path40.dirname(lockPath));
|
|
19261
19895
|
const owner = outputOwners.get(absolute);
|
|
19262
19896
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
19263
19897
|
outputOwners.set(absolute, key);
|
|
@@ -19272,7 +19906,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19272
19906
|
} else {
|
|
19273
19907
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
19274
19908
|
}
|
|
19275
|
-
const cacheDir =
|
|
19909
|
+
const cacheDir = path40.join(path40.dirname(lockPath), ".pixelkiln", "cache");
|
|
19276
19910
|
const stranded = [];
|
|
19277
19911
|
const invalidState = [];
|
|
19278
19912
|
const staleMapUrls = [];
|
|
@@ -19288,11 +19922,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19288
19922
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
19289
19923
|
let hasCache = false;
|
|
19290
19924
|
for (const output of entry.outputs) {
|
|
19291
|
-
const cached =
|
|
19925
|
+
const cached = path40.join(
|
|
19292
19926
|
cacheDir,
|
|
19293
19927
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
19294
19928
|
);
|
|
19295
|
-
if (
|
|
19929
|
+
if (existsSync32(cached) && await sha256File(cached) === output.sha256) {
|
|
19296
19930
|
hasCache = true;
|
|
19297
19931
|
break;
|
|
19298
19932
|
}
|
|
@@ -19388,16 +20022,16 @@ async function nearestExistingDirectory(start) {
|
|
|
19388
20022
|
if ((await stat4(current)).isDirectory()) return current;
|
|
19389
20023
|
} catch {
|
|
19390
20024
|
}
|
|
19391
|
-
const parent =
|
|
20025
|
+
const parent = path40.dirname(current);
|
|
19392
20026
|
if (parent === current) return current;
|
|
19393
20027
|
current = parent;
|
|
19394
20028
|
}
|
|
19395
20029
|
}
|
|
19396
20030
|
|
|
19397
20031
|
// src/pipeline/audit.ts
|
|
19398
|
-
import { readFile as
|
|
19399
|
-
import { existsSync as
|
|
19400
|
-
import
|
|
20032
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
20033
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20034
|
+
import path41 from "path";
|
|
19401
20035
|
function paletteDistance(asset, reference) {
|
|
19402
20036
|
if (!asset.length || !reference.length) return 0;
|
|
19403
20037
|
let total = 0;
|
|
@@ -19433,12 +20067,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19433
20067
|
const unreadable = [];
|
|
19434
20068
|
for (const spec of mine) {
|
|
19435
20069
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
19436
|
-
if (!
|
|
20070
|
+
if (!existsSync33(output.absolutePath)) {
|
|
19437
20071
|
missing.push(output.id);
|
|
19438
20072
|
continue;
|
|
19439
20073
|
}
|
|
19440
20074
|
try {
|
|
19441
|
-
const png = decodePng(await
|
|
20075
|
+
const png = decodePng(await readFile28(output.absolutePath));
|
|
19442
20076
|
const palette = extractPalette(png, 12);
|
|
19443
20077
|
assets.push({
|
|
19444
20078
|
assetId: spec.assetId,
|
|
@@ -19462,10 +20096,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19462
20096
|
let referenceFromStyleImages = false;
|
|
19463
20097
|
const refPalettes = [];
|
|
19464
20098
|
for (const img of style.styleImages) {
|
|
19465
|
-
const abs =
|
|
19466
|
-
if (!
|
|
20099
|
+
const abs = path41.resolve(loaded.root, img.path);
|
|
20100
|
+
if (!existsSync33(abs)) continue;
|
|
19467
20101
|
try {
|
|
19468
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
20102
|
+
refPalettes.push(extractPalette(decodePng(await readFile28(abs)), 12));
|
|
19469
20103
|
} catch {
|
|
19470
20104
|
}
|
|
19471
20105
|
}
|
|
@@ -19693,24 +20327,24 @@ async function runAudit(args) {
|
|
|
19693
20327
|
}
|
|
19694
20328
|
|
|
19695
20329
|
// src/cli/commands/pack.ts
|
|
19696
|
-
import
|
|
19697
|
-
import { readFile as
|
|
20330
|
+
import path43 from "path";
|
|
20331
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
19698
20332
|
|
|
19699
20333
|
// src/pipeline/pack.ts
|
|
19700
20334
|
import { readFileSync as readFileSync6 } from "fs";
|
|
19701
|
-
import
|
|
20335
|
+
import path42 from "path";
|
|
19702
20336
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
19703
20337
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
19704
20338
|
if (!Array.isArray(raw) || !raw.length) {
|
|
19705
20339
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
19706
20340
|
}
|
|
19707
|
-
const baseDir =
|
|
20341
|
+
const baseDir = path42.dirname(path42.resolve(inputsFilePath));
|
|
19708
20342
|
return raw.map((entry, i) => {
|
|
19709
20343
|
const e = entry;
|
|
19710
20344
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
19711
20345
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
19712
20346
|
}
|
|
19713
|
-
return { id: e.id, path:
|
|
20347
|
+
return { id: e.id, path: path42.resolve(baseDir, e.path) };
|
|
19714
20348
|
});
|
|
19715
20349
|
}
|
|
19716
20350
|
function packSprites(inputs, options = {}) {
|
|
@@ -19794,7 +20428,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19794
20428
|
}
|
|
19795
20429
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
19796
20430
|
id,
|
|
19797
|
-
path:
|
|
20431
|
+
path: path42.resolve(source)
|
|
19798
20432
|
}));
|
|
19799
20433
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
19800
20434
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -19818,7 +20452,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19818
20452
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
19819
20453
|
const source = sources[id];
|
|
19820
20454
|
if (source && outputs.length <= 1) {
|
|
19821
|
-
inputs.push({ id, path:
|
|
20455
|
+
inputs.push({ id, path: path42.resolve(manifestDir, source) });
|
|
19822
20456
|
continue;
|
|
19823
20457
|
}
|
|
19824
20458
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -19847,12 +20481,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19847
20481
|
id,
|
|
19848
20482
|
kind: isFrames ? "frames" : "members",
|
|
19849
20483
|
...fps ? { fps } : {},
|
|
20484
|
+
...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
|
|
19850
20485
|
frames: selected.map((output) => output.id)
|
|
19851
20486
|
});
|
|
19852
20487
|
}
|
|
19853
20488
|
}
|
|
19854
20489
|
for (const [id, source] of Object.entries(sources)) {
|
|
19855
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
20490
|
+
if (!locked.has(id)) inputs.push({ id, path: path42.resolve(manifestDir, source) });
|
|
19856
20491
|
}
|
|
19857
20492
|
if (!inputs.length) {
|
|
19858
20493
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19960,7 +20595,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19960
20595
|
const source = sources[id];
|
|
19961
20596
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19962
20597
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19963
|
-
placements.push({ id, path:
|
|
20598
|
+
placements.push({ id, path: path42.resolve(manifestDir, source), cell });
|
|
19964
20599
|
continue;
|
|
19965
20600
|
}
|
|
19966
20601
|
if (source && entry) {
|
|
@@ -19993,7 +20628,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19993
20628
|
`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}`
|
|
19994
20629
|
);
|
|
19995
20630
|
}
|
|
19996
|
-
const basePng = mount.base ? readFileSync6(
|
|
20631
|
+
const basePng = mount.base ? readFileSync6(path42.resolve(manifestDir, mount.base)) : void 0;
|
|
19997
20632
|
const mounted = mountSprites(placements, {
|
|
19998
20633
|
cellWidth: mount.cellWidth,
|
|
19999
20634
|
cellHeight: mount.cellHeight,
|
|
@@ -20003,7 +20638,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
20003
20638
|
...mounted,
|
|
20004
20639
|
sources: mount.base ? [{
|
|
20005
20640
|
id: "$base",
|
|
20006
|
-
path:
|
|
20641
|
+
path: path42.resolve(manifestDir, mount.base),
|
|
20007
20642
|
sha256: sha256(basePng),
|
|
20008
20643
|
included: true
|
|
20009
20644
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -20385,7 +21020,7 @@ function renderGodotSpriteFrames(atlas, opts) {
|
|
|
20385
21020
|
);
|
|
20386
21021
|
};
|
|
20387
21022
|
for (const set of atlas.sets ?? []) {
|
|
20388
|
-
if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, true);
|
|
21023
|
+
if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, set.loop ?? true);
|
|
20389
21024
|
else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
|
|
20390
21025
|
}
|
|
20391
21026
|
for (const frame of atlas.frames) {
|
|
@@ -20407,12 +21042,12 @@ async function runPack(args) {
|
|
|
20407
21042
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
20408
21043
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
20409
21044
|
}
|
|
20410
|
-
const raw = JSON.parse(await
|
|
21045
|
+
const raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
|
|
20411
21046
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
20412
21047
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
20413
21048
|
const format = sheetFormat(args);
|
|
20414
|
-
const base =
|
|
20415
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21049
|
+
const base = path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
21050
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
|
|
20416
21051
|
const outputs = [
|
|
20417
21052
|
{ path: `${base}.png`, data: png },
|
|
20418
21053
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20425,7 +21060,7 @@ async function runPack(args) {
|
|
|
20425
21060
|
log(
|
|
20426
21061
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
20427
21062
|
);
|
|
20428
|
-
log(` ${
|
|
21063
|
+
log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20429
21064
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20430
21065
|
return;
|
|
20431
21066
|
}
|
|
@@ -20433,7 +21068,7 @@ async function runPack(args) {
|
|
|
20433
21068
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
20434
21069
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
20435
21070
|
}
|
|
20436
|
-
const manifestDir =
|
|
21071
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20437
21072
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20438
21073
|
for (const styleId of styleIds) {
|
|
20439
21074
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20448,9 +21083,9 @@ async function runPack(args) {
|
|
|
20448
21083
|
sourceOverrides: qualitySources,
|
|
20449
21084
|
sources: manifestSources(loaded.manifest, styleId)
|
|
20450
21085
|
});
|
|
20451
|
-
const base = args.out ?
|
|
21086
|
+
const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path43.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
20452
21087
|
const format = sheetFormat(args);
|
|
20453
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21088
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
|
|
20454
21089
|
const outputs = [
|
|
20455
21090
|
{ path: `${base}.png`, data: png },
|
|
20456
21091
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20482,13 +21117,13 @@ async function runPack(args) {
|
|
|
20482
21117
|
log(
|
|
20483
21118
|
` ${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})`
|
|
20484
21119
|
);
|
|
20485
|
-
log(` ${
|
|
21120
|
+
log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20486
21121
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20487
21122
|
}
|
|
20488
21123
|
}
|
|
20489
21124
|
async function runMount(args) {
|
|
20490
21125
|
const { loaded, lock } = await openProject2(args);
|
|
20491
|
-
const manifestDir =
|
|
21126
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20492
21127
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20493
21128
|
for (const styleId of styleIds) {
|
|
20494
21129
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20527,9 +21162,9 @@ async function runMount(args) {
|
|
|
20527
21162
|
sources,
|
|
20528
21163
|
outputRoles
|
|
20529
21164
|
);
|
|
20530
|
-
const out =
|
|
21165
|
+
const out = path43.resolve(manifestDir, style.mount.out);
|
|
20531
21166
|
const format = sheetFormat(args);
|
|
20532
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21167
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(out) });
|
|
20533
21168
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
20534
21169
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
20535
21170
|
const outputs = [
|
|
@@ -20549,7 +21184,7 @@ async function runMount(args) {
|
|
|
20549
21184
|
await provenanceFile("$lock", args.lock),
|
|
20550
21185
|
...qualityRecords,
|
|
20551
21186
|
...artifactSources.filter(
|
|
20552
|
-
(source) => source.id !== "$base" ||
|
|
21187
|
+
(source) => source.id !== "$base" || path43.resolve(source.path) !== out
|
|
20553
21188
|
)
|
|
20554
21189
|
],
|
|
20555
21190
|
options: {
|
|
@@ -20563,14 +21198,14 @@ async function runMount(args) {
|
|
|
20563
21198
|
log(
|
|
20564
21199
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
20565
21200
|
);
|
|
20566
|
-
log(` ${
|
|
21201
|
+
log(` ${path43.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
20567
21202
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20568
21203
|
}
|
|
20569
21204
|
}
|
|
20570
21205
|
async function runExport(args) {
|
|
20571
21206
|
const { loaded, specs, lock } = await openProject2(args);
|
|
20572
21207
|
const format = args.format ?? "generic";
|
|
20573
|
-
const manifestDir =
|
|
21208
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20574
21209
|
const selected = specs.filter((spec) => {
|
|
20575
21210
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
20576
21211
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -20586,12 +21221,12 @@ async function runExport(args) {
|
|
|
20586
21221
|
for (const spec of selected) {
|
|
20587
21222
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
20588
21223
|
const style = loaded.manifest.styles[spec.styleId];
|
|
20589
|
-
const defaultBase =
|
|
20590
|
-
const base = args.out ?
|
|
21224
|
+
const defaultBase = path43.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
21225
|
+
const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
20591
21226
|
const result = exportTileset(entry, spec, {
|
|
20592
21227
|
format,
|
|
20593
21228
|
manifestDir,
|
|
20594
|
-
imageName:
|
|
21229
|
+
imageName: path43.basename(`${base}.png`),
|
|
20595
21230
|
columns: args.columns
|
|
20596
21231
|
});
|
|
20597
21232
|
const outputs = [
|
|
@@ -20609,7 +21244,7 @@ async function runExport(args) {
|
|
|
20609
21244
|
asset: spec.assetId,
|
|
20610
21245
|
columns: args.columns ?? null,
|
|
20611
21246
|
format,
|
|
20612
|
-
image:
|
|
21247
|
+
image: path43.basename(`${base}.png`),
|
|
20613
21248
|
providerRules: result.generic.providerRules,
|
|
20614
21249
|
style: spec.styleId,
|
|
20615
21250
|
tileType: spec.tileType ?? null
|
|
@@ -20619,7 +21254,7 @@ async function runExport(args) {
|
|
|
20619
21254
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
20620
21255
|
);
|
|
20621
21256
|
log(
|
|
20622
|
-
` ${
|
|
21257
|
+
` ${path43.relative(process.cwd(), base)}.png + ${path43.basename(base)}${result.extension} + .pixelkiln.json`
|
|
20623
21258
|
);
|
|
20624
21259
|
}
|
|
20625
21260
|
}
|
|
@@ -20632,23 +21267,23 @@ function sheetFormat(args) {
|
|
|
20632
21267
|
}
|
|
20633
21268
|
|
|
20634
21269
|
// src/cli/commands/pixellab-utils.ts
|
|
20635
|
-
import
|
|
21270
|
+
import path45 from "path";
|
|
20636
21271
|
|
|
20637
21272
|
// src/pixellab-utils.ts
|
|
20638
|
-
import { existsSync as
|
|
20639
|
-
import { mkdir as
|
|
20640
|
-
import
|
|
21273
|
+
import { existsSync as existsSync34 } from "fs";
|
|
21274
|
+
import { mkdir as mkdir14, readFile as readFile30, writeFile as writeFile19 } from "fs/promises";
|
|
21275
|
+
import path44 from "path";
|
|
20641
21276
|
var UNZOOM_MIN_SIDE = 256;
|
|
20642
21277
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
20643
21278
|
function defaultUnzoomOut(input) {
|
|
20644
|
-
const ext =
|
|
21279
|
+
const ext = path44.extname(input);
|
|
20645
21280
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
20646
21281
|
}
|
|
20647
21282
|
async function unzoomFile(client2, input, opts = {}) {
|
|
20648
|
-
const out =
|
|
20649
|
-
if (
|
|
20650
|
-
if (
|
|
20651
|
-
const bytes = await
|
|
21283
|
+
const out = path44.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
21284
|
+
if (path44.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
21285
|
+
if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
21286
|
+
const bytes = await readFile30(input);
|
|
20652
21287
|
const metadata = imageMetadata(bytes);
|
|
20653
21288
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20654
21289
|
const { width, height, format } = metadata;
|
|
@@ -20661,22 +21296,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
20661
21296
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
20662
21297
|
}
|
|
20663
21298
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
20664
|
-
await
|
|
20665
|
-
await
|
|
21299
|
+
await mkdir14(path44.dirname(out), { recursive: true });
|
|
21300
|
+
await writeFile19(out, res.png);
|
|
20666
21301
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
20667
21302
|
}
|
|
20668
21303
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
20669
21304
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
20670
|
-
var FONT_COST_GENERATIONS =
|
|
21305
|
+
var FONT_COST_GENERATIONS = 15;
|
|
20671
21306
|
function fontOutputPaths(out) {
|
|
20672
|
-
const base =
|
|
21307
|
+
const base = path44.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
20673
21308
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
20674
21309
|
}
|
|
20675
21310
|
async function generateFont(client2, opts) {
|
|
20676
21311
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
20677
21312
|
if (!opts.force) {
|
|
20678
21313
|
for (const file of [ttf, atlas]) {
|
|
20679
|
-
if (
|
|
21314
|
+
if (existsSync34(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
20680
21315
|
}
|
|
20681
21316
|
}
|
|
20682
21317
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -20703,9 +21338,9 @@ async function generateFont(client2, opts) {
|
|
|
20703
21338
|
client2.download(job.download_ttf_url),
|
|
20704
21339
|
client2.download(job.download_atlas_url)
|
|
20705
21340
|
]);
|
|
20706
|
-
await
|
|
20707
|
-
await
|
|
20708
|
-
await
|
|
21341
|
+
await mkdir14(path44.dirname(ttf), { recursive: true });
|
|
21342
|
+
await writeFile19(ttf, ttfBytes);
|
|
21343
|
+
await writeFile19(atlas, atlasBytes);
|
|
20709
21344
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
20710
21345
|
}
|
|
20711
21346
|
if (Date.now() - started > timeout) {
|
|
@@ -20716,10 +21351,65 @@ async function generateFont(client2, opts) {
|
|
|
20716
21351
|
await sleep3(interval);
|
|
20717
21352
|
}
|
|
20718
21353
|
}
|
|
21354
|
+
var PIXELATE_COST_GENERATIONS = 6;
|
|
21355
|
+
var PIXELATE_MAX_SIDE = 2048;
|
|
21356
|
+
function defaultPixelateOut(input) {
|
|
21357
|
+
const ext = path44.extname(input);
|
|
21358
|
+
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
21359
|
+
}
|
|
21360
|
+
async function pixelateFile(client2, input, opts = {}) {
|
|
21361
|
+
const out = path44.resolve(opts.out ?? defaultPixelateOut(input));
|
|
21362
|
+
if (path44.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
21363
|
+
if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
21364
|
+
const bytes = await readFile30(input);
|
|
21365
|
+
const metadata = imageMetadata(bytes);
|
|
21366
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
21367
|
+
const { width, height, format } = metadata;
|
|
21368
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
21369
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
21370
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
21371
|
+
const submitted = await client2.imageToPixelartProFlash({
|
|
21372
|
+
image: { base64: bytes.toString("base64"), format },
|
|
21373
|
+
description: opts.description,
|
|
21374
|
+
seed: opts.seed
|
|
21375
|
+
});
|
|
21376
|
+
const jobId = submitted.background_job_id;
|
|
21377
|
+
const started = Date.now();
|
|
21378
|
+
for (; ; ) {
|
|
21379
|
+
const job = await client2.getBackgroundJob(jobId);
|
|
21380
|
+
opts.onProgress?.(job.status);
|
|
21381
|
+
if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
|
|
21382
|
+
if (job.status === "completed") {
|
|
21383
|
+
const images = job.last_response?.images;
|
|
21384
|
+
const first = Array.isArray(images) ? images[0] : void 0;
|
|
21385
|
+
if (!first || typeof first.base64 !== "string" || !first.base64) {
|
|
21386
|
+
throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
|
|
21387
|
+
}
|
|
21388
|
+
const png = Buffer.from(first.base64, "base64");
|
|
21389
|
+
const size = imageMetadata(png);
|
|
21390
|
+
await mkdir14(path44.dirname(out), { recursive: true });
|
|
21391
|
+
await writeFile19(out, png);
|
|
21392
|
+
return {
|
|
21393
|
+
jobId,
|
|
21394
|
+
out,
|
|
21395
|
+
original: { width, height },
|
|
21396
|
+
pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
|
|
21397
|
+
imageId: submitted.image_id,
|
|
21398
|
+
usage: job.usage ?? null
|
|
21399
|
+
};
|
|
21400
|
+
}
|
|
21401
|
+
if (Date.now() - started > timeout) {
|
|
21402
|
+
throw new Error(
|
|
21403
|
+
`PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
21404
|
+
);
|
|
21405
|
+
}
|
|
21406
|
+
await sleep3(interval);
|
|
21407
|
+
}
|
|
21408
|
+
}
|
|
20719
21409
|
|
|
20720
21410
|
// src/cli/commands/pixellab-utils.ts
|
|
20721
21411
|
function loadKeys(args) {
|
|
20722
|
-
loadEnvFiles(
|
|
21412
|
+
loadEnvFiles(path45.dirname(path45.resolve(args.manifest)));
|
|
20723
21413
|
loadEnvFiles(process.cwd());
|
|
20724
21414
|
}
|
|
20725
21415
|
async function runUnzoom(args) {
|
|
@@ -20783,21 +21473,109 @@ async function runFont(args) {
|
|
|
20783
21473
|
log(` wrote ${res.ttf}`);
|
|
20784
21474
|
log(` wrote ${res.atlas}`);
|
|
20785
21475
|
}
|
|
21476
|
+
async function runPixelate(args) {
|
|
21477
|
+
if (!args.from) throw new UsageError("pixelate needs --from <image>: the photo, painting, or render to convert");
|
|
21478
|
+
const out = args.out ?? defaultPixelateOut(args.from);
|
|
21479
|
+
log(` pixelate ${args.from} \u2192 ${out}${args.description ? ` ("${args.description}")` : ""}; PixelLab picks the output size`);
|
|
21480
|
+
log(` cost: ${PIXELATE_COST_GENERATIONS} generations (Image to Pixel Art Pro Flash, flat)`);
|
|
21481
|
+
if (args.budget !== void 0 && args.budget < PIXELATE_COST_GENERATIONS) {
|
|
21482
|
+
throw new Error(`--budget ${args.budget} is below this conversion's ${PIXELATE_COST_GENERATIONS}-generation cost`);
|
|
21483
|
+
}
|
|
21484
|
+
if (args.dryRun) {
|
|
21485
|
+
log(" dry run: nothing sent");
|
|
21486
|
+
return;
|
|
21487
|
+
}
|
|
21488
|
+
if (!await confirm(` Spend ${PIXELATE_COST_GENERATIONS} generations on this conversion?`, args.yes)) return;
|
|
21489
|
+
loadKeys(args);
|
|
21490
|
+
let last = "";
|
|
21491
|
+
const res = await pixelateFile(clientFromEnv(), args.from, {
|
|
21492
|
+
out,
|
|
21493
|
+
description: args.description,
|
|
21494
|
+
force: args.force,
|
|
21495
|
+
onProgress: (status) => {
|
|
21496
|
+
if (status !== last) log(` ${status}\u2026`);
|
|
21497
|
+
last = status;
|
|
21498
|
+
}
|
|
21499
|
+
});
|
|
21500
|
+
if (args.json) {
|
|
21501
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
21502
|
+
return;
|
|
21503
|
+
}
|
|
21504
|
+
const scaled = Math.max(res.original.width, res.original.height) > PIXELATE_MAX_SIDE ? " (scaled down upstream first)" : "";
|
|
21505
|
+
log(
|
|
21506
|
+
` ${res.original.width}x${res.original.height}${scaled} \u2192 ${res.pixelated.width}x${res.pixelated.height}; wrote ${res.out}`
|
|
21507
|
+
);
|
|
21508
|
+
}
|
|
21509
|
+
async function runVfx(args) {
|
|
21510
|
+
if (args.subcommand === "presets" || args.subcommand === "parts") {
|
|
21511
|
+
if (args.dryRun) {
|
|
21512
|
+
log(` would list PixelLab's VFX ${args.subcommand}; nothing sent`);
|
|
21513
|
+
return;
|
|
21514
|
+
}
|
|
21515
|
+
loadKeys(args);
|
|
21516
|
+
log(await describeVfx(mcpClientFromEnv(), args.subcommand));
|
|
21517
|
+
return;
|
|
21518
|
+
}
|
|
21519
|
+
if (!args.out) throw new UsageError("vfx needs --out <base>: <base>.png, .gif, .json, and .vfx.json are written");
|
|
21520
|
+
const starts = [args.preset, args.recipe, args.effect].filter(Boolean);
|
|
21521
|
+
if (starts.length > 1) throw new UsageError("vfx takes one of --preset, --recipe, or --effect, not several");
|
|
21522
|
+
if (starts.length === 0 && args.parts.length === 0) {
|
|
21523
|
+
throw new UsageError("vfx needs something to start from: --preset <name>, --recipe <file>, --effect <id>, or --part <name>");
|
|
21524
|
+
}
|
|
21525
|
+
const paths = vfxOutputPaths(args.out);
|
|
21526
|
+
const fromRecipe = args.recipe ? await readVfxRecipe(args.recipe) : void 0;
|
|
21527
|
+
const sprite = args.sprite ?? fromRecipe?.sprite;
|
|
21528
|
+
const recipeName = typeof fromRecipe?.recipe.name === "string" ? fromRecipe.recipe.name : void 0;
|
|
21529
|
+
const name = args.name ?? (args.effect ? void 0 : recipeName ?? args.preset ?? path45.basename(paths.sheet, ".png"));
|
|
21530
|
+
const from = args.preset ? `preset ${args.preset}` : args.recipe ? `recipe ${args.recipe}` : args.effect ? `saved effect ${args.effect}` : "nothing";
|
|
21531
|
+
log(
|
|
21532
|
+
` vfx ${name ? `"${name}"` : "(keeping its saved name)"} from ${from}${args.parts.length ? ` + ${args.parts.join(", ")}` : ""}${sprite ? ` around ${sprite}` : ""} \u2192 ${paths.sheet} (+ .gif, .json, .vfx.json${args.gamePack ? ", -game.zip" : ""})`
|
|
21533
|
+
);
|
|
21534
|
+
log(" cost: free on a PixelLab subscription (a renderer, not a generation); saved to your My effects");
|
|
21535
|
+
if (args.dryRun) {
|
|
21536
|
+
log(" dry run: nothing sent");
|
|
21537
|
+
return;
|
|
21538
|
+
}
|
|
21539
|
+
if (fromRecipe?.sprite && !args.sprite) log(` using the sprite the record names: ${fromRecipe.sprite}`);
|
|
21540
|
+
loadKeys(args);
|
|
21541
|
+
const res = await makeVfx(mcpClientFromEnv(), {
|
|
21542
|
+
preset: args.preset,
|
|
21543
|
+
recipe: fromRecipe?.recipe,
|
|
21544
|
+
effect: args.effect,
|
|
21545
|
+
parts: args.parts.map((part) => ({ name: part })),
|
|
21546
|
+
sprite,
|
|
21547
|
+
fps: args.fps,
|
|
21548
|
+
name: args.name,
|
|
21549
|
+
out: args.out,
|
|
21550
|
+
gamePack: args.gamePack,
|
|
21551
|
+
force: args.force,
|
|
21552
|
+
onProgress: (message6) => log(` ${message6}\u2026`)
|
|
21553
|
+
});
|
|
21554
|
+
if (args.json) {
|
|
21555
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
21556
|
+
return;
|
|
21557
|
+
}
|
|
21558
|
+
log(` ${res.width}x${res.height}, ${res.frames} frames @ ${res.fps} fps, ${res.loop ? "loops" : "plays once"}`);
|
|
21559
|
+
for (const file of [res.paths.sheet, res.paths.gif, res.paths.atlas, res.paths.gamePack, res.paths.provenance]) {
|
|
21560
|
+
if (file) log(` wrote ${file}`);
|
|
21561
|
+
}
|
|
21562
|
+
if (res.editUrl) log(` tweak it with sliders: ${res.editUrl} (then: pixelkiln vfx --effect ${res.effectId} --out ${args.out} --force)`);
|
|
21563
|
+
}
|
|
20786
21564
|
|
|
20787
21565
|
// src/cli/commands/quality.ts
|
|
20788
|
-
import
|
|
20789
|
-
import { readFile as
|
|
21566
|
+
import path47 from "path";
|
|
21567
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
20790
21568
|
|
|
20791
21569
|
// src/pipeline/quality-regression.ts
|
|
20792
|
-
import
|
|
20793
|
-
import { existsSync as
|
|
20794
|
-
import { readFile as
|
|
21570
|
+
import path46 from "path";
|
|
21571
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21572
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
20795
21573
|
import { z as z13 } from "zod";
|
|
20796
21574
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20797
21575
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
20798
21576
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
20799
21577
|
var QualityPathSchema = z13.string().min(1).refine(
|
|
20800
|
-
(value) => !
|
|
21578
|
+
(value) => !path46.posix.isAbsolute(value) && !value.includes("\\"),
|
|
20801
21579
|
"expected a portable relative path"
|
|
20802
21580
|
);
|
|
20803
21581
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -20944,10 +21722,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20944
21722
|
};
|
|
20945
21723
|
}
|
|
20946
21724
|
async function measureImageQuality(file) {
|
|
20947
|
-
const absolute =
|
|
21725
|
+
const absolute = path46.resolve(file);
|
|
20948
21726
|
let bytes;
|
|
20949
21727
|
try {
|
|
20950
|
-
bytes = await
|
|
21728
|
+
bytes = await readFile31(absolute);
|
|
20951
21729
|
} catch (error) {
|
|
20952
21730
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
20953
21731
|
}
|
|
@@ -20966,11 +21744,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20966
21744
|
}
|
|
20967
21745
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20968
21746
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20969
|
-
const root =
|
|
21747
|
+
const root = path46.dirname(path46.resolve(inputsFilePath));
|
|
20970
21748
|
return parsed.data.map((entry) => ({
|
|
20971
21749
|
id: entry.id,
|
|
20972
|
-
file:
|
|
20973
|
-
...entry.record ? { record:
|
|
21750
|
+
file: path46.resolve(root, entry.path),
|
|
21751
|
+
...entry.record ? { record: path46.resolve(root, entry.record) } : {},
|
|
20974
21752
|
tolerances: QualityToleranceSchema.parse({
|
|
20975
21753
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20976
21754
|
...entry.tolerances
|
|
@@ -20978,7 +21756,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20978
21756
|
}));
|
|
20979
21757
|
}
|
|
20980
21758
|
function portableRelative2(from, to) {
|
|
20981
|
-
return
|
|
21759
|
+
return path46.relative(from, path46.resolve(to)).split(path46.sep).join("/") || ".";
|
|
20982
21760
|
}
|
|
20983
21761
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20984
21762
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20993,20 +21771,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20993
21771
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20994
21772
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20995
21773
|
}
|
|
20996
|
-
const outputs = record.outputs.map((output) =>
|
|
20997
|
-
if (!outputs.includes(
|
|
21774
|
+
const outputs = record.outputs.map((output) => path46.resolve(path46.dirname(recordPath), output.path));
|
|
21775
|
+
if (!outputs.includes(path46.resolve(imagePath))) {
|
|
20998
21776
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20999
21777
|
}
|
|
21000
21778
|
}
|
|
21001
21779
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
21002
21780
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
21003
|
-
const absolute =
|
|
21781
|
+
const absolute = path46.resolve(baselinePath);
|
|
21004
21782
|
for (const input of inputs) {
|
|
21005
|
-
if (
|
|
21783
|
+
if (path46.resolve(input.file) === absolute || input.record && path46.resolve(input.record) === absolute) {
|
|
21006
21784
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
21007
21785
|
}
|
|
21008
21786
|
}
|
|
21009
|
-
const root =
|
|
21787
|
+
const root = path46.dirname(absolute);
|
|
21010
21788
|
const cases = [];
|
|
21011
21789
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
21012
21790
|
const expected = await measureImageQuality(input.file);
|
|
@@ -21033,8 +21811,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
21033
21811
|
cases
|
|
21034
21812
|
});
|
|
21035
21813
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
21036
|
-
if (
|
|
21037
|
-
const current = await
|
|
21814
|
+
if (existsSync35(absolute) && !options.force) {
|
|
21815
|
+
const current = await readFile31(absolute);
|
|
21038
21816
|
if (!current.equals(data)) {
|
|
21039
21817
|
throw new OverwriteRefusedError(
|
|
21040
21818
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -21046,10 +21824,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
21046
21824
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
21047
21825
|
}
|
|
21048
21826
|
async function readQualityBaseline(baselinePath) {
|
|
21049
|
-
const absolute =
|
|
21827
|
+
const absolute = path46.resolve(baselinePath);
|
|
21050
21828
|
let raw;
|
|
21051
21829
|
try {
|
|
21052
|
-
raw = JSON.parse(await
|
|
21830
|
+
raw = JSON.parse(await readFile31(absolute, "utf8"));
|
|
21053
21831
|
} catch (error) {
|
|
21054
21832
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
21055
21833
|
}
|
|
@@ -21119,12 +21897,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
21119
21897
|
return { violations, warnings };
|
|
21120
21898
|
}
|
|
21121
21899
|
async function checkQualityBaseline(baselinePath) {
|
|
21122
|
-
const absolute =
|
|
21900
|
+
const absolute = path46.resolve(baselinePath);
|
|
21123
21901
|
const baseline = await readQualityBaseline(absolute);
|
|
21124
|
-
const root =
|
|
21902
|
+
const root = path46.dirname(absolute);
|
|
21125
21903
|
const cases = [];
|
|
21126
21904
|
for (const entry of baseline.cases) {
|
|
21127
|
-
const file =
|
|
21905
|
+
const file = path46.resolve(root, entry.file);
|
|
21128
21906
|
let actual = null;
|
|
21129
21907
|
const violations = [];
|
|
21130
21908
|
const warnings = [];
|
|
@@ -21137,7 +21915,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
21137
21915
|
violations.push(message4(error));
|
|
21138
21916
|
}
|
|
21139
21917
|
if (entry.record) {
|
|
21140
|
-
const recordPath =
|
|
21918
|
+
const recordPath = path46.resolve(root, entry.record.path);
|
|
21141
21919
|
try {
|
|
21142
21920
|
const recordHash = await sha256File(recordPath);
|
|
21143
21921
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -21179,15 +21957,15 @@ async function runQuality(args) {
|
|
|
21179
21957
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
21180
21958
|
let raw;
|
|
21181
21959
|
try {
|
|
21182
|
-
raw = JSON.parse(await
|
|
21960
|
+
raw = JSON.parse(await readFile32(path47.resolve(args.inputs), "utf8"));
|
|
21183
21961
|
} catch (error) {
|
|
21184
21962
|
throw new Error(
|
|
21185
|
-
`Could not read quality inputs ${
|
|
21963
|
+
`Could not read quality inputs ${path47.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
21186
21964
|
{ cause: error }
|
|
21187
21965
|
);
|
|
21188
21966
|
}
|
|
21189
21967
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
21190
|
-
if (
|
|
21968
|
+
if (path47.resolve(args.inputs) === path47.resolve(args.out)) {
|
|
21191
21969
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
21192
21970
|
}
|
|
21193
21971
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -21200,7 +21978,7 @@ async function runQuality(args) {
|
|
|
21200
21978
|
}, null, 2));
|
|
21201
21979
|
} else {
|
|
21202
21980
|
log(
|
|
21203
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
21981
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path47.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
21204
21982
|
);
|
|
21205
21983
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
21206
21984
|
}
|
|
@@ -21226,12 +22004,12 @@ async function runQuality(args) {
|
|
|
21226
22004
|
}
|
|
21227
22005
|
|
|
21228
22006
|
// src/cli/commands/recipe.ts
|
|
21229
|
-
import
|
|
22007
|
+
import path49 from "path";
|
|
21230
22008
|
|
|
21231
22009
|
// src/recipes.ts
|
|
21232
|
-
import
|
|
21233
|
-
import { existsSync as
|
|
21234
|
-
import { readdir as readdir3, readFile as
|
|
22010
|
+
import path48 from "path";
|
|
22011
|
+
import { existsSync as existsSync36 } from "fs";
|
|
22012
|
+
import { readdir as readdir3, readFile as readFile33 } from "fs/promises";
|
|
21235
22013
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
21236
22014
|
import { z as z14 } from "zod";
|
|
21237
22015
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -21240,7 +22018,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
21240
22018
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
21241
22019
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
21242
22020
|
var PortablePathSchema = z14.string().min(1).refine((value) => {
|
|
21243
|
-
if (value.includes("\\") ||
|
|
22021
|
+
if (value.includes("\\") || path48.posix.isAbsolute(value)) return false;
|
|
21244
22022
|
const parts = value.split("/");
|
|
21245
22023
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
21246
22024
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -21416,10 +22194,10 @@ function message5(error) {
|
|
|
21416
22194
|
return error instanceof Error ? error.message : String(error);
|
|
21417
22195
|
}
|
|
21418
22196
|
async function readRecipeFile(recipePath, bundled) {
|
|
21419
|
-
const absolute =
|
|
22197
|
+
const absolute = path48.resolve(recipePath);
|
|
21420
22198
|
let raw;
|
|
21421
22199
|
try {
|
|
21422
|
-
raw = JSON.parse(await
|
|
22200
|
+
raw = JSON.parse(await readFile33(absolute, "utf8"));
|
|
21423
22201
|
} catch (error) {
|
|
21424
22202
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
21425
22203
|
}
|
|
@@ -21430,13 +22208,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
21430
22208
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
21431
22209
|
);
|
|
21432
22210
|
}
|
|
21433
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
22211
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path48.dirname(absolute), bundled };
|
|
21434
22212
|
}
|
|
21435
22213
|
async function walkRecipeFiles(root) {
|
|
21436
|
-
if (!
|
|
22214
|
+
if (!existsSync36(root)) return [];
|
|
21437
22215
|
const out = [];
|
|
21438
22216
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
21439
|
-
const resolved =
|
|
22217
|
+
const resolved = path48.join(root, entry.name);
|
|
21440
22218
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
21441
22219
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
21442
22220
|
}
|
|
@@ -21447,7 +22225,7 @@ function bundledRecipeRoot() {
|
|
|
21447
22225
|
}
|
|
21448
22226
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
21449
22227
|
const recipes = await Promise.all(
|
|
21450
|
-
(await walkRecipeFiles(
|
|
22228
|
+
(await walkRecipeFiles(path48.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
21451
22229
|
);
|
|
21452
22230
|
return recipes.sort(
|
|
21453
22231
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -21470,9 +22248,9 @@ function parseSelector(selector) {
|
|
|
21470
22248
|
return { id: match[1], version: match[2] };
|
|
21471
22249
|
}
|
|
21472
22250
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
21473
|
-
const local =
|
|
21474
|
-
if (
|
|
21475
|
-
const recipePath =
|
|
22251
|
+
const local = path48.resolve(target);
|
|
22252
|
+
if (existsSync36(local)) {
|
|
22253
|
+
const recipePath = path48.basename(local) === RECIPE_FILE ? local : path48.join(local, RECIPE_FILE);
|
|
21476
22254
|
return readRecipeFile(recipePath, false);
|
|
21477
22255
|
}
|
|
21478
22256
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -21486,8 +22264,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
21486
22264
|
return matches[0];
|
|
21487
22265
|
}
|
|
21488
22266
|
async function verifyFile(root, file) {
|
|
21489
|
-
const absolute =
|
|
21490
|
-
if (!
|
|
22267
|
+
const absolute = path48.resolve(root, ...file.path.split("/"));
|
|
22268
|
+
if (!existsSync36(absolute)) {
|
|
21491
22269
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
21492
22270
|
}
|
|
21493
22271
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -21502,7 +22280,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21502
22280
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21503
22281
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
21504
22282
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
21505
|
-
const modelRoot = options.modelRoot ?
|
|
22283
|
+
const modelRoot = options.modelRoot ? path48.resolve(options.modelRoot) : null;
|
|
21506
22284
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
21507
22285
|
...await verifyFile(modelRoot, model),
|
|
21508
22286
|
source: model.source,
|
|
@@ -21536,7 +22314,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21536
22314
|
};
|
|
21537
22315
|
}
|
|
21538
22316
|
function renderStyle(style, destination, cwd) {
|
|
21539
|
-
const portableDir =
|
|
22317
|
+
const portableDir = path48.relative(cwd, destination).split(path48.sep).join("/") || ".";
|
|
21540
22318
|
const replace = (value) => {
|
|
21541
22319
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
21542
22320
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -21548,25 +22326,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
21548
22326
|
return replace(style);
|
|
21549
22327
|
}
|
|
21550
22328
|
async function installRecipe(target, options = {}) {
|
|
21551
|
-
const cwd =
|
|
22329
|
+
const cwd = path48.resolve(options.cwd ?? process.cwd());
|
|
21552
22330
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21553
22331
|
const verification = await verifyRecipe(loaded.path);
|
|
21554
22332
|
if (!verification.ok) {
|
|
21555
22333
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
21556
22334
|
}
|
|
21557
|
-
const destination = options.out ?
|
|
22335
|
+
const destination = options.out ? path48.resolve(cwd, options.out) : path48.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
21558
22336
|
const sources = [
|
|
21559
22337
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
21560
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
22338
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path48.join(loaded.dir, ...file.path.split("/")) }))
|
|
21561
22339
|
];
|
|
21562
22340
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
21563
|
-
path:
|
|
21564
|
-
data: await
|
|
22341
|
+
path: path48.join(destination, ...file.path.split("/")),
|
|
22342
|
+
data: await readFile33(file.source)
|
|
21565
22343
|
})));
|
|
21566
22344
|
if (!options.force) {
|
|
21567
22345
|
for (const file of files) {
|
|
21568
|
-
if (!
|
|
21569
|
-
const current = await
|
|
22346
|
+
if (!existsSync36(file.path)) continue;
|
|
22347
|
+
const current = await readFile33(file.path);
|
|
21570
22348
|
if (!current.equals(file.data)) {
|
|
21571
22349
|
throw new OverwriteRefusedError(
|
|
21572
22350
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -21660,14 +22438,14 @@ async function runRecipe(args) {
|
|
|
21660
22438
|
}, null, 2));
|
|
21661
22439
|
} else {
|
|
21662
22440
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
21663
|
-
log(` ${
|
|
22441
|
+
log(` ${path49.relative(process.cwd(), result.destination) || "."}`);
|
|
21664
22442
|
log(`
|
|
21665
22443
|
Add this entry under your manifest's styles object:`);
|
|
21666
22444
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
21667
22445
|
if (result.recipe.models.length) {
|
|
21668
22446
|
log(`
|
|
21669
22447
|
Models are not downloaded automatically. Verify them with:`);
|
|
21670
|
-
log(` pixelkiln recipe verify ${
|
|
22448
|
+
log(` pixelkiln recipe verify ${path49.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
21671
22449
|
}
|
|
21672
22450
|
}
|
|
21673
22451
|
return;
|
|
@@ -21675,7 +22453,7 @@ async function runRecipe(args) {
|
|
|
21675
22453
|
}
|
|
21676
22454
|
|
|
21677
22455
|
// src/cli/commands/refine.ts
|
|
21678
|
-
import
|
|
22456
|
+
import path50 from "path";
|
|
21679
22457
|
async function runRefine(args) {
|
|
21680
22458
|
if (args.from || args.subcommand === "approve") {
|
|
21681
22459
|
if (!args.from) {
|
|
@@ -21705,12 +22483,12 @@ async function runRefine(args) {
|
|
|
21705
22483
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
21706
22484
|
);
|
|
21707
22485
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
21708
|
-
log(` wrote ${
|
|
21709
|
-
log(` quality record: ${
|
|
22486
|
+
log(` wrote ${path50.relative(process.cwd(), result3.output)}`);
|
|
22487
|
+
log(` quality record: ${path50.relative(process.cwd(), result3.record)}`);
|
|
21710
22488
|
log(`
|
|
21711
22489
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
21712
22490
|
log(
|
|
21713
|
-
` pixelkiln refine approve --from ${
|
|
22491
|
+
` pixelkiln refine approve --from ${path50.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
21714
22492
|
);
|
|
21715
22493
|
}
|
|
21716
22494
|
return;
|
|
@@ -21738,7 +22516,7 @@ async function runRefine(args) {
|
|
|
21738
22516
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
21739
22517
|
else {
|
|
21740
22518
|
log(
|
|
21741
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
22519
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path50.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path50.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
21742
22520
|
);
|
|
21743
22521
|
}
|
|
21744
22522
|
return;
|
|
@@ -21747,7 +22525,7 @@ async function runRefine(args) {
|
|
|
21747
22525
|
if (args.json) {
|
|
21748
22526
|
log(JSON.stringify(result2, null, 2));
|
|
21749
22527
|
} else {
|
|
21750
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
22528
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path50.relative(process.cwd(), result2.output || result2.record)}`);
|
|
21751
22529
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
21752
22530
|
}
|
|
21753
22531
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -21793,7 +22571,7 @@ async function runRefine(args) {
|
|
|
21793
22571
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
21794
22572
|
if (item.state === "needs-approval") {
|
|
21795
22573
|
log(
|
|
21796
|
-
` pixelkiln refine approve --from ${
|
|
22574
|
+
` pixelkiln refine approve --from ${path50.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
21797
22575
|
);
|
|
21798
22576
|
}
|
|
21799
22577
|
}
|
|
@@ -21804,9 +22582,9 @@ async function runRefine(args) {
|
|
|
21804
22582
|
}
|
|
21805
22583
|
|
|
21806
22584
|
// src/cli/commands/skeleton.ts
|
|
21807
|
-
import
|
|
21808
|
-
import { existsSync as
|
|
21809
|
-
import { readFile as
|
|
22585
|
+
import path51 from "path";
|
|
22586
|
+
import { existsSync as existsSync37 } from "fs";
|
|
22587
|
+
import { readFile as readFile34, writeFile as writeFile20 } from "fs/promises";
|
|
21810
22588
|
|
|
21811
22589
|
// src/skeleton-preview.ts
|
|
21812
22590
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -21917,12 +22695,12 @@ var Canvas = class {
|
|
|
21917
22695
|
async function runEstimateSkeleton(args) {
|
|
21918
22696
|
const image = args.target;
|
|
21919
22697
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
21920
|
-
if (args.out &&
|
|
22698
|
+
if (args.out && existsSync37(path51.resolve(args.out)) && !args.force) {
|
|
21921
22699
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
21922
22700
|
}
|
|
21923
|
-
loadEnvFiles(
|
|
22701
|
+
loadEnvFiles(path51.dirname(path51.resolve(args.manifest)));
|
|
21924
22702
|
loadEnvFiles(process.cwd());
|
|
21925
|
-
const bytes = await
|
|
22703
|
+
const bytes = await readFile34(path51.resolve(image));
|
|
21926
22704
|
const metadata = imageMetadata(bytes);
|
|
21927
22705
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21928
22706
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21934,7 +22712,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21934
22712
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21935
22713
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21936
22714
|
if (args.out) {
|
|
21937
|
-
await
|
|
22715
|
+
await writeFile20(path51.resolve(args.out), json + "\n");
|
|
21938
22716
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21939
22717
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21940
22718
|
} else {
|
|
@@ -21947,8 +22725,8 @@ async function runSkeletonPreview(args) {
|
|
|
21947
22725
|
let sourceFile;
|
|
21948
22726
|
let label;
|
|
21949
22727
|
if (target.endsWith(".json")) {
|
|
21950
|
-
keypointsFile =
|
|
21951
|
-
sourceFile = args.from ?
|
|
22728
|
+
keypointsFile = path51.resolve(target);
|
|
22729
|
+
sourceFile = args.from ? path51.resolve(args.from) : void 0;
|
|
21952
22730
|
label = target;
|
|
21953
22731
|
} else {
|
|
21954
22732
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21960,31 +22738,31 @@ async function runSkeletonPreview(args) {
|
|
|
21960
22738
|
);
|
|
21961
22739
|
}
|
|
21962
22740
|
keypointsFile = spec.revision.keypointsFile;
|
|
21963
|
-
sourceFile = args.from ?
|
|
22741
|
+
sourceFile = args.from ? path51.resolve(args.from) : spec.revision.sourceFile;
|
|
21964
22742
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21965
22743
|
}
|
|
21966
|
-
if (!
|
|
22744
|
+
if (!existsSync37(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21967
22745
|
let json;
|
|
21968
22746
|
try {
|
|
21969
|
-
json = JSON.parse(await
|
|
22747
|
+
json = JSON.parse(await readFile34(keypointsFile, "utf8"));
|
|
21970
22748
|
} catch (error) {
|
|
21971
22749
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
21972
22750
|
}
|
|
21973
22751
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21974
22752
|
let source;
|
|
21975
22753
|
if (sourceFile) {
|
|
21976
|
-
if (!
|
|
22754
|
+
if (!existsSync37(sourceFile)) {
|
|
21977
22755
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21978
22756
|
} else {
|
|
21979
|
-
const bytes = await
|
|
22757
|
+
const bytes = await readFile34(sourceFile);
|
|
21980
22758
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
21981
22759
|
source = decodePng(bytes);
|
|
21982
22760
|
}
|
|
21983
22761
|
}
|
|
21984
|
-
const out =
|
|
21985
|
-
await
|
|
22762
|
+
const out = path51.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
22763
|
+
await writeFile20(out, renderSkeletonSheet(set, { source }));
|
|
21986
22764
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21987
|
-
const shown =
|
|
22765
|
+
const shown = path51.relative(process.cwd(), out);
|
|
21988
22766
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21989
22767
|
}
|
|
21990
22768
|
|
|
@@ -22030,15 +22808,15 @@ async function runTools(args) {
|
|
|
22030
22808
|
}
|
|
22031
22809
|
|
|
22032
22810
|
// src/cli/commands/workspace.ts
|
|
22033
|
-
import
|
|
22034
|
-
import { existsSync as
|
|
22811
|
+
import path52 from "path";
|
|
22812
|
+
import { existsSync as existsSync38 } from "fs";
|
|
22035
22813
|
async function runWorkspace(args) {
|
|
22036
|
-
const workspacePath =
|
|
22037
|
-
const dir =
|
|
22814
|
+
const workspacePath = path52.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
22815
|
+
const dir = path52.dirname(workspacePath);
|
|
22038
22816
|
if (args.subcommand === "add") {
|
|
22039
|
-
const manifestPath =
|
|
22817
|
+
const manifestPath = path52.resolve(args.target);
|
|
22040
22818
|
const loadedTarget = await loadManifest(manifestPath);
|
|
22041
|
-
const lockPath = args.explicitLock ?
|
|
22819
|
+
const lockPath = args.explicitLock ? path52.resolve(args.explicitLock) : path52.join(path52.dirname(manifestPath), "pixelkiln.lock.json");
|
|
22042
22820
|
const ws = await loadWorkspace(workspacePath);
|
|
22043
22821
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
22044
22822
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -22058,10 +22836,10 @@ async function runWorkspace(args) {
|
|
|
22058
22836
|
...args.account ? { account: args.account } : {}
|
|
22059
22837
|
};
|
|
22060
22838
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
22061
|
-
log(` registered "${id}" in ${
|
|
22839
|
+
log(` registered "${id}" in ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22062
22840
|
log(` manifest: ${project.manifest}`);
|
|
22063
22841
|
log(` lock: ${project.lock}`);
|
|
22064
|
-
if (!
|
|
22842
|
+
if (!existsSync38(lockPath)) {
|
|
22065
22843
|
log(
|
|
22066
22844
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
22067
22845
|
);
|
|
@@ -22070,7 +22848,7 @@ async function runWorkspace(args) {
|
|
|
22070
22848
|
}
|
|
22071
22849
|
if (args.subcommand === "remove") {
|
|
22072
22850
|
const ws = await loadWorkspace(workspacePath);
|
|
22073
|
-
const resolvedTarget =
|
|
22851
|
+
const resolvedTarget = path52.resolve(args.target);
|
|
22074
22852
|
const match = ws.projects.find(
|
|
22075
22853
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
22076
22854
|
);
|
|
@@ -22081,11 +22859,11 @@ async function runWorkspace(args) {
|
|
|
22081
22859
|
version: 1,
|
|
22082
22860
|
projects: ws.projects.filter((p) => p !== match)
|
|
22083
22861
|
});
|
|
22084
|
-
log(` removed "${match.id}" from ${
|
|
22862
|
+
log(` removed "${match.id}" from ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22085
22863
|
return;
|
|
22086
22864
|
}
|
|
22087
22865
|
if (args.subcommand === "list") {
|
|
22088
|
-
if (!
|
|
22866
|
+
if (!existsSync38(workspacePath)) {
|
|
22089
22867
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
22090
22868
|
}
|
|
22091
22869
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -22093,9 +22871,9 @@ async function runWorkspace(args) {
|
|
|
22093
22871
|
if (args.json) {
|
|
22094
22872
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
22095
22873
|
} else if (!ws.projects.length) {
|
|
22096
|
-
log(` no projects registered in ${
|
|
22874
|
+
log(` no projects registered in ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22097
22875
|
} else {
|
|
22098
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
22876
|
+
log(` ${ws.projects.length} project(s) in ${path52.relative(process.cwd(), workspacePath)}:`);
|
|
22099
22877
|
for (const p of ws.projects) {
|
|
22100
22878
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
22101
22879
|
}
|
|
@@ -22105,7 +22883,7 @@ async function runWorkspace(args) {
|
|
|
22105
22883
|
return;
|
|
22106
22884
|
}
|
|
22107
22885
|
if (args.subcommand === "status") {
|
|
22108
|
-
if (!
|
|
22886
|
+
if (!existsSync38(workspacePath)) {
|
|
22109
22887
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
22110
22888
|
}
|
|
22111
22889
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -22113,7 +22891,7 @@ async function runWorkspace(args) {
|
|
|
22113
22891
|
if (args.json) {
|
|
22114
22892
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
22115
22893
|
} else {
|
|
22116
|
-
log(` workspace: ${
|
|
22894
|
+
log(` workspace: ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22117
22895
|
for (const p of report.projects) {
|
|
22118
22896
|
if (p.error) {
|
|
22119
22897
|
log(`
|
|
@@ -22167,8 +22945,8 @@ async function runHelp() {
|
|
|
22167
22945
|
async function runVersion() {
|
|
22168
22946
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
22169
22947
|
const url = new URL(up, import.meta.url);
|
|
22170
|
-
if (!
|
|
22171
|
-
const pkg = JSON.parse(await
|
|
22948
|
+
if (!existsSync39(url)) continue;
|
|
22949
|
+
const pkg = JSON.parse(await readFile35(url, "utf8"));
|
|
22172
22950
|
if (pkg.name === "pixelkiln") {
|
|
22173
22951
|
log(`${pkg.name} ${pkg.version}`);
|
|
22174
22952
|
return;
|
|
@@ -22215,6 +22993,8 @@ var RUNNERS = {
|
|
|
22215
22993
|
"estimate-skeleton": runEstimateSkeleton,
|
|
22216
22994
|
"skeleton-preview": runSkeletonPreview,
|
|
22217
22995
|
unzoom: runUnzoom,
|
|
22996
|
+
pixelate: runPixelate,
|
|
22997
|
+
vfx: runVfx,
|
|
22218
22998
|
font: runFont
|
|
22219
22999
|
};
|
|
22220
23000
|
async function main(argv = process.argv.slice(2)) {
|