pixelkiln 0.38.0 → 0.40.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/CONTRIBUTING.md +1 -0
- package/PROVIDERS.md +4 -1
- package/README.md +4 -4
- package/dist/cli.js +1492 -442
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +32 -32
- package/dist/index.cjs +823 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1552 -207
- package/dist/index.d.ts +1552 -207
- package/dist/index.js +814 -73
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/ARTIFACTS.md +2 -2
- package/docs/CLI.md +62 -30
- package/docs/COMFYUI.md +12 -24
- package/docs/ENDPOINTS.md +2 -1
- package/docs/GENERATORS.md +35 -4
- package/docs/MANIFEST.md +111 -2
- package/docs/PIXELLAB.md +4 -2
- package/docs/README.md +1 -1
- package/docs/SCENARIO.md +1 -1
- package/docs/TILES.md +5 -0
- package/package.json +2 -1
- package/schema/manifest.schema.json +137 -2
- package/schema/recipe.schema.json +21 -1
- package/skills/pixelkiln/references/pixellab.md +1 -0
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 existsSync32 } from "fs";
|
|
71
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -411,7 +411,7 @@ function parseArgs(argv) {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
// src/cli/help.ts
|
|
414
|
-
var HELP = `pixelkiln
|
|
414
|
+
var HELP = `pixelkiln: manifest-driven pixel art generation
|
|
415
415
|
|
|
416
416
|
pixelkiln <command> [options]
|
|
417
417
|
|
|
@@ -428,7 +428,7 @@ Commands
|
|
|
428
428
|
--generation <n|hash> brings a previous generation back instead.
|
|
429
429
|
history List the generations each asset has replaced and can restore.
|
|
430
430
|
adopt Match existing account objects to files already in the repo.
|
|
431
|
-
accept Keep existing art after a style reword
|
|
431
|
+
accept Keep existing art after a style reword. Re-baseline, do not regenerate.
|
|
432
432
|
salvage Triage account objects no lockfile claims. Recovers usable art.
|
|
433
433
|
One session per matching style unless --style forces a single one.
|
|
434
434
|
audit Measure how consistently a style's assets hold together. Offline.
|
|
@@ -618,10 +618,10 @@ import { createReadStream } from "fs";
|
|
|
618
618
|
function sha256(data) {
|
|
619
619
|
return createHash("sha256").update(data).digest("hex");
|
|
620
620
|
}
|
|
621
|
-
async function sha256File(
|
|
621
|
+
async function sha256File(path44) {
|
|
622
622
|
return new Promise((resolve, reject) => {
|
|
623
623
|
const hash = createHash("sha256");
|
|
624
|
-
const stream = createReadStream(
|
|
624
|
+
const stream = createReadStream(path44);
|
|
625
625
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
626
626
|
stream.on("error", reject);
|
|
627
627
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -647,13 +647,26 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
647
647
|
palette: spec.palette,
|
|
648
648
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
649
649
|
// undefined for every other generator. `tileSize` is intentionally
|
|
650
|
-
// absent
|
|
650
|
+
// absent; width/height are derived from it, so it is already covered.
|
|
651
651
|
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
652
652
|
tileType: spec.tileType,
|
|
653
653
|
tileView: spec.tileView,
|
|
654
654
|
tileFeature: spec.tileFeature,
|
|
655
655
|
outlineMode: spec.outlineMode,
|
|
656
656
|
styleImages: styleImageHashes,
|
|
657
|
+
// A character family: the engine and rotations of a base, the parent's
|
|
658
|
+
// generated bytes for a state or animation (so a regenerated parent
|
|
659
|
+
// makes its children stale), and each shape's own settings.
|
|
660
|
+
character: spec.character ? {
|
|
661
|
+
kind: spec.character.kind,
|
|
662
|
+
mode: spec.character.mode,
|
|
663
|
+
directions: spec.character.directions,
|
|
664
|
+
template: spec.character.template,
|
|
665
|
+
parent: spec.character.parentAssetId,
|
|
666
|
+
parentSha256: spec.character.parentSha256 ?? null,
|
|
667
|
+
state: spec.character.state,
|
|
668
|
+
animation: spec.character.animation
|
|
669
|
+
} : void 0,
|
|
657
670
|
revision: spec.revision ? {
|
|
658
671
|
mode: spec.revision.mode,
|
|
659
672
|
from: spec.revision.sourceAssetId,
|
|
@@ -677,7 +690,7 @@ import path2 from "path";
|
|
|
677
690
|
// src/types.ts
|
|
678
691
|
import { z } from "zod";
|
|
679
692
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
680
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames"]);
|
|
693
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character"]);
|
|
681
694
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
682
695
|
var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
|
|
683
696
|
function tileVariationCount(descriptions) {
|
|
@@ -735,6 +748,77 @@ var QualityProfileSchema = QualityProfileObjectSchema.refine(
|
|
|
735
748
|
(profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
|
|
736
749
|
{ message: "quality palette colors must be unique", path: ["palette"] }
|
|
737
750
|
);
|
|
751
|
+
var CharacterDirectionSchema = z.enum([
|
|
752
|
+
"south",
|
|
753
|
+
"south-east",
|
|
754
|
+
"east",
|
|
755
|
+
"north-east",
|
|
756
|
+
"north",
|
|
757
|
+
"north-west",
|
|
758
|
+
"west",
|
|
759
|
+
"south-west"
|
|
760
|
+
]);
|
|
761
|
+
var CHARACTER_DIRECTIONS_8 = [
|
|
762
|
+
"south",
|
|
763
|
+
"south-east",
|
|
764
|
+
"east",
|
|
765
|
+
"north-east",
|
|
766
|
+
"north",
|
|
767
|
+
"north-west",
|
|
768
|
+
"west",
|
|
769
|
+
"south-west"
|
|
770
|
+
];
|
|
771
|
+
var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
|
|
772
|
+
var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
|
|
773
|
+
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
774
|
+
var CharacterStateSchema = z.object({
|
|
775
|
+
/** The base character, or another state, in the same style. */
|
|
776
|
+
of: z.string().min(1),
|
|
777
|
+
/** Snap the edited rotations to the parent's colours. Off when the edit adds colours. */
|
|
778
|
+
paletteFromReference: z.boolean().default(false),
|
|
779
|
+
/** A larger canvas when the edit adds something big. Multiples of 4, no smaller than the parent. */
|
|
780
|
+
canvas: z.object({
|
|
781
|
+
width: z.number().int().min(16).max(256),
|
|
782
|
+
height: z.number().int().min(16).max(256)
|
|
783
|
+
}).strict().optional()
|
|
784
|
+
}).strict();
|
|
785
|
+
var CharacterAnimationSchema = z.object({
|
|
786
|
+
/** The character to animate: a base or a state in the same style. */
|
|
787
|
+
of: z.string().min(1),
|
|
788
|
+
/** A PixelLab template id (`walk`, `breathing-idle`, ...). Costs 1 generation; frame count is the template's. */
|
|
789
|
+
template: z.string().min(1).optional(),
|
|
790
|
+
/** Direction to animate. */
|
|
791
|
+
direction: CharacterDirectionSchema.default("south"),
|
|
792
|
+
/** Frames to generate without a template (v3). Even, 4 to 16. */
|
|
793
|
+
frames: z.number().int().min(4).max(16).optional(),
|
|
794
|
+
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
795
|
+
fps: z.number().int().min(1).max(60).default(8),
|
|
796
|
+
/** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
|
|
797
|
+
keepFirstFrame: z.boolean().default(true),
|
|
798
|
+
/** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
|
|
799
|
+
mode: CharacterAnimationModeSchema.optional()
|
|
800
|
+
}).strict().superRefine((animation, context) => {
|
|
801
|
+
if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
|
|
802
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
803
|
+
}
|
|
804
|
+
if (animation.template && animation.mode && animation.mode !== "template") {
|
|
805
|
+
context.addIssue({
|
|
806
|
+
code: z.ZodIssueCode.custom,
|
|
807
|
+
message: `a template animation cannot use mode "${animation.mode}"`,
|
|
808
|
+
path: ["mode"]
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (animation.template && animation.frames !== void 0) {
|
|
812
|
+
context.addIssue({
|
|
813
|
+
code: z.ZodIssueCode.custom,
|
|
814
|
+
message: "a template decides its own frame count; drop frames",
|
|
815
|
+
path: ["frames"]
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
if (!animation.template && animation.mode === "template") {
|
|
819
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
|
|
820
|
+
}
|
|
821
|
+
});
|
|
738
822
|
var RevisionSchema = z.object({
|
|
739
823
|
/** What kind of controlled change the provider workflow performs. */
|
|
740
824
|
mode: RevisionModeSchema,
|
|
@@ -784,7 +868,7 @@ var StyleObjectSchema = z.object({
|
|
|
784
868
|
/**
|
|
785
869
|
* `tiles` generator only. Edge length of one tile, 16-256.
|
|
786
870
|
*
|
|
787
|
-
* Ignored when `styleImages` is set
|
|
871
|
+
* Ignored when `styleImages` is set: style mode takes the tile's shape
|
|
788
872
|
* and dimensions from the reference image, which is the whole reason to
|
|
789
873
|
* use it against an existing sheet.
|
|
790
874
|
*/
|
|
@@ -798,7 +882,7 @@ var StyleObjectSchema = z.object({
|
|
|
798
882
|
* independent variations:
|
|
799
883
|
*
|
|
800
884
|
* roads 18-configuration path set
|
|
801
|
-
* tileset 16-tile Wang corner set for a terrain transition
|
|
885
|
+
* tileset 16-tile Wang corner set for a terrain transition. Describe
|
|
802
886
|
* the asset as the transition itself ("fairway grass to rough
|
|
803
887
|
* meadow"), not as one terrain
|
|
804
888
|
* building floor/wall/doorway construction kit
|
|
@@ -816,7 +900,7 @@ var StyleObjectSchema = z.object({
|
|
|
816
900
|
* surface into visible quilting, with a dark seam at every cell edge.
|
|
817
901
|
* `segmentation` omits them and the same set tiles seamlessly.
|
|
818
902
|
*
|
|
819
|
-
* Measured on a fairway-to-rough terrain set
|
|
903
|
+
* Measured on a fairway-to-rough terrain set, where the difference decided
|
|
820
904
|
* whether the art was usable at all, so it is worth setting deliberately
|
|
821
905
|
* rather than inheriting.
|
|
822
906
|
*/
|
|
@@ -825,7 +909,7 @@ var StyleObjectSchema = z.object({
|
|
|
825
909
|
* `pixflux` only. Whether to strip the generated background.
|
|
826
910
|
*
|
|
827
911
|
* Defaults to true, which is right for the sprites this tool was built
|
|
828
|
-
* for
|
|
912
|
+
* for; a prop or an icon wants to sit on whatever is behind it. It is
|
|
829
913
|
* wrong for anything that IS a scene: a cover banner, a splash, a
|
|
830
914
|
* backdrop. The API's own default is false; forcing it true unconditionally
|
|
831
915
|
* meant a full-bleed image came back as a small subject floating in a
|
|
@@ -833,12 +917,23 @@ var StyleObjectSchema = z.object({
|
|
|
833
917
|
* override it.
|
|
834
918
|
*/
|
|
835
919
|
noBackground: z.boolean().default(true),
|
|
920
|
+
/**
|
|
921
|
+
* `character` only. `standard` draws from a skeleton template for 1
|
|
922
|
+
* generation; `v3` is the highest quality at 2 to 9 by size; `pro` is a
|
|
923
|
+
* reference-based engine at 20 to 40 by size. States inherit the
|
|
924
|
+
* parent's engine; animations choose their own.
|
|
925
|
+
*/
|
|
926
|
+
mode: CharacterModeSchema.optional(),
|
|
927
|
+
/** `character` only. Rotations per base or state. `v3` and `pro` always draw 8. */
|
|
928
|
+
directions: z.union([z.literal(4), z.literal(8)]).optional(),
|
|
929
|
+
/** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
|
|
930
|
+
template: z.string().min(1).optional(),
|
|
836
931
|
/** Fixed seed for reproducibility where the endpoint supports it. */
|
|
837
932
|
seed: z.number().int().optional(),
|
|
838
933
|
/**
|
|
839
934
|
* Forced palette, as `#rrggbb` values. `pixflux` only.
|
|
840
935
|
*
|
|
841
|
-
* Unlike prose, this is a hard constraint
|
|
936
|
+
* Unlike prose, this is a hard constraint. The API is handed a swatch
|
|
842
937
|
* image and the output is limited to those colours. Verified: a four-colour
|
|
843
938
|
* Game Boy palette produced output containing exactly those four values.
|
|
844
939
|
* Prose asking for the same thing does not reliably hold, which is why the
|
|
@@ -863,8 +958,8 @@ var StyleObjectSchema = z.object({
|
|
|
863
958
|
/**
|
|
864
959
|
* Composites this style's assets into declared cells of a sheet, rather
|
|
865
960
|
* than letting `pack` derive a layout. Use it when the atlas coordinates
|
|
866
|
-
* are already load-bearing somewhere else
|
|
867
|
-
* cell
|
|
961
|
+
* are already load-bearing somewhere else, a tile engine naming tiles by
|
|
962
|
+
* cell or saved data storing cell indices, since `pack`'s id-sorted grid
|
|
868
963
|
* moves every position when an asset is added or renamed.
|
|
869
964
|
*
|
|
870
965
|
* Assets in a mounted style declare their own `cell`; ones that do not are
|
|
@@ -889,7 +984,7 @@ var StyleObjectSchema = z.object({
|
|
|
889
984
|
providerOptions: z.record(z.record(z.unknown())).default({})
|
|
890
985
|
}).strict();
|
|
891
986
|
var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
892
|
-
message: "tileFeature and styleImages cannot be combined
|
|
987
|
+
message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
|
|
893
988
|
path: ["tileFeature"]
|
|
894
989
|
}).superRefine((style, ctx) => {
|
|
895
990
|
if (style.quality && (style.generator === "tiles" || style.generator === "animation")) {
|
|
@@ -906,6 +1001,23 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
906
1001
|
path: ["enforcePalette"]
|
|
907
1002
|
});
|
|
908
1003
|
}
|
|
1004
|
+
if (style.generator !== "character") {
|
|
1005
|
+
for (const field of ["mode", "directions", "template"]) {
|
|
1006
|
+
if (style[field] !== void 0) {
|
|
1007
|
+
ctx.addIssue({
|
|
1008
|
+
code: z.ZodIssueCode.custom,
|
|
1009
|
+
message: `${field} applies to the character generator only`,
|
|
1010
|
+
path: [field]
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} else if (style.mode && style.mode !== "standard" && style.directions === 4) {
|
|
1015
|
+
ctx.addIssue({
|
|
1016
|
+
code: z.ZodIssueCode.custom,
|
|
1017
|
+
message: `${style.mode} characters always have 8 directions`,
|
|
1018
|
+
path: ["directions"]
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
909
1021
|
});
|
|
910
1022
|
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
911
1023
|
extends: z.string().min(1),
|
|
@@ -938,14 +1050,14 @@ var AssetSchema = z.object({
|
|
|
938
1050
|
cell: z.tuple([z.number().int().min(0), z.number().int().min(0)]).optional(),
|
|
939
1051
|
/**
|
|
940
1052
|
* Path, relative to the manifest, of the art that goes on a mounted
|
|
941
|
-
* sheet
|
|
1053
|
+
* sheet, when that is not the raw generated output.
|
|
942
1054
|
*
|
|
943
1055
|
* `mount` otherwise takes its pixels from the lockfile, which records
|
|
944
1056
|
* what the API returned. That is the wrong file whenever the art needs a
|
|
945
1057
|
* step pixelkiln does not perform: a palette reduction onto a sheet's
|
|
946
1058
|
* closed palette, a hand touch-up, an alignment shift. Without somewhere
|
|
947
1059
|
* to say so, the choice is to mount the unprocessed art or to abandon
|
|
948
|
-
* `mount` and composite by hand
|
|
1060
|
+
* `mount` and composite by hand, and the hand-composited sheet is
|
|
949
1061
|
* exactly the unreproducible artifact `mount` exists to replace.
|
|
950
1062
|
*
|
|
951
1063
|
* An asset with a `source` needs no lockfile entry at all, so art
|
|
@@ -964,6 +1076,17 @@ var AssetSchema = z.object({
|
|
|
964
1076
|
sourceByStyle: z.record(z.string().min(1)).default({}),
|
|
965
1077
|
/** Controlled generation derived from another asset in the same style. */
|
|
966
1078
|
revision: RevisionSchema.optional(),
|
|
1079
|
+
/**
|
|
1080
|
+
* The provider's own id for art that already exists on the account, so
|
|
1081
|
+
* `adopt` can map it without matching bytes: an object id, a character
|
|
1082
|
+
* id, or `<character id>#<animation group id>` for a character animation.
|
|
1083
|
+
* Not part of the spec's identity.
|
|
1084
|
+
*/
|
|
1085
|
+
remoteId: z.string().min(1).optional(),
|
|
1086
|
+
/** `character` styles: this asset is a pose or outfit of another character asset. */
|
|
1087
|
+
state: CharacterStateSchema.optional(),
|
|
1088
|
+
/** `character` styles: this asset is a loop of another character asset in one direction. */
|
|
1089
|
+
animation: CharacterAnimationSchema.optional(),
|
|
967
1090
|
/**
|
|
968
1091
|
* Generated output role to place in `cell` when this asset expands to
|
|
969
1092
|
* several files. Omit for ordinary single-output assets. A structural set
|
|
@@ -1003,6 +1126,14 @@ var AssetSchema = z.object({
|
|
|
1003
1126
|
path: ["revision"]
|
|
1004
1127
|
});
|
|
1005
1128
|
}
|
|
1129
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
|
|
1130
|
+
if (shapes.length > 1) {
|
|
1131
|
+
context.addIssue({
|
|
1132
|
+
code: z.ZodIssueCode.custom,
|
|
1133
|
+
message: `${shapes.join(" and ")} are mutually exclusive`,
|
|
1134
|
+
path: [shapes[1]]
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1006
1137
|
});
|
|
1007
1138
|
var DEFAULT_HISTORY_LIMIT = 5;
|
|
1008
1139
|
var HistoryLimitSchema = z.number().int().min(0).max(100);
|
|
@@ -1134,8 +1265,8 @@ var LockEntrySchema = z.object({
|
|
|
1134
1265
|
).default([]),
|
|
1135
1266
|
/**
|
|
1136
1267
|
* Files this entry produced. A plain object generates one; asset kinds that
|
|
1137
|
-
* expand into many
|
|
1138
|
-
* resource
|
|
1268
|
+
* expand into many, an animated character being ~35 spritesheets plus an engine
|
|
1269
|
+
* resource, need the list, which is why v1's single `file` became this.
|
|
1139
1270
|
*
|
|
1140
1271
|
* `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
|
|
1141
1272
|
* consumer can find the one it wants without pattern-matching on paths.
|
|
@@ -1700,7 +1831,7 @@ function paletteSwatch(hexes, size = 64) {
|
|
|
1700
1831
|
}
|
|
1701
1832
|
function parseHex(hex2) {
|
|
1702
1833
|
const m = /^#?([0-9a-f]{6})$/i.exec(hex2.trim());
|
|
1703
|
-
if (!m) throw new Error(`invalid hex colour "${hex2}"
|
|
1834
|
+
if (!m) throw new Error(`invalid hex colour "${hex2}"; expected #rrggbb`);
|
|
1704
1835
|
const n = parseInt(m[1], 16);
|
|
1705
1836
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
1706
1837
|
}
|
|
@@ -3224,9 +3355,28 @@ function applyPostprocess(bytes, mediaType, record) {
|
|
|
3224
3355
|
import { existsSync as existsSync4 } from "fs";
|
|
3225
3356
|
import path7 from "path";
|
|
3226
3357
|
async function inspectRevisionReadiness(spec, lock) {
|
|
3358
|
+
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
3227
3359
|
if (!spec.revision) return null;
|
|
3228
3360
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
3229
3361
|
}
|
|
3362
|
+
async function inspectCharacterParent(spec, lock, seen) {
|
|
3363
|
+
const character = spec.character;
|
|
3364
|
+
const parentSpec = character.parentSpec;
|
|
3365
|
+
const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
|
|
3366
|
+
const dependency = await inspectParent(parentSpec, lock, seen);
|
|
3367
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
3368
|
+
if (!character.parentSha256 || !character.parentFile || !existsSync4(character.parentFile)) {
|
|
3369
|
+
return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
3370
|
+
}
|
|
3371
|
+
if (await sha256File(character.parentFile) !== character.parentSha256) {
|
|
3372
|
+
return { ready: false, reason: `${label} changed after the manifest was resolved` };
|
|
3373
|
+
}
|
|
3374
|
+
const parentEntry = lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)];
|
|
3375
|
+
if (!parentEntry?.objectId) {
|
|
3376
|
+
return { ready: false, reason: `${label} has no PixelLab character id recorded` };
|
|
3377
|
+
}
|
|
3378
|
+
return { ready: true, reason: `${label} is current` };
|
|
3379
|
+
}
|
|
3230
3380
|
async function inspectRevision(spec, lock, seen) {
|
|
3231
3381
|
const revision = spec.revision;
|
|
3232
3382
|
if (!revision) return { ready: true, reason: "asset has no revision inputs" };
|
|
@@ -3266,6 +3416,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3266
3416
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
3267
3417
|
if (!inputs.ready) return inputs;
|
|
3268
3418
|
}
|
|
3419
|
+
if (spec.character?.parentSpec) {
|
|
3420
|
+
const inputs = await inspectCharacterParent(spec, lock, nextSeen);
|
|
3421
|
+
if (!inputs.ready) return inputs;
|
|
3422
|
+
}
|
|
3269
3423
|
if (spec.quality) {
|
|
3270
3424
|
const quality = await inspectQualityProfile(spec, lock);
|
|
3271
3425
|
return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
|
|
@@ -3400,7 +3554,19 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3400
3554
|
const quality = await inspectQualityProfile(spec, lock);
|
|
3401
3555
|
items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
|
|
3402
3556
|
}
|
|
3403
|
-
const
|
|
3557
|
+
const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
|
|
3558
|
+
const actionableKeys = new Set(items.filter(isActionable).map((i) => i.key));
|
|
3559
|
+
for (const item of items) {
|
|
3560
|
+
if (!isActionable(item)) continue;
|
|
3561
|
+
const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
3562
|
+
if (!parent) continue;
|
|
3563
|
+
const parentKey = lockKey(parent.styleId, parent.assetId);
|
|
3564
|
+
if (actionableKeys.has(parentKey)) {
|
|
3565
|
+
item.state = "blocked";
|
|
3566
|
+
item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
const actionable = items.filter(isActionable);
|
|
3404
3570
|
const groupsByProvider = /* @__PURE__ */ new Map();
|
|
3405
3571
|
for (const item of actionable) {
|
|
3406
3572
|
const provider = item.spec.provider;
|
|
@@ -3476,7 +3642,7 @@ function announceGalleryReady(url, count, stdout = process.stdout, stderr = proc
|
|
|
3476
3642
|
gallery of ${count} generation${count === 1 ? "" : "s"}: ${url}
|
|
3477
3643
|
` + (notes.length ? ` (${notes.join("; ")}; Ctrl+C to stop)
|
|
3478
3644
|
|
|
3479
|
-
` : " (read-only
|
|
3645
|
+
` : " (read-only; add --edit to change prompts or edit sprites, --budget <n> to generate; Ctrl+C to stop)\n\n")
|
|
3480
3646
|
);
|
|
3481
3647
|
}
|
|
3482
3648
|
function printPlan(plan) {
|
|
@@ -3509,7 +3675,7 @@ function printPlan(plan) {
|
|
|
3509
3675
|
if (plan.actionable.length) {
|
|
3510
3676
|
for (const group of plan.groups) {
|
|
3511
3677
|
log(
|
|
3512
|
-
` ${group.provider}: ${group.actionable.length} asset(s)
|
|
3678
|
+
` ${group.provider}: ${group.actionable.length} asset(s), ${formatCost(group.costUnit, group.cost)}, ${group.candidates} candidate/output image(s)`
|
|
3513
3679
|
);
|
|
3514
3680
|
}
|
|
3515
3681
|
} else {
|
|
@@ -3531,7 +3697,7 @@ function printResumeActions(specs, lock) {
|
|
|
3531
3697
|
const shown = action.keys.slice(0, 3).join(", ");
|
|
3532
3698
|
const more = action.keys.length > 3 ? `, +${action.keys.length - 3} more` : "";
|
|
3533
3699
|
log(
|
|
3534
|
-
` next: pixelkiln ${action.command}
|
|
3700
|
+
` next: pixelkiln ${action.command} for ${action.keys.length} asset${action.keys.length === 1 ? "" : "s"} (${shown}${more})`
|
|
3535
3701
|
);
|
|
3536
3702
|
}
|
|
3537
3703
|
return actions.length;
|
|
@@ -3552,8 +3718,8 @@ async function confirm(question, auto) {
|
|
|
3552
3718
|
}
|
|
3553
3719
|
|
|
3554
3720
|
// src/cli/commands/account.ts
|
|
3555
|
-
import
|
|
3556
|
-
import { existsSync as
|
|
3721
|
+
import path21 from "path";
|
|
3722
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3557
3723
|
|
|
3558
3724
|
// src/env.ts
|
|
3559
3725
|
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
@@ -4605,6 +4771,60 @@ var MapObjectSchema = z2.object({
|
|
|
4605
4771
|
var ObjectListSchema = z2.object({ objects: z2.array(PixelLabObjectSchema), total: z2.number().int().min(0) }).passthrough();
|
|
4606
4772
|
var PixfluxResponseSchema = z2.object({ image: z2.object({ base64: z2.string().min(1) }).passthrough(), usage: z2.unknown().optional() }).passthrough();
|
|
4607
4773
|
var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
|
|
4774
|
+
var UsageSchema = z2.object({
|
|
4775
|
+
type: z2.string().optional(),
|
|
4776
|
+
usd: z2.number().nullable().optional(),
|
|
4777
|
+
generations: z2.number().nullable().optional()
|
|
4778
|
+
}).passthrough().nullable().optional();
|
|
4779
|
+
var CharacterSubmitSchema = z2.object({
|
|
4780
|
+
background_job_id: z2.string().min(1),
|
|
4781
|
+
character_id: z2.string().min(1),
|
|
4782
|
+
status: z2.string().default("processing"),
|
|
4783
|
+
usage: UsageSchema
|
|
4784
|
+
}).passthrough();
|
|
4785
|
+
var AnimateSubmitSchema = z2.object({
|
|
4786
|
+
background_job_ids: z2.array(z2.string()),
|
|
4787
|
+
directions: z2.array(z2.string()),
|
|
4788
|
+
status: z2.string().default("processing"),
|
|
4789
|
+
usage: UsageSchema
|
|
4790
|
+
}).passthrough();
|
|
4791
|
+
var CharacterAnimationDirectionSchema = z2.object({
|
|
4792
|
+
direction: z2.string(),
|
|
4793
|
+
frame_count: z2.number().int().min(0),
|
|
4794
|
+
frames: z2.array(z2.string())
|
|
4795
|
+
}).passthrough();
|
|
4796
|
+
var CharacterAnimationSchema2 = z2.object({
|
|
4797
|
+
animation_type: z2.string(),
|
|
4798
|
+
display_name: z2.string().nullable().optional(),
|
|
4799
|
+
animation_group_id: z2.string().nullable().optional(),
|
|
4800
|
+
directions: z2.array(CharacterAnimationDirectionSchema)
|
|
4801
|
+
}).passthrough();
|
|
4802
|
+
var CharacterSchema = z2.object({
|
|
4803
|
+
id: z2.string().min(1),
|
|
4804
|
+
name: z2.string().default(""),
|
|
4805
|
+
state_name: z2.string().nullable().optional(),
|
|
4806
|
+
prompt: z2.string().default(""),
|
|
4807
|
+
size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }),
|
|
4808
|
+
directions: z2.number().int(),
|
|
4809
|
+
created_at: z2.string(),
|
|
4810
|
+
updated_at: z2.string().nullable().optional(),
|
|
4811
|
+
animation_count: z2.number().int().default(0),
|
|
4812
|
+
template_id: z2.string().default("mannequin"),
|
|
4813
|
+
view: z2.string().nullable().optional(),
|
|
4814
|
+
status: z2.string().default("completed"),
|
|
4815
|
+
rotation_urls: z2.record(z2.string().nullable()).nullable().optional(),
|
|
4816
|
+
tags: z2.array(z2.string()).default([]),
|
|
4817
|
+
group_id: z2.string().nullable().optional(),
|
|
4818
|
+
animations: z2.array(CharacterAnimationSchema2).default([]),
|
|
4819
|
+
preview_url: z2.string().nullable().optional()
|
|
4820
|
+
}).passthrough();
|
|
4821
|
+
var CharacterListSchema = z2.object({ characters: z2.array(CharacterSchema), total: z2.number().int().min(0) }).passthrough();
|
|
4822
|
+
var BackgroundJobSchema = z2.object({
|
|
4823
|
+
id: z2.string(),
|
|
4824
|
+
status: z2.string(),
|
|
4825
|
+
last_response: z2.record(z2.unknown()).nullable().optional()
|
|
4826
|
+
}).passthrough();
|
|
4827
|
+
var DeleteAnimationsSchema = z2.object({ success: z2.boolean(), deleted_count: z2.number().int().default(0), error: z2.string().nullable().optional() }).passthrough();
|
|
4608
4828
|
function validateResponse(schema, raw, operation) {
|
|
4609
4829
|
const parsed = schema.safeParse(raw);
|
|
4610
4830
|
if (parsed.success) return parsed.data;
|
|
@@ -4628,9 +4848,9 @@ var PixelLabClient = class {
|
|
|
4628
4848
|
apiKey;
|
|
4629
4849
|
http;
|
|
4630
4850
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
4631
|
-
async request(
|
|
4851
|
+
async request(path44, init) {
|
|
4632
4852
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
4633
|
-
const res = await this.http(`${BASE}${
|
|
4853
|
+
const res = await this.http(`${BASE}${path44}`, {
|
|
4634
4854
|
...init,
|
|
4635
4855
|
headers: {
|
|
4636
4856
|
Authorization: auth,
|
|
@@ -4640,13 +4860,13 @@ var PixelLabClient = class {
|
|
|
4640
4860
|
});
|
|
4641
4861
|
const text = await res.text();
|
|
4642
4862
|
if (!res.ok) {
|
|
4643
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
4863
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path44} \u2192 ${res.status}`, res.status, text);
|
|
4644
4864
|
}
|
|
4645
4865
|
if (!text) return {};
|
|
4646
4866
|
try {
|
|
4647
4867
|
return JSON.parse(text);
|
|
4648
4868
|
} catch {
|
|
4649
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
4869
|
+
throw new Error(`${init?.method ?? "GET"} ${path44} returned invalid JSON`);
|
|
4650
4870
|
}
|
|
4651
4871
|
}
|
|
4652
4872
|
async balance() {
|
|
@@ -4663,7 +4883,7 @@ var PixelLabClient = class {
|
|
|
4663
4883
|
*
|
|
4664
4884
|
* `size` and `styleImages` are mutually exclusive at the API level: when style
|
|
4665
4885
|
* images are supplied the largest one dictates the output size. So style
|
|
4666
|
-
* references must already be at the target resolution
|
|
4886
|
+
* references must already be at the target resolution; a 128px reference
|
|
4667
4887
|
* silently produces 128px output and a different candidate count.
|
|
4668
4888
|
*/
|
|
4669
4889
|
async create1Direction(args) {
|
|
@@ -4687,7 +4907,7 @@ var PixelLabClient = class {
|
|
|
4687
4907
|
return parsed;
|
|
4688
4908
|
}
|
|
4689
4909
|
/**
|
|
4690
|
-
* Arbitrary width x height. Returns a single result
|
|
4910
|
+
* Arbitrary width x height. Returns a single result, no selection step.
|
|
4691
4911
|
*
|
|
4692
4912
|
* These AUTO-DELETE AFTER 8 HOURS, so `fetch` must run in the same session as
|
|
4693
4913
|
* `submit`. The pipeline warns when a map-object entry is older than that.
|
|
@@ -4709,12 +4929,12 @@ var PixelLabClient = class {
|
|
|
4709
4929
|
);
|
|
4710
4930
|
}
|
|
4711
4931
|
/**
|
|
4712
|
-
* Draws a whole tile set in one call
|
|
4932
|
+
* Draws a whole tile set in one call: many variations, or a connectable
|
|
4713
4933
|
* set when `tileFeature` is given.
|
|
4714
4934
|
*
|
|
4715
4935
|
* `styleImages` here is NOT the shape `create-1-direction-object` uses.
|
|
4716
|
-
* TilesProStyleImage is flat
|
|
4717
|
-
* required
|
|
4936
|
+
* TilesProStyleImage is flat, `{base64, width, height}` with all three
|
|
4937
|
+
* required, where 1dir wants `{type, base64, format}`. Confirmed against
|
|
4718
4938
|
* the OpenAPI schema; sending 1dir's shape is rejected as an extra field.
|
|
4719
4939
|
*
|
|
4720
4940
|
* Passing style images also makes the API ignore `tileType` and `tileView`
|
|
@@ -4735,7 +4955,7 @@ var PixelLabClient = class {
|
|
|
4735
4955
|
"create tiles"
|
|
4736
4956
|
);
|
|
4737
4957
|
}
|
|
4738
|
-
/** Throws PixelLabError(423) while the set is still drawing
|
|
4958
|
+
/** Throws PixelLabError(423) while the set is still drawing; see TilesPro. */
|
|
4739
4959
|
async getTilesPro(tileId) {
|
|
4740
4960
|
return validateResponse(
|
|
4741
4961
|
TilesProSchema,
|
|
@@ -4745,7 +4965,7 @@ var PixelLabClient = class {
|
|
|
4745
4965
|
}
|
|
4746
4966
|
/**
|
|
4747
4967
|
* Synchronous single-image generation. Returns the PNG inline rather than a
|
|
4748
|
-
* job id, and is the only endpoint that honours a forced palette
|
|
4968
|
+
* job id, and is the only endpoint that honours a forced palette;
|
|
4749
4969
|
* `color_image` on /map-objects returns a 500 whatever the payload shape.
|
|
4750
4970
|
*/
|
|
4751
4971
|
async createImagePixflux(args) {
|
|
@@ -4803,7 +5023,7 @@ var PixelLabClient = class {
|
|
|
4803
5023
|
/**
|
|
4804
5024
|
* Promotes chosen candidates to standalone objects, each with its own id.
|
|
4805
5025
|
* The review parent survives until nothing is left in it, so the returned
|
|
4806
|
-
* `created_object_ids
|
|
5026
|
+
* `created_object_ids`, not the parent id, is what should be recorded.
|
|
4807
5027
|
*/
|
|
4808
5028
|
async selectFrames(objectId, indices, commonTag) {
|
|
4809
5029
|
const raw = await this.request(`/objects/${objectId}/select-frames`, {
|
|
@@ -4819,10 +5039,140 @@ var PixelLabClient = class {
|
|
|
4819
5039
|
dismissReview(objectId) {
|
|
4820
5040
|
return this.request(`/objects/${objectId}/dismiss-review`, { method: "POST" });
|
|
4821
5041
|
}
|
|
4822
|
-
/** Free and synchronous. Replaces the full tag set
|
|
5042
|
+
/** Free and synchronous. Replaces the full tag set, so include tags you want to keep. */
|
|
4823
5043
|
setTags(objectId, tags) {
|
|
4824
5044
|
return this.request(`/objects/${objectId}/tags`, { method: "PATCH", body: JSON.stringify({ tags }) });
|
|
4825
5045
|
}
|
|
5046
|
+
/**
|
|
5047
|
+
* A base character. Standard mode picks the 4- or 8-direction template
|
|
5048
|
+
* endpoint; v3 and pro have their own and always draw 8. Every one answers
|
|
5049
|
+
* at once with the character id and a background job.
|
|
5050
|
+
*/
|
|
5051
|
+
async createCharacter(args) {
|
|
5052
|
+
const imageSize = { width: args.size, height: args.size };
|
|
5053
|
+
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
5054
|
+
let path44;
|
|
5055
|
+
let body;
|
|
5056
|
+
if (args.mode === "v3") {
|
|
5057
|
+
path44 = "/create-character-v3";
|
|
5058
|
+
body = {
|
|
5059
|
+
description: args.description,
|
|
5060
|
+
image_size: imageSize,
|
|
5061
|
+
template_id: args.template,
|
|
5062
|
+
no_background: args.noBackground ?? true,
|
|
5063
|
+
...args.view ? { view: args.view } : {},
|
|
5064
|
+
...args.outline ? { outline: args.outline } : {},
|
|
5065
|
+
...args.detail ? { detail: args.detail } : {},
|
|
5066
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
5067
|
+
};
|
|
5068
|
+
} else if (args.mode === "pro") {
|
|
5069
|
+
path44 = "/create-character-pro";
|
|
5070
|
+
body = {
|
|
5071
|
+
description: args.description,
|
|
5072
|
+
image_size: imageSize,
|
|
5073
|
+
template_id: args.template,
|
|
5074
|
+
no_background: args.noBackground ?? true,
|
|
5075
|
+
...args.view ? { view: args.view } : {},
|
|
5076
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
5077
|
+
};
|
|
5078
|
+
} else {
|
|
5079
|
+
path44 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
5080
|
+
body = {
|
|
5081
|
+
description: args.description,
|
|
5082
|
+
image_size: imageSize,
|
|
5083
|
+
template_id: args.template,
|
|
5084
|
+
...args.view ? { view: args.view } : {},
|
|
5085
|
+
...args.outline ? { outline: args.outline } : {},
|
|
5086
|
+
...args.shading ? { shading: args.shading } : {},
|
|
5087
|
+
...args.detail ? { detail: args.detail } : {},
|
|
5088
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
5089
|
+
...palette
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
const raw = await this.request(path44, { method: "POST", body: JSON.stringify(body) });
|
|
5093
|
+
return validateResponse(CharacterSubmitSchema, raw, path44.slice(1));
|
|
5094
|
+
}
|
|
5095
|
+
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
5096
|
+
async createCharacterState(args) {
|
|
5097
|
+
const raw = await this.request("/create-character-state", {
|
|
5098
|
+
method: "POST",
|
|
5099
|
+
body: JSON.stringify({
|
|
5100
|
+
character_id: args.characterId,
|
|
5101
|
+
edit_description: args.editDescription,
|
|
5102
|
+
no_background: args.noBackground ?? true,
|
|
5103
|
+
use_color_palette_from_reference: args.paletteFromReference ?? false,
|
|
5104
|
+
...args.stateName ? { state_name: args.stateName } : {},
|
|
5105
|
+
...args.canvas ? { override_frame_size: args.canvas } : {},
|
|
5106
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
5107
|
+
})
|
|
5108
|
+
});
|
|
5109
|
+
return validateResponse(CharacterSubmitSchema, raw, "create-character-state");
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* One animation of one character. Template mode costs a generation per
|
|
5113
|
+
* direction and fixes the frame count; v3 draws `frameCount` frames from
|
|
5114
|
+
* the action text; pro is the sequential engine. One job per direction.
|
|
5115
|
+
*/
|
|
5116
|
+
async animateCharacter(args) {
|
|
5117
|
+
const raw = await this.request("/animate-character", {
|
|
5118
|
+
method: "POST",
|
|
5119
|
+
body: JSON.stringify({
|
|
5120
|
+
character_id: args.characterId,
|
|
5121
|
+
animation_name: args.animationName,
|
|
5122
|
+
mode: args.mode,
|
|
5123
|
+
directions: args.directions,
|
|
5124
|
+
...args.template ? { template_animation_id: args.template } : {},
|
|
5125
|
+
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
5126
|
+
...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
5127
|
+
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
5128
|
+
...args.seed != null ? { seed: args.seed } : {}
|
|
5129
|
+
})
|
|
5130
|
+
});
|
|
5131
|
+
return validateResponse(AnimateSubmitSchema, raw, "animate-character");
|
|
5132
|
+
}
|
|
5133
|
+
async getCharacter(characterId) {
|
|
5134
|
+
const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
|
|
5135
|
+
return validateResponse(CharacterSchema, raw, "characters/{id}");
|
|
5136
|
+
}
|
|
5137
|
+
async listCharacters(limit = 100, offset = 0) {
|
|
5138
|
+
const raw = await this.request(`/characters?limit=${limit}&offset=${offset}`);
|
|
5139
|
+
return validateResponse(CharacterListSchema, raw, "characters");
|
|
5140
|
+
}
|
|
5141
|
+
async *iterateCharacters(pageSize = 100) {
|
|
5142
|
+
let offset = 0;
|
|
5143
|
+
for (; ; ) {
|
|
5144
|
+
const page = await this.listCharacters(pageSize, offset);
|
|
5145
|
+
for (const character of page.characters) yield character;
|
|
5146
|
+
offset += page.characters.length;
|
|
5147
|
+
if (page.characters.length === 0 || offset >= page.total) return;
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
async getBackgroundJob(jobId) {
|
|
5151
|
+
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
5152
|
+
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
5153
|
+
}
|
|
5154
|
+
/** Remove one animation group from a character, or one direction of it. */
|
|
5155
|
+
async deleteCharacterAnimations(characterId, selector, direction) {
|
|
5156
|
+
const query = new URLSearchParams(
|
|
5157
|
+
"animationGroupId" in selector ? { animation_group_id: selector.animationGroupId } : { animation_type: selector.animationType }
|
|
5158
|
+
);
|
|
5159
|
+
if (direction) query.set("direction", direction);
|
|
5160
|
+
const raw = await this.request(
|
|
5161
|
+
`/characters/${encodeURIComponent(characterId)}/animations?${query.toString()}`,
|
|
5162
|
+
{ method: "DELETE" }
|
|
5163
|
+
);
|
|
5164
|
+
return validateResponse(DeleteAnimationsSchema, raw, "characters/{id}/animations");
|
|
5165
|
+
}
|
|
5166
|
+
/** Irreversible: the character, its states are separate records, and its animations. */
|
|
5167
|
+
deleteCharacter(characterId) {
|
|
5168
|
+
return this.request(`/characters/${encodeURIComponent(characterId)}`, { method: "DELETE" });
|
|
5169
|
+
}
|
|
5170
|
+
setCharacterTags(characterId, tags) {
|
|
5171
|
+
return this.request(`/characters/${encodeURIComponent(characterId)}/tags`, {
|
|
5172
|
+
method: "PATCH",
|
|
5173
|
+
body: JSON.stringify({ tags })
|
|
5174
|
+
});
|
|
5175
|
+
}
|
|
4826
5176
|
/** Storage URLs are public; no auth header, and sending one can break the CDN request. */
|
|
4827
5177
|
async download(url) {
|
|
4828
5178
|
const res = await this.http(url);
|
|
@@ -4860,6 +5210,39 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
4860
5210
|
}
|
|
4861
5211
|
return null;
|
|
4862
5212
|
}
|
|
5213
|
+
function characterCost(spec) {
|
|
5214
|
+
const character = spec.character;
|
|
5215
|
+
if (!character) return generationCost(spec.width, spec.height, "1dir");
|
|
5216
|
+
const px = spec.width * spec.height;
|
|
5217
|
+
if (character.kind === "animation") {
|
|
5218
|
+
const animation = character.animation;
|
|
5219
|
+
if (animation.mode === "template") return 1;
|
|
5220
|
+
if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
|
|
5221
|
+
return generationCost(spec.width, spec.height, "1dir");
|
|
5222
|
+
}
|
|
5223
|
+
if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
|
|
5224
|
+
if (character.mode === "standard") return 1;
|
|
5225
|
+
if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
|
|
5226
|
+
return generationCost(spec.width, spec.height, "1dir");
|
|
5227
|
+
}
|
|
5228
|
+
function characterAnimationName(spec) {
|
|
5229
|
+
return `pixelkiln:${spec.styleId}/${spec.assetId}`;
|
|
5230
|
+
}
|
|
5231
|
+
var CHARACTER_TEMPLATES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
|
|
5232
|
+
function encodeAnimationJob(job) {
|
|
5233
|
+
return `character-animation:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
|
|
5234
|
+
}
|
|
5235
|
+
function decodeAnimationJob(jobId) {
|
|
5236
|
+
if (!jobId.startsWith("character-animation:")) return null;
|
|
5237
|
+
try {
|
|
5238
|
+
const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
|
|
5239
|
+
if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
|
|
5240
|
+
return { characterId: value.characterId, name: value.name, direction: value.direction, jobIds: Array.isArray(value.jobIds) ? value.jobIds : [] };
|
|
5241
|
+
}
|
|
5242
|
+
} catch {
|
|
5243
|
+
}
|
|
5244
|
+
return null;
|
|
5245
|
+
}
|
|
4863
5246
|
var PixelLabProvider = class _PixelLabProvider {
|
|
4864
5247
|
constructor(client2) {
|
|
4865
5248
|
this.client = client2;
|
|
@@ -4878,11 +5261,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4878
5261
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
4879
5262
|
}
|
|
4880
5263
|
supports(generator) {
|
|
4881
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles";
|
|
5264
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
4882
5265
|
}
|
|
4883
5266
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
4884
5267
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
4885
|
-
* Tier 2=10, Tier 3=20)
|
|
5268
|
+
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
4886
5269
|
rateLimit() {
|
|
4887
5270
|
return { spacingMs: 2500, maxInFlight: 8 };
|
|
4888
5271
|
}
|
|
@@ -4903,6 +5286,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4903
5286
|
if (spec.generator === "tiles") {
|
|
4904
5287
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
4905
5288
|
}
|
|
5289
|
+
if (spec.generator === "character") {
|
|
5290
|
+
return { unit: "generations", amount: characterCost(spec), candidates: 1 };
|
|
5291
|
+
}
|
|
4906
5292
|
return {
|
|
4907
5293
|
unit: "generations",
|
|
4908
5294
|
amount: generationCost(spec.width, spec.height, spec.generator),
|
|
@@ -4935,7 +5321,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4935
5321
|
"high detail"
|
|
4936
5322
|
]);
|
|
4937
5323
|
}
|
|
4938
|
-
if (
|
|
5324
|
+
if (spec.generator === "character") this.validateCharacter(spec);
|
|
5325
|
+
if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
|
|
4939
5326
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
4940
5327
|
}
|
|
4941
5328
|
for (const image of styleImages) {
|
|
@@ -4951,8 +5338,198 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4951
5338
|
);
|
|
4952
5339
|
}
|
|
4953
5340
|
}
|
|
4954
|
-
|
|
5341
|
+
validateCharacter(spec) {
|
|
5342
|
+
const character = spec.character;
|
|
5343
|
+
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
5344
|
+
const maxSize = character.mode === "v3" ? 256 : 128;
|
|
5345
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
5346
|
+
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
5347
|
+
}
|
|
5348
|
+
if (!CHARACTER_TEMPLATES.includes(character.template)) {
|
|
5349
|
+
throw new Error(`PixelLab character template must be one of: ${CHARACTER_TEMPLATES.join(", ")}`);
|
|
5350
|
+
}
|
|
5351
|
+
const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
|
|
5352
|
+
if (!views.includes(spec.view)) {
|
|
5353
|
+
throw new Error(`PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`);
|
|
5354
|
+
}
|
|
5355
|
+
if (spec.view === "oblique" && character.directions !== 4) {
|
|
5356
|
+
throw new Error("PixelLab oblique characters are 4-direction only");
|
|
5357
|
+
}
|
|
5358
|
+
if (character.kind !== "base" && !character.parentAssetId) {
|
|
5359
|
+
throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
|
|
5360
|
+
}
|
|
5361
|
+
if (character.animation) {
|
|
5362
|
+
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5363
|
+
if (!allowed.includes(character.animation.direction)) {
|
|
5364
|
+
throw new Error(
|
|
5365
|
+
`${spec.styleId}/${spec.assetId} animates "${character.animation.direction}", but its character has ${character.directions} directions: ${allowed.join(", ")}`
|
|
5366
|
+
);
|
|
5367
|
+
}
|
|
5368
|
+
if (character.animation.mode !== "template" && !spec.prompt.trim()) {
|
|
5369
|
+
throw new Error(`${spec.styleId}/${spec.assetId} needs a prompt describing the motion, or a template`);
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
/**
|
|
5374
|
+
* A base is one request that answers with the character id; a state is
|
|
5375
|
+
* the same with the parent's id; an animation names itself after the
|
|
5376
|
+
* asset so a later run can find it, and clears its own earlier take for
|
|
5377
|
+
* that direction first, since PixelLab skips a direction that exists.
|
|
5378
|
+
*/
|
|
5379
|
+
async submitCharacter(spec, context) {
|
|
5380
|
+
const character = spec.character;
|
|
5381
|
+
if (character.kind === "base") {
|
|
5382
|
+
const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
5383
|
+
const res2 = await this.client.createCharacter({
|
|
5384
|
+
mode: character.mode,
|
|
5385
|
+
description: spec.prompt,
|
|
5386
|
+
size: spec.width,
|
|
5387
|
+
directions: character.directions,
|
|
5388
|
+
view: spec.view,
|
|
5389
|
+
template: character.template,
|
|
5390
|
+
outline: spec.outline,
|
|
5391
|
+
shading: spec.shading,
|
|
5392
|
+
detail: spec.detail,
|
|
5393
|
+
seed: spec.seed,
|
|
5394
|
+
noBackground: spec.noBackground,
|
|
5395
|
+
paletteSwatchBase64: swatch
|
|
5396
|
+
});
|
|
5397
|
+
return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
5398
|
+
}
|
|
5399
|
+
const parentId = context?.parentObjectId;
|
|
5400
|
+
if (!parentId) {
|
|
5401
|
+
throw new Error(
|
|
5402
|
+
`${spec.styleId}/${spec.assetId} is a ${character.kind} of ${character.parentAssetId}, which has no PixelLab character id in the lockfile yet; generate the parent first`
|
|
5403
|
+
);
|
|
5404
|
+
}
|
|
5405
|
+
if (character.kind === "state") {
|
|
5406
|
+
const res2 = await this.client.createCharacterState({
|
|
5407
|
+
characterId: parentId,
|
|
5408
|
+
editDescription: spec.prompt,
|
|
5409
|
+
stateName: spec.assetId.slice(0, 50),
|
|
5410
|
+
paletteFromReference: character.state?.paletteFromReference,
|
|
5411
|
+
canvas: character.state?.canvas,
|
|
5412
|
+
seed: spec.seed,
|
|
5413
|
+
noBackground: spec.noBackground
|
|
5414
|
+
});
|
|
5415
|
+
return { jobId: res2.character_id, metadata: { character: { kind: "state", characterId: res2.character_id, parentCharacterId: parentId, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
5416
|
+
}
|
|
5417
|
+
const animation = character.animation;
|
|
5418
|
+
const name = characterAnimationName(spec);
|
|
5419
|
+
const replaced = context?.replacedMetadata?.character ?? {};
|
|
5420
|
+
const existing = await this.client.getCharacter(parentId);
|
|
5421
|
+
for (const group of existing.animations) {
|
|
5422
|
+
const ours = group.animation_group_id && (group.animation_group_id === replaced.animationGroupId || group.display_name === name || replaced.animationId && group.directions.some((d) => d.frames.some((url) => url.includes(`/animations/${replaced.animationId}/`))));
|
|
5423
|
+
if (!ours || !group.animation_group_id) continue;
|
|
5424
|
+
if (group.directions.some((d) => d.direction === animation.direction)) {
|
|
5425
|
+
await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
const res = await this.client.animateCharacter({
|
|
5429
|
+
characterId: parentId,
|
|
5430
|
+
animationName: name,
|
|
5431
|
+
actionDescription: spec.prompt.trim() || void 0,
|
|
5432
|
+
template: animation.template,
|
|
5433
|
+
mode: animation.mode,
|
|
5434
|
+
frameCount: animation.frames,
|
|
5435
|
+
keepFirstFrame: animation.keepFirstFrame,
|
|
5436
|
+
directions: [animation.direction],
|
|
5437
|
+
seed: spec.seed
|
|
5438
|
+
});
|
|
5439
|
+
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
|
|
5440
|
+
return {
|
|
5441
|
+
jobId: encodeAnimationJob(job),
|
|
5442
|
+
metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
|
|
5443
|
+
};
|
|
5444
|
+
}
|
|
5445
|
+
async pollCharacter(jobId, context) {
|
|
5446
|
+
const animation = decodeAnimationJob(jobId);
|
|
5447
|
+
if (animation) return this.pollCharacterAnimation(animation, context);
|
|
5448
|
+
const character = await this.client.getCharacter(jobId);
|
|
5449
|
+
if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
|
|
5450
|
+
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
5451
|
+
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5452
|
+
const sources = [];
|
|
5453
|
+
for (const direction of order) {
|
|
5454
|
+
const url = character.rotation_urls[direction];
|
|
5455
|
+
if (url) sources.push({ url, role: direction });
|
|
5456
|
+
}
|
|
5457
|
+
if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
|
|
5458
|
+
return {
|
|
5459
|
+
status: "ready",
|
|
5460
|
+
objectId: character.id,
|
|
5461
|
+
sourceUrl: sources[0].url,
|
|
5462
|
+
sources,
|
|
5463
|
+
metadata: {
|
|
5464
|
+
character: {
|
|
5465
|
+
characterId: character.id,
|
|
5466
|
+
groupId: character.group_id ?? null,
|
|
5467
|
+
name: character.name,
|
|
5468
|
+
stateName: character.state_name ?? null,
|
|
5469
|
+
directions: character.directions,
|
|
5470
|
+
size: character.size,
|
|
5471
|
+
updatedAt: character.updated_at ?? null
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
/**
|
|
5477
|
+
* The background job is the authoritative record of one direction: it
|
|
5478
|
+
* completes with the frame URLs, the animation id, and the direction, and
|
|
5479
|
+
* it reports a failure. The character's own animation list is the
|
|
5480
|
+
* fallback once PixelLab has cleaned the job up, and the place the group
|
|
5481
|
+
* id (what a later delete needs) is read from.
|
|
5482
|
+
*/
|
|
5483
|
+
async pollCharacterAnimation(job, context) {
|
|
5484
|
+
const fps = context?.spec?.character?.animation?.fps ?? 8;
|
|
5485
|
+
const review = (frames, animationId, groupId) => ({
|
|
5486
|
+
status: "review-set",
|
|
5487
|
+
objectId: `${job.characterId}#${groupId ?? animationId ?? job.name}`,
|
|
5488
|
+
frameUrls: frames,
|
|
5489
|
+
sources: frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
|
|
5490
|
+
fps,
|
|
5491
|
+
metadata: {
|
|
5492
|
+
frameSet: { fps, count: frames.length },
|
|
5493
|
+
character: {
|
|
5494
|
+
kind: "animation",
|
|
5495
|
+
characterId: job.characterId,
|
|
5496
|
+
animationId,
|
|
5497
|
+
animationGroupId: groupId,
|
|
5498
|
+
animationName: job.name,
|
|
5499
|
+
direction: job.direction
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
});
|
|
5503
|
+
let cleanedUp = job.jobIds.length === 0;
|
|
5504
|
+
for (const id of job.jobIds) {
|
|
5505
|
+
const status = await this.client.getBackgroundJob(id).catch((err) => {
|
|
5506
|
+
if (err instanceof PixelLabError && err.status === 404) return null;
|
|
5507
|
+
throw err;
|
|
5508
|
+
});
|
|
5509
|
+
if (!status) {
|
|
5510
|
+
cleanedUp = true;
|
|
5511
|
+
continue;
|
|
5512
|
+
}
|
|
5513
|
+
if (status.status === "failed") return { status: "failed", error: "animation generation failed upstream" };
|
|
5514
|
+
if (status.status !== "completed") return { status: "processing" };
|
|
5515
|
+
const done = status.last_response ?? {};
|
|
5516
|
+
const frames = done.storage_urls?.frames;
|
|
5517
|
+
const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
|
|
5518
|
+
if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
|
|
5519
|
+
const character2 = await this.client.getCharacter(job.characterId);
|
|
5520
|
+
const group = findAnimation(character2, job.name, job.direction, animationId);
|
|
5521
|
+
return review(frames, animationId, group?.groupId ?? null);
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
const recorded = context?.metadata?.character?.animationId ?? null;
|
|
5525
|
+
const character = await this.client.getCharacter(job.characterId);
|
|
5526
|
+
const found = findAnimation(character, job.name, job.direction, recorded);
|
|
5527
|
+
if (found) return review(found.frames, found.animationId, found.groupId);
|
|
5528
|
+
return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
|
|
5529
|
+
}
|
|
5530
|
+
async submit(spec, styleImages, context) {
|
|
4955
5531
|
this.validate(spec, styleImages);
|
|
5532
|
+
if (spec.generator === "character") return this.submitCharacter(spec, context);
|
|
4956
5533
|
if (spec.generator === "pixflux") {
|
|
4957
5534
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
4958
5535
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -5017,6 +5594,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5017
5594
|
}
|
|
5018
5595
|
if (generator === "map") return this.pollMap(jobId);
|
|
5019
5596
|
if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature));
|
|
5597
|
+
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
5020
5598
|
const obj = await this.client.getObject(jobId);
|
|
5021
5599
|
if (obj.status === "review") {
|
|
5022
5600
|
return { status: "review", candidateUrls: obj.frame_urls ?? [] };
|
|
@@ -5118,7 +5696,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5118
5696
|
const b = await this.client.balance();
|
|
5119
5697
|
return { unit: "generations", remaining: b.generations, total: b.total, plan: b.plan };
|
|
5120
5698
|
}
|
|
5121
|
-
async setTags(objectId, tags) {
|
|
5699
|
+
async setTags(objectId, tags, generator) {
|
|
5700
|
+
if (generator === "character") {
|
|
5701
|
+
await this.client.setCharacterTags(objectId.split("#")[0], tags.slice(0, 20));
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5122
5704
|
await this.client.setTags(objectId, tags.slice(0, 20));
|
|
5123
5705
|
}
|
|
5124
5706
|
async *list() {
|
|
@@ -5138,7 +5720,49 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5138
5720
|
async delete(assetId) {
|
|
5139
5721
|
await this.client.deleteObject(assetId);
|
|
5140
5722
|
}
|
|
5723
|
+
async *listCharacters() {
|
|
5724
|
+
for await (const character of this.client.iterateCharacters(100)) {
|
|
5725
|
+
yield remoteCharacter(character);
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
async getCharacter(id) {
|
|
5729
|
+
const character = await this.client.getCharacter(id);
|
|
5730
|
+
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5731
|
+
const rotations = [];
|
|
5732
|
+
for (const direction of order) {
|
|
5733
|
+
const url = character.rotation_urls?.[direction];
|
|
5734
|
+
if (url) rotations.push({ url, role: direction });
|
|
5735
|
+
}
|
|
5736
|
+
return {
|
|
5737
|
+
...remoteCharacter(character),
|
|
5738
|
+
rotations,
|
|
5739
|
+
animations: character.animations.flatMap((group) => group.directions.map((d) => ({
|
|
5740
|
+
groupId: group.animation_group_id ?? null,
|
|
5741
|
+
name: group.display_name ?? null,
|
|
5742
|
+
type: group.animation_type,
|
|
5743
|
+
direction: d.direction,
|
|
5744
|
+
frames: d.frames
|
|
5745
|
+
})))
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5141
5748
|
};
|
|
5749
|
+
function remoteCharacter(character) {
|
|
5750
|
+
return {
|
|
5751
|
+
id: character.id,
|
|
5752
|
+
name: character.name,
|
|
5753
|
+
stateName: character.state_name ?? null,
|
|
5754
|
+
prompt: character.prompt,
|
|
5755
|
+
groupId: character.group_id ?? null,
|
|
5756
|
+
directions: character.directions,
|
|
5757
|
+
width: character.size.width,
|
|
5758
|
+
height: character.size.height,
|
|
5759
|
+
createdAt: character.created_at,
|
|
5760
|
+
previewUrl: character.preview_url ?? character.rotation_urls?.south ?? null,
|
|
5761
|
+
tags: character.tags,
|
|
5762
|
+
status: character.status,
|
|
5763
|
+
animationCount: character.animation_count
|
|
5764
|
+
};
|
|
5765
|
+
}
|
|
5142
5766
|
function requirePixelLabOption(name, value, allowed) {
|
|
5143
5767
|
if (value != null && !allowed.includes(value)) {
|
|
5144
5768
|
throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
|
|
@@ -5151,6 +5775,18 @@ function firstUrl(urls) {
|
|
|
5151
5775
|
if (!urls) return null;
|
|
5152
5776
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
5153
5777
|
}
|
|
5778
|
+
function findAnimation(character, name, direction, animationId) {
|
|
5779
|
+
for (const group of character.animations) {
|
|
5780
|
+
const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
|
|
5781
|
+
if (!match) continue;
|
|
5782
|
+
const byName = group.display_name === name || group.animation_type === name;
|
|
5783
|
+
const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
|
|
5784
|
+
if (!byName && !byId) continue;
|
|
5785
|
+
const fromUrl = /\/animations\/([^/]+)\//.exec(match.frames[0])?.[1] ?? null;
|
|
5786
|
+
return { frames: match.frames, groupId: group.animation_group_id ?? null, animationId: animationId ?? fromUrl };
|
|
5787
|
+
}
|
|
5788
|
+
return null;
|
|
5789
|
+
}
|
|
5154
5790
|
|
|
5155
5791
|
// src/providers/retrodiffusion.ts
|
|
5156
5792
|
var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
|
|
@@ -5200,9 +5836,9 @@ var RetroDiffusionClient = class {
|
|
|
5200
5836
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
5201
5837
|
return Buffer.from(await response.arrayBuffer());
|
|
5202
5838
|
}
|
|
5203
|
-
async call(
|
|
5839
|
+
async call(path44, init = {}) {
|
|
5204
5840
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
5205
|
-
const response = await this.request(`${this.baseUrl}${
|
|
5841
|
+
const response = await this.request(`${this.baseUrl}${path44}`, {
|
|
5206
5842
|
...init,
|
|
5207
5843
|
headers: {
|
|
5208
5844
|
"Content-Type": "application/json",
|
|
@@ -6111,33 +6747,50 @@ ${issues}`);
|
|
|
6111
6747
|
unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
|
|
6112
6748
|
}
|
|
6113
6749
|
}
|
|
6750
|
+
for (const shape of ["state", "animation"]) {
|
|
6751
|
+
const of = asset[shape]?.of;
|
|
6752
|
+
if (!of) continue;
|
|
6753
|
+
const parent = parsed.data.assets[of];
|
|
6754
|
+
if (!parent) {
|
|
6755
|
+
unknownReferences.push(`assets.${assetId}.${shape}.of: unknown asset "${of}"`);
|
|
6756
|
+
} else if (of === assetId) {
|
|
6757
|
+
unknownReferences.push(`assets.${assetId}.${shape}.of: an asset cannot be a ${shape} of itself`);
|
|
6758
|
+
} else if (parent.animation) {
|
|
6759
|
+
unknownReferences.push(`assets.${assetId}.${shape}.of: "${of}" is an animation and cannot be a parent`);
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6114
6762
|
}
|
|
6115
6763
|
const visiting = /* @__PURE__ */ new Set();
|
|
6116
6764
|
const visited = /* @__PURE__ */ new Set();
|
|
6117
|
-
const
|
|
6765
|
+
const visitParents = (assetId, chain) => {
|
|
6118
6766
|
if (visited.has(assetId)) return;
|
|
6119
6767
|
if (visiting.has(assetId)) {
|
|
6120
6768
|
const start = chain.indexOf(assetId);
|
|
6769
|
+
const shape = parsed.data.assets[assetId]?.revision ? "revision" : parsed.data.assets[assetId]?.state ? "state" : "animation";
|
|
6770
|
+
const field = shape === "revision" ? "revision.from" : `${shape}.of`;
|
|
6121
6771
|
unknownReferences.push(
|
|
6122
|
-
`assets.${assetId}
|
|
6772
|
+
`assets.${assetId}.${field}: ${shape} cycle ${[...chain.slice(start), assetId].join(" -> ")}`
|
|
6123
6773
|
);
|
|
6124
6774
|
return;
|
|
6125
6775
|
}
|
|
6126
6776
|
visiting.add(assetId);
|
|
6127
|
-
const parent = parsed.data.assets[assetId]
|
|
6777
|
+
const parent = parentAssetId(parsed.data.assets[assetId]);
|
|
6128
6778
|
if (parent && parent !== assetId && parsed.data.assets[parent]) {
|
|
6129
|
-
|
|
6779
|
+
visitParents(parent, [...chain, assetId]);
|
|
6130
6780
|
}
|
|
6131
6781
|
visiting.delete(assetId);
|
|
6132
6782
|
visited.add(assetId);
|
|
6133
6783
|
};
|
|
6134
|
-
for (const assetId of Object.keys(parsed.data.assets))
|
|
6784
|
+
for (const assetId of Object.keys(parsed.data.assets)) visitParents(assetId, []);
|
|
6135
6785
|
if (unknownReferences.length) {
|
|
6136
6786
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6137
6787
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
6138
6788
|
}
|
|
6139
6789
|
return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
|
|
6140
6790
|
}
|
|
6791
|
+
function parentAssetId(asset) {
|
|
6792
|
+
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
6793
|
+
}
|
|
6141
6794
|
function formatManifestIssues(issues) {
|
|
6142
6795
|
return issues.flatMap((issue) => {
|
|
6143
6796
|
if (issue.code === "invalid_union") {
|
|
@@ -6225,10 +6878,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6225
6878
|
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
6226
6879
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
6227
6880
|
for (const assetId of [...requestedAssetIds]) {
|
|
6228
|
-
let current = manifest.assets[assetId]
|
|
6881
|
+
let current = parentAssetId(manifest.assets[assetId]);
|
|
6229
6882
|
while (current && !resolutionAssetIds.has(current)) {
|
|
6230
6883
|
resolutionAssetIds.add(current);
|
|
6231
|
-
current = manifest.assets[current]
|
|
6884
|
+
current = parentAssetId(manifest.assets[current]);
|
|
6232
6885
|
}
|
|
6233
6886
|
}
|
|
6234
6887
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
@@ -6301,13 +6954,23 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6301
6954
|
width = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.width)) : style.tileSize ?? 32;
|
|
6302
6955
|
height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
|
|
6303
6956
|
size = Math.max(width, height);
|
|
6957
|
+
} else if (generator === "character") {
|
|
6958
|
+
size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
|
|
6959
|
+
width = asset.state?.canvas?.width ?? size;
|
|
6960
|
+
height = asset.state?.canvas?.height ?? size;
|
|
6304
6961
|
} else {
|
|
6305
6962
|
width = asset.width ?? style.size ?? 64;
|
|
6306
6963
|
height = asset.height ?? style.size ?? 64;
|
|
6307
6964
|
size = Math.max(width, height);
|
|
6308
6965
|
}
|
|
6966
|
+
if ((asset.state || asset.animation) && generator !== "character") {
|
|
6967
|
+
throw new Error(
|
|
6968
|
+
`assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character style; "${styleId}" generates ${generator}`
|
|
6969
|
+
);
|
|
6970
|
+
}
|
|
6971
|
+
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
6309
6972
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
6310
|
-
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
6973
|
+
const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
6311
6974
|
const relFile = asset.file ?? path12.join(asset.category ?? "", `${assetId}.png`);
|
|
6312
6975
|
const outFile = path12.resolve(root, style.outDir, relFile);
|
|
6313
6976
|
const qualityOutFile = style.quality ? path12.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
@@ -6324,7 +6987,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6324
6987
|
width,
|
|
6325
6988
|
height,
|
|
6326
6989
|
size,
|
|
6327
|
-
view: style.view ?? (generator === "1dir" ? "top-down" : "high top-down"),
|
|
6990
|
+
view: style.view ?? (generator === "1dir" ? "top-down" : generator === "character" ? "low top-down" : "high top-down"),
|
|
6328
6991
|
styleImagePaths: style.styleImages.map((s) => s.path),
|
|
6329
6992
|
outline: style.outline,
|
|
6330
6993
|
shading: style.shading,
|
|
@@ -6338,6 +7001,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6338
7001
|
tileView: generator === "tiles" ? style.tileView : void 0,
|
|
6339
7002
|
tileFeature: generator === "tiles" ? style.tileFeature : void 0,
|
|
6340
7003
|
outlineMode: generator === "tiles" ? style.outlineMode : void 0,
|
|
7004
|
+
...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
|
|
6341
7005
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
|
|
6342
7006
|
costUnit: "generations",
|
|
6343
7007
|
candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
|
|
@@ -6368,6 +7032,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6368
7032
|
} : {},
|
|
6369
7033
|
tags,
|
|
6370
7034
|
source: asset.sourceByStyle[styleId] ?? asset.source,
|
|
7035
|
+
...asset.remoteId ? { remoteId: asset.remoteId } : {},
|
|
6371
7036
|
// Revision identity is attached after every dependency in this style
|
|
6372
7037
|
// has a concrete output target. Finalization below computes the hash.
|
|
6373
7038
|
specHash: ""
|
|
@@ -6384,6 +7049,22 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6384
7049
|
}
|
|
6385
7050
|
if (finalized.has(assetId)) return resolved;
|
|
6386
7051
|
const asset = manifest.assets[assetId];
|
|
7052
|
+
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
7053
|
+
if (resolved.character && characterParent) {
|
|
7054
|
+
const parentSpec = await finalize(characterParent);
|
|
7055
|
+
const parentDirections = parentSpec.character?.directions ?? 8;
|
|
7056
|
+
const parentFile = memberPath(parentSpec.outFile, "south", 0, parentDirections, MediaType.PNG);
|
|
7057
|
+
resolved.character = {
|
|
7058
|
+
...resolved.character,
|
|
7059
|
+
parentAssetId: characterParent,
|
|
7060
|
+
parentSpec,
|
|
7061
|
+
parentFile,
|
|
7062
|
+
parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
|
|
7063
|
+
// A state and its animations keep the engine and rotations of the base.
|
|
7064
|
+
mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
|
|
7065
|
+
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
7066
|
+
};
|
|
7067
|
+
}
|
|
6387
7068
|
if (asset.revision) {
|
|
6388
7069
|
if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
|
|
6389
7070
|
throw new Error(
|
|
@@ -6531,6 +7212,34 @@ function imageMetadata(buf) {
|
|
|
6531
7212
|
}
|
|
6532
7213
|
return null;
|
|
6533
7214
|
}
|
|
7215
|
+
function resolveCharacterShape(asset, style, kind) {
|
|
7216
|
+
const mode2 = style.mode ?? "standard";
|
|
7217
|
+
const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
|
|
7218
|
+
const shape = {
|
|
7219
|
+
kind,
|
|
7220
|
+
mode: mode2,
|
|
7221
|
+
directions,
|
|
7222
|
+
template: style.template ?? "mannequin"
|
|
7223
|
+
};
|
|
7224
|
+
if (asset.state) {
|
|
7225
|
+
shape.state = {
|
|
7226
|
+
paletteFromReference: asset.state.paletteFromReference,
|
|
7227
|
+
...asset.state.canvas ? { canvas: asset.state.canvas } : {}
|
|
7228
|
+
};
|
|
7229
|
+
}
|
|
7230
|
+
if (asset.animation) {
|
|
7231
|
+
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
7232
|
+
shape.animation = {
|
|
7233
|
+
mode: animationMode,
|
|
7234
|
+
...asset.animation.template ? { template: asset.animation.template } : {},
|
|
7235
|
+
direction: asset.animation.direction,
|
|
7236
|
+
frames: asset.animation.frames ?? 8,
|
|
7237
|
+
fps: asset.animation.fps,
|
|
7238
|
+
keepFirstFrame: asset.animation.keepFirstFrame
|
|
7239
|
+
};
|
|
7240
|
+
}
|
|
7241
|
+
return shape;
|
|
7242
|
+
}
|
|
6534
7243
|
|
|
6535
7244
|
// src/pipeline/fetch.ts
|
|
6536
7245
|
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
|
|
@@ -6833,7 +7542,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
6833
7542
|
if (entry.generator === "pixflux") continue;
|
|
6834
7543
|
if (!provider.setTags) return tagged;
|
|
6835
7544
|
try {
|
|
6836
|
-
await provider.setTags(entry.objectId, spec.tags);
|
|
7545
|
+
await provider.setTags(entry.objectId, spec.tags, entry.generator);
|
|
6837
7546
|
tagged++;
|
|
6838
7547
|
} catch (err) {
|
|
6839
7548
|
log2(` tag failed ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -6864,10 +7573,10 @@ function isSha256Hash(value) {
|
|
|
6864
7573
|
function parseCache(value) {
|
|
6865
7574
|
return HashCacheSchema.parse(value);
|
|
6866
7575
|
}
|
|
6867
|
-
async function loadCache(
|
|
6868
|
-
if (!existsSync10(
|
|
7576
|
+
async function loadCache(path44) {
|
|
7577
|
+
if (!existsSync10(path44)) return { version: 1, hashes: {} };
|
|
6869
7578
|
try {
|
|
6870
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(
|
|
7579
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path44, "utf8")));
|
|
6871
7580
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6872
7581
|
return {
|
|
6873
7582
|
version: 1,
|
|
@@ -6879,27 +7588,30 @@ async function loadCache(path43) {
|
|
|
6879
7588
|
return { version: 1, hashes: {} };
|
|
6880
7589
|
}
|
|
6881
7590
|
}
|
|
6882
|
-
async function saveCache(
|
|
7591
|
+
async function saveCache(path44, cache) {
|
|
6883
7592
|
const sorted = {};
|
|
6884
7593
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6885
7594
|
const hash = cache.hashes[key];
|
|
6886
7595
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6887
7596
|
sorted[key] = hash;
|
|
6888
7597
|
}
|
|
6889
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6890
|
-
const tmp = `${
|
|
7598
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path44)), { recursive: true });
|
|
7599
|
+
const tmp = `${path44}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6891
7600
|
try {
|
|
6892
7601
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6893
|
-
await rename4(tmp,
|
|
7602
|
+
await rename4(tmp, path44);
|
|
6894
7603
|
} finally {
|
|
6895
7604
|
await rm5(tmp, { force: true });
|
|
6896
7605
|
}
|
|
6897
7606
|
}
|
|
6898
|
-
function pruneCache(cache, liveIds) {
|
|
7607
|
+
function pruneCache(cache, liveIds, namespace = "") {
|
|
6899
7608
|
let removed = 0;
|
|
6900
|
-
for (const
|
|
7609
|
+
for (const key of Object.keys(cache.hashes)) {
|
|
7610
|
+
const owned = namespace ? key.startsWith(namespace) : !key.includes(":");
|
|
7611
|
+
if (!owned) continue;
|
|
7612
|
+
const id = namespace ? key.slice(namespace.length) : key;
|
|
6901
7613
|
if (!liveIds.has(id)) {
|
|
6902
|
-
delete cache.hashes[
|
|
7614
|
+
delete cache.hashes[key];
|
|
6903
7615
|
removed++;
|
|
6904
7616
|
}
|
|
6905
7617
|
}
|
|
@@ -7033,7 +7745,7 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
7033
7745
|
if (!spec || !entry.objectId) continue;
|
|
7034
7746
|
if (!provider.setTags) return count;
|
|
7035
7747
|
try {
|
|
7036
|
-
await provider.setTags(entry.objectId, spec.tags);
|
|
7748
|
+
await provider.setTags(entry.objectId, spec.tags, entry.generator);
|
|
7037
7749
|
count++;
|
|
7038
7750
|
} catch (err) {
|
|
7039
7751
|
log2(` tag failed ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7077,20 +7789,272 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7077
7789
|
return lines.join("\n");
|
|
7078
7790
|
}
|
|
7079
7791
|
|
|
7080
|
-
// src/pipeline/
|
|
7081
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
7792
|
+
// src/pipeline/adopt-characters.ts
|
|
7082
7793
|
import { existsSync as existsSync12 } from "fs";
|
|
7794
|
+
import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
7083
7795
|
import path15 from "path";
|
|
7796
|
+
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
7797
|
+
const log2 = opts.onProgress ?? (() => {
|
|
7798
|
+
});
|
|
7799
|
+
const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
|
|
7800
|
+
if (!provider.listCharacters || !provider.getCharacter) return result;
|
|
7801
|
+
const wanted = specs.filter((spec) => spec.character);
|
|
7802
|
+
if (!wanted.length) return result;
|
|
7803
|
+
const remote = [];
|
|
7804
|
+
for await (const character of provider.listCharacters()) remote.push(character);
|
|
7805
|
+
result.scanned = remote.length;
|
|
7806
|
+
log2(` found ${remote.length} character(s) on the account`);
|
|
7807
|
+
const cachePath = cachePathFor(lockPath);
|
|
7808
|
+
const cache = opts.noCache ? { version: 1, hashes: {} } : await loadCache(cachePath);
|
|
7809
|
+
const details = /* @__PURE__ */ new Map();
|
|
7810
|
+
const detail = async (id) => {
|
|
7811
|
+
let known = details.get(id);
|
|
7812
|
+
if (!known) {
|
|
7813
|
+
known = await provider.getCharacter(id);
|
|
7814
|
+
details.set(id, known);
|
|
7815
|
+
}
|
|
7816
|
+
return known;
|
|
7817
|
+
};
|
|
7818
|
+
const bytesOf = /* @__PURE__ */ new Map();
|
|
7819
|
+
const download = async (url) => {
|
|
7820
|
+
let bytes = bytesOf.get(url);
|
|
7821
|
+
if (!bytes) {
|
|
7822
|
+
bytes = await provider.download(url);
|
|
7823
|
+
decodePng(bytes);
|
|
7824
|
+
bytesOf.set(url, bytes);
|
|
7825
|
+
}
|
|
7826
|
+
return bytes;
|
|
7827
|
+
};
|
|
7828
|
+
const southHash = async (character) => {
|
|
7829
|
+
const cached = cache.hashes[`character:${character.id}`];
|
|
7830
|
+
if (cached) return cached;
|
|
7831
|
+
if (!character.previewUrl || character.status !== "completed") return null;
|
|
7832
|
+
try {
|
|
7833
|
+
const hash = sha256(await download(character.previewUrl));
|
|
7834
|
+
cache.hashes[`character:${character.id}`] = hash;
|
|
7835
|
+
return hash;
|
|
7836
|
+
} catch {
|
|
7837
|
+
return null;
|
|
7838
|
+
}
|
|
7839
|
+
};
|
|
7840
|
+
const materialize = async (spec, sources) => {
|
|
7841
|
+
const outputs = [];
|
|
7842
|
+
for (const [index, source] of sources.entries()) {
|
|
7843
|
+
const bytes = await download(source.url);
|
|
7844
|
+
const hash = sha256(bytes);
|
|
7845
|
+
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
7846
|
+
if (existsSync12(file)) {
|
|
7847
|
+
if (sha256(await readFile9(file)) !== hash) result.differing.push(path15.relative(process.cwd(), file));
|
|
7848
|
+
} else {
|
|
7849
|
+
await mkdir5(path15.dirname(file), { recursive: true });
|
|
7850
|
+
await writeFile6(file, bytes);
|
|
7851
|
+
result.written.push(path15.relative(process.cwd(), file));
|
|
7852
|
+
}
|
|
7853
|
+
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
7854
|
+
}
|
|
7855
|
+
return outputs;
|
|
7856
|
+
};
|
|
7857
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
7858
|
+
for (const entry of Object.values(lock.entries)) {
|
|
7859
|
+
if (entry.provider === provider.id && entry.generator === "character" && entry.objectId) {
|
|
7860
|
+
claimed.add(entry.objectId.split("#")[0]);
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
let current = specs;
|
|
7864
|
+
for (const kind of ["base", "state", "animation"]) {
|
|
7865
|
+
const wave = current.filter((spec) => spec.character?.kind === kind);
|
|
7866
|
+
let adoptedThisWave = 0;
|
|
7867
|
+
for (const spec of wave) {
|
|
7868
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
7869
|
+
if (lock.entries[key]?.status === "downloaded" && lock.entries[key]?.objectId) continue;
|
|
7870
|
+
try {
|
|
7871
|
+
const adopted = kind === "animation" ? await adoptAnimation(spec, key) : await adoptCharacter(spec, key);
|
|
7872
|
+
if (adopted) adoptedThisWave++;
|
|
7873
|
+
} catch (err) {
|
|
7874
|
+
result.unmatched.push(`${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
if (adoptedThisWave) {
|
|
7878
|
+
await saveLock(lockPath, lock);
|
|
7879
|
+
current = await opts.resolve();
|
|
7880
|
+
}
|
|
7881
|
+
}
|
|
7882
|
+
async function adoptCharacter(spec, key) {
|
|
7883
|
+
let match;
|
|
7884
|
+
if (spec.remoteId) {
|
|
7885
|
+
match = remote.find((c) => c.id === spec.remoteId);
|
|
7886
|
+
if (!match) throw new Error(`remoteId ${spec.remoteId} is not a character on this account`);
|
|
7887
|
+
} else {
|
|
7888
|
+
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
7889
|
+
if (!existsSync12(south)) {
|
|
7890
|
+
result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), south)} to match by`);
|
|
7891
|
+
return false;
|
|
7892
|
+
}
|
|
7893
|
+
const localHash = sha256(await readFile9(south));
|
|
7894
|
+
for (const candidate of remote) {
|
|
7895
|
+
if (claimed.has(candidate.id)) continue;
|
|
7896
|
+
if (await southHash(candidate) === localHash) {
|
|
7897
|
+
match = candidate;
|
|
7898
|
+
break;
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
if (!match) {
|
|
7902
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path15.relative(process.cwd(), south)}`);
|
|
7903
|
+
return false;
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
const full = await detail(match.id);
|
|
7907
|
+
if (!full.rotations.length) throw new Error(`character ${match.id} has no rotations yet (${full.status})`);
|
|
7908
|
+
const outputs = await materialize(spec, full.rotations);
|
|
7909
|
+
const durable = full.rotations.filter((source) => shouldPersistSourceUrl(source.url));
|
|
7910
|
+
upsert(lock, key, {
|
|
7911
|
+
styleId: spec.styleId,
|
|
7912
|
+
assetId: spec.assetId,
|
|
7913
|
+
specHash: spec.specHash,
|
|
7914
|
+
generator: spec.generator,
|
|
7915
|
+
prompt: full.prompt || spec.prompt,
|
|
7916
|
+
width: spec.width,
|
|
7917
|
+
height: spec.height,
|
|
7918
|
+
jobId: full.id,
|
|
7919
|
+
objectId: full.id,
|
|
7920
|
+
reviewObjectId: null,
|
|
7921
|
+
candidateIndex: null,
|
|
7922
|
+
status: "downloaded",
|
|
7923
|
+
error: null,
|
|
7924
|
+
sourceUrl: durable[0]?.url ?? null,
|
|
7925
|
+
sourceUrls: durable,
|
|
7926
|
+
outputs,
|
|
7927
|
+
providerMetadata: {
|
|
7928
|
+
[provider.id]: {
|
|
7929
|
+
character: {
|
|
7930
|
+
characterId: full.id,
|
|
7931
|
+
groupId: full.groupId,
|
|
7932
|
+
name: full.name,
|
|
7933
|
+
stateName: full.stateName,
|
|
7934
|
+
directions: full.directions,
|
|
7935
|
+
size: { width: full.width, height: full.height },
|
|
7936
|
+
adopted: true
|
|
7937
|
+
}
|
|
7938
|
+
}
|
|
7939
|
+
},
|
|
7940
|
+
submittedAt: full.createdAt,
|
|
7941
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7942
|
+
cost: 0,
|
|
7943
|
+
costUnit: "generations",
|
|
7944
|
+
provider: provider.id
|
|
7945
|
+
});
|
|
7946
|
+
claimed.add(full.id);
|
|
7947
|
+
result.matched++;
|
|
7948
|
+
log2(` adopted ${key} \u2190 character ${full.id}${full.stateName && full.stateName !== "Idle" ? ` (${full.stateName})` : ""}`);
|
|
7949
|
+
return true;
|
|
7950
|
+
}
|
|
7951
|
+
async function adoptAnimation(spec, key) {
|
|
7952
|
+
const character = spec.character;
|
|
7953
|
+
const animation = character.animation;
|
|
7954
|
+
const parentKey = lockKey(spec.styleId, character.parentAssetId);
|
|
7955
|
+
const parentId = spec.remoteId?.split("#")[0] ?? lock.entries[parentKey]?.objectId;
|
|
7956
|
+
if (!parentId) {
|
|
7957
|
+
result.unmatched.push(`${key}: its parent ${parentKey} is not adopted yet`);
|
|
7958
|
+
return false;
|
|
7959
|
+
}
|
|
7960
|
+
const full = await detail(parentId);
|
|
7961
|
+
const candidates = full.animations.filter((a) => a.direction === animation.direction && a.frames.length);
|
|
7962
|
+
let match;
|
|
7963
|
+
if (spec.remoteId) {
|
|
7964
|
+
const groupId = spec.remoteId.split("#")[1];
|
|
7965
|
+
match = candidates.find((a) => a.groupId === groupId);
|
|
7966
|
+
if (!match) throw new Error(`remoteId ${spec.remoteId} names no ${animation.direction} animation of character ${parentId}`);
|
|
7967
|
+
} else {
|
|
7968
|
+
const first = expectedOutputPath(spec, "frame-00", 0, 2);
|
|
7969
|
+
if (!existsSync12(first)) {
|
|
7970
|
+
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
7971
|
+
if (!byName) {
|
|
7972
|
+
result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), first)} to match by`);
|
|
7973
|
+
return false;
|
|
7974
|
+
}
|
|
7975
|
+
match = byName;
|
|
7976
|
+
} else {
|
|
7977
|
+
const localHash = sha256(await readFile9(first));
|
|
7978
|
+
for (const candidate of candidates) {
|
|
7979
|
+
if (sha256(await download(candidate.frames[0])) === localHash) {
|
|
7980
|
+
match = candidate;
|
|
7981
|
+
break;
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
if (!match) {
|
|
7985
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path15.relative(process.cwd(), first)}`);
|
|
7986
|
+
return false;
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
const sources = match.frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
7991
|
+
const outputs = await materialize(spec, sources);
|
|
7992
|
+
const durable = sources.filter((source) => shouldPersistSourceUrl(source.url));
|
|
7993
|
+
const animationId = /\/animations\/([^/]+)\//.exec(match.frames[0])?.[1] ?? null;
|
|
7994
|
+
upsert(lock, key, {
|
|
7995
|
+
styleId: spec.styleId,
|
|
7996
|
+
assetId: spec.assetId,
|
|
7997
|
+
specHash: spec.specHash,
|
|
7998
|
+
generator: spec.generator,
|
|
7999
|
+
prompt: spec.prompt,
|
|
8000
|
+
width: spec.width,
|
|
8001
|
+
height: spec.height,
|
|
8002
|
+
jobId: `${parentId}#${match.groupId ?? animationId ?? match.type}`,
|
|
8003
|
+
objectId: `${parentId}#${match.groupId ?? animationId ?? match.type}`,
|
|
8004
|
+
reviewObjectId: null,
|
|
8005
|
+
candidateIndex: null,
|
|
8006
|
+
status: "downloaded",
|
|
8007
|
+
error: null,
|
|
8008
|
+
sourceUrl: durable[0]?.url ?? null,
|
|
8009
|
+
sourceUrls: durable,
|
|
8010
|
+
outputs,
|
|
8011
|
+
providerMetadata: {
|
|
8012
|
+
[provider.id]: {
|
|
8013
|
+
frameSet: { fps: animation.fps, count: sources.length },
|
|
8014
|
+
character: {
|
|
8015
|
+
kind: "animation",
|
|
8016
|
+
characterId: parentId,
|
|
8017
|
+
animationId,
|
|
8018
|
+
animationGroupId: match.groupId,
|
|
8019
|
+
animationName: match.name,
|
|
8020
|
+
direction: animation.direction,
|
|
8021
|
+
adopted: true
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
},
|
|
8025
|
+
submittedAt: full.createdAt,
|
|
8026
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8027
|
+
cost: 0,
|
|
8028
|
+
costUnit: "generations",
|
|
8029
|
+
provider: provider.id
|
|
8030
|
+
});
|
|
8031
|
+
result.matched++;
|
|
8032
|
+
log2(` adopted ${key} \u2190 ${animation.direction} animation ${match.groupId ?? animationId ?? match.type} of ${parentId}`);
|
|
8033
|
+
return true;
|
|
8034
|
+
}
|
|
8035
|
+
await saveLock(lockPath, lock);
|
|
8036
|
+
if (!opts.noCache) {
|
|
8037
|
+
pruneCache(cache, new Set(remote.map((character) => character.id)), "character:");
|
|
8038
|
+
await saveCache(cachePath, cache);
|
|
8039
|
+
}
|
|
8040
|
+
result.remaining = remote.filter((character) => !claimed.has(character.id));
|
|
8041
|
+
return result;
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
// src/pipeline/salvage.ts
|
|
8045
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
8046
|
+
import { existsSync as existsSync13 } from "fs";
|
|
8047
|
+
import path16 from "path";
|
|
7084
8048
|
function providerClaimId(provider, objectId) {
|
|
7085
8049
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
7086
8050
|
}
|
|
7087
8051
|
async function loadClaims(lockPaths, opts = {}) {
|
|
7088
8052
|
const claimed = /* @__PURE__ */ new Set();
|
|
7089
8053
|
for (const p of lockPaths) {
|
|
7090
|
-
if (!
|
|
8054
|
+
if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
7091
8055
|
let parsed;
|
|
7092
8056
|
try {
|
|
7093
|
-
parsed = parseLock(JSON.parse(await
|
|
8057
|
+
parsed = parseLock(JSON.parse(await readFile10(p, "utf8")));
|
|
7094
8058
|
} catch {
|
|
7095
8059
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
7096
8060
|
}
|
|
@@ -7138,20 +8102,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
7138
8102
|
return null;
|
|
7139
8103
|
}
|
|
7140
8104
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
7141
|
-
const own =
|
|
8105
|
+
const own = path16.resolve(ownManifestPath);
|
|
7142
8106
|
const siblingManifestPaths = [
|
|
7143
8107
|
.../* @__PURE__ */ new Set([
|
|
7144
8108
|
...workspaceManifestPaths,
|
|
7145
|
-
...claimPaths.map((c) =>
|
|
8109
|
+
...claimPaths.map((c) => path16.join(path16.dirname(path16.resolve(c)), "pixelkiln.manifest.json"))
|
|
7146
8110
|
])
|
|
7147
8111
|
];
|
|
7148
8112
|
const siblings = [];
|
|
7149
8113
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
7150
|
-
if (
|
|
7151
|
-
if (!
|
|
8114
|
+
if (path16.resolve(siblingManifestPath) === own) continue;
|
|
8115
|
+
if (!existsSync13(siblingManifestPath)) continue;
|
|
7152
8116
|
try {
|
|
7153
8117
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
7154
|
-
siblings.push({ label:
|
|
8118
|
+
siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest });
|
|
7155
8119
|
} catch {
|
|
7156
8120
|
}
|
|
7157
8121
|
}
|
|
@@ -7267,9 +8231,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
7267
8231
|
}
|
|
7268
8232
|
|
|
7269
8233
|
// src/workspace.ts
|
|
7270
|
-
import { mkdir as
|
|
7271
|
-
import { existsSync as
|
|
7272
|
-
import
|
|
8234
|
+
import { mkdir as mkdir6, readFile as readFile11, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
8235
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8236
|
+
import path17 from "path";
|
|
7273
8237
|
import { z as z4 } from "zod";
|
|
7274
8238
|
var WorkspaceProjectSchema = z4.object({
|
|
7275
8239
|
id: z4.string().min(1),
|
|
@@ -7294,10 +8258,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
7294
8258
|
);
|
|
7295
8259
|
}
|
|
7296
8260
|
async function loadWorkspace(workspacePath) {
|
|
7297
|
-
if (!
|
|
8261
|
+
if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
|
|
7298
8262
|
let raw;
|
|
7299
8263
|
try {
|
|
7300
|
-
raw = JSON.parse(await
|
|
8264
|
+
raw = JSON.parse(await readFile11(workspacePath, "utf8"));
|
|
7301
8265
|
} catch (err) {
|
|
7302
8266
|
throw new ProjectError(
|
|
7303
8267
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -7311,22 +8275,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7311
8275
|
version: 1,
|
|
7312
8276
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
7313
8277
|
};
|
|
7314
|
-
await
|
|
8278
|
+
await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
|
|
7315
8279
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7316
8280
|
try {
|
|
7317
|
-
await
|
|
8281
|
+
await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
7318
8282
|
await rename5(tmp, workspacePath);
|
|
7319
8283
|
} finally {
|
|
7320
8284
|
await rm6(tmp, { force: true });
|
|
7321
8285
|
}
|
|
7322
8286
|
}
|
|
7323
8287
|
function toPortablePath(dir, absolute) {
|
|
7324
|
-
return
|
|
8288
|
+
return path17.relative(dir, absolute).split(path17.sep).join("/");
|
|
7325
8289
|
}
|
|
7326
8290
|
function resolveProject(dir, project) {
|
|
7327
8291
|
return {
|
|
7328
|
-
manifestPath:
|
|
7329
|
-
lockPath:
|
|
8292
|
+
manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
|
|
8293
|
+
lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
|
|
7330
8294
|
};
|
|
7331
8295
|
}
|
|
7332
8296
|
function validateWorkspace(ws, dir) {
|
|
@@ -7339,21 +8303,21 @@ function validateWorkspace(ws, dir) {
|
|
|
7339
8303
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
7340
8304
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
7341
8305
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
7342
|
-
if (
|
|
8306
|
+
if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
|
|
7343
8307
|
diagnostics.push({
|
|
7344
8308
|
id: "absolute-path",
|
|
7345
8309
|
level: "warning",
|
|
7346
|
-
message: `project "${project.id}" stores an absolute path
|
|
8310
|
+
message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
7347
8311
|
});
|
|
7348
8312
|
}
|
|
7349
|
-
if (!
|
|
8313
|
+
if (!existsSync14(manifestPath)) {
|
|
7350
8314
|
diagnostics.push({
|
|
7351
8315
|
id: "missing-manifest",
|
|
7352
8316
|
level: "error",
|
|
7353
8317
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
7354
8318
|
});
|
|
7355
8319
|
}
|
|
7356
|
-
if (!
|
|
8320
|
+
if (!existsSync14(lockPath)) {
|
|
7357
8321
|
diagnostics.push({
|
|
7358
8322
|
id: "missing-lock",
|
|
7359
8323
|
level: "error",
|
|
@@ -7384,7 +8348,7 @@ function validateWorkspace(ws, dir) {
|
|
|
7384
8348
|
diagnostics.push({
|
|
7385
8349
|
id: "duplicate-manifest",
|
|
7386
8350
|
level: "warning",
|
|
7387
|
-
message: `${ids.join(", ")} share manifest ${manifestPath}
|
|
8351
|
+
message: `${ids.join(", ")} share manifest ${manifestPath}; expected only when they are variant lockfiles beside one manifest`
|
|
7388
8352
|
});
|
|
7389
8353
|
}
|
|
7390
8354
|
}
|
|
@@ -7393,15 +8357,15 @@ function validateWorkspace(ws, dir) {
|
|
|
7393
8357
|
diagnostics.push({
|
|
7394
8358
|
id: "mixed-provider",
|
|
7395
8359
|
level: "warning",
|
|
7396
|
-
message: `registered
|
|
8360
|
+
message: `registered providers differ (${[...providers].sort().join(", ")}); spend totals are kept separate per unit, but confirm this is intentional`
|
|
7397
8361
|
});
|
|
7398
8362
|
}
|
|
7399
8363
|
return diagnostics;
|
|
7400
8364
|
}
|
|
7401
8365
|
|
|
7402
8366
|
// src/pick/salvage-server.ts
|
|
7403
|
-
import { mkdir as
|
|
7404
|
-
import
|
|
8367
|
+
import { mkdir as mkdir7, writeFile as writeFile8, readFile as readFile13 } from "fs/promises";
|
|
8368
|
+
import path18 from "path";
|
|
7405
8369
|
|
|
7406
8370
|
// src/pick/salvage-sheet.ts
|
|
7407
8371
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -7414,7 +8378,7 @@ function renderSalvageSheet(orphans, ctx) {
|
|
|
7414
8378
|
<html lang="en">
|
|
7415
8379
|
<head>
|
|
7416
8380
|
<meta charset="utf-8">
|
|
7417
|
-
<title
|
|
8381
|
+
<title>${styleId ? `${styleId} | ` : ""}pixelkiln salvage</title>
|
|
7418
8382
|
<style>
|
|
7419
8383
|
:root { --bg:#0f1115; --panel:#171a21; --line:#262b36; --text:#e6e8ee; --dim:#8b93a7;
|
|
7420
8384
|
--import:#6ee7a8; --keep:#7cc4ff; --discard:#f87171; }
|
|
@@ -7465,7 +8429,7 @@ function renderSalvageSheet(orphans, ctx) {
|
|
|
7465
8429
|
</head>
|
|
7466
8430
|
<body>
|
|
7467
8431
|
<header>
|
|
7468
|
-
<h1>pixelkiln salvage${styleId ? ` <span class="ctx"
|
|
8432
|
+
<h1>pixelkiln salvage${styleId ? ` <span class="ctx">${styleId}<span class="dest">, imports to ${importDir}/_salvaged/</span></span>` : ""}</h1>
|
|
7469
8433
|
<span class="tally"><b id="ti">0</b> import \xB7 <b id="tk">0</b> keep \xB7 <b id="td">0</b> discard \xB7
|
|
7470
8434
|
<b id="tu">0</b> undecided</span>
|
|
7471
8435
|
<span style="flex:1"></span>
|
|
@@ -7476,7 +8440,7 @@ function renderSalvageSheet(orphans, ctx) {
|
|
|
7476
8440
|
<main id="grid"></main>
|
|
7477
8441
|
<footer>
|
|
7478
8442
|
<b>import</b> downloads it and adds it to your manifest \xB7 <b>keep</b> tags it and leaves it \xB7
|
|
7479
|
-
<b>discard</b> only tags it
|
|
8443
|
+
<b>discard</b> only tags it; nothing is deleted here.
|
|
7480
8444
|
Hover a card and press <kbd>i</kbd> / <kbd>k</kbd> / <kbd>d</kbd>, or <kbd>u</kbd> to undo.
|
|
7481
8445
|
</footer>
|
|
7482
8446
|
<script>
|
|
@@ -7547,7 +8511,7 @@ document.getElementById('submit').onclick = async () => {
|
|
|
7547
8511
|
body: JSON.stringify({ decisions }),
|
|
7548
8512
|
});
|
|
7549
8513
|
if (!res.ok) throw new Error(await res.text());
|
|
7550
|
-
btn.textContent = 'done
|
|
8514
|
+
btn.textContent = 'done, close this tab';
|
|
7551
8515
|
document.body.style.opacity = '.6';
|
|
7552
8516
|
} catch (err) {
|
|
7553
8517
|
btn.textContent = 'failed: ' + err.message; btn.disabled = false;
|
|
@@ -7561,7 +8525,7 @@ refresh();
|
|
|
7561
8525
|
|
|
7562
8526
|
// src/pick/review-server.ts
|
|
7563
8527
|
import { createServer } from "http";
|
|
7564
|
-
import { readFile as
|
|
8528
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
7565
8529
|
|
|
7566
8530
|
// src/open.ts
|
|
7567
8531
|
import { spawn } from "child_process";
|
|
@@ -7588,7 +8552,7 @@ function serveReviewPage(opts) {
|
|
|
7588
8552
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
7589
8553
|
try {
|
|
7590
8554
|
const asset = opts.assets.get(req.url);
|
|
7591
|
-
const bytes = await
|
|
8555
|
+
const bytes = await readFile12(asset.path);
|
|
7592
8556
|
res.writeHead(200, {
|
|
7593
8557
|
"Content-Type": asset.contentType,
|
|
7594
8558
|
"Cache-Control": "no-store",
|
|
@@ -7665,7 +8629,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7665
8629
|
});
|
|
7666
8630
|
const html = renderSalvageSheet(orphans, {
|
|
7667
8631
|
styleId: ctx.styleId,
|
|
7668
|
-
importDir:
|
|
8632
|
+
importDir: path18.relative(process.cwd(), ctx.importDir) || "."
|
|
7669
8633
|
});
|
|
7670
8634
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
7671
8635
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -7677,7 +8641,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7677
8641
|
onReady: (url) => {
|
|
7678
8642
|
log2(`
|
|
7679
8643
|
${orphans.length} unclaimed object(s) to triage: ${url}`);
|
|
7680
|
-
log2(` (nothing is deleted here
|
|
8644
|
+
log2(` (nothing is deleted here; discard only tags)
|
|
7681
8645
|
`);
|
|
7682
8646
|
},
|
|
7683
8647
|
handleApply: async (body) => {
|
|
@@ -7694,10 +8658,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7694
8658
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
7695
8659
|
decodePng(buf);
|
|
7696
8660
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
7697
|
-
const rel =
|
|
7698
|
-
const outFile =
|
|
7699
|
-
await
|
|
7700
|
-
await
|
|
8661
|
+
const rel = path18.join("_salvaged", `${assetId}.png`);
|
|
8662
|
+
const outFile = path18.resolve(ctx.importDir, rel);
|
|
8663
|
+
await mkdir7(path18.dirname(outFile), { recursive: true });
|
|
8664
|
+
await writeFile8(outFile, buf);
|
|
7701
8665
|
ctx.manifest.assets[assetId] = {
|
|
7702
8666
|
prompt: orphan.prompt,
|
|
7703
8667
|
promptByStyle: {},
|
|
@@ -7729,7 +8693,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7729
8693
|
sourceUrl: durableSource,
|
|
7730
8694
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
7731
8695
|
outputs: [{
|
|
7732
|
-
path: portableOutputPath(outFile,
|
|
8696
|
+
path: portableOutputPath(outFile, path18.dirname(ctx.manifestPath)),
|
|
7733
8697
|
sha256: sha256(buf)
|
|
7734
8698
|
}],
|
|
7735
8699
|
submittedAt: orphan.createdAt,
|
|
@@ -7753,9 +8717,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7753
8717
|
}
|
|
7754
8718
|
}
|
|
7755
8719
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
7756
|
-
const raw = JSON.parse(await
|
|
8720
|
+
const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
|
|
7757
8721
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
7758
|
-
await
|
|
8722
|
+
await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7759
8723
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
7760
8724
|
return result;
|
|
7761
8725
|
}
|
|
@@ -7763,19 +8727,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7763
8727
|
}
|
|
7764
8728
|
|
|
7765
8729
|
// src/cli/project.ts
|
|
7766
|
-
import
|
|
7767
|
-
import { existsSync as
|
|
8730
|
+
import path20 from "path";
|
|
8731
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7768
8732
|
|
|
7769
8733
|
// src/project.ts
|
|
7770
|
-
import
|
|
8734
|
+
import path19 from "path";
|
|
7771
8735
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
7772
|
-
const absolute =
|
|
7773
|
-
const root =
|
|
8736
|
+
const absolute = path19.resolve(manifestPath);
|
|
8737
|
+
const root = path19.dirname(absolute);
|
|
7774
8738
|
if (opts.env !== false) {
|
|
7775
8739
|
loadEnvFiles(root);
|
|
7776
|
-
if (
|
|
8740
|
+
if (path19.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
7777
8741
|
}
|
|
7778
|
-
const lockPath =
|
|
8742
|
+
const lockPath = path19.resolve(opts.lockPath ?? path19.join(root, "pixelkiln.lock.json"));
|
|
7779
8743
|
const loaded = await loadManifest(absolute);
|
|
7780
8744
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
7781
8745
|
const lock = await loadLock(lockPath);
|
|
@@ -7906,11 +8870,11 @@ async function workspaceStatus(ws, dir) {
|
|
|
7906
8870
|
|
|
7907
8871
|
// src/cli/project.ts
|
|
7908
8872
|
async function provenanceFile(id, file) {
|
|
7909
|
-
const absolute =
|
|
8873
|
+
const absolute = path20.resolve(file);
|
|
7910
8874
|
return {
|
|
7911
8875
|
id,
|
|
7912
8876
|
path: absolute,
|
|
7913
|
-
sha256:
|
|
8877
|
+
sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
|
|
7914
8878
|
included: true
|
|
7915
8879
|
};
|
|
7916
8880
|
}
|
|
@@ -7982,10 +8946,10 @@ function budgetsForPlan(plan, args) {
|
|
|
7982
8946
|
return out;
|
|
7983
8947
|
}
|
|
7984
8948
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
7985
|
-
if (!
|
|
8949
|
+
if (!existsSync15(workspacePath)) {
|
|
7986
8950
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
7987
8951
|
}
|
|
7988
|
-
const dir =
|
|
8952
|
+
const dir = path20.dirname(path20.resolve(workspacePath));
|
|
7989
8953
|
const ws = await loadWorkspace(workspacePath);
|
|
7990
8954
|
const diagnostics = validateWorkspace(ws, dir);
|
|
7991
8955
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -8008,9 +8972,9 @@ async function openAccountProject(args) {
|
|
|
8008
8972
|
return project;
|
|
8009
8973
|
}
|
|
8010
8974
|
async function open(args) {
|
|
8011
|
-
if (!
|
|
8975
|
+
if (!existsSync15(path20.resolve(args.manifest))) {
|
|
8012
8976
|
throw new ProjectError(
|
|
8013
|
-
`No manifest at ${
|
|
8977
|
+
`No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
8014
8978
|
);
|
|
8015
8979
|
}
|
|
8016
8980
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -8052,7 +9016,7 @@ function providerCache(mode2) {
|
|
|
8052
9016
|
|
|
8053
9017
|
// src/cli/commands/account.ts
|
|
8054
9018
|
async function runBalance(args) {
|
|
8055
|
-
loadEnvFiles(
|
|
9019
|
+
loadEnvFiles(path21.dirname(path21.resolve(args.manifest)));
|
|
8056
9020
|
loadEnvFiles(process.cwd());
|
|
8057
9021
|
const loaded = await loadManifest(args.manifest);
|
|
8058
9022
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -8063,14 +9027,40 @@ async function runBalance(args) {
|
|
|
8063
9027
|
log(` remaining: ${formatCost(b.unit, b.remaining)}${b.total ? ` of ${b.total}` : ""}`);
|
|
8064
9028
|
}
|
|
8065
9029
|
async function runAdopt(args) {
|
|
8066
|
-
const { specs, lock, accountProvider } = await openAccountProject(args);
|
|
9030
|
+
const { loaded, specs, lock, accountProvider } = await openAccountProject(args);
|
|
8067
9031
|
const providerFor = providerCache("online");
|
|
8068
9032
|
const provider = providerFor(accountProvider);
|
|
8069
9033
|
log(`
|
|
8070
9034
|
Reconciling account objects against files already on disk\u2026`);
|
|
8071
|
-
const res = await adopt(provider, specs, lock, args.lock, { onProgress: log });
|
|
9035
|
+
const res = await adopt(provider, specs.filter((spec) => spec.generator !== "character"), lock, args.lock, { onProgress: log });
|
|
8072
9036
|
log(`
|
|
8073
9037
|
scanned ${res.scanned} remote object(s), adopted ${res.matched}`);
|
|
9038
|
+
if (specs.some((spec) => spec.character) && provider.listCharacters) {
|
|
9039
|
+
log(`
|
|
9040
|
+
Reconciling account characters against the manifest's character assets\u2026`);
|
|
9041
|
+
const characters = await adoptCharacters(provider, specs, lock, args.lock, {
|
|
9042
|
+
onProgress: log,
|
|
9043
|
+
resolve: async () => (await resolveSpecs(loaded, { styles: args.styles, assets: args.assets })).filter((spec) => spec.provider === accountProvider)
|
|
9044
|
+
});
|
|
9045
|
+
log(`
|
|
9046
|
+
scanned ${characters.scanned} character(s), adopted ${characters.matched}`);
|
|
9047
|
+
if (characters.written.length) log(` wrote ${characters.written.length} file(s) that were not on disk`);
|
|
9048
|
+
for (const file of characters.differing) log(` differs from the account, left alone: ${file}`);
|
|
9049
|
+
if (characters.unmatched.length) {
|
|
9050
|
+
log(`
|
|
9051
|
+
character assets not matched (${characters.unmatched.length}):`);
|
|
9052
|
+
for (const line of characters.unmatched.slice(0, 15)) log(` ${line}`);
|
|
9053
|
+
}
|
|
9054
|
+
if (characters.remaining.length) {
|
|
9055
|
+
log(`
|
|
9056
|
+
characters on the account no entry claims (${characters.remaining.length}):`);
|
|
9057
|
+
for (const c of characters.remaining.slice(0, 15)) {
|
|
9058
|
+
log(` ${c.id} ${c.name}${c.stateName && c.stateName !== "Idle" ? ` / ${c.stateName}` : ""} ${c.directions}dir ${c.width}x${c.height} ${c.animationCount} animation(s)`);
|
|
9059
|
+
}
|
|
9060
|
+
if (characters.remaining.length > 15) log(` \u2026 and ${characters.remaining.length - 15} more`);
|
|
9061
|
+
log(` Declare one as a character asset with "remoteId" and run adopt again to bring it in.`);
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
8074
9064
|
if (res.ambiguous.length) {
|
|
8075
9065
|
log(`
|
|
8076
9066
|
ambiguous (${res.ambiguous.length}):`);
|
|
@@ -8089,7 +9079,7 @@ async function runAdopt(args) {
|
|
|
8089
9079
|
`
|
|
8090
9080
|
One account is shared across projects, so this list mixes discarded
|
|
8091
9081
|
candidates with assets belonging to other manifests. Do not bulk-delete
|
|
8092
|
-
it
|
|
9082
|
+
it. Run adopt from each project first, then \`pixelkiln salvage\` to
|
|
8093
9083
|
triage what is left over.`
|
|
8094
9084
|
);
|
|
8095
9085
|
}
|
|
@@ -8099,12 +9089,12 @@ async function runAdopt(args) {
|
|
|
8099
9089
|
tagged ${n} object(s) upstream`);
|
|
8100
9090
|
}
|
|
8101
9091
|
if (args.writePrompts) {
|
|
8102
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
9092
|
+
const { filled, stillEmpty } = await writePromptsBack(path21.resolve(args.manifest), lock, {
|
|
8103
9093
|
onProgress: log,
|
|
8104
9094
|
provider: provider.id,
|
|
8105
9095
|
assetIds: specs.map((spec) => spec.assetId)
|
|
8106
9096
|
});
|
|
8107
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
9097
|
+
log(` recovered ${filled} prompt(s) into ${path21.relative(process.cwd(), args.manifest)}`);
|
|
8108
9098
|
const reloaded = await loadManifest(args.manifest);
|
|
8109
9099
|
const rebased = (await resolveSpecs(reloaded, {
|
|
8110
9100
|
styles: args.styles,
|
|
@@ -8134,12 +9124,12 @@ async function runSalvage2(args) {
|
|
|
8134
9124
|
const provider = providerFor(accountProvider);
|
|
8135
9125
|
const jsonMode = args.dryRun && args.json;
|
|
8136
9126
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
8137
|
-
const ownLock =
|
|
9127
|
+
const ownLock = path21.resolve(args.lock);
|
|
8138
9128
|
let workspaceProjects = [];
|
|
8139
9129
|
let workspaceDir = "";
|
|
8140
9130
|
if (args.workspace) {
|
|
8141
|
-
const workspacePath =
|
|
8142
|
-
workspaceDir =
|
|
9131
|
+
const workspacePath = path21.resolve(args.workspace);
|
|
9132
|
+
workspaceDir = path21.dirname(workspacePath);
|
|
8143
9133
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
8144
9134
|
workspaceProjects = complete.ws.projects;
|
|
8145
9135
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -8148,12 +9138,12 @@ async function runSalvage2(args) {
|
|
|
8148
9138
|
const lockPaths = [
|
|
8149
9139
|
.../* @__PURE__ */ new Set([
|
|
8150
9140
|
...workspaceLockPaths,
|
|
8151
|
-
...
|
|
8152
|
-
...args.claims.map((c) =>
|
|
9141
|
+
...existsSync16(ownLock) ? [ownLock] : [],
|
|
9142
|
+
...args.claims.map((c) => path21.resolve(c))
|
|
8153
9143
|
])
|
|
8154
9144
|
];
|
|
8155
9145
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
8156
|
-
for (const p of lockPaths) diag(` ${
|
|
9146
|
+
for (const p of lockPaths) diag(` ${path21.relative(process.cwd(), p)}`);
|
|
8157
9147
|
if (!args.claims.length && !args.workspace) {
|
|
8158
9148
|
diag(
|
|
8159
9149
|
`
|
|
@@ -8161,11 +9151,11 @@ async function runSalvage2(args) {
|
|
|
8161
9151
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
8162
9152
|
register every project in a workspace catalog and pass --workspace.`
|
|
8163
9153
|
);
|
|
8164
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
9154
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path21.resolve(args.manifest))) {
|
|
8165
9155
|
diag(
|
|
8166
9156
|
`
|
|
8167
9157
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
8168
|
-
lockfile is still included above, so this run's claim set is complete
|
|
9158
|
+
lockfile is still included above, so this run's claim set is complete, but
|
|
8169
9159
|
\`pixelkiln workspace add ${args.manifest}\` would keep it aggregated too.`
|
|
8170
9160
|
);
|
|
8171
9161
|
}
|
|
@@ -8195,13 +9185,13 @@ async function runSalvage2(args) {
|
|
|
8195
9185
|
diag(`
|
|
8196
9186
|
matched a sibling project's own style instead of this one:`);
|
|
8197
9187
|
for (const [label, list] of elsewhere) diag(` ${label.padEnd(28)} ${list.length}`);
|
|
8198
|
-
diag(` excluded from every session below
|
|
9188
|
+
diag(` excluded from every session below; not this project's art.`);
|
|
8199
9189
|
}
|
|
8200
9190
|
if (unmatched.length) {
|
|
8201
9191
|
diag(
|
|
8202
9192
|
`
|
|
8203
9193
|
${unmatched.length} object(s) don't match any known style pattern.
|
|
8204
|
-
If the account is shared, they may belong to a different project
|
|
9194
|
+
If the account is shared, they may belong to a different project. Check
|
|
8205
9195
|
you've passed every sibling project's lockfile via --claims. They're left
|
|
8206
9196
|
out of the sessions below; force them into one style with --style <id>.`
|
|
8207
9197
|
);
|
|
@@ -8245,7 +9235,7 @@ async function runSalvage2(args) {
|
|
|
8245
9235
|
manifestPath: loaded.path,
|
|
8246
9236
|
manifest: accountManifest,
|
|
8247
9237
|
styleId,
|
|
8248
|
-
importDir:
|
|
9238
|
+
importDir: path21.resolve(loaded.root, style.outDir),
|
|
8249
9239
|
lock,
|
|
8250
9240
|
lockPath: args.lock
|
|
8251
9241
|
},
|
|
@@ -8269,7 +9259,7 @@ async function runSalvage2(args) {
|
|
|
8269
9259
|
}
|
|
8270
9260
|
if (!matched.size) {
|
|
8271
9261
|
log(`
|
|
8272
|
-
nothing matched a known style
|
|
9262
|
+
nothing matched a known style; nothing to triage`);
|
|
8273
9263
|
return;
|
|
8274
9264
|
}
|
|
8275
9265
|
log(`
|
|
@@ -8277,7 +9267,7 @@ async function runSalvage2(args) {
|
|
|
8277
9267
|
let totalDiscarded = 0;
|
|
8278
9268
|
for (const [styleId, list] of matched) {
|
|
8279
9269
|
log(`
|
|
8280
|
-
|
|
9270
|
+
${styleId} (${list.length})`);
|
|
8281
9271
|
const res = await runOne(styleId, list);
|
|
8282
9272
|
totalDiscarded += res.discarded;
|
|
8283
9273
|
}
|
|
@@ -8299,7 +9289,7 @@ async function runPurge(args) {
|
|
|
8299
9289
|
}
|
|
8300
9290
|
}
|
|
8301
9291
|
if (!doomed.length) {
|
|
8302
|
-
log(` nothing tagged pixelkiln:discard
|
|
9292
|
+
log(` nothing tagged pixelkiln:discard; run \`pixelkiln salvage\` first`);
|
|
8303
9293
|
return;
|
|
8304
9294
|
}
|
|
8305
9295
|
log(`
|
|
@@ -8346,17 +9336,17 @@ async function runTag(args) {
|
|
|
8346
9336
|
}
|
|
8347
9337
|
|
|
8348
9338
|
// src/cli/commands/edit.ts
|
|
8349
|
-
import
|
|
9339
|
+
import path24 from "path";
|
|
8350
9340
|
|
|
8351
9341
|
// src/pipeline/hand-edit.ts
|
|
8352
|
-
import { existsSync as
|
|
8353
|
-
import { copyFile, mkdir as
|
|
8354
|
-
import
|
|
9342
|
+
import { existsSync as existsSync17 } from "fs";
|
|
9343
|
+
import { copyFile, mkdir as mkdir8, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
|
|
9344
|
+
import path23 from "path";
|
|
8355
9345
|
import { z as z6 } from "zod";
|
|
8356
9346
|
|
|
8357
9347
|
// src/manifest-edit.ts
|
|
8358
|
-
import { readFile as
|
|
8359
|
-
import
|
|
9348
|
+
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
|
|
9349
|
+
import path22 from "path";
|
|
8360
9350
|
import { z as z5 } from "zod";
|
|
8361
9351
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
8362
9352
|
var AssetPatchSchema = z5.object({
|
|
@@ -8571,8 +9561,8 @@ function applyEdit(raw, edit) {
|
|
|
8571
9561
|
}
|
|
8572
9562
|
}
|
|
8573
9563
|
async function applyManifestEdit(manifestPath, edit) {
|
|
8574
|
-
const absolute =
|
|
8575
|
-
const original = await
|
|
9564
|
+
const absolute = path22.resolve(manifestPath);
|
|
9565
|
+
const original = await readFile14(absolute, "utf8");
|
|
8576
9566
|
const before = sha256(original);
|
|
8577
9567
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
8578
9568
|
let raw;
|
|
@@ -8586,12 +9576,12 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8586
9576
|
applyEdit(raw, edit);
|
|
8587
9577
|
const next = serializeLike(original, raw);
|
|
8588
9578
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
8589
|
-
const tmp =
|
|
8590
|
-
|
|
8591
|
-
`.${
|
|
9579
|
+
const tmp = path22.join(
|
|
9580
|
+
path22.dirname(absolute),
|
|
9581
|
+
`.${path22.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
8592
9582
|
);
|
|
8593
9583
|
try {
|
|
8594
|
-
await
|
|
9584
|
+
await writeFile9(tmp, next);
|
|
8595
9585
|
try {
|
|
8596
9586
|
const loaded = await loadManifest(tmp);
|
|
8597
9587
|
await resolveSpecs(loaded);
|
|
@@ -8610,11 +9600,11 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8610
9600
|
var HAND_EDIT_DIR = "edits";
|
|
8611
9601
|
function handEditPath(loaded, spec) {
|
|
8612
9602
|
const style = loaded.manifest.styles[spec.styleId];
|
|
8613
|
-
const outDir =
|
|
8614
|
-
const relative =
|
|
8615
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
8616
|
-
const file = inside ? relative :
|
|
8617
|
-
return
|
|
9603
|
+
const outDir = path23.resolve(loaded.root, style?.outDir ?? ".");
|
|
9604
|
+
const relative = path23.relative(outDir, spec.outFile);
|
|
9605
|
+
const inside = relative && !relative.startsWith("..") && !path23.isAbsolute(relative);
|
|
9606
|
+
const file = inside ? relative : path23.basename(spec.outFile);
|
|
9607
|
+
return path23.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8618
9608
|
}
|
|
8619
9609
|
function handEditBases(spec, lock) {
|
|
8620
9610
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8627,10 +9617,10 @@ function handEditBases(spec, lock) {
|
|
|
8627
9617
|
}));
|
|
8628
9618
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
8629
9619
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
8630
|
-
if (!members.every((member) =>
|
|
9620
|
+
if (!members.every((member) => existsSync17(member.path))) return null;
|
|
8631
9621
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
8632
9622
|
}
|
|
8633
|
-
return
|
|
9623
|
+
return existsSync17(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
8634
9624
|
}
|
|
8635
9625
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
8636
9626
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8656,18 +9646,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
8656
9646
|
`${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`
|
|
8657
9647
|
);
|
|
8658
9648
|
}
|
|
8659
|
-
const existing = spec.source ?
|
|
9649
|
+
const existing = spec.source ? path23.resolve(loaded.root, spec.source) : null;
|
|
8660
9650
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8661
9651
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
8662
9652
|
let created = false;
|
|
8663
9653
|
for (const member of members) {
|
|
8664
|
-
if (
|
|
9654
|
+
if (existsSync17(member.path)) continue;
|
|
8665
9655
|
if (!member.base) {
|
|
8666
9656
|
throw new ManifestEditError(
|
|
8667
9657
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8668
9658
|
);
|
|
8669
9659
|
}
|
|
8670
|
-
await
|
|
9660
|
+
await mkdir8(path23.dirname(member.path), { recursive: true });
|
|
8671
9661
|
await copyFile(member.base, member.path);
|
|
8672
9662
|
created = true;
|
|
8673
9663
|
}
|
|
@@ -8746,7 +9736,7 @@ function handEditProjectPath(editPath) {
|
|
|
8746
9736
|
async function readHandEditCompanion(editPath) {
|
|
8747
9737
|
let text;
|
|
8748
9738
|
try {
|
|
8749
|
-
text = await
|
|
9739
|
+
text = await readFile15(handEditCompanionPath(editPath), "utf8");
|
|
8750
9740
|
} catch {
|
|
8751
9741
|
return null;
|
|
8752
9742
|
}
|
|
@@ -8770,11 +9760,11 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8770
9760
|
const got = incoming.map((frame) => frame.role);
|
|
8771
9761
|
if (got.length !== expected.length || expected.some((role, index) => got[index] !== role)) {
|
|
8772
9762
|
throw new ManifestEditError(
|
|
8773
|
-
`${spec.styleId}/${spec.assetId} is a set of ${expected.length} members (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")})
|
|
9763
|
+
`${spec.styleId}/${spec.assetId} is a set of ${expected.length} members (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")}); keep the frames it opened with`
|
|
8774
9764
|
);
|
|
8775
9765
|
}
|
|
8776
9766
|
} else if (incoming.length !== 1) {
|
|
8777
|
-
throw new ManifestEditError(`${spec.styleId}/${spec.assetId} is one image; the editor returned ${incoming.length} frames
|
|
9767
|
+
throw new ManifestEditError(`${spec.styleId}/${spec.assetId} is one image; the editor returned ${incoming.length} frames; keep a single frame`);
|
|
8778
9768
|
}
|
|
8779
9769
|
const decoded = incoming.map((frame, index) => {
|
|
8780
9770
|
if (frame.png.length > MAX_HAND_EDIT_BYTES) {
|
|
@@ -8789,7 +9779,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8789
9779
|
}
|
|
8790
9780
|
});
|
|
8791
9781
|
const bases = handEditBases(spec, lock);
|
|
8792
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
9782
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
8793
9783
|
for (const [index, image] of decoded.entries()) {
|
|
8794
9784
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
8795
9785
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -8805,7 +9795,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8805
9795
|
editor: input.editor,
|
|
8806
9796
|
protocol: input.protocol,
|
|
8807
9797
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8808
|
-
project: projectPath ?
|
|
9798
|
+
project: projectPath ? path23.basename(projectPath) : null,
|
|
8809
9799
|
outputs: incoming.map((frame, index) => ({
|
|
8810
9800
|
role: frame.role,
|
|
8811
9801
|
basedOn: basedOnHash(entry, index),
|
|
@@ -8835,7 +9825,7 @@ function basedOnHash(entry, index) {
|
|
|
8835
9825
|
async function replaceFile(file, bytes) {
|
|
8836
9826
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8837
9827
|
try {
|
|
8838
|
-
await
|
|
9828
|
+
await writeFile10(tmp, bytes);
|
|
8839
9829
|
await rename7(tmp, file);
|
|
8840
9830
|
} finally {
|
|
8841
9831
|
await rm8(tmp, { force: true });
|
|
@@ -8865,7 +9855,7 @@ async function runEdit(args) {
|
|
|
8865
9855
|
const set = started.members.length > 1;
|
|
8866
9856
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
8867
9857
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
8868
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
9858
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path24.relative(process.cwd(), member.path)}`);
|
|
8869
9859
|
if (args.noOpen) {
|
|
8870
9860
|
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`);
|
|
8871
9861
|
} else {
|
|
@@ -8875,17 +9865,17 @@ async function runEdit(args) {
|
|
|
8875
9865
|
}
|
|
8876
9866
|
|
|
8877
9867
|
// src/cli/commands/gallery.ts
|
|
8878
|
-
import
|
|
8879
|
-
import { existsSync as
|
|
9868
|
+
import path31 from "path";
|
|
9869
|
+
import { existsSync as existsSync23 } from "fs";
|
|
8880
9870
|
|
|
8881
9871
|
// src/gallery/snapshot.ts
|
|
8882
9872
|
import { createHash as createHash3 } from "crypto";
|
|
8883
|
-
import { existsSync as
|
|
8884
|
-
import { readFile as
|
|
8885
|
-
import
|
|
9873
|
+
import { existsSync as existsSync19 } from "fs";
|
|
9874
|
+
import { readFile as readFile16, stat as stat2 } from "fs/promises";
|
|
9875
|
+
import path27 from "path";
|
|
8886
9876
|
|
|
8887
9877
|
// src/gallery/edit.ts
|
|
8888
|
-
import
|
|
9878
|
+
import path25 from "path";
|
|
8889
9879
|
import { z as z7 } from "zod";
|
|
8890
9880
|
var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
8891
9881
|
var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
@@ -8975,7 +9965,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8975
9965
|
}
|
|
8976
9966
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8977
9967
|
if (result.changed) {
|
|
8978
|
-
const relative =
|
|
9968
|
+
const relative = path25.relative(process.cwd(), result.manifestPath);
|
|
8979
9969
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8980
9970
|
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
|
|
8981
9971
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -8985,8 +9975,8 @@ function createGalleryEditHandler(opts) {
|
|
|
8985
9975
|
}
|
|
8986
9976
|
|
|
8987
9977
|
// src/pipeline/history.ts
|
|
8988
|
-
import { existsSync as
|
|
8989
|
-
import
|
|
9978
|
+
import { existsSync as existsSync18 } from "fs";
|
|
9979
|
+
import path26 from "path";
|
|
8990
9980
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
8991
9981
|
function historyLimit(manifest, env = process.env) {
|
|
8992
9982
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -9072,10 +10062,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
9072
10062
|
}
|
|
9073
10063
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
9074
10064
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
9075
|
-
if (!
|
|
10065
|
+
if (!existsSync18(file)) continue;
|
|
9076
10066
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
9077
10067
|
throw new OverwriteRefusedError(
|
|
9078
|
-
`${
|
|
10068
|
+
`${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
9079
10069
|
[file]
|
|
9080
10070
|
);
|
|
9081
10071
|
}
|
|
@@ -9127,7 +10117,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
9127
10117
|
// src/gallery/snapshot.ts
|
|
9128
10118
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
9129
10119
|
function galleryMediaId(absolutePath) {
|
|
9130
|
-
return createHash3("sha256").update(
|
|
10120
|
+
return createHash3("sha256").update(path27.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
9131
10121
|
}
|
|
9132
10122
|
function galleryMediaRoute(id) {
|
|
9133
10123
|
return `/media/${id}`;
|
|
@@ -9146,9 +10136,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
9146
10136
|
return (entry.history ?? []).map((generation, i) => {
|
|
9147
10137
|
const outputs = generation.outputs.map((output) => {
|
|
9148
10138
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
9149
|
-
const file =
|
|
10139
|
+
const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
9150
10140
|
let url = null;
|
|
9151
|
-
if (
|
|
10141
|
+
if (existsSync19(file)) {
|
|
9152
10142
|
const id = galleryMediaId(file);
|
|
9153
10143
|
media.set(id, { path: file, contentType: mediaType });
|
|
9154
10144
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -9177,7 +10167,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
9177
10167
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
9178
10168
|
async function samePixels(a, b) {
|
|
9179
10169
|
try {
|
|
9180
|
-
const [left, right] = await Promise.all([
|
|
10170
|
+
const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
|
|
9181
10171
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
9182
10172
|
const x = decodePng(left);
|
|
9183
10173
|
const y = decodePng(right);
|
|
@@ -9221,10 +10211,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
9221
10211
|
async function describeQuality(media, root, inspection) {
|
|
9222
10212
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
9223
10213
|
const outputs = await Promise.all(
|
|
9224
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
10214
|
+
outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
|
|
9225
10215
|
);
|
|
9226
|
-
const recordPath =
|
|
9227
|
-
const recordExists =
|
|
10216
|
+
const recordPath = path27.resolve(root, inspection.record);
|
|
10217
|
+
const recordExists = existsSync19(recordPath);
|
|
9228
10218
|
let options = null;
|
|
9229
10219
|
let check = null;
|
|
9230
10220
|
if (recordExists) {
|
|
@@ -9243,7 +10233,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
9243
10233
|
return {
|
|
9244
10234
|
state: inspection.state,
|
|
9245
10235
|
reason: inspection.reason,
|
|
9246
|
-
output: portableOutputPath(
|
|
10236
|
+
output: portableOutputPath(path27.resolve(root, inspection.output), root),
|
|
9247
10237
|
record: portableOutputPath(recordPath, root),
|
|
9248
10238
|
recordExists,
|
|
9249
10239
|
outputs,
|
|
@@ -9280,9 +10270,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
9280
10270
|
const media = /* @__PURE__ */ new Map();
|
|
9281
10271
|
const plan = await buildPlan(specs, lock);
|
|
9282
10272
|
const items = [];
|
|
9283
|
-
const manifestText = await
|
|
10273
|
+
const manifestText = await readFile16(loaded.path, "utf8");
|
|
9284
10274
|
const manifestSha256 = sha256(manifestText);
|
|
9285
|
-
const cacheDir =
|
|
10275
|
+
const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
9286
10276
|
let rawStyles = {};
|
|
9287
10277
|
try {
|
|
9288
10278
|
const raw = JSON.parse(manifestText);
|
|
@@ -9306,12 +10296,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
9306
10296
|
)
|
|
9307
10297
|
);
|
|
9308
10298
|
if (spec.source) {
|
|
9309
|
-
const editPath =
|
|
10299
|
+
const editPath = path27.resolve(root, spec.source);
|
|
9310
10300
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
9311
10301
|
const editShas = await Promise.all(
|
|
9312
10302
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
9313
10303
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
9314
|
-
return { file, sha256:
|
|
10304
|
+
return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
|
|
9315
10305
|
})
|
|
9316
10306
|
);
|
|
9317
10307
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -9319,7 +10309,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9319
10309
|
const companion = await readHandEditCompanion(editPath);
|
|
9320
10310
|
if (companion) {
|
|
9321
10311
|
const projectPath = handEditProjectPath(editPath);
|
|
9322
|
-
const project = companion.project &&
|
|
10312
|
+
const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
|
|
9323
10313
|
let projectUrl = null;
|
|
9324
10314
|
if (project) {
|
|
9325
10315
|
const id = galleryMediaId(projectPath);
|
|
@@ -9355,8 +10345,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9355
10345
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
9356
10346
|
}
|
|
9357
10347
|
} else if (spec.source) {
|
|
9358
|
-
outputs = [await describeOutput(media, root,
|
|
9359
|
-
} else if (!entry &&
|
|
10348
|
+
outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
|
|
10349
|
+
} else if (!entry && existsSync19(spec.outFile)) {
|
|
9360
10350
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
9361
10351
|
} else {
|
|
9362
10352
|
outputs = [];
|
|
@@ -9538,7 +10528,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9538
10528
|
id: loaded.manifest.name,
|
|
9539
10529
|
name: loaded.manifest.name,
|
|
9540
10530
|
manifest: loaded.path,
|
|
9541
|
-
lock:
|
|
10531
|
+
lock: path27.resolve(opts.lockPath),
|
|
9542
10532
|
root,
|
|
9543
10533
|
provider: loaded.manifest.provider,
|
|
9544
10534
|
account: null,
|
|
@@ -9564,7 +10554,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9564
10554
|
return { snapshot, media };
|
|
9565
10555
|
}
|
|
9566
10556
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
9567
|
-
const dir =
|
|
10557
|
+
const dir = path27.dirname(path27.resolve(opts.workspacePath));
|
|
9568
10558
|
const media = /* @__PURE__ */ new Map();
|
|
9569
10559
|
const projects = [];
|
|
9570
10560
|
const items = [];
|
|
@@ -9607,11 +10597,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9607
10597
|
id: project.id,
|
|
9608
10598
|
name: loaded.manifest.name,
|
|
9609
10599
|
manifest: manifestPath,
|
|
9610
|
-
lock:
|
|
10600
|
+
lock: path27.resolve(lockPath),
|
|
9611
10601
|
root: loaded.root,
|
|
9612
10602
|
provider: loaded.manifest.provider,
|
|
9613
10603
|
account: project.account ?? null,
|
|
9614
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
10604
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
|
|
9615
10605
|
historyLimit: historyLimit(loaded.manifest),
|
|
9616
10606
|
entries: Object.keys(lock.entries).length,
|
|
9617
10607
|
items: projectItems.length,
|
|
@@ -9623,8 +10613,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9623
10613
|
id: project.id,
|
|
9624
10614
|
name: project.id,
|
|
9625
10615
|
manifest: manifestPath,
|
|
9626
|
-
lock:
|
|
9627
|
-
root:
|
|
10616
|
+
lock: path27.resolve(lockPath),
|
|
10617
|
+
root: path27.dirname(manifestPath),
|
|
9628
10618
|
provider: project.provider,
|
|
9629
10619
|
account: project.account ?? null,
|
|
9630
10620
|
manifestSha256: null,
|
|
@@ -9651,7 +10641,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9651
10641
|
version: 1,
|
|
9652
10642
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
9653
10643
|
project: null,
|
|
9654
|
-
workspace: { path:
|
|
10644
|
+
workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
|
|
9655
10645
|
filter,
|
|
9656
10646
|
totals: {
|
|
9657
10647
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -9670,7 +10660,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9670
10660
|
// src/gallery/server.ts
|
|
9671
10661
|
import { randomBytes } from "crypto";
|
|
9672
10662
|
import { createServer as createServer2 } from "http";
|
|
9673
|
-
import { readFile as
|
|
10663
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
9674
10664
|
|
|
9675
10665
|
// src/gallery/page.ts
|
|
9676
10666
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -9689,7 +10679,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9689
10679
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
9690
10680
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
9691
10681
|
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
9692
|
-
const title =
|
|
10682
|
+
const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
|
|
9693
10683
|
const { css, js } = clientAssets();
|
|
9694
10684
|
return `<!doctype html>
|
|
9695
10685
|
<html lang="en">
|
|
@@ -9732,7 +10722,7 @@ ${css}</style>
|
|
|
9732
10722
|
<footer>
|
|
9733
10723
|
Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
|
|
9734
10724
|
is open, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
|
|
9735
|
-
and disk only
|
|
10725
|
+
and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
|
|
9736
10726
|
Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
|
|
9737
10727
|
Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.
|
|
9738
10728
|
<span id="note"></span>
|
|
@@ -9911,7 +10901,7 @@ async function serveGallery(opts) {
|
|
|
9911
10901
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
9912
10902
|
if (!asset) return fail(404, "no such review asset");
|
|
9913
10903
|
try {
|
|
9914
|
-
const bytes = await
|
|
10904
|
+
const bytes = await readFile17(asset.path);
|
|
9915
10905
|
res.writeHead(200, {
|
|
9916
10906
|
"Content-Type": asset.contentType,
|
|
9917
10907
|
"Cache-Control": "no-store",
|
|
@@ -9987,7 +10977,7 @@ async function serveGallery(opts) {
|
|
|
9987
10977
|
if (!asset) return fail(404, "no such gallery media");
|
|
9988
10978
|
let bytes;
|
|
9989
10979
|
try {
|
|
9990
|
-
bytes = await
|
|
10980
|
+
bytes = await readFile17(asset.path);
|
|
9991
10981
|
} catch {
|
|
9992
10982
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
9993
10983
|
}
|
|
@@ -10057,7 +11047,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
10057
11047
|
while (pending().length > 0) {
|
|
10058
11048
|
if (Date.now() - started > timeout) {
|
|
10059
11049
|
result.stillRunning += pending().length;
|
|
10060
|
-
log2(` timed out with ${result.stillRunning} job(s) still running
|
|
11050
|
+
log2(` timed out with ${result.stillRunning} job(s) still running; re-run \`poll\` to resume`);
|
|
10061
11051
|
break;
|
|
10062
11052
|
}
|
|
10063
11053
|
for (const [key, entry] of pending()) {
|
|
@@ -10065,7 +11055,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
10065
11055
|
const currentSpec = specByKey.get(key);
|
|
10066
11056
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
10067
11057
|
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
10068
|
-
spec: currentSpec
|
|
11058
|
+
spec: currentSpec,
|
|
11059
|
+
metadata: entry.providerMetadata?.[provider.id]
|
|
10069
11060
|
});
|
|
10070
11061
|
if (state.status === "review" || state.status === "review-set") {
|
|
10071
11062
|
upsert(lock, key, {
|
|
@@ -10237,8 +11228,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
10237
11228
|
lastSubmitAt = Date.now();
|
|
10238
11229
|
try {
|
|
10239
11230
|
const refs = styleImages.get(spec.styleId) ?? [];
|
|
11231
|
+
const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
11232
|
+
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
10240
11233
|
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
10241
11234
|
...previousJobId ? { previousJobId } : {},
|
|
11235
|
+
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
11236
|
+
...replacedMetadata ? { replacedMetadata } : {},
|
|
10242
11237
|
...previousMetadata ? { previousMetadata } : {},
|
|
10243
11238
|
checkpoint: async (checkpoint) => {
|
|
10244
11239
|
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
@@ -10300,13 +11295,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
10300
11295
|
}
|
|
10301
11296
|
|
|
10302
11297
|
// src/pick/server.ts
|
|
10303
|
-
import { existsSync as
|
|
10304
|
-
import { readFile as
|
|
11298
|
+
import { existsSync as existsSync21 } from "fs";
|
|
11299
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
10305
11300
|
|
|
10306
11301
|
// src/pipeline/init.ts
|
|
10307
|
-
import { readdir, readFile as
|
|
10308
|
-
import { existsSync as
|
|
10309
|
-
import
|
|
11302
|
+
import { readdir, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
|
|
11303
|
+
import { existsSync as existsSync20 } from "fs";
|
|
11304
|
+
import path28 from "path";
|
|
10310
11305
|
function pngSize(buf) {
|
|
10311
11306
|
if (buf.length < 24) return null;
|
|
10312
11307
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -10320,7 +11315,7 @@ function slugify(value) {
|
|
|
10320
11315
|
async function walk(dir, exclude) {
|
|
10321
11316
|
const out = [];
|
|
10322
11317
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
10323
|
-
const full =
|
|
11318
|
+
const full = path28.join(dir, entry.name);
|
|
10324
11319
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
10325
11320
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
10326
11321
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -10334,14 +11329,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
10334
11329
|
const skipped = [];
|
|
10335
11330
|
const seen = /* @__PURE__ */ new Map();
|
|
10336
11331
|
for (const file of files.sort()) {
|
|
10337
|
-
const size = pngSize(await
|
|
11332
|
+
const size = pngSize(await readFile18(file));
|
|
10338
11333
|
if (!size) {
|
|
10339
11334
|
skipped.push(`${file} (not a readable PNG)`);
|
|
10340
11335
|
continue;
|
|
10341
11336
|
}
|
|
10342
|
-
const rel =
|
|
10343
|
-
const category =
|
|
10344
|
-
let id = slugify(
|
|
11337
|
+
const rel = path28.relative(root, file);
|
|
11338
|
+
const category = path28.dirname(rel) === "." ? "" : path28.dirname(rel);
|
|
11339
|
+
let id = slugify(path28.basename(rel));
|
|
10345
11340
|
if (seen.has(id)) {
|
|
10346
11341
|
const n = seen.get(id) + 1;
|
|
10347
11342
|
seen.set(id, n);
|
|
@@ -10399,10 +11394,10 @@ function mode(values) {
|
|
|
10399
11394
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
10400
11395
|
}
|
|
10401
11396
|
async function writeManifestFile(target, manifest) {
|
|
10402
|
-
if (
|
|
10403
|
-
throw new Error(`${target} already exists
|
|
11397
|
+
if (existsSync20(target)) {
|
|
11398
|
+
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
10404
11399
|
}
|
|
10405
|
-
await
|
|
11400
|
+
await writeFile11(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
10406
11401
|
}
|
|
10407
11402
|
|
|
10408
11403
|
// src/pick/sheet.ts
|
|
@@ -10431,7 +11426,7 @@ function renderSheet(groups, options = {}) {
|
|
|
10431
11426
|
<head>
|
|
10432
11427
|
<meta charset="utf-8">
|
|
10433
11428
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10434
|
-
<title>
|
|
11429
|
+
<title>pick candidates | pixelkiln</title>
|
|
10435
11430
|
<style>
|
|
10436
11431
|
:root {
|
|
10437
11432
|
--bg: #17150f; --panel: #201d17; --panel-deep: #100f0c;
|
|
@@ -10532,7 +11527,7 @@ function renderSheet(groups, options = {}) {
|
|
|
10532
11527
|
click again to undo. In the focused row,
|
|
10533
11528
|
<kbd>\u2190</kbd>/<kbd>\u2192</kbd> browses every candidate and <kbd>Enter</kbd> chooses;
|
|
10534
11529
|
<kbd>1</kbd>\u2013<kbd>9</kbd> picks directly, <kbd>0</kbd> skips, and
|
|
10535
|
-
<kbd>\u2191</kbd>/<kbd>\u2193</kbd> changes rows. Unchosen rows stay in review
|
|
11530
|
+
<kbd>\u2191</kbd>/<kbd>\u2193</kbd> changes rows. Unchosen rows stay in review; nothing is
|
|
10536
11531
|
discarded by closing this page.
|
|
10537
11532
|
</footer>
|
|
10538
11533
|
<script>
|
|
@@ -10781,7 +11776,7 @@ document.getElementById('submit').onclick = async () => {
|
|
|
10781
11776
|
});
|
|
10782
11777
|
if (!res.ok) throw new Error(await res.text());
|
|
10783
11778
|
const result = await res.json();
|
|
10784
|
-
status.textContent = APPLY.embedded ? 'applied' : 'done
|
|
11779
|
+
status.textContent = APPLY.embedded ? 'applied' : 'done, you can close this tab';
|
|
10785
11780
|
document.body.style.opacity = '.6';
|
|
10786
11781
|
if (APPLY.embedded && window.parent !== window) {
|
|
10787
11782
|
window.parent.postMessage({ type: 'pixelkiln:review-applied', result }, location.origin);
|
|
@@ -10802,13 +11797,13 @@ async function currentArt(entry, spec) {
|
|
|
10802
11797
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
10803
11798
|
if (!previous) return null;
|
|
10804
11799
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
10805
|
-
if (!
|
|
11800
|
+
if (!existsSync21(file)) return null;
|
|
10806
11801
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
10807
11802
|
if (!contentType) return null;
|
|
10808
11803
|
let width = entry.width;
|
|
10809
11804
|
let height = entry.height;
|
|
10810
11805
|
if (contentType === "image/png") {
|
|
10811
|
-
const size = pngSize(await
|
|
11806
|
+
const size = pngSize(await readFile19(file));
|
|
10812
11807
|
if (size) ({ width, height } = size);
|
|
10813
11808
|
}
|
|
10814
11809
|
return { path: file, contentType, width, height };
|
|
@@ -10828,7 +11823,7 @@ async function prepareReview(provider, lock, opts = {}) {
|
|
|
10828
11823
|
if (entry.status !== "review" || !entry.reviewObjectId) continue;
|
|
10829
11824
|
try {
|
|
10830
11825
|
const spec = specByKey.get(key);
|
|
10831
|
-
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
11826
|
+
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec, metadata: entry.providerMetadata?.[provider.id] });
|
|
10832
11827
|
if (state.status !== "review" && state.status !== "review-set" || (state.status === "review" ? !state.candidateUrls.length : !state.frameUrls.length)) continue;
|
|
10833
11828
|
const frameUrls = state.status === "review" ? state.candidateUrls : state.frameUrls;
|
|
10834
11829
|
const sourceRoute = spec?.revision ? `${prefix}/revision-source/${encodeURIComponent(String(groups.length))}` : null;
|
|
@@ -10888,7 +11883,7 @@ async function prepareReview(provider, lock, opts = {}) {
|
|
|
10888
11883
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
10889
11884
|
if (group.mode === "frame-set") {
|
|
10890
11885
|
const spec = specByKey.get(key);
|
|
10891
|
-
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
11886
|
+
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec, metadata: entry.providerMetadata?.[provider.id] });
|
|
10892
11887
|
if (state.status !== "review-set") {
|
|
10893
11888
|
throw new Error(`Frame set ${key} is no longer ready for review`);
|
|
10894
11889
|
}
|
|
@@ -11220,7 +12215,7 @@ function createGenerateHandlers(opts) {
|
|
|
11220
12215
|
if (!request.resume && !request.refresh && !request.restore && request.revert === void 0) {
|
|
11221
12216
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
11222
12217
|
if (!plan.actionable.length) {
|
|
11223
|
-
const reasons = plan.items.map((item) => `${item.key}: ${item.state}
|
|
12218
|
+
const reasons = plan.items.map((item) => `${item.key}: ${item.state}, ${item.reason}`).slice(0, 5);
|
|
11224
12219
|
throw new GenerateRequestError(`nothing to generate; ${reasons.join("; ")}`);
|
|
11225
12220
|
}
|
|
11226
12221
|
groups = plan.groups;
|
|
@@ -11301,15 +12296,15 @@ function createGenerateHandlers(opts) {
|
|
|
11301
12296
|
}
|
|
11302
12297
|
|
|
11303
12298
|
// src/gallery/editor.ts
|
|
11304
|
-
import { readFile as
|
|
11305
|
-
import
|
|
12299
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
12300
|
+
import path30 from "path";
|
|
11306
12301
|
|
|
11307
12302
|
// src/editor/install.ts
|
|
11308
12303
|
import { createHash as createHash4 } from "crypto";
|
|
11309
|
-
import { existsSync as
|
|
11310
|
-
import { mkdir as
|
|
12304
|
+
import { existsSync as existsSync22 } from "fs";
|
|
12305
|
+
import { mkdir as mkdir9, readFile as readFile20, rename as rename8, rm as rm9, stat as stat3, writeFile as writeFile12 } from "fs/promises";
|
|
11311
12306
|
import os2 from "os";
|
|
11312
|
-
import
|
|
12307
|
+
import path29 from "path";
|
|
11313
12308
|
|
|
11314
12309
|
// tools/pixelorama-bridge/pin.json
|
|
11315
12310
|
var pin_default = {
|
|
@@ -11373,15 +12368,15 @@ var EditorInstallError = class extends Error {
|
|
|
11373
12368
|
};
|
|
11374
12369
|
function editorToolsRoot() {
|
|
11375
12370
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
11376
|
-
if (override) return
|
|
11377
|
-
if (process.platform === "darwin") return
|
|
12371
|
+
if (override) return path29.resolve(override);
|
|
12372
|
+
if (process.platform === "darwin") return path29.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
11378
12373
|
if (process.platform === "win32") {
|
|
11379
|
-
return
|
|
12374
|
+
return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
11380
12375
|
}
|
|
11381
|
-
return
|
|
12376
|
+
return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
11382
12377
|
}
|
|
11383
12378
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
11384
|
-
return
|
|
12379
|
+
return path29.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
11385
12380
|
}
|
|
11386
12381
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
11387
12382
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -11391,7 +12386,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
11391
12386
|
}
|
|
11392
12387
|
async function sha256Of(file) {
|
|
11393
12388
|
try {
|
|
11394
|
-
return createHash4("sha256").update(await
|
|
12389
|
+
return createHash4("sha256").update(await readFile20(file)).digest("hex");
|
|
11395
12390
|
} catch {
|
|
11396
12391
|
return null;
|
|
11397
12392
|
}
|
|
@@ -11404,8 +12399,8 @@ async function editorStatus(opts = {}) {
|
|
|
11404
12399
|
let totalBytes = 0;
|
|
11405
12400
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
11406
12401
|
totalBytes += expected.bytes;
|
|
11407
|
-
const file =
|
|
11408
|
-
const info =
|
|
12402
|
+
const file = path29.join(dir, name);
|
|
12403
|
+
const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
|
|
11409
12404
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
11410
12405
|
installedBytes += expected.bytes;
|
|
11411
12406
|
} else {
|
|
@@ -11434,7 +12429,7 @@ async function installEditor(opts = {}) {
|
|
|
11434
12429
|
);
|
|
11435
12430
|
}
|
|
11436
12431
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
11437
|
-
await
|
|
12432
|
+
await mkdir9(dir, { recursive: true });
|
|
11438
12433
|
const downloaded = [];
|
|
11439
12434
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
11440
12435
|
let fetchedBytes = before.installedBytes;
|
|
@@ -11462,10 +12457,10 @@ async function installEditor(opts = {}) {
|
|
|
11462
12457
|
if (digest2 !== expected.sha256) {
|
|
11463
12458
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
11464
12459
|
}
|
|
11465
|
-
const final =
|
|
12460
|
+
const final = path29.join(dir, name);
|
|
11466
12461
|
const tmp = `${final}.${process.pid}.part`;
|
|
11467
12462
|
try {
|
|
11468
|
-
await
|
|
12463
|
+
await writeFile12(tmp, bytes);
|
|
11469
12464
|
await rename8(tmp, final);
|
|
11470
12465
|
} finally {
|
|
11471
12466
|
await rm9(tmp, { force: true });
|
|
@@ -11508,7 +12503,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
11508
12503
|
".json": "application/json; charset=utf-8"
|
|
11509
12504
|
};
|
|
11510
12505
|
function editorContentType(name) {
|
|
11511
|
-
return EDITOR_CONTENT_TYPES[
|
|
12506
|
+
return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
|
|
11512
12507
|
}
|
|
11513
12508
|
|
|
11514
12509
|
// src/gallery/editor.ts
|
|
@@ -11584,7 +12579,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
11584
12579
|
if (!status.installed) return null;
|
|
11585
12580
|
let bytes;
|
|
11586
12581
|
try {
|
|
11587
|
-
bytes = await
|
|
12582
|
+
bytes = await readFile21(path30.join(dir, name));
|
|
11588
12583
|
} catch {
|
|
11589
12584
|
verified = null;
|
|
11590
12585
|
return null;
|
|
@@ -11624,13 +12619,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
11624
12619
|
};
|
|
11625
12620
|
const loadProject = async (project) => {
|
|
11626
12621
|
const ctx = await access2.loadProject(project);
|
|
11627
|
-
const dir =
|
|
12622
|
+
const dir = path31.dirname(ctx.loaded.path);
|
|
11628
12623
|
const declared = readEnvFiles(dir);
|
|
11629
12624
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
11630
12625
|
for (const [name, value] of Object.entries(declared)) {
|
|
11631
12626
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
11632
12627
|
throw new Error(
|
|
11633
|
-
`${
|
|
12628
|
+
`${path31.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.`
|
|
11634
12629
|
);
|
|
11635
12630
|
}
|
|
11636
12631
|
}
|
|
@@ -11675,8 +12670,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
11675
12670
|
}
|
|
11676
12671
|
async function runGallery(args) {
|
|
11677
12672
|
if (args.workspace) {
|
|
11678
|
-
const workspacePath =
|
|
11679
|
-
if (!
|
|
12673
|
+
const workspacePath = path31.resolve(args.workspace);
|
|
12674
|
+
if (!existsSync23(workspacePath)) {
|
|
11680
12675
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
11681
12676
|
}
|
|
11682
12677
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -11691,12 +12686,12 @@ async function runGallery(args) {
|
|
|
11691
12686
|
return;
|
|
11692
12687
|
}
|
|
11693
12688
|
for (const project of initial.snapshot.workspace?.projects ?? []) {
|
|
11694
|
-
if (project.error) log(` ${project.id}: unreadable
|
|
12689
|
+
if (project.error) log(` ${project.id}: unreadable: ${project.error}`);
|
|
11695
12690
|
}
|
|
11696
12691
|
const registered = async (projectId) => {
|
|
11697
12692
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
11698
12693
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
11699
|
-
return resolveProject(
|
|
12694
|
+
return resolveProject(path31.dirname(workspacePath), project);
|
|
11700
12695
|
};
|
|
11701
12696
|
await serveUntilStopped(initial, build2, args, {
|
|
11702
12697
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -11721,25 +12716,70 @@ async function runGallery(args) {
|
|
|
11721
12716
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
11722
12717
|
};
|
|
11723
12718
|
await serveUntilStopped(await build(), reload, args, {
|
|
11724
|
-
manifestFor: () =>
|
|
12719
|
+
manifestFor: () => path31.resolve(args.manifest),
|
|
11725
12720
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
11726
12721
|
});
|
|
11727
12722
|
}
|
|
11728
12723
|
|
|
11729
12724
|
// src/cli/commands/generate.ts
|
|
11730
12725
|
async function runGenerate(args) {
|
|
12726
|
+
if (args.command !== "gen") {
|
|
12727
|
+
await runLifecycle(args, 1, /* @__PURE__ */ new Map());
|
|
12728
|
+
return;
|
|
12729
|
+
}
|
|
12730
|
+
const spent = /* @__PURE__ */ new Map();
|
|
12731
|
+
for (let wave = 1; ; wave++) {
|
|
12732
|
+
const again = await runLifecycle(args, wave, spent);
|
|
12733
|
+
if (!again) return;
|
|
12734
|
+
}
|
|
12735
|
+
}
|
|
12736
|
+
function remainingBudgets(args, spent) {
|
|
12737
|
+
if (!spent.size) return args;
|
|
12738
|
+
const total = [...spent.values()].reduce((sum, n) => sum + n, 0);
|
|
12739
|
+
const providerBudgets = /* @__PURE__ */ Object.create(null);
|
|
12740
|
+
for (const [provider, ceiling] of Object.entries(args.providerBudgets)) {
|
|
12741
|
+
providerBudgets[provider] = Math.max(0, ceiling - (spent.get(provider) ?? 0));
|
|
12742
|
+
}
|
|
12743
|
+
return {
|
|
12744
|
+
...args,
|
|
12745
|
+
...args.budget === void 0 ? {} : { budget: Math.max(0, args.budget - total) },
|
|
12746
|
+
providerBudgets
|
|
12747
|
+
};
|
|
12748
|
+
}
|
|
12749
|
+
async function runLifecycle(args, wave, spent) {
|
|
11731
12750
|
const { loaded, specs, lock } = await openProject2(args);
|
|
11732
|
-
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
12751
|
+
const plan = await buildPlan(specs, lock, { force: args.force && wave === 1 });
|
|
11733
12752
|
const providerFor = providerCache(providerModeFor(args));
|
|
12753
|
+
let submitted = 0;
|
|
11734
12754
|
if (args.command === "submit" || args.command === "gen") {
|
|
12755
|
+
if (wave > 1) {
|
|
12756
|
+
if (!plan.actionable.length) {
|
|
12757
|
+
log(`
|
|
12758
|
+
wave ${wave}: nothing else became actionable`);
|
|
12759
|
+
return false;
|
|
12760
|
+
}
|
|
12761
|
+
log(`
|
|
12762
|
+
wave ${wave}: ${plan.actionable.length} asset(s) became actionable`);
|
|
12763
|
+
}
|
|
11735
12764
|
printPlan(plan);
|
|
11736
12765
|
if (args.dryRun) {
|
|
11737
12766
|
log(`
|
|
11738
12767
|
--dry-run: stopping before any spend.`);
|
|
11739
|
-
return;
|
|
12768
|
+
return false;
|
|
11740
12769
|
}
|
|
11741
12770
|
if (plan.actionable.length) {
|
|
11742
|
-
|
|
12771
|
+
let budgets;
|
|
12772
|
+
try {
|
|
12773
|
+
budgets = budgetsForPlan(plan, remainingBudgets(args, spent));
|
|
12774
|
+
} catch (err) {
|
|
12775
|
+
if (wave > 1 && err instanceof BudgetError) {
|
|
12776
|
+
log(`
|
|
12777
|
+
stopping before wave ${wave}: ${err.message}`);
|
|
12778
|
+
log(` run pixelkiln gen again with a budget for the remaining work`);
|
|
12779
|
+
return false;
|
|
12780
|
+
}
|
|
12781
|
+
throw err;
|
|
12782
|
+
}
|
|
11743
12783
|
const balances = /* @__PURE__ */ new Map();
|
|
11744
12784
|
for (const group of plan.groups) {
|
|
11745
12785
|
const groupProvider = providerFor(group.provider);
|
|
@@ -11768,19 +12808,24 @@ async function runGenerate(args) {
|
|
|
11768
12808
|
}
|
|
11769
12809
|
const ceiling = budgets.get(group.provider);
|
|
11770
12810
|
if (ceiling !== void 0 && group.cost > ceiling) {
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
12811
|
+
const message6 = `${group.provider} would spend ${formatCost(group.costUnit, group.cost)} but ` + (wave > 1 ? `only ${formatCost(group.costUnit, ceiling)} of the budget is left after ${wave - 1} wave(s).` : `its budget is ${formatCost(group.costUnit, ceiling)}.`);
|
|
12812
|
+
if (wave > 1) {
|
|
12813
|
+
log(`
|
|
12814
|
+
stopping before wave ${wave}: ${message6}`);
|
|
12815
|
+
log(` run pixelkiln gen again with a budget for the remaining work`);
|
|
12816
|
+
return false;
|
|
12817
|
+
}
|
|
12818
|
+
throw new BudgetError(message6);
|
|
11774
12819
|
}
|
|
11775
12820
|
}
|
|
11776
12821
|
const spendSummary = plan.groups.map((group) => `${group.provider} ${formatCost(group.costUnit, group.cost)}`).join("; ");
|
|
11777
12822
|
const ok = await confirm(
|
|
11778
|
-
` Spend ${spendSummary} on ${plan.actionable.length} asset(s)?`,
|
|
12823
|
+
` Spend ${spendSummary} on ${plan.actionable.length} asset(s)${wave > 1 ? ` (wave ${wave})` : ""}?`,
|
|
11779
12824
|
args.yes
|
|
11780
12825
|
);
|
|
11781
12826
|
if (!ok) {
|
|
11782
12827
|
log(` aborted`);
|
|
11783
|
-
return;
|
|
12828
|
+
return false;
|
|
11784
12829
|
}
|
|
11785
12830
|
log(`
|
|
11786
12831
|
submitting\u2026`);
|
|
@@ -11794,6 +12839,8 @@ async function runGenerate(args) {
|
|
|
11794
12839
|
`
|
|
11795
12840
|
${group.provider}: submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`
|
|
11796
12841
|
);
|
|
12842
|
+
submitted += res.submitted;
|
|
12843
|
+
spent.set(group.provider, (spent.get(group.provider) ?? 0) + res.spent);
|
|
11797
12844
|
try {
|
|
11798
12845
|
const before = balances.get(group.provider);
|
|
11799
12846
|
const after = before && groupProvider.balance ? await groupProvider.balance() : null;
|
|
@@ -11813,7 +12860,7 @@ async function runGenerate(args) {
|
|
|
11813
12860
|
if (res.failed) process.exitCode = 1;
|
|
11814
12861
|
}
|
|
11815
12862
|
}
|
|
11816
|
-
if (args.command === "submit") return;
|
|
12863
|
+
if (args.command === "submit") return false;
|
|
11817
12864
|
}
|
|
11818
12865
|
if (args.command === "poll" || args.command === "gen") {
|
|
11819
12866
|
log(`
|
|
@@ -11836,7 +12883,7 @@ async function runGenerate(args) {
|
|
|
11836
12883
|
if (total.failed || total.stillRunning) process.exitCode = 1;
|
|
11837
12884
|
if (args.command === "poll") {
|
|
11838
12885
|
printResumeActions(specs, lock);
|
|
11839
|
-
return;
|
|
12886
|
+
return false;
|
|
11840
12887
|
}
|
|
11841
12888
|
}
|
|
11842
12889
|
if (args.command === "pick" || args.command === "gen") {
|
|
@@ -11862,7 +12909,7 @@ async function runGenerate(args) {
|
|
|
11862
12909
|
if (args.command === "gen" && total.skipped) process.exitCode = 1;
|
|
11863
12910
|
if (args.command === "pick") {
|
|
11864
12911
|
printResumeActions(specs, lock);
|
|
11865
|
-
return;
|
|
12912
|
+
return false;
|
|
11866
12913
|
}
|
|
11867
12914
|
}
|
|
11868
12915
|
if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
|
|
@@ -11912,8 +12959,9 @@ async function runGenerate(args) {
|
|
|
11912
12959
|
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
11913
12960
|
}
|
|
11914
12961
|
}
|
|
11915
|
-
return;
|
|
12962
|
+
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision);
|
|
11916
12963
|
}
|
|
12964
|
+
return false;
|
|
11917
12965
|
}
|
|
11918
12966
|
|
|
11919
12967
|
// src/cli/commands/history.ts
|
|
@@ -11973,18 +13021,18 @@ async function runRestoreGeneration(args) {
|
|
|
11973
13021
|
}
|
|
11974
13022
|
|
|
11975
13023
|
// src/cli/commands/housekeeping.ts
|
|
11976
|
-
import { existsSync as
|
|
13024
|
+
import { existsSync as existsSync25 } from "fs";
|
|
11977
13025
|
|
|
11978
13026
|
// src/pipeline/cache-health.ts
|
|
11979
|
-
import { existsSync as
|
|
11980
|
-
import { readFile as
|
|
11981
|
-
import
|
|
13027
|
+
import { existsSync as existsSync24 } from "fs";
|
|
13028
|
+
import { readFile as readFile22, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
13029
|
+
import path32 from "path";
|
|
11982
13030
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11983
|
-
if (options.prune && !
|
|
11984
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
13031
|
+
if (options.prune && !existsSync24(lockPath)) {
|
|
13032
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path32.resolve(lockPath)}`);
|
|
11985
13033
|
}
|
|
11986
|
-
const contentDir =
|
|
11987
|
-
const remotePath =
|
|
13034
|
+
const contentDir = path32.resolve(path32.dirname(lockPath), ".pixelkiln", "cache");
|
|
13035
|
+
const remotePath = path32.resolve(cachePathFor(lockPath));
|
|
11988
13036
|
const referenced = referencedHashes(lock);
|
|
11989
13037
|
let content = await inspectContentCache(contentDir, referenced);
|
|
11990
13038
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -11996,7 +13044,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11996
13044
|
]);
|
|
11997
13045
|
for (const name of names) {
|
|
11998
13046
|
try {
|
|
11999
|
-
await rm10(
|
|
13047
|
+
await rm10(path32.join(contentDir, name), { force: true });
|
|
12000
13048
|
removed.contentFiles++;
|
|
12001
13049
|
} catch {
|
|
12002
13050
|
}
|
|
@@ -12005,7 +13053,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12005
13053
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
12006
13054
|
removed.resetRemoteHashCache = true;
|
|
12007
13055
|
} else if (remoteHashes.invalidIds.length) {
|
|
12008
|
-
const cache = parseCache(JSON.parse(await
|
|
13056
|
+
const cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
|
|
12009
13057
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
12010
13058
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
12011
13059
|
await saveCache(remotePath, cache);
|
|
@@ -12023,7 +13071,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12023
13071
|
async function inspectContentCache(contentDir, referenced) {
|
|
12024
13072
|
const report = {
|
|
12025
13073
|
path: contentDir,
|
|
12026
|
-
exists:
|
|
13074
|
+
exists: existsSync24(contentDir),
|
|
12027
13075
|
files: 0,
|
|
12028
13076
|
bytes: 0,
|
|
12029
13077
|
valid: 0,
|
|
@@ -12044,12 +13092,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12044
13092
|
continue;
|
|
12045
13093
|
}
|
|
12046
13094
|
report.files++;
|
|
12047
|
-
const file =
|
|
13095
|
+
const file = path32.join(contentDir, entry.name);
|
|
12048
13096
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
12049
13097
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
12050
13098
|
let bytes;
|
|
12051
13099
|
try {
|
|
12052
|
-
bytes = await
|
|
13100
|
+
bytes = await readFile22(file);
|
|
12053
13101
|
report.bytes += bytes.length;
|
|
12054
13102
|
} catch (err) {
|
|
12055
13103
|
report.invalid.push({
|
|
@@ -12087,7 +13135,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12087
13135
|
async function inspectRemoteHashCache(remotePath) {
|
|
12088
13136
|
const report = {
|
|
12089
13137
|
path: remotePath,
|
|
12090
|
-
exists:
|
|
13138
|
+
exists: existsSync24(remotePath),
|
|
12091
13139
|
entries: 0,
|
|
12092
13140
|
valid: 0,
|
|
12093
13141
|
invalidIds: [],
|
|
@@ -12096,7 +13144,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
12096
13144
|
if (!report.exists) return report;
|
|
12097
13145
|
let cache;
|
|
12098
13146
|
try {
|
|
12099
|
-
cache = parseCache(JSON.parse(await
|
|
13147
|
+
cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
|
|
12100
13148
|
} catch (err) {
|
|
12101
13149
|
report.error = err instanceof Error ? err.message : String(err);
|
|
12102
13150
|
return report;
|
|
@@ -12120,7 +13168,7 @@ async function runCache(args) {
|
|
|
12120
13168
|
const size = report.content.bytes < 1024 ? `${report.content.bytes} B` : `${(report.content.bytes / 1024).toFixed(1)} KB`;
|
|
12121
13169
|
log(` content cache: ${report.content.path}`);
|
|
12122
13170
|
log(
|
|
12123
|
-
report.content.exists ? ` ${report.content.valid} valid PNG(s), ${report.content.referenced} referenced, ${report.content.unreferenced.length} unreferenced
|
|
13171
|
+
report.content.exists ? ` ${report.content.valid} valid PNG(s), ${report.content.referenced} referenced, ${report.content.unreferenced.length} unreferenced, ${size}` : " not created yet"
|
|
12124
13172
|
);
|
|
12125
13173
|
if (report.content.missingReferenced.length) {
|
|
12126
13174
|
log(` ${report.content.missingReferenced.length} lockfile hash(es) are not locally cached`);
|
|
@@ -12160,7 +13208,7 @@ async function runAccept(args) {
|
|
|
12160
13208
|
let intact = true;
|
|
12161
13209
|
for (const output of entry.outputs) {
|
|
12162
13210
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
12163
|
-
if (!
|
|
13211
|
+
if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
|
|
12164
13212
|
intact = false;
|
|
12165
13213
|
break;
|
|
12166
13214
|
}
|
|
@@ -12183,7 +13231,7 @@ async function runPrune(args) {
|
|
|
12183
13231
|
const declared = new Set((await resolveSpecs(loaded)).map((s) => lockKey(s.styleId, s.assetId)));
|
|
12184
13232
|
const orphans = Object.keys(lock.entries).filter((key) => !declared.has(key)).sort();
|
|
12185
13233
|
if (!orphans.length) {
|
|
12186
|
-
log(` every lock entry is declared by the manifest
|
|
13234
|
+
log(` every lock entry is declared by the manifest; nothing to prune`);
|
|
12187
13235
|
return;
|
|
12188
13236
|
}
|
|
12189
13237
|
log(`
|
|
@@ -12201,7 +13249,7 @@ ${orphans.length} lock entr(ies) the manifest no longer declares:`);
|
|
|
12201
13249
|
}
|
|
12202
13250
|
log(`
|
|
12203
13251
|
This drops their provenance from the lockfile. The art on disk is`);
|
|
12204
|
-
log(` untouched, and nothing is deleted from your provider account
|
|
13252
|
+
log(` untouched, and nothing is deleted from your provider account, but`);
|
|
12205
13253
|
log(` the link from those files back to the objects that made them is gone,`);
|
|
12206
13254
|
log(` and the objects will read as unclaimed the next time you run salvage.`);
|
|
12207
13255
|
if (!await confirm(` Remove ${orphans.length} lock entr(ies)?`, args.yes)) {
|
|
@@ -12215,42 +13263,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
12215
13263
|
}
|
|
12216
13264
|
|
|
12217
13265
|
// src/cli/commands/init.ts
|
|
12218
|
-
import
|
|
12219
|
-
import { existsSync as
|
|
13266
|
+
import path33 from "path";
|
|
13267
|
+
import { existsSync as existsSync26 } from "fs";
|
|
12220
13268
|
async function runInit(args) {
|
|
12221
13269
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
12222
|
-
const root =
|
|
12223
|
-
if (!
|
|
13270
|
+
const root = path33.resolve(args.from);
|
|
13271
|
+
if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
|
|
12224
13272
|
const generator = args.generator ?? "map";
|
|
12225
13273
|
if (generator !== "1dir" && generator !== "map") {
|
|
12226
13274
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
12227
13275
|
}
|
|
12228
|
-
const target =
|
|
13276
|
+
const target = path33.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
12229
13277
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
12230
13278
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
12231
13279
|
const manifest = buildManifest(
|
|
12232
|
-
args.name ??
|
|
13280
|
+
args.name ?? path33.basename(path33.dirname(target)),
|
|
12233
13281
|
args.styles[0] ?? "base",
|
|
12234
13282
|
generator,
|
|
12235
|
-
|
|
13283
|
+
path33.relative(path33.dirname(target), root) || ".",
|
|
12236
13284
|
assets
|
|
12237
13285
|
);
|
|
12238
13286
|
await writeManifestFile(target, manifest);
|
|
12239
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
13287
|
+
log(` scanned ${assets.length} PNG(s) under ${path33.relative(process.cwd(), root)}`);
|
|
12240
13288
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
12241
|
-
log(` wrote ${
|
|
13289
|
+
log(` wrote ${path33.relative(process.cwd(), target)}`);
|
|
12242
13290
|
log(`
|
|
12243
13291
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
12244
13292
|
log(` PixelLab account instead of inventing them:`);
|
|
12245
13293
|
log(`
|
|
12246
|
-
pixelkiln adopt --manifest ${
|
|
13294
|
+
pixelkiln adopt --manifest ${path33.relative(process.cwd(), target)} --write-prompts
|
|
12247
13295
|
`);
|
|
12248
13296
|
}
|
|
12249
13297
|
|
|
12250
13298
|
// src/pipeline/doctor.ts
|
|
12251
|
-
import { constants, existsSync as
|
|
13299
|
+
import { constants, existsSync as existsSync27 } from "fs";
|
|
12252
13300
|
import { access, stat as stat4 } from "fs/promises";
|
|
12253
|
-
import
|
|
13301
|
+
import path34 from "path";
|
|
12254
13302
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
12255
13303
|
const checks = [];
|
|
12256
13304
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -12261,8 +13309,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12261
13309
|
);
|
|
12262
13310
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
12263
13311
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
12264
|
-
|
|
12265
|
-
...spec.quality ? [
|
|
13312
|
+
path34.dirname(spec.outFile),
|
|
13313
|
+
...spec.quality ? [path34.dirname(spec.quality.outFile)] : []
|
|
12266
13314
|
]))];
|
|
12267
13315
|
const unwritable = [];
|
|
12268
13316
|
for (const dir of dirs) {
|
|
@@ -12285,7 +13333,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12285
13333
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
12286
13334
|
const output = entry.outputs[index];
|
|
12287
13335
|
const spec = specByKey.get(key);
|
|
12288
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
13336
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path34.dirname(lockPath));
|
|
12289
13337
|
const owner = outputOwners.get(absolute);
|
|
12290
13338
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
12291
13339
|
outputOwners.set(absolute, key);
|
|
@@ -12295,12 +13343,12 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12295
13343
|
add(
|
|
12296
13344
|
"lock-outputs",
|
|
12297
13345
|
"error",
|
|
12298
|
-
`lock entries share output paths: ${duplicateOutputs.slice(0, 3).join(", ")}${duplicateOutputs.length > 3 ? `, and ${duplicateOutputs.length - 3} more` : ""}
|
|
13346
|
+
`lock entries share output paths: ${duplicateOutputs.slice(0, 3).join(", ")}${duplicateOutputs.length > 3 ? `, and ${duplicateOutputs.length - 3} more` : ""}; run \`pixelkiln prune\` if the manifest no longer declares them`
|
|
12299
13347
|
);
|
|
12300
13348
|
} else {
|
|
12301
13349
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
12302
13350
|
}
|
|
12303
|
-
const cacheDir =
|
|
13351
|
+
const cacheDir = path34.join(path34.dirname(lockPath), ".pixelkiln", "cache");
|
|
12304
13352
|
const stranded = [];
|
|
12305
13353
|
const invalidState = [];
|
|
12306
13354
|
const staleMapUrls = [];
|
|
@@ -12316,11 +13364,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12316
13364
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
12317
13365
|
let hasCache = false;
|
|
12318
13366
|
for (const output of entry.outputs) {
|
|
12319
|
-
const cached =
|
|
13367
|
+
const cached = path34.join(
|
|
12320
13368
|
cacheDir,
|
|
12321
13369
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
12322
13370
|
);
|
|
12323
|
-
if (
|
|
13371
|
+
if (existsSync27(cached) && await sha256File(cached) === output.sha256) {
|
|
12324
13372
|
hasCache = true;
|
|
12325
13373
|
break;
|
|
12326
13374
|
}
|
|
@@ -12416,16 +13464,16 @@ async function nearestExistingDirectory(start) {
|
|
|
12416
13464
|
if ((await stat4(current)).isDirectory()) return current;
|
|
12417
13465
|
} catch {
|
|
12418
13466
|
}
|
|
12419
|
-
const parent =
|
|
13467
|
+
const parent = path34.dirname(current);
|
|
12420
13468
|
if (parent === current) return current;
|
|
12421
13469
|
current = parent;
|
|
12422
13470
|
}
|
|
12423
13471
|
}
|
|
12424
13472
|
|
|
12425
13473
|
// src/pipeline/audit.ts
|
|
12426
|
-
import { readFile as
|
|
12427
|
-
import { existsSync as
|
|
12428
|
-
import
|
|
13474
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
13475
|
+
import { existsSync as existsSync28 } from "fs";
|
|
13476
|
+
import path35 from "path";
|
|
12429
13477
|
function paletteDistance(asset, reference) {
|
|
12430
13478
|
if (!asset.length || !reference.length) return 0;
|
|
12431
13479
|
let total = 0;
|
|
@@ -12461,12 +13509,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12461
13509
|
const unreadable = [];
|
|
12462
13510
|
for (const spec of mine) {
|
|
12463
13511
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
12464
|
-
if (!
|
|
13512
|
+
if (!existsSync28(output.absolutePath)) {
|
|
12465
13513
|
missing.push(output.id);
|
|
12466
13514
|
continue;
|
|
12467
13515
|
}
|
|
12468
13516
|
try {
|
|
12469
|
-
const png = decodePng(await
|
|
13517
|
+
const png = decodePng(await readFile23(output.absolutePath));
|
|
12470
13518
|
const palette = extractPalette(png, 12);
|
|
12471
13519
|
assets.push({
|
|
12472
13520
|
assetId: spec.assetId,
|
|
@@ -12490,10 +13538,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12490
13538
|
let referenceFromStyleImages = false;
|
|
12491
13539
|
const refPalettes = [];
|
|
12492
13540
|
for (const img of style.styleImages) {
|
|
12493
|
-
const abs =
|
|
12494
|
-
if (!
|
|
13541
|
+
const abs = path35.resolve(loaded.root, img.path);
|
|
13542
|
+
if (!existsSync28(abs)) continue;
|
|
12495
13543
|
try {
|
|
12496
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
13544
|
+
refPalettes.push(extractPalette(decodePng(await readFile23(abs)), 12));
|
|
12497
13545
|
} catch {
|
|
12498
13546
|
}
|
|
12499
13547
|
}
|
|
@@ -12677,12 +13725,12 @@ async function runAudit(args) {
|
|
|
12677
13725
|
reports.push({ audit, evaluation });
|
|
12678
13726
|
if (args.json) continue;
|
|
12679
13727
|
log(`
|
|
12680
|
-
${styleId}
|
|
13728
|
+
${styleId}: ${audit.assets.length} asset(s) measured`);
|
|
12681
13729
|
log(
|
|
12682
13730
|
` reference palette: ${audit.referenceFromStyleImages ? "style images" : "the set's own average"} ${audit.reference.slice(0, 6).map(hex).join(" ")}`
|
|
12683
13731
|
);
|
|
12684
13732
|
if (!audit.referenceFromStyleImages) {
|
|
12685
|
-
log(` (no styleImages set
|
|
13733
|
+
log(` (no styleImages set; this finds outliers but cannot tell you the whole set drifted)`);
|
|
12686
13734
|
}
|
|
12687
13735
|
const offIds = new Set(evaluation.outliers);
|
|
12688
13736
|
log(`
|
|
@@ -12713,24 +13761,24 @@ async function runAudit(args) {
|
|
|
12713
13761
|
}
|
|
12714
13762
|
|
|
12715
13763
|
// src/cli/commands/pack.ts
|
|
12716
|
-
import
|
|
12717
|
-
import { readFile as
|
|
13764
|
+
import path37 from "path";
|
|
13765
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
12718
13766
|
|
|
12719
13767
|
// src/pipeline/pack.ts
|
|
12720
13768
|
import { readFileSync as readFileSync5 } from "fs";
|
|
12721
|
-
import
|
|
13769
|
+
import path36 from "path";
|
|
12722
13770
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
12723
13771
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
12724
13772
|
if (!Array.isArray(raw) || !raw.length) {
|
|
12725
13773
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
12726
13774
|
}
|
|
12727
|
-
const baseDir =
|
|
13775
|
+
const baseDir = path36.dirname(path36.resolve(inputsFilePath));
|
|
12728
13776
|
return raw.map((entry, i) => {
|
|
12729
13777
|
const e = entry;
|
|
12730
13778
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
12731
13779
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
12732
13780
|
}
|
|
12733
|
-
return { id: e.id, path:
|
|
13781
|
+
return { id: e.id, path: path36.resolve(baseDir, e.path) };
|
|
12734
13782
|
});
|
|
12735
13783
|
}
|
|
12736
13784
|
function packSprites(inputs, options = {}) {
|
|
@@ -12762,7 +13810,7 @@ function packSprites(inputs, options = {}) {
|
|
|
12762
13810
|
}
|
|
12763
13811
|
}
|
|
12764
13812
|
if (!sprites.length) {
|
|
12765
|
-
throw new Error(`packSprites: nothing readable
|
|
13813
|
+
throw new Error(`packSprites: nothing readable; ${skipped.length} skipped.`);
|
|
12766
13814
|
}
|
|
12767
13815
|
const cellW = Math.max(...sprites.map((s) => s.width));
|
|
12768
13816
|
const cellH = Math.max(...sprites.map((s) => s.height));
|
|
@@ -12802,7 +13850,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12802
13850
|
}
|
|
12803
13851
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
12804
13852
|
id,
|
|
12805
|
-
path:
|
|
13853
|
+
path: path36.resolve(source)
|
|
12806
13854
|
}));
|
|
12807
13855
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
12808
13856
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -12826,7 +13874,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12826
13874
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
12827
13875
|
const source = sources[id];
|
|
12828
13876
|
if (source && outputs.length <= 1) {
|
|
12829
|
-
inputs.push({ id, path:
|
|
13877
|
+
inputs.push({ id, path: path36.resolve(manifestDir, source) });
|
|
12830
13878
|
continue;
|
|
12831
13879
|
}
|
|
12832
13880
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -12848,20 +13896,22 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12848
13896
|
}
|
|
12849
13897
|
for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
|
|
12850
13898
|
if (entry.outputs.length > 1) {
|
|
12851
|
-
const
|
|
13899
|
+
const recordedFps = frameSetFps(entry);
|
|
13900
|
+
const isFrames = entry.generator === "frames" || recordedFps !== null;
|
|
13901
|
+
const fps = isFrames ? recordedFps ?? DEFAULT_FRAME_SET_FPS : null;
|
|
12852
13902
|
sets.push({
|
|
12853
13903
|
id,
|
|
12854
|
-
kind:
|
|
13904
|
+
kind: isFrames ? "frames" : "members",
|
|
12855
13905
|
...fps ? { fps } : {},
|
|
12856
13906
|
frames: selected.map((output) => output.id)
|
|
12857
13907
|
});
|
|
12858
13908
|
}
|
|
12859
13909
|
}
|
|
12860
13910
|
for (const [id, source] of Object.entries(sources)) {
|
|
12861
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
13911
|
+
if (!locked.has(id)) inputs.push({ id, path: path36.resolve(manifestDir, source) });
|
|
12862
13912
|
}
|
|
12863
13913
|
if (!inputs.length) {
|
|
12864
|
-
throw new Error(`No readable sprites for style "${styleId}"
|
|
13914
|
+
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
12865
13915
|
}
|
|
12866
13916
|
const packed = packSprites(inputs, options);
|
|
12867
13917
|
const packedIds = new Set(packed.atlas.frames.map((frame) => frame.id));
|
|
@@ -12884,7 +13934,7 @@ function mountSprites(placements, options) {
|
|
|
12884
13934
|
const owner = byCell.get(key);
|
|
12885
13935
|
if (owner) {
|
|
12886
13936
|
throw new Error(
|
|
12887
|
-
`mountSprites: "${p.id}" and "${owner}" both claim cell ${key}
|
|
13937
|
+
`mountSprites: "${p.id}" and "${owner}" both claim cell ${key}; a cell has exactly one owner`
|
|
12888
13938
|
);
|
|
12889
13939
|
}
|
|
12890
13940
|
byCell.set(key, p.id);
|
|
@@ -12911,7 +13961,7 @@ function mountSprites(placements, options) {
|
|
|
12911
13961
|
}
|
|
12912
13962
|
}
|
|
12913
13963
|
if (!loaded.length) {
|
|
12914
|
-
throw new Error(`mountSprites: nothing readable
|
|
13964
|
+
throw new Error(`mountSprites: nothing readable; ${skipped.length} skipped.`);
|
|
12915
13965
|
}
|
|
12916
13966
|
const maxCol = Math.max(...loaded.map((l) => l.p.cell[0]));
|
|
12917
13967
|
const maxRow = Math.max(...loaded.map((l) => l.p.cell[1]));
|
|
@@ -12966,7 +14016,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
12966
14016
|
const source = sources[id];
|
|
12967
14017
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
12968
14018
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
12969
|
-
placements.push({ id, path:
|
|
14019
|
+
placements.push({ id, path: path36.resolve(manifestDir, source), cell });
|
|
12970
14020
|
continue;
|
|
12971
14021
|
}
|
|
12972
14022
|
if (source && entry) {
|
|
@@ -12999,7 +14049,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
12999
14049
|
`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}`
|
|
13000
14050
|
);
|
|
13001
14051
|
}
|
|
13002
|
-
const basePng = mount.base ? readFileSync5(
|
|
14052
|
+
const basePng = mount.base ? readFileSync5(path36.resolve(manifestDir, mount.base)) : void 0;
|
|
13003
14053
|
const mounted = mountSprites(placements, {
|
|
13004
14054
|
cellWidth: mount.cellWidth,
|
|
13005
14055
|
cellHeight: mount.cellHeight,
|
|
@@ -13009,7 +14059,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
13009
14059
|
...mounted,
|
|
13010
14060
|
sources: mount.base ? [{
|
|
13011
14061
|
id: "$base",
|
|
13012
|
-
path:
|
|
14062
|
+
path: path36.resolve(manifestDir, mount.base),
|
|
13013
14063
|
sha256: sha256(basePng),
|
|
13014
14064
|
included: true
|
|
13015
14065
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -13376,12 +14426,12 @@ async function runPack(args) {
|
|
|
13376
14426
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
13377
14427
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
13378
14428
|
}
|
|
13379
|
-
const raw = JSON.parse(await
|
|
14429
|
+
const raw = JSON.parse(await readFile24(path37.resolve(args.inputs), "utf8"));
|
|
13380
14430
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13381
14431
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13382
14432
|
const format = sheetFormat(args);
|
|
13383
|
-
const base =
|
|
13384
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14433
|
+
const base = path37.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
14434
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
|
|
13385
14435
|
const outputs = [
|
|
13386
14436
|
{ path: `${base}.png`, data: png },
|
|
13387
14437
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -13392,9 +14442,9 @@ async function runPack(args) {
|
|
|
13392
14442
|
options: { columns: args.columns ?? null, format, order: "id", style: null }
|
|
13393
14443
|
}, { force: args.force });
|
|
13394
14444
|
log(
|
|
13395
|
-
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)
|
|
14445
|
+
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
13396
14446
|
);
|
|
13397
|
-
log(` ${
|
|
14447
|
+
log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
13398
14448
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13399
14449
|
return;
|
|
13400
14450
|
}
|
|
@@ -13402,7 +14452,7 @@ async function runPack(args) {
|
|
|
13402
14452
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
13403
14453
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
13404
14454
|
}
|
|
13405
|
-
const manifestDir =
|
|
14455
|
+
const manifestDir = path37.dirname(path37.resolve(args.manifest));
|
|
13406
14456
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
13407
14457
|
for (const styleId of styleIds) {
|
|
13408
14458
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -13415,9 +14465,9 @@ async function runPack(args) {
|
|
|
13415
14465
|
sources: manifestSources(loaded.manifest, styleId)
|
|
13416
14466
|
});
|
|
13417
14467
|
const style = loaded.manifest.styles[styleId];
|
|
13418
|
-
const base = args.out ?
|
|
14468
|
+
const base = args.out ? path37.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path37.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
13419
14469
|
const format = sheetFormat(args);
|
|
13420
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14470
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
|
|
13421
14471
|
const outputs = [
|
|
13422
14472
|
{ path: `${base}.png`, data: png },
|
|
13423
14473
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -13446,15 +14496,15 @@ async function runPack(args) {
|
|
|
13446
14496
|
}
|
|
13447
14497
|
}, { force: args.force });
|
|
13448
14498
|
log(
|
|
13449
|
-
` ${styleId}
|
|
14499
|
+
` ${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})`
|
|
13450
14500
|
);
|
|
13451
|
-
log(` ${
|
|
14501
|
+
log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
13452
14502
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13453
14503
|
}
|
|
13454
14504
|
}
|
|
13455
14505
|
async function runMount(args) {
|
|
13456
14506
|
const { loaded, lock } = await openProject2(args);
|
|
13457
|
-
const manifestDir =
|
|
14507
|
+
const manifestDir = path37.dirname(path37.resolve(args.manifest));
|
|
13458
14508
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
13459
14509
|
for (const styleId of styleIds) {
|
|
13460
14510
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -13493,9 +14543,9 @@ async function runMount(args) {
|
|
|
13493
14543
|
sources,
|
|
13494
14544
|
outputRoles
|
|
13495
14545
|
);
|
|
13496
|
-
const out =
|
|
14546
|
+
const out = path37.resolve(manifestDir, style.mount.out);
|
|
13497
14547
|
const format = sheetFormat(args);
|
|
13498
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14548
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(out) });
|
|
13499
14549
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
13500
14550
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
13501
14551
|
const outputs = [
|
|
@@ -13515,7 +14565,7 @@ async function runMount(args) {
|
|
|
13515
14565
|
await provenanceFile("$lock", args.lock),
|
|
13516
14566
|
...qualityRecords,
|
|
13517
14567
|
...artifactSources.filter(
|
|
13518
|
-
(source) => source.id !== "$base" ||
|
|
14568
|
+
(source) => source.id !== "$base" || path37.resolve(source.path) !== out
|
|
13519
14569
|
)
|
|
13520
14570
|
],
|
|
13521
14571
|
options: {
|
|
@@ -13527,16 +14577,16 @@ async function runMount(args) {
|
|
|
13527
14577
|
}
|
|
13528
14578
|
}, { force: args.force });
|
|
13529
14579
|
log(
|
|
13530
|
-
` ${styleId}
|
|
14580
|
+
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
13531
14581
|
);
|
|
13532
|
-
log(` ${
|
|
14582
|
+
log(` ${path37.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
13533
14583
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13534
14584
|
}
|
|
13535
14585
|
}
|
|
13536
14586
|
async function runExport(args) {
|
|
13537
14587
|
const { loaded, specs, lock } = await openProject2(args);
|
|
13538
14588
|
const format = args.format ?? "generic";
|
|
13539
|
-
const manifestDir =
|
|
14589
|
+
const manifestDir = path37.dirname(path37.resolve(args.manifest));
|
|
13540
14590
|
const selected = specs.filter((spec) => {
|
|
13541
14591
|
if (spec.generator !== "tiles") return false;
|
|
13542
14592
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -13552,12 +14602,12 @@ async function runExport(args) {
|
|
|
13552
14602
|
for (const spec of selected) {
|
|
13553
14603
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
13554
14604
|
const style = loaded.manifest.styles[spec.styleId];
|
|
13555
|
-
const defaultBase =
|
|
13556
|
-
const base = args.out ?
|
|
14605
|
+
const defaultBase = path37.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
14606
|
+
const base = args.out ? path37.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
13557
14607
|
const result = exportTileset(entry, spec, {
|
|
13558
14608
|
format,
|
|
13559
14609
|
manifestDir,
|
|
13560
|
-
imageName:
|
|
14610
|
+
imageName: path37.basename(`${base}.png`),
|
|
13561
14611
|
columns: args.columns
|
|
13562
14612
|
});
|
|
13563
14613
|
const outputs = [
|
|
@@ -13575,17 +14625,17 @@ async function runExport(args) {
|
|
|
13575
14625
|
asset: spec.assetId,
|
|
13576
14626
|
columns: args.columns ?? null,
|
|
13577
14627
|
format,
|
|
13578
|
-
image:
|
|
14628
|
+
image: path37.basename(`${base}.png`),
|
|
13579
14629
|
providerRules: result.generic.providerRules,
|
|
13580
14630
|
style: spec.styleId,
|
|
13581
14631
|
tileType: spec.tileType ?? null
|
|
13582
14632
|
}
|
|
13583
14633
|
}, { force: args.force });
|
|
13584
14634
|
log(
|
|
13585
|
-
` ${spec.styleId}/${spec.assetId}
|
|
14635
|
+
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
13586
14636
|
);
|
|
13587
14637
|
log(
|
|
13588
|
-
` ${
|
|
14638
|
+
` ${path37.relative(process.cwd(), base)}.png + ${path37.basename(base)}${result.extension} + .pixelkiln.json`
|
|
13589
14639
|
);
|
|
13590
14640
|
}
|
|
13591
14641
|
}
|
|
@@ -13598,19 +14648,19 @@ function sheetFormat(args) {
|
|
|
13598
14648
|
}
|
|
13599
14649
|
|
|
13600
14650
|
// src/cli/commands/quality.ts
|
|
13601
|
-
import
|
|
13602
|
-
import { readFile as
|
|
14651
|
+
import path39 from "path";
|
|
14652
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
13603
14653
|
|
|
13604
14654
|
// src/pipeline/quality-regression.ts
|
|
13605
|
-
import
|
|
13606
|
-
import { existsSync as
|
|
13607
|
-
import { readFile as
|
|
14655
|
+
import path38 from "path";
|
|
14656
|
+
import { existsSync as existsSync29 } from "fs";
|
|
14657
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
13608
14658
|
import { z as z9 } from "zod";
|
|
13609
14659
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
13610
14660
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
13611
14661
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
13612
14662
|
var QualityPathSchema = z9.string().min(1).refine(
|
|
13613
|
-
(value) => !
|
|
14663
|
+
(value) => !path38.posix.isAbsolute(value) && !value.includes("\\"),
|
|
13614
14664
|
"expected a portable relative path"
|
|
13615
14665
|
);
|
|
13616
14666
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -13757,10 +14807,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
13757
14807
|
};
|
|
13758
14808
|
}
|
|
13759
14809
|
async function measureImageQuality(file) {
|
|
13760
|
-
const absolute =
|
|
14810
|
+
const absolute = path38.resolve(file);
|
|
13761
14811
|
let bytes;
|
|
13762
14812
|
try {
|
|
13763
|
-
bytes = await
|
|
14813
|
+
bytes = await readFile25(absolute);
|
|
13764
14814
|
} catch (error) {
|
|
13765
14815
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
13766
14816
|
}
|
|
@@ -13779,11 +14829,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13779
14829
|
}
|
|
13780
14830
|
const ids = parsed.data.map((entry) => entry.id);
|
|
13781
14831
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
13782
|
-
const root =
|
|
14832
|
+
const root = path38.dirname(path38.resolve(inputsFilePath));
|
|
13783
14833
|
return parsed.data.map((entry) => ({
|
|
13784
14834
|
id: entry.id,
|
|
13785
|
-
file:
|
|
13786
|
-
...entry.record ? { record:
|
|
14835
|
+
file: path38.resolve(root, entry.path),
|
|
14836
|
+
...entry.record ? { record: path38.resolve(root, entry.record) } : {},
|
|
13787
14837
|
tolerances: QualityToleranceSchema.parse({
|
|
13788
14838
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
13789
14839
|
...entry.tolerances
|
|
@@ -13791,7 +14841,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13791
14841
|
}));
|
|
13792
14842
|
}
|
|
13793
14843
|
function portableRelative2(from, to) {
|
|
13794
|
-
return
|
|
14844
|
+
return path38.relative(from, path38.resolve(to)).split(path38.sep).join("/") || ".";
|
|
13795
14845
|
}
|
|
13796
14846
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
13797
14847
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -13806,20 +14856,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
13806
14856
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
13807
14857
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
13808
14858
|
}
|
|
13809
|
-
const outputs = record.outputs.map((output) =>
|
|
13810
|
-
if (!outputs.includes(
|
|
14859
|
+
const outputs = record.outputs.map((output) => path38.resolve(path38.dirname(recordPath), output.path));
|
|
14860
|
+
if (!outputs.includes(path38.resolve(imagePath))) {
|
|
13811
14861
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
13812
14862
|
}
|
|
13813
14863
|
}
|
|
13814
14864
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
13815
14865
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
13816
|
-
const absolute =
|
|
14866
|
+
const absolute = path38.resolve(baselinePath);
|
|
13817
14867
|
for (const input of inputs) {
|
|
13818
|
-
if (
|
|
14868
|
+
if (path38.resolve(input.file) === absolute || input.record && path38.resolve(input.record) === absolute) {
|
|
13819
14869
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
13820
14870
|
}
|
|
13821
14871
|
}
|
|
13822
|
-
const root =
|
|
14872
|
+
const root = path38.dirname(absolute);
|
|
13823
14873
|
const cases = [];
|
|
13824
14874
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
13825
14875
|
const expected = await measureImageQuality(input.file);
|
|
@@ -13846,8 +14896,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
13846
14896
|
cases
|
|
13847
14897
|
});
|
|
13848
14898
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
13849
|
-
if (
|
|
13850
|
-
const current = await
|
|
14899
|
+
if (existsSync29(absolute) && !options.force) {
|
|
14900
|
+
const current = await readFile25(absolute);
|
|
13851
14901
|
if (!current.equals(data)) {
|
|
13852
14902
|
throw new OverwriteRefusedError(
|
|
13853
14903
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -13859,10 +14909,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
13859
14909
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
13860
14910
|
}
|
|
13861
14911
|
async function readQualityBaseline(baselinePath) {
|
|
13862
|
-
const absolute =
|
|
14912
|
+
const absolute = path38.resolve(baselinePath);
|
|
13863
14913
|
let raw;
|
|
13864
14914
|
try {
|
|
13865
|
-
raw = JSON.parse(await
|
|
14915
|
+
raw = JSON.parse(await readFile25(absolute, "utf8"));
|
|
13866
14916
|
} catch (error) {
|
|
13867
14917
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
13868
14918
|
}
|
|
@@ -13932,12 +14982,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
13932
14982
|
return { violations, warnings };
|
|
13933
14983
|
}
|
|
13934
14984
|
async function checkQualityBaseline(baselinePath) {
|
|
13935
|
-
const absolute =
|
|
14985
|
+
const absolute = path38.resolve(baselinePath);
|
|
13936
14986
|
const baseline = await readQualityBaseline(absolute);
|
|
13937
|
-
const root =
|
|
14987
|
+
const root = path38.dirname(absolute);
|
|
13938
14988
|
const cases = [];
|
|
13939
14989
|
for (const entry of baseline.cases) {
|
|
13940
|
-
const file =
|
|
14990
|
+
const file = path38.resolve(root, entry.file);
|
|
13941
14991
|
let actual = null;
|
|
13942
14992
|
const violations = [];
|
|
13943
14993
|
const warnings = [];
|
|
@@ -13950,7 +15000,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
13950
15000
|
violations.push(message4(error));
|
|
13951
15001
|
}
|
|
13952
15002
|
if (entry.record) {
|
|
13953
|
-
const recordPath =
|
|
15003
|
+
const recordPath = path38.resolve(root, entry.record.path);
|
|
13954
15004
|
try {
|
|
13955
15005
|
const recordHash = await sha256File(recordPath);
|
|
13956
15006
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -13992,15 +15042,15 @@ async function runQuality(args) {
|
|
|
13992
15042
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
13993
15043
|
let raw;
|
|
13994
15044
|
try {
|
|
13995
|
-
raw = JSON.parse(await
|
|
15045
|
+
raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
|
|
13996
15046
|
} catch (error) {
|
|
13997
15047
|
throw new Error(
|
|
13998
|
-
`Could not read quality inputs ${
|
|
15048
|
+
`Could not read quality inputs ${path39.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13999
15049
|
{ cause: error }
|
|
14000
15050
|
);
|
|
14001
15051
|
}
|
|
14002
15052
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
14003
|
-
if (
|
|
15053
|
+
if (path39.resolve(args.inputs) === path39.resolve(args.out)) {
|
|
14004
15054
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
14005
15055
|
}
|
|
14006
15056
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -14013,7 +15063,7 @@ async function runQuality(args) {
|
|
|
14013
15063
|
}, null, 2));
|
|
14014
15064
|
} else {
|
|
14015
15065
|
log(
|
|
14016
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
15066
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path39.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
14017
15067
|
);
|
|
14018
15068
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
14019
15069
|
}
|
|
@@ -14039,12 +15089,12 @@ async function runQuality(args) {
|
|
|
14039
15089
|
}
|
|
14040
15090
|
|
|
14041
15091
|
// src/cli/commands/recipe.ts
|
|
14042
|
-
import
|
|
15092
|
+
import path41 from "path";
|
|
14043
15093
|
|
|
14044
15094
|
// src/recipes.ts
|
|
14045
|
-
import
|
|
14046
|
-
import { existsSync as
|
|
14047
|
-
import { readdir as readdir3, readFile as
|
|
15095
|
+
import path40 from "path";
|
|
15096
|
+
import { existsSync as existsSync30 } from "fs";
|
|
15097
|
+
import { readdir as readdir3, readFile as readFile27 } from "fs/promises";
|
|
14048
15098
|
import { fileURLToPath } from "url";
|
|
14049
15099
|
import { z as z10 } from "zod";
|
|
14050
15100
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -14053,7 +15103,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
14053
15103
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
14054
15104
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
14055
15105
|
var PortablePathSchema = z10.string().min(1).refine((value) => {
|
|
14056
|
-
if (value.includes("\\") ||
|
|
15106
|
+
if (value.includes("\\") || path40.posix.isAbsolute(value)) return false;
|
|
14057
15107
|
const parts = value.split("/");
|
|
14058
15108
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
14059
15109
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -14229,10 +15279,10 @@ function message5(error) {
|
|
|
14229
15279
|
return error instanceof Error ? error.message : String(error);
|
|
14230
15280
|
}
|
|
14231
15281
|
async function readRecipeFile(recipePath, bundled) {
|
|
14232
|
-
const absolute =
|
|
15282
|
+
const absolute = path40.resolve(recipePath);
|
|
14233
15283
|
let raw;
|
|
14234
15284
|
try {
|
|
14235
|
-
raw = JSON.parse(await
|
|
15285
|
+
raw = JSON.parse(await readFile27(absolute, "utf8"));
|
|
14236
15286
|
} catch (error) {
|
|
14237
15287
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
14238
15288
|
}
|
|
@@ -14243,13 +15293,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
14243
15293
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
14244
15294
|
);
|
|
14245
15295
|
}
|
|
14246
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
15296
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path40.dirname(absolute), bundled };
|
|
14247
15297
|
}
|
|
14248
15298
|
async function walkRecipeFiles(root) {
|
|
14249
|
-
if (!
|
|
15299
|
+
if (!existsSync30(root)) return [];
|
|
14250
15300
|
const out = [];
|
|
14251
15301
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
14252
|
-
const resolved =
|
|
15302
|
+
const resolved = path40.join(root, entry.name);
|
|
14253
15303
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
14254
15304
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
14255
15305
|
}
|
|
@@ -14260,7 +15310,7 @@ function bundledRecipeRoot() {
|
|
|
14260
15310
|
}
|
|
14261
15311
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
14262
15312
|
const recipes = await Promise.all(
|
|
14263
|
-
(await walkRecipeFiles(
|
|
15313
|
+
(await walkRecipeFiles(path40.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
14264
15314
|
);
|
|
14265
15315
|
return recipes.sort(
|
|
14266
15316
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -14283,9 +15333,9 @@ function parseSelector(selector) {
|
|
|
14283
15333
|
return { id: match[1], version: match[2] };
|
|
14284
15334
|
}
|
|
14285
15335
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
14286
|
-
const local =
|
|
14287
|
-
if (
|
|
14288
|
-
const recipePath =
|
|
15336
|
+
const local = path40.resolve(target);
|
|
15337
|
+
if (existsSync30(local)) {
|
|
15338
|
+
const recipePath = path40.basename(local) === RECIPE_FILE ? local : path40.join(local, RECIPE_FILE);
|
|
14289
15339
|
return readRecipeFile(recipePath, false);
|
|
14290
15340
|
}
|
|
14291
15341
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -14299,8 +15349,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
14299
15349
|
return matches[0];
|
|
14300
15350
|
}
|
|
14301
15351
|
async function verifyFile(root, file) {
|
|
14302
|
-
const absolute =
|
|
14303
|
-
if (!
|
|
15352
|
+
const absolute = path40.resolve(root, ...file.path.split("/"));
|
|
15353
|
+
if (!existsSync30(absolute)) {
|
|
14304
15354
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
14305
15355
|
}
|
|
14306
15356
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -14315,7 +15365,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
14315
15365
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
14316
15366
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
14317
15367
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
14318
|
-
const modelRoot = options.modelRoot ?
|
|
15368
|
+
const modelRoot = options.modelRoot ? path40.resolve(options.modelRoot) : null;
|
|
14319
15369
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
14320
15370
|
...await verifyFile(modelRoot, model),
|
|
14321
15371
|
source: model.source,
|
|
@@ -14349,7 +15399,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
14349
15399
|
};
|
|
14350
15400
|
}
|
|
14351
15401
|
function renderStyle(style, destination, cwd) {
|
|
14352
|
-
const portableDir =
|
|
15402
|
+
const portableDir = path40.relative(cwd, destination).split(path40.sep).join("/") || ".";
|
|
14353
15403
|
const replace = (value) => {
|
|
14354
15404
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
14355
15405
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -14361,25 +15411,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
14361
15411
|
return replace(style);
|
|
14362
15412
|
}
|
|
14363
15413
|
async function installRecipe(target, options = {}) {
|
|
14364
|
-
const cwd =
|
|
15414
|
+
const cwd = path40.resolve(options.cwd ?? process.cwd());
|
|
14365
15415
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
14366
15416
|
const verification = await verifyRecipe(loaded.path);
|
|
14367
15417
|
if (!verification.ok) {
|
|
14368
15418
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
14369
15419
|
}
|
|
14370
|
-
const destination = options.out ?
|
|
15420
|
+
const destination = options.out ? path40.resolve(cwd, options.out) : path40.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
14371
15421
|
const sources = [
|
|
14372
15422
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
14373
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
15423
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path40.join(loaded.dir, ...file.path.split("/")) }))
|
|
14374
15424
|
];
|
|
14375
15425
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
14376
|
-
path:
|
|
14377
|
-
data: await
|
|
15426
|
+
path: path40.join(destination, ...file.path.split("/")),
|
|
15427
|
+
data: await readFile27(file.source)
|
|
14378
15428
|
})));
|
|
14379
15429
|
if (!options.force) {
|
|
14380
15430
|
for (const file of files) {
|
|
14381
|
-
if (!
|
|
14382
|
-
const current = await
|
|
15431
|
+
if (!existsSync30(file.path)) continue;
|
|
15432
|
+
const current = await readFile27(file.path);
|
|
14383
15433
|
if (!current.equals(file.data)) {
|
|
14384
15434
|
throw new OverwriteRefusedError(
|
|
14385
15435
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -14473,14 +15523,14 @@ async function runRecipe(args) {
|
|
|
14473
15523
|
}, null, 2));
|
|
14474
15524
|
} else {
|
|
14475
15525
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
14476
|
-
log(` ${
|
|
15526
|
+
log(` ${path41.relative(process.cwd(), result.destination) || "."}`);
|
|
14477
15527
|
log(`
|
|
14478
15528
|
Add this entry under your manifest's styles object:`);
|
|
14479
15529
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
14480
15530
|
if (result.recipe.models.length) {
|
|
14481
15531
|
log(`
|
|
14482
15532
|
Models are not downloaded automatically. Verify them with:`);
|
|
14483
|
-
log(` pixelkiln recipe verify ${
|
|
15533
|
+
log(` pixelkiln recipe verify ${path41.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
14484
15534
|
}
|
|
14485
15535
|
}
|
|
14486
15536
|
return;
|
|
@@ -14488,7 +15538,7 @@ async function runRecipe(args) {
|
|
|
14488
15538
|
}
|
|
14489
15539
|
|
|
14490
15540
|
// src/cli/commands/refine.ts
|
|
14491
|
-
import
|
|
15541
|
+
import path42 from "path";
|
|
14492
15542
|
async function runRefine(args) {
|
|
14493
15543
|
if (args.from || args.subcommand === "approve") {
|
|
14494
15544
|
if (!args.from) {
|
|
@@ -14518,12 +15568,12 @@ async function runRefine(args) {
|
|
|
14518
15568
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
14519
15569
|
);
|
|
14520
15570
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
14521
|
-
log(` wrote ${
|
|
14522
|
-
log(` quality record: ${
|
|
15571
|
+
log(` wrote ${path42.relative(process.cwd(), result3.output)}`);
|
|
15572
|
+
log(` quality record: ${path42.relative(process.cwd(), result3.record)}`);
|
|
14523
15573
|
log(`
|
|
14524
15574
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
14525
15575
|
log(
|
|
14526
|
-
` pixelkiln refine approve --from ${
|
|
15576
|
+
` pixelkiln refine approve --from ${path42.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
14527
15577
|
);
|
|
14528
15578
|
}
|
|
14529
15579
|
return;
|
|
@@ -14551,7 +15601,7 @@ async function runRefine(args) {
|
|
|
14551
15601
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
14552
15602
|
else {
|
|
14553
15603
|
log(
|
|
14554
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
15604
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path42.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path42.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
14555
15605
|
);
|
|
14556
15606
|
}
|
|
14557
15607
|
return;
|
|
@@ -14560,7 +15610,7 @@ async function runRefine(args) {
|
|
|
14560
15610
|
if (args.json) {
|
|
14561
15611
|
log(JSON.stringify(result2, null, 2));
|
|
14562
15612
|
} else {
|
|
14563
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
15613
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path42.relative(process.cwd(), result2.output || result2.record)}`);
|
|
14564
15614
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
14565
15615
|
}
|
|
14566
15616
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -14606,7 +15656,7 @@ async function runRefine(args) {
|
|
|
14606
15656
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
14607
15657
|
if (item.state === "needs-approval") {
|
|
14608
15658
|
log(
|
|
14609
|
-
` pixelkiln refine approve --from ${
|
|
15659
|
+
` pixelkiln refine approve --from ${path42.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
14610
15660
|
);
|
|
14611
15661
|
}
|
|
14612
15662
|
}
|
|
@@ -14631,7 +15681,7 @@ async function runTools(args) {
|
|
|
14631
15681
|
return;
|
|
14632
15682
|
}
|
|
14633
15683
|
log(` release ${status2.release}`);
|
|
14634
|
-
log(` ${status2.installed ? "installed and verified" : status2.installedBytes ? `partial: ${status2.missing.length} of ${Object.keys(EDITOR_PIN.files).length} files missing` : "not installed"}
|
|
15684
|
+
log(` ${status2.installed ? "installed and verified" : status2.installedBytes ? `partial: ${status2.missing.length} of ${Object.keys(EDITOR_PIN.files).length} files missing` : "not installed"}, ${mb(status2.totalBytes)} in ${status2.dir}`);
|
|
14635
15685
|
if (!status2.installed) log(" run: pixelkiln tools install editor");
|
|
14636
15686
|
return;
|
|
14637
15687
|
}
|
|
@@ -14658,15 +15708,15 @@ async function runTools(args) {
|
|
|
14658
15708
|
}
|
|
14659
15709
|
|
|
14660
15710
|
// src/cli/commands/workspace.ts
|
|
14661
|
-
import
|
|
14662
|
-
import { existsSync as
|
|
15711
|
+
import path43 from "path";
|
|
15712
|
+
import { existsSync as existsSync31 } from "fs";
|
|
14663
15713
|
async function runWorkspace(args) {
|
|
14664
|
-
const workspacePath =
|
|
14665
|
-
const dir =
|
|
15714
|
+
const workspacePath = path43.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
15715
|
+
const dir = path43.dirname(workspacePath);
|
|
14666
15716
|
if (args.subcommand === "add") {
|
|
14667
|
-
const manifestPath =
|
|
15717
|
+
const manifestPath = path43.resolve(args.target);
|
|
14668
15718
|
const loadedTarget = await loadManifest(manifestPath);
|
|
14669
|
-
const lockPath = args.explicitLock ?
|
|
15719
|
+
const lockPath = args.explicitLock ? path43.resolve(args.explicitLock) : path43.join(path43.dirname(manifestPath), "pixelkiln.lock.json");
|
|
14670
15720
|
const ws = await loadWorkspace(workspacePath);
|
|
14671
15721
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
14672
15722
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -14686,19 +15736,19 @@ async function runWorkspace(args) {
|
|
|
14686
15736
|
...args.account ? { account: args.account } : {}
|
|
14687
15737
|
};
|
|
14688
15738
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
14689
|
-
log(` registered "${id}" in ${
|
|
15739
|
+
log(` registered "${id}" in ${path43.relative(process.cwd(), workspacePath)}`);
|
|
14690
15740
|
log(` manifest: ${project.manifest}`);
|
|
14691
15741
|
log(` lock: ${project.lock}`);
|
|
14692
|
-
if (!
|
|
15742
|
+
if (!existsSync31(lockPath)) {
|
|
14693
15743
|
log(
|
|
14694
|
-
` warning: no lockfile there yet
|
|
15744
|
+
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
14695
15745
|
);
|
|
14696
15746
|
}
|
|
14697
15747
|
return;
|
|
14698
15748
|
}
|
|
14699
15749
|
if (args.subcommand === "remove") {
|
|
14700
15750
|
const ws = await loadWorkspace(workspacePath);
|
|
14701
|
-
const resolvedTarget =
|
|
15751
|
+
const resolvedTarget = path43.resolve(args.target);
|
|
14702
15752
|
const match = ws.projects.find(
|
|
14703
15753
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
14704
15754
|
);
|
|
@@ -14709,11 +15759,11 @@ async function runWorkspace(args) {
|
|
|
14709
15759
|
version: 1,
|
|
14710
15760
|
projects: ws.projects.filter((p) => p !== match)
|
|
14711
15761
|
});
|
|
14712
|
-
log(` removed "${match.id}" from ${
|
|
15762
|
+
log(` removed "${match.id}" from ${path43.relative(process.cwd(), workspacePath)}`);
|
|
14713
15763
|
return;
|
|
14714
15764
|
}
|
|
14715
15765
|
if (args.subcommand === "list") {
|
|
14716
|
-
if (!
|
|
15766
|
+
if (!existsSync31(workspacePath)) {
|
|
14717
15767
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14718
15768
|
}
|
|
14719
15769
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14721,9 +15771,9 @@ async function runWorkspace(args) {
|
|
|
14721
15771
|
if (args.json) {
|
|
14722
15772
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
14723
15773
|
} else if (!ws.projects.length) {
|
|
14724
|
-
log(` no projects registered in ${
|
|
15774
|
+
log(` no projects registered in ${path43.relative(process.cwd(), workspacePath)}`);
|
|
14725
15775
|
} else {
|
|
14726
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
15776
|
+
log(` ${ws.projects.length} project(s) in ${path43.relative(process.cwd(), workspacePath)}:`);
|
|
14727
15777
|
for (const p of ws.projects) {
|
|
14728
15778
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
14729
15779
|
}
|
|
@@ -14733,7 +15783,7 @@ async function runWorkspace(args) {
|
|
|
14733
15783
|
return;
|
|
14734
15784
|
}
|
|
14735
15785
|
if (args.subcommand === "status") {
|
|
14736
|
-
if (!
|
|
15786
|
+
if (!existsSync31(workspacePath)) {
|
|
14737
15787
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14738
15788
|
}
|
|
14739
15789
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14741,11 +15791,11 @@ async function runWorkspace(args) {
|
|
|
14741
15791
|
if (args.json) {
|
|
14742
15792
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
14743
15793
|
} else {
|
|
14744
|
-
log(` workspace: ${
|
|
15794
|
+
log(` workspace: ${path43.relative(process.cwd(), workspacePath)}`);
|
|
14745
15795
|
for (const p of report.projects) {
|
|
14746
15796
|
if (p.error) {
|
|
14747
15797
|
log(`
|
|
14748
|
-
${p.id}
|
|
15798
|
+
${p.id}: ERROR: ${p.error}`);
|
|
14749
15799
|
continue;
|
|
14750
15800
|
}
|
|
14751
15801
|
log(
|
|
@@ -14795,8 +15845,8 @@ async function runHelp() {
|
|
|
14795
15845
|
async function runVersion() {
|
|
14796
15846
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
14797
15847
|
const url = new URL(up, import.meta.url);
|
|
14798
|
-
if (!
|
|
14799
|
-
const pkg = JSON.parse(await
|
|
15848
|
+
if (!existsSync32(url)) continue;
|
|
15849
|
+
const pkg = JSON.parse(await readFile28(url, "utf8"));
|
|
14800
15850
|
if (pkg.name === "pixelkiln") {
|
|
14801
15851
|
log(`${pkg.name} ${pkg.version}`);
|
|
14802
15852
|
return;
|