pixelkiln 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +954 -550
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +14 -0
- package/dist/index.cjs +940 -514
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1038 -29
- package/dist/index.d.ts +1038 -29
- package/dist/index.js +902 -480
- package/dist/index.js.map +1 -1
- package/docs/GENERATORS.md +14 -6
- package/docs/MANIFEST.md +94 -6
- package/package.json +1 -1
- package/schema/manifest.schema.json +188 -3
- package/schema/recipe.schema.json +55 -0
- package/skills/pixelkiln/references/pixellab.md +12 -1
package/dist/cli.js
CHANGED
|
@@ -68,7 +68,7 @@ function exitCodeFor(err) {
|
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
70
|
import { existsSync as existsSync32 } from "fs";
|
|
71
|
-
import { readFile as
|
|
71
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -610,7 +610,7 @@ function formatCost(unit, amount) {
|
|
|
610
610
|
|
|
611
611
|
// src/pipeline/plan.ts
|
|
612
612
|
import { existsSync as existsSync5 } from "fs";
|
|
613
|
-
import
|
|
613
|
+
import path9 from "path";
|
|
614
614
|
|
|
615
615
|
// src/hash.ts
|
|
616
616
|
import { createHash } from "crypto";
|
|
@@ -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(path45) {
|
|
622
622
|
return new Promise((resolve, reject) => {
|
|
623
623
|
const hash = createHash("sha256");
|
|
624
|
-
const stream = createReadStream(
|
|
624
|
+
const stream = createReadStream(path45);
|
|
625
625
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
626
626
|
stream.on("error", reject);
|
|
627
627
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -665,8 +665,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
665
665
|
parent: spec.character.parentAssetId,
|
|
666
666
|
parentSha256: spec.character.parentSha256 ?? null,
|
|
667
667
|
state: spec.character.state,
|
|
668
|
-
animation: spec.character.animation
|
|
668
|
+
animation: spec.character.animation,
|
|
669
|
+
// Absent keys keep the hashes of manifests that never set these.
|
|
670
|
+
proportions: spec.character.proportions,
|
|
671
|
+
textGuidanceScale: spec.character.textGuidanceScale,
|
|
672
|
+
isometric: spec.character.isometric,
|
|
673
|
+
reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
|
|
669
674
|
} : void 0,
|
|
675
|
+
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
676
|
+
// compares those directly, so only the choice of source is identity.
|
|
677
|
+
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
670
678
|
revision: spec.revision ? {
|
|
671
679
|
mode: spec.revision.mode,
|
|
672
680
|
from: spec.revision.sourceAssetId,
|
|
@@ -678,9 +686,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
678
686
|
);
|
|
679
687
|
}
|
|
680
688
|
|
|
681
|
-
// src/
|
|
682
|
-
import {
|
|
683
|
-
import
|
|
689
|
+
// src/pipeline/mirror.ts
|
|
690
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
691
|
+
import path4 from "path";
|
|
684
692
|
|
|
685
693
|
// src/lock.ts
|
|
686
694
|
import { readFile, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
@@ -769,7 +777,28 @@ var CHARACTER_DIRECTIONS_8 = [
|
|
|
769
777
|
"south-west"
|
|
770
778
|
];
|
|
771
779
|
var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
|
|
780
|
+
var MIRRORED_DIRECTION = {
|
|
781
|
+
south: "south",
|
|
782
|
+
"south-east": "south-west",
|
|
783
|
+
east: "west",
|
|
784
|
+
"north-east": "north-west",
|
|
785
|
+
north: "north",
|
|
786
|
+
"north-west": "north-east",
|
|
787
|
+
west: "east",
|
|
788
|
+
"south-west": "south-east"
|
|
789
|
+
};
|
|
772
790
|
var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
|
|
791
|
+
var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
|
|
792
|
+
var CharacterProportionsSchema = z.union([
|
|
793
|
+
z.enum(CHARACTER_PROPORTION_PRESETS),
|
|
794
|
+
z.object({
|
|
795
|
+
headSize: z.number().min(0.5).max(2).optional(),
|
|
796
|
+
armsLength: z.number().min(0.5).max(2).optional(),
|
|
797
|
+
legsLength: z.number().min(0.5).max(2).optional(),
|
|
798
|
+
shoulderWidth: z.number().min(0.5).max(2).optional(),
|
|
799
|
+
hipWidth: z.number().min(0.5).max(2).optional()
|
|
800
|
+
}).strict()
|
|
801
|
+
]);
|
|
773
802
|
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
774
803
|
var CharacterStateSchema = z.object({
|
|
775
804
|
/** The base character, or another state, in the same style. */
|
|
@@ -928,6 +957,16 @@ var StyleObjectSchema = z.object({
|
|
|
928
957
|
directions: z.union([z.literal(4), z.literal(8)]).optional(),
|
|
929
958
|
/** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
|
|
930
959
|
template: z.string().min(1).optional(),
|
|
960
|
+
/** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
|
|
961
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
962
|
+
/**
|
|
963
|
+
* `character` only. How closely a `standard` base and a template loop
|
|
964
|
+
* follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
|
|
965
|
+
* engines do not take it.
|
|
966
|
+
*/
|
|
967
|
+
textGuidanceScale: z.number().min(1).max(20).optional(),
|
|
968
|
+
/** `character` only. Draw `standard` bases and every loop in isometric view. */
|
|
969
|
+
isometric: z.boolean().optional(),
|
|
931
970
|
/** Fixed seed for reproducibility where the endpoint supports it. */
|
|
932
971
|
seed: z.number().int().optional(),
|
|
933
972
|
/**
|
|
@@ -1028,8 +1067,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
|
1028
1067
|
}).strict();
|
|
1029
1068
|
var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
|
|
1030
1069
|
var AssetSchema = z.object({
|
|
1031
|
-
/** The subject. Style wrapping comes from the style's prefix/suffix. */
|
|
1032
|
-
prompt: z.string(),
|
|
1070
|
+
/** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
|
|
1071
|
+
prompt: z.string().optional(),
|
|
1033
1072
|
/** Subdirectory under the style's outDir. Optional. */
|
|
1034
1073
|
category: z.string().optional(),
|
|
1035
1074
|
/** Overrides the style default. `map` generator only. */
|
|
@@ -1087,6 +1126,32 @@ var AssetSchema = z.object({
|
|
|
1087
1126
|
state: CharacterStateSchema.optional(),
|
|
1088
1127
|
/** `character` styles: this asset is a loop of another character asset in one direction. */
|
|
1089
1128
|
animation: CharacterAnimationSchema.optional(),
|
|
1129
|
+
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1130
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
1131
|
+
/**
|
|
1132
|
+
* `character` styles, bases only: the character's own sprite, which
|
|
1133
|
+
* PixelLab rotates into the other directions instead of drawing from
|
|
1134
|
+
* the prompt. A manifest-relative PNG or JPEG of the south-facing
|
|
1135
|
+
* sprite, or an object keyed by direction (`{ "south": ..., "east":
|
|
1136
|
+
* ... }`); quadrupeds in `standard` mode need south and east, and only
|
|
1137
|
+
* `standard` takes more than south. `standard` wants each image at the
|
|
1138
|
+
* style's size; v3 accepts up to 256px, pro up to 168px. The prompt
|
|
1139
|
+
* still guides the result.
|
|
1140
|
+
*/
|
|
1141
|
+
reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
|
|
1142
|
+
/**
|
|
1143
|
+
* Another asset of the same style flipped left to right, made locally
|
|
1144
|
+
* from that asset's downloaded files at no generation cost.
|
|
1145
|
+
*
|
|
1146
|
+
* The saving is in character loops, where each direction is its own
|
|
1147
|
+
* generation: a walk facing west mirrored is the walk facing east, and a
|
|
1148
|
+
* diagonal mirrors to the other diagonal. South and north mirror onto
|
|
1149
|
+
* themselves and are refused. A single image or a frame set from any
|
|
1150
|
+
* generator can be mirrored too; a tile set cannot, since its edges
|
|
1151
|
+
* carry meaning. Mirroring swaps handedness, so an asymmetric character
|
|
1152
|
+
* (a sword hand, a parted fringe) comes out the other way round.
|
|
1153
|
+
*/
|
|
1154
|
+
mirror: z.string().min(1).optional(),
|
|
1090
1155
|
/**
|
|
1091
1156
|
* Generated output role to place in `cell` when this asset expands to
|
|
1092
1157
|
* several files. Omit for ordinary single-output assets. A structural set
|
|
@@ -1126,7 +1191,7 @@ var AssetSchema = z.object({
|
|
|
1126
1191
|
path: ["revision"]
|
|
1127
1192
|
});
|
|
1128
1193
|
}
|
|
1129
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
|
|
1194
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
|
|
1130
1195
|
if (shapes.length > 1) {
|
|
1131
1196
|
context.addIssue({
|
|
1132
1197
|
code: z.ZodIssueCode.custom,
|
|
@@ -1134,6 +1199,30 @@ var AssetSchema = z.object({
|
|
|
1134
1199
|
path: [shapes[1]]
|
|
1135
1200
|
});
|
|
1136
1201
|
}
|
|
1202
|
+
if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
|
|
1203
|
+
context.addIssue({
|
|
1204
|
+
code: z.ZodIssueCode.custom,
|
|
1205
|
+
message: "a mirror is made from another asset's files; it cannot also declare a source",
|
|
1206
|
+
path: ["mirror"]
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (asset.prompt === void 0 && !asset.mirror) {
|
|
1210
|
+
context.addIssue({
|
|
1211
|
+
code: z.ZodIssueCode.custom,
|
|
1212
|
+
message: "Required",
|
|
1213
|
+
path: ["prompt"]
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
|
|
1217
|
+
context.addIssue({
|
|
1218
|
+
code: z.ZodIssueCode.custom,
|
|
1219
|
+
message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
|
|
1220
|
+
path: ["reference"]
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
|
|
1224
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
|
|
1225
|
+
}
|
|
1137
1226
|
});
|
|
1138
1227
|
var DEFAULT_HISTORY_LIMIT = 5;
|
|
1139
1228
|
var HistoryLimitSchema = z.number().int().min(0).max(100);
|
|
@@ -1223,6 +1312,11 @@ var LockEntrySchema = z.object({
|
|
|
1223
1312
|
maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1224
1313
|
strength: z.number().min(0).max(1).optional()
|
|
1225
1314
|
}).strict().nullable().default(null),
|
|
1315
|
+
/** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
|
|
1316
|
+
mirror: z.object({
|
|
1317
|
+
sourceAssetId: z.string().min(1),
|
|
1318
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1319
|
+
}).strict().nullable().default(null),
|
|
1226
1320
|
/**
|
|
1227
1321
|
* Output hashes owned by the previous generation while its replacement is
|
|
1228
1322
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1922,6 +2016,8 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
|
1922
2016
|
}
|
|
1923
2017
|
|
|
1924
2018
|
// src/outputs.ts
|
|
2019
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2020
|
+
import path3 from "path";
|
|
1925
2021
|
function portableOutputPath(file, manifestDir) {
|
|
1926
2022
|
const root = path3.resolve(manifestDir);
|
|
1927
2023
|
const absolute = path3.isAbsolute(file) ? path3.normalize(file) : path3.resolve(root, file);
|
|
@@ -2047,21 +2143,219 @@ function frameSetFps(entry) {
|
|
|
2047
2143
|
return null;
|
|
2048
2144
|
}
|
|
2049
2145
|
|
|
2146
|
+
// src/palette.ts
|
|
2147
|
+
function normalizePalette(values) {
|
|
2148
|
+
const colors = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (const value of values) {
|
|
2150
|
+
const rgb = parseHex(value);
|
|
2151
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
2152
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
2153
|
+
}
|
|
2154
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
2155
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
2156
|
+
}
|
|
2157
|
+
return [...colors.values()];
|
|
2158
|
+
}
|
|
2159
|
+
function colorDistance(a, b) {
|
|
2160
|
+
const rmean = (a.r + b.r) / 2;
|
|
2161
|
+
const dr = a.r - b.r;
|
|
2162
|
+
const dg = a.g - b.g;
|
|
2163
|
+
const db = a.b - b.b;
|
|
2164
|
+
return Math.sqrt(
|
|
2165
|
+
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
function quantize(png, palette) {
|
|
2169
|
+
const colors = normalizePalette(palette);
|
|
2170
|
+
const pixels = Buffer.from(png.pixels);
|
|
2171
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
2172
|
+
let changed = 0;
|
|
2173
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
2174
|
+
const alpha = pixels[i + 3];
|
|
2175
|
+
if (alpha === 0) {
|
|
2176
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
2177
|
+
pixels[i] = 0;
|
|
2178
|
+
pixels[i + 1] = 0;
|
|
2179
|
+
pixels[i + 2] = 0;
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
2183
|
+
let nearest = nearestOf.get(key);
|
|
2184
|
+
if (!nearest) {
|
|
2185
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
2186
|
+
nearest = colors[0];
|
|
2187
|
+
let distance = colorDistance(pixel, nearest);
|
|
2188
|
+
for (let j = 1; j < colors.length; j++) {
|
|
2189
|
+
const candidate = colors[j];
|
|
2190
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
2191
|
+
if (candidateDistance < distance) {
|
|
2192
|
+
nearest = candidate;
|
|
2193
|
+
distance = candidateDistance;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
nearestOf.set(key, nearest);
|
|
2197
|
+
}
|
|
2198
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
2199
|
+
pixels[i] = nearest.r;
|
|
2200
|
+
pixels[i + 1] = nearest.g;
|
|
2201
|
+
pixels[i + 2] = nearest.b;
|
|
2202
|
+
}
|
|
2203
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
2204
|
+
}
|
|
2205
|
+
function quantizeToPalette(png, palette) {
|
|
2206
|
+
return quantize(png, palette).png;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// src/pipeline/postprocess.ts
|
|
2210
|
+
function postprocessFor(spec) {
|
|
2211
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
2212
|
+
return {
|
|
2213
|
+
palette: {
|
|
2214
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
2215
|
+
dither: "none",
|
|
2216
|
+
distance: "redmean"
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function samePostprocess(a, b) {
|
|
2221
|
+
const pa = a?.palette;
|
|
2222
|
+
const pb = b?.palette;
|
|
2223
|
+
if (!pa && !pb) return true;
|
|
2224
|
+
if (!pa || !pb) return false;
|
|
2225
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
2226
|
+
}
|
|
2227
|
+
function postprocessCurrent(entry, spec) {
|
|
2228
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
2229
|
+
}
|
|
2230
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
2231
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
2232
|
+
const png = decodePng(bytes);
|
|
2233
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
2234
|
+
if (!changed) return { bytes, changed: false };
|
|
2235
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/pipeline/mirror.ts
|
|
2239
|
+
function mirrorSourceHash(source) {
|
|
2240
|
+
return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
|
|
2241
|
+
}
|
|
2242
|
+
function mirrorCurrent(entry, source) {
|
|
2243
|
+
return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
|
|
2244
|
+
}
|
|
2245
|
+
function mirroredRole(role) {
|
|
2246
|
+
if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
|
|
2247
|
+
return role;
|
|
2248
|
+
}
|
|
2249
|
+
function flipHorizontal(png) {
|
|
2250
|
+
const { width, height, pixels } = decodePng(png);
|
|
2251
|
+
const flipped = Buffer.alloc(pixels.length);
|
|
2252
|
+
const stride = width * 4;
|
|
2253
|
+
for (let y = 0; y < height; y++) {
|
|
2254
|
+
const row = y * stride;
|
|
2255
|
+
for (let x = 0; x < width; x++) {
|
|
2256
|
+
pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return encodeRgbaPng(width, height, flipped);
|
|
2260
|
+
}
|
|
2261
|
+
async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
2262
|
+
const log2 = opts.onProgress ?? (() => {
|
|
2263
|
+
});
|
|
2264
|
+
const mirror = spec.mirror;
|
|
2265
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
2266
|
+
const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
|
|
2267
|
+
const source = lock.entries[sourceKey];
|
|
2268
|
+
if (!source || source.status !== "downloaded" || !source.outputs.length) {
|
|
2269
|
+
throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
|
|
2270
|
+
}
|
|
2271
|
+
const members = source.outputs.map((output, index) => ({
|
|
2272
|
+
file: currentEntryOutputPath(source, mirror.sourceSpec, index),
|
|
2273
|
+
role: mirroredRole(output.role)
|
|
2274
|
+
}));
|
|
2275
|
+
if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
|
|
2276
|
+
members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
|
|
2277
|
+
}
|
|
2278
|
+
const outputs = [];
|
|
2279
|
+
const written = [];
|
|
2280
|
+
for (const [index, member] of members.entries()) {
|
|
2281
|
+
const bytes = flipHorizontal(await readFile2(member.file));
|
|
2282
|
+
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
2283
|
+
await mkdir2(path4.dirname(target), { recursive: true });
|
|
2284
|
+
const tmp = `${target}.pixelkiln.tmp`;
|
|
2285
|
+
await writeFile2(tmp, bytes);
|
|
2286
|
+
await rename2(tmp, target);
|
|
2287
|
+
written.push(target);
|
|
2288
|
+
outputs.push({
|
|
2289
|
+
path: portableOutputPath(target, spec.root),
|
|
2290
|
+
sha256: sha256(bytes),
|
|
2291
|
+
...member.role ? { role: member.role } : {},
|
|
2292
|
+
mediaType: MediaType.PNG
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
|
|
2296
|
+
const character = spec.character;
|
|
2297
|
+
const entry = upsert(lock, key, {
|
|
2298
|
+
styleId: spec.styleId,
|
|
2299
|
+
assetId: spec.assetId,
|
|
2300
|
+
specHash: spec.specHash,
|
|
2301
|
+
generator: spec.generator,
|
|
2302
|
+
tileFeature: null,
|
|
2303
|
+
prompt: "",
|
|
2304
|
+
width: spec.width,
|
|
2305
|
+
height: spec.height,
|
|
2306
|
+
revision: null,
|
|
2307
|
+
mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
|
|
2308
|
+
supersededOutputs: [],
|
|
2309
|
+
jobId: null,
|
|
2310
|
+
submissionComplete: void 0,
|
|
2311
|
+
reviewObjectId: null,
|
|
2312
|
+
objectId: null,
|
|
2313
|
+
candidateIndex: null,
|
|
2314
|
+
status: "downloaded",
|
|
2315
|
+
error: null,
|
|
2316
|
+
outputs,
|
|
2317
|
+
providerMetadata: {
|
|
2318
|
+
[source.provider]: {
|
|
2319
|
+
...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
|
|
2320
|
+
...character ? {
|
|
2321
|
+
character: {
|
|
2322
|
+
kind: character.kind,
|
|
2323
|
+
mirrorOf: mirror.sourceAssetId,
|
|
2324
|
+
...character.animation ? { direction: character.animation.direction } : {}
|
|
2325
|
+
}
|
|
2326
|
+
} : {}
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
sourceUrl: null,
|
|
2330
|
+
sourceUrls: [],
|
|
2331
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2332
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2333
|
+
history: [],
|
|
2334
|
+
cost: 0,
|
|
2335
|
+
costUnit: spec.costUnit,
|
|
2336
|
+
provider: source.provider,
|
|
2337
|
+
postprocess: postprocessFor(spec)
|
|
2338
|
+
});
|
|
2339
|
+
await saveLock(lockPath, lock);
|
|
2340
|
+
log2(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
|
|
2341
|
+
return entry;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2050
2344
|
// src/pipeline/quality-profile.ts
|
|
2051
2345
|
import { existsSync as existsSync3 } from "fs";
|
|
2052
|
-
import
|
|
2346
|
+
import path7 from "path";
|
|
2053
2347
|
|
|
2054
2348
|
// src/pipeline/refine.ts
|
|
2055
|
-
import
|
|
2349
|
+
import path6 from "path";
|
|
2056
2350
|
import { execFile } from "child_process";
|
|
2057
|
-
import { mkdtemp, readFile as
|
|
2351
|
+
import { mkdtemp, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
2058
2352
|
import { tmpdir } from "os";
|
|
2059
2353
|
import { promisify } from "util";
|
|
2060
2354
|
|
|
2061
2355
|
// src/artifacts.ts
|
|
2062
|
-
import
|
|
2356
|
+
import path5 from "path";
|
|
2063
2357
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2064
|
-
import { mkdir as
|
|
2358
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
2065
2359
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
2066
2360
|
function message(error) {
|
|
2067
2361
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2070,7 +2364,7 @@ function digest(data) {
|
|
|
2070
2364
|
return createHash2("sha256").update(data).digest("hex");
|
|
2071
2365
|
}
|
|
2072
2366
|
function portableRelative(from, to) {
|
|
2073
|
-
return
|
|
2367
|
+
return path5.relative(from, path5.resolve(to)).split(path5.sep).join("/") || ".";
|
|
2074
2368
|
}
|
|
2075
2369
|
function canonical(value) {
|
|
2076
2370
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -2120,12 +2414,12 @@ function parseArtifactManifest(absolute, data) {
|
|
|
2120
2414
|
return raw;
|
|
2121
2415
|
}
|
|
2122
2416
|
async function readArtifactBundleManifest(manifestPath) {
|
|
2123
|
-
const absolute =
|
|
2124
|
-
return parseArtifactManifest(absolute, await
|
|
2417
|
+
const absolute = path5.resolve(manifestPath);
|
|
2418
|
+
return parseArtifactManifest(absolute, await readFile3(absolute, "utf8"));
|
|
2125
2419
|
}
|
|
2126
2420
|
async function readOptional(file) {
|
|
2127
2421
|
try {
|
|
2128
|
-
return await
|
|
2422
|
+
return await readFile3(file);
|
|
2129
2423
|
} catch (error) {
|
|
2130
2424
|
if (isCode(error, "ENOENT")) return null;
|
|
2131
2425
|
throw error;
|
|
@@ -2144,8 +2438,8 @@ function processIsAlive(pid) {
|
|
|
2144
2438
|
}
|
|
2145
2439
|
}
|
|
2146
2440
|
function validTemporaryPath(candidate, destination, type) {
|
|
2147
|
-
return
|
|
2148
|
-
`.${
|
|
2441
|
+
return path5.dirname(candidate) === path5.dirname(destination) && path5.basename(candidate).startsWith(
|
|
2442
|
+
`.${path5.basename(destination)}.pixelkiln-${type}-`
|
|
2149
2443
|
);
|
|
2150
2444
|
}
|
|
2151
2445
|
function parseTransaction(journal, data) {
|
|
@@ -2165,7 +2459,7 @@ async function removeTransactionFiles(journal) {
|
|
|
2165
2459
|
await rm2(transactionMarker(journal), { force: true });
|
|
2166
2460
|
}
|
|
2167
2461
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
2168
|
-
const journal =
|
|
2462
|
+
const journal = path5.resolve(recoveryFile);
|
|
2169
2463
|
const bytes = await readOptional(journal);
|
|
2170
2464
|
if (!bytes) {
|
|
2171
2465
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -2181,7 +2475,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2181
2475
|
);
|
|
2182
2476
|
}
|
|
2183
2477
|
for (const entry of transaction.entries) {
|
|
2184
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
2478
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path5.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
2185
2479
|
throw new Error(
|
|
2186
2480
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
2187
2481
|
);
|
|
@@ -2207,7 +2501,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2207
2501
|
}
|
|
2208
2502
|
await remove2(entry.destination, errors);
|
|
2209
2503
|
try {
|
|
2210
|
-
await
|
|
2504
|
+
await rename3(entry.backup, entry.destination);
|
|
2211
2505
|
} catch (error) {
|
|
2212
2506
|
errors.push(
|
|
2213
2507
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -2227,7 +2521,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2227
2521
|
await removeTransactionFiles(journal);
|
|
2228
2522
|
}
|
|
2229
2523
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
2230
|
-
const root =
|
|
2524
|
+
const root = path5.dirname(path5.resolve(manifestPath));
|
|
2231
2525
|
const sources = provenance.sources.map((source) => ({
|
|
2232
2526
|
...source,
|
|
2233
2527
|
path: portableRelative(root, source.path)
|
|
@@ -2257,11 +2551,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
2257
2551
|
];
|
|
2258
2552
|
}
|
|
2259
2553
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
2260
|
-
const absoluteManifest =
|
|
2554
|
+
const absoluteManifest = path5.resolve(manifestPath);
|
|
2261
2555
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
2262
2556
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
2263
2557
|
absoluteManifest,
|
|
2264
|
-
...outputs.map((output) =>
|
|
2558
|
+
...outputs.map((output) => path5.resolve(output.path))
|
|
2265
2559
|
]);
|
|
2266
2560
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
2267
2561
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -2279,13 +2573,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
2279
2573
|
}
|
|
2280
2574
|
}
|
|
2281
2575
|
if (!options.force) {
|
|
2282
|
-
const root =
|
|
2576
|
+
const root = path5.dirname(absoluteManifest);
|
|
2283
2577
|
const recorded = new Map(
|
|
2284
|
-
previous?.outputs.map((output) => [
|
|
2578
|
+
previous?.outputs.map((output) => [path5.resolve(root, output.path), output.sha256]) ?? []
|
|
2285
2579
|
);
|
|
2286
2580
|
const conflicts = [];
|
|
2287
2581
|
for (const output of outputs) {
|
|
2288
|
-
const destination =
|
|
2582
|
+
const destination = path5.resolve(output.path);
|
|
2289
2583
|
const current = await readOptional(destination);
|
|
2290
2584
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
2291
2585
|
const expected = recorded.get(destination);
|
|
@@ -2307,15 +2601,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
2307
2601
|
);
|
|
2308
2602
|
}
|
|
2309
2603
|
async function verifyArtifactBundle(manifestPath) {
|
|
2310
|
-
const absolute =
|
|
2604
|
+
const absolute = path5.resolve(manifestPath);
|
|
2311
2605
|
const raw = await readArtifactBundleManifest(absolute);
|
|
2312
|
-
const root =
|
|
2606
|
+
const root = path5.dirname(absolute);
|
|
2313
2607
|
const changedSources = [];
|
|
2314
2608
|
const changedOutputs = [];
|
|
2315
2609
|
for (const source of raw.sources) {
|
|
2316
2610
|
let actual = null;
|
|
2317
2611
|
try {
|
|
2318
|
-
actual = digest(await
|
|
2612
|
+
actual = digest(await readFile3(path5.resolve(root, source.path)));
|
|
2319
2613
|
} catch (error) {
|
|
2320
2614
|
if (!isCode(error, "ENOENT")) throw error;
|
|
2321
2615
|
}
|
|
@@ -2323,7 +2617,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
2323
2617
|
}
|
|
2324
2618
|
for (const output of raw.outputs) {
|
|
2325
2619
|
try {
|
|
2326
|
-
if (digest(await
|
|
2620
|
+
if (digest(await readFile3(path5.resolve(root, output.path))) !== output.sha256) {
|
|
2327
2621
|
changedOutputs.push(output.path);
|
|
2328
2622
|
}
|
|
2329
2623
|
} catch (error) {
|
|
@@ -2362,7 +2656,7 @@ async function rollback(prepared) {
|
|
|
2362
2656
|
for (const artifact of [...prepared].reverse()) {
|
|
2363
2657
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
2364
2658
|
try {
|
|
2365
|
-
await
|
|
2659
|
+
await rename3(artifact.backup, artifact.destination);
|
|
2366
2660
|
artifact.backedUp = false;
|
|
2367
2661
|
} catch (error) {
|
|
2368
2662
|
errors.push(
|
|
@@ -2379,7 +2673,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2379
2673
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
2380
2674
|
const normalized = files.map((file) => {
|
|
2381
2675
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
2382
|
-
return { destination:
|
|
2676
|
+
return { destination: path5.resolve(file.path), data: Buffer.from(file.data) };
|
|
2383
2677
|
});
|
|
2384
2678
|
const destinations = /* @__PURE__ */ new Set();
|
|
2385
2679
|
for (const artifact of normalized) {
|
|
@@ -2395,7 +2689,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2395
2689
|
const unchanged = [];
|
|
2396
2690
|
for (const artifact of normalized) {
|
|
2397
2691
|
try {
|
|
2398
|
-
const current = await
|
|
2692
|
+
const current = await readFile3(artifact.destination);
|
|
2399
2693
|
if (current.equals(artifact.data)) {
|
|
2400
2694
|
unchanged.push(artifact.destination);
|
|
2401
2695
|
} else {
|
|
@@ -2409,21 +2703,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2409
2703
|
if (!changed.length) return { changed: [], unchanged };
|
|
2410
2704
|
const token = randomUUID();
|
|
2411
2705
|
for (const [index, artifact] of changed.entries()) {
|
|
2412
|
-
const basename =
|
|
2413
|
-
artifact.stage =
|
|
2414
|
-
|
|
2706
|
+
const basename = path5.basename(artifact.destination);
|
|
2707
|
+
artifact.stage = path5.join(
|
|
2708
|
+
path5.dirname(artifact.destination),
|
|
2415
2709
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
2416
2710
|
);
|
|
2417
2711
|
if (artifact.existed) {
|
|
2418
|
-
artifact.backup =
|
|
2419
|
-
|
|
2712
|
+
artifact.backup = path5.join(
|
|
2713
|
+
path5.dirname(artifact.destination),
|
|
2420
2714
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
2421
2715
|
);
|
|
2422
2716
|
}
|
|
2423
2717
|
}
|
|
2424
|
-
const journal = options.recoveryFile ?
|
|
2718
|
+
const journal = options.recoveryFile ? path5.resolve(options.recoveryFile) : null;
|
|
2425
2719
|
if (journal) {
|
|
2426
|
-
await
|
|
2720
|
+
await mkdir3(path5.dirname(journal), { recursive: true });
|
|
2427
2721
|
const transaction = {
|
|
2428
2722
|
format: "pixelkiln-artifact-transaction",
|
|
2429
2723
|
version: 1,
|
|
@@ -2437,7 +2731,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2437
2731
|
}))
|
|
2438
2732
|
};
|
|
2439
2733
|
try {
|
|
2440
|
-
await
|
|
2734
|
+
await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
2441
2735
|
activeTransactions.add(journal);
|
|
2442
2736
|
} catch (error) {
|
|
2443
2737
|
throw new Error(
|
|
@@ -2448,9 +2742,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2448
2742
|
}
|
|
2449
2743
|
try {
|
|
2450
2744
|
for (const [index, artifact] of changed.entries()) {
|
|
2451
|
-
await
|
|
2745
|
+
await mkdir3(path5.dirname(artifact.destination), { recursive: true });
|
|
2452
2746
|
await options.beforeStage?.(artifact.destination, index);
|
|
2453
|
-
await
|
|
2747
|
+
await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
|
|
2454
2748
|
}
|
|
2455
2749
|
} catch (error) {
|
|
2456
2750
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -2463,17 +2757,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2463
2757
|
try {
|
|
2464
2758
|
for (const artifact of changed) {
|
|
2465
2759
|
if (!artifact.existed) continue;
|
|
2466
|
-
await
|
|
2760
|
+
await rename3(artifact.destination, artifact.backup);
|
|
2467
2761
|
artifact.backedUp = true;
|
|
2468
2762
|
}
|
|
2469
2763
|
for (const [index, artifact] of changed.entries()) {
|
|
2470
2764
|
await options.beforePromote?.(artifact.destination, index);
|
|
2471
|
-
await
|
|
2765
|
+
await rename3(artifact.stage, artifact.destination);
|
|
2472
2766
|
artifact.stage = void 0;
|
|
2473
2767
|
artifact.promoted = true;
|
|
2474
2768
|
}
|
|
2475
2769
|
if (journal) {
|
|
2476
|
-
await
|
|
2770
|
+
await writeFile3(transactionMarker(journal), `${token}
|
|
2477
2771
|
`, { flag: "wx" });
|
|
2478
2772
|
durableCommit = true;
|
|
2479
2773
|
await options.afterCommit?.();
|
|
@@ -2515,69 +2809,6 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2515
2809
|
};
|
|
2516
2810
|
}
|
|
2517
2811
|
|
|
2518
|
-
// src/palette.ts
|
|
2519
|
-
function normalizePalette(values) {
|
|
2520
|
-
const colors = /* @__PURE__ */ new Map();
|
|
2521
|
-
for (const value of values) {
|
|
2522
|
-
const rgb = parseHex(value);
|
|
2523
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
2524
|
-
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
2525
|
-
}
|
|
2526
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
2527
|
-
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
2528
|
-
}
|
|
2529
|
-
return [...colors.values()];
|
|
2530
|
-
}
|
|
2531
|
-
function colorDistance(a, b) {
|
|
2532
|
-
const rmean = (a.r + b.r) / 2;
|
|
2533
|
-
const dr = a.r - b.r;
|
|
2534
|
-
const dg = a.g - b.g;
|
|
2535
|
-
const db = a.b - b.b;
|
|
2536
|
-
return Math.sqrt(
|
|
2537
|
-
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
2538
|
-
);
|
|
2539
|
-
}
|
|
2540
|
-
function quantize(png, palette) {
|
|
2541
|
-
const colors = normalizePalette(palette);
|
|
2542
|
-
const pixels = Buffer.from(png.pixels);
|
|
2543
|
-
const nearestOf = /* @__PURE__ */ new Map();
|
|
2544
|
-
let changed = 0;
|
|
2545
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
2546
|
-
const alpha = pixels[i + 3];
|
|
2547
|
-
if (alpha === 0) {
|
|
2548
|
-
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
2549
|
-
pixels[i] = 0;
|
|
2550
|
-
pixels[i + 1] = 0;
|
|
2551
|
-
pixels[i + 2] = 0;
|
|
2552
|
-
continue;
|
|
2553
|
-
}
|
|
2554
|
-
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
2555
|
-
let nearest = nearestOf.get(key);
|
|
2556
|
-
if (!nearest) {
|
|
2557
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
2558
|
-
nearest = colors[0];
|
|
2559
|
-
let distance = colorDistance(pixel, nearest);
|
|
2560
|
-
for (let j = 1; j < colors.length; j++) {
|
|
2561
|
-
const candidate = colors[j];
|
|
2562
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
2563
|
-
if (candidateDistance < distance) {
|
|
2564
|
-
nearest = candidate;
|
|
2565
|
-
distance = candidateDistance;
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
nearestOf.set(key, nearest);
|
|
2569
|
-
}
|
|
2570
|
-
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
2571
|
-
pixels[i] = nearest.r;
|
|
2572
|
-
pixels[i + 1] = nearest.g;
|
|
2573
|
-
pixels[i + 2] = nearest.b;
|
|
2574
|
-
}
|
|
2575
|
-
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
2576
|
-
}
|
|
2577
|
-
function quantizeToPalette(png, palette) {
|
|
2578
|
-
return quantize(png, palette).png;
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
2812
|
// src/pipeline/refine.ts
|
|
2582
2813
|
var execFileAsync = promisify(execFile);
|
|
2583
2814
|
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
@@ -2777,16 +3008,16 @@ function sameFrameGrid(first, next) {
|
|
|
2777
3008
|
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
2778
3009
|
}
|
|
2779
3010
|
async function refineAsset(options) {
|
|
2780
|
-
const source =
|
|
2781
|
-
const output =
|
|
3011
|
+
const source = path6.resolve(options.source);
|
|
3012
|
+
const output = path6.resolve(options.output);
|
|
2782
3013
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
2783
|
-
const record =
|
|
3014
|
+
const record = path6.resolve(qualityRecordPath(output));
|
|
2784
3015
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
2785
3016
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
2786
3017
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
2787
3018
|
let sourcePng;
|
|
2788
3019
|
try {
|
|
2789
|
-
sourcePng = decodePng(await
|
|
3020
|
+
sourcePng = decodePng(await readFile4(source));
|
|
2790
3021
|
} catch (error) {
|
|
2791
3022
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
2792
3023
|
}
|
|
@@ -2803,8 +3034,8 @@ async function refineAsset(options) {
|
|
|
2803
3034
|
);
|
|
2804
3035
|
}
|
|
2805
3036
|
}
|
|
2806
|
-
const temp = await mkdtemp(
|
|
2807
|
-
const recovered =
|
|
3037
|
+
const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-"));
|
|
3038
|
+
const recovered = path6.join(temp, "native.png");
|
|
2808
3039
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
2809
3040
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
2810
3041
|
try {
|
|
@@ -2825,7 +3056,7 @@ async function refineAsset(options) {
|
|
|
2825
3056
|
);
|
|
2826
3057
|
}
|
|
2827
3058
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
2828
|
-
const native = decodePng(await
|
|
3059
|
+
const native = decodePng(await readFile4(recovered));
|
|
2829
3060
|
const quantized = quantizeToPalette(native, palette);
|
|
2830
3061
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
2831
3062
|
const audit = auditRefinement(
|
|
@@ -2889,18 +3120,18 @@ async function refineFrameSet(options) {
|
|
|
2889
3120
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
2890
3121
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
2891
3122
|
}
|
|
2892
|
-
const output =
|
|
2893
|
-
const record =
|
|
3123
|
+
const output = path6.resolve(options.output);
|
|
3124
|
+
const record = path6.resolve(qualityRecordPath(output));
|
|
2894
3125
|
const palette = normalizeRefinementPalette(options.palette);
|
|
2895
3126
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
2896
3127
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
2897
3128
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
2898
3129
|
}
|
|
2899
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
3130
|
+
const sourceSet = new Set(options.sources.map((source) => path6.resolve(source.path)));
|
|
2900
3131
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
2901
3132
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
2902
3133
|
}
|
|
2903
|
-
const temp = await mkdtemp(
|
|
3134
|
+
const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
2904
3135
|
try {
|
|
2905
3136
|
const frames = [];
|
|
2906
3137
|
const outputs = [];
|
|
@@ -2908,8 +3139,8 @@ async function refineFrameSet(options) {
|
|
|
2908
3139
|
const sourcePaths = [];
|
|
2909
3140
|
let firstOptions = null;
|
|
2910
3141
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
2911
|
-
const source =
|
|
2912
|
-
const temporaryOutput =
|
|
3142
|
+
const source = path6.resolve(sourceEntry.path);
|
|
3143
|
+
const temporaryOutput = path6.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
2913
3144
|
const refined = await refineAsset({
|
|
2914
3145
|
source,
|
|
2915
3146
|
output: temporaryOutput,
|
|
@@ -2941,7 +3172,7 @@ async function refineFrameSet(options) {
|
|
|
2941
3172
|
});
|
|
2942
3173
|
outputs.push({
|
|
2943
3174
|
path: finalOutputs[index],
|
|
2944
|
-
data: await
|
|
3175
|
+
data: await readFile4(temporaryOutput)
|
|
2945
3176
|
});
|
|
2946
3177
|
}
|
|
2947
3178
|
const first = frames[0];
|
|
@@ -2984,7 +3215,7 @@ async function refineFrameSet(options) {
|
|
|
2984
3215
|
);
|
|
2985
3216
|
return {
|
|
2986
3217
|
sources: sourcePaths,
|
|
2987
|
-
outputs: outputs.map((item) =>
|
|
3218
|
+
outputs: outputs.map((item) => path6.resolve(item.path)),
|
|
2988
3219
|
record,
|
|
2989
3220
|
frames,
|
|
2990
3221
|
palette,
|
|
@@ -2995,12 +3226,12 @@ async function refineFrameSet(options) {
|
|
|
2995
3226
|
}
|
|
2996
3227
|
}
|
|
2997
3228
|
async function checkQualityRecord(recordPath) {
|
|
2998
|
-
const record =
|
|
3229
|
+
const record = path6.resolve(recordPath);
|
|
2999
3230
|
const manifest = await readArtifactBundleManifest(record);
|
|
3000
3231
|
const options = parseRecordOptions(manifest, record);
|
|
3001
3232
|
const verification = await verifyArtifactBundle(record);
|
|
3002
|
-
const sources = manifest.sources.map((source2) =>
|
|
3003
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
3233
|
+
const sources = manifest.sources.map((source2) => path6.resolve(path6.dirname(record), source2.path));
|
|
3234
|
+
const outputs = manifest.outputs.map((output2) => path6.resolve(path6.dirname(record), output2.path));
|
|
3004
3235
|
const source = sources[0] ?? "";
|
|
3005
3236
|
const output = outputs[0] ?? "";
|
|
3006
3237
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -3033,7 +3264,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
3033
3264
|
async function approveQualityRecord(recordPath, approval) {
|
|
3034
3265
|
const reviewer = approval.reviewer.trim();
|
|
3035
3266
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
3036
|
-
const record =
|
|
3267
|
+
const record = path6.resolve(recordPath);
|
|
3037
3268
|
const manifest = await readArtifactBundleManifest(record);
|
|
3038
3269
|
const options = parseRecordOptions(manifest, record);
|
|
3039
3270
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -3054,14 +3285,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
3054
3285
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
3055
3286
|
);
|
|
3056
3287
|
}
|
|
3057
|
-
const root =
|
|
3288
|
+
const root = path6.dirname(record);
|
|
3058
3289
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
3059
|
-
const outputPath =
|
|
3060
|
-
return { path: outputPath, data: await
|
|
3290
|
+
const outputPath = path6.resolve(root, entry.path);
|
|
3291
|
+
return { path: outputPath, data: await readFile4(outputPath) };
|
|
3061
3292
|
}));
|
|
3062
3293
|
const sources = manifest.sources.map((source) => ({
|
|
3063
3294
|
...source,
|
|
3064
|
-
path:
|
|
3295
|
+
path: path6.resolve(root, source.path)
|
|
3065
3296
|
}));
|
|
3066
3297
|
const approved = {
|
|
3067
3298
|
...options,
|
|
@@ -3093,7 +3324,7 @@ async function qualitySource(spec, lock) {
|
|
|
3093
3324
|
if (spec.generator === "frames") {
|
|
3094
3325
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
3095
3326
|
}
|
|
3096
|
-
const source =
|
|
3327
|
+
const source = path7.resolve(spec.root, spec.source);
|
|
3097
3328
|
return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
3098
3329
|
}
|
|
3099
3330
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -3160,7 +3391,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
3160
3391
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
3161
3392
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
3162
3393
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
3163
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
3394
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path7.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path7.resolve(item))) || JSON.stringify(check.outputs.map((item) => path7.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path7.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
3164
3395
|
if (contractChanged) {
|
|
3165
3396
|
return {
|
|
3166
3397
|
key,
|
|
@@ -3322,43 +3553,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
3322
3553
|
return sources;
|
|
3323
3554
|
}
|
|
3324
3555
|
|
|
3325
|
-
// src/pipeline/postprocess.ts
|
|
3326
|
-
function postprocessFor(spec) {
|
|
3327
|
-
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
3328
|
-
return {
|
|
3329
|
-
palette: {
|
|
3330
|
-
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
3331
|
-
dither: "none",
|
|
3332
|
-
distance: "redmean"
|
|
3333
|
-
}
|
|
3334
|
-
};
|
|
3335
|
-
}
|
|
3336
|
-
function samePostprocess(a, b) {
|
|
3337
|
-
const pa = a?.palette;
|
|
3338
|
-
const pb = b?.palette;
|
|
3339
|
-
if (!pa && !pb) return true;
|
|
3340
|
-
if (!pa || !pb) return false;
|
|
3341
|
-
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
3342
|
-
}
|
|
3343
|
-
function postprocessCurrent(entry, spec) {
|
|
3344
|
-
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
3345
|
-
}
|
|
3346
|
-
function applyPostprocess(bytes, mediaType, record) {
|
|
3347
|
-
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
3348
|
-
const png = decodePng(bytes);
|
|
3349
|
-
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
3350
|
-
if (!changed) return { bytes, changed: false };
|
|
3351
|
-
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
3556
|
// src/pipeline/revision.ts
|
|
3355
3557
|
import { existsSync as existsSync4 } from "fs";
|
|
3356
|
-
import
|
|
3558
|
+
import path8 from "path";
|
|
3357
3559
|
async function inspectRevisionReadiness(spec, lock) {
|
|
3560
|
+
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
3358
3561
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
3359
3562
|
if (!spec.revision) return null;
|
|
3360
3563
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
3361
3564
|
}
|
|
3565
|
+
async function inspectMirror(spec, lock, seen) {
|
|
3566
|
+
const source = spec.mirror.sourceSpec;
|
|
3567
|
+
const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
|
|
3568
|
+
const dependency = await inspectParent(source, lock, seen);
|
|
3569
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
3570
|
+
}
|
|
3362
3571
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
3363
3572
|
const character = spec.character;
|
|
3364
3573
|
const parentSpec = character.parentSpec;
|
|
@@ -3412,6 +3621,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3412
3621
|
if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
|
|
3413
3622
|
const nextSeen = new Set(seen);
|
|
3414
3623
|
nextSeen.add(key);
|
|
3624
|
+
if (spec.mirror) {
|
|
3625
|
+
const inputs = await inspectMirror(spec, lock, nextSeen);
|
|
3626
|
+
if (!inputs.ready) return inputs;
|
|
3627
|
+
}
|
|
3415
3628
|
if (spec.revision) {
|
|
3416
3629
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
3417
3630
|
if (!inputs.ready) return inputs;
|
|
@@ -3428,7 +3641,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3428
3641
|
};
|
|
3429
3642
|
}
|
|
3430
3643
|
if (spec.source) {
|
|
3431
|
-
const source =
|
|
3644
|
+
const source = path8.resolve(spec.root, spec.source);
|
|
3432
3645
|
return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
3433
3646
|
}
|
|
3434
3647
|
const entry = lock.entries[key];
|
|
@@ -3498,7 +3711,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3498
3711
|
state = "missing";
|
|
3499
3712
|
reason = "--force";
|
|
3500
3713
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
3501
|
-
if (existsSync5(
|
|
3714
|
+
if (existsSync5(path9.resolve(spec.root, spec.source))) {
|
|
3502
3715
|
state = "ok";
|
|
3503
3716
|
reason = `placed from ${spec.source}; not generated`;
|
|
3504
3717
|
} else {
|
|
@@ -3511,11 +3724,17 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3511
3724
|
reason = "file on disk, no provenance recorded";
|
|
3512
3725
|
} else {
|
|
3513
3726
|
state = "missing";
|
|
3514
|
-
reason = "not in lockfile";
|
|
3727
|
+
reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
|
|
3515
3728
|
}
|
|
3516
3729
|
} else if (entry.specHash !== spec.specHash) {
|
|
3517
3730
|
state = "stale";
|
|
3518
3731
|
reason = "prompt, size, or style changed";
|
|
3732
|
+
} else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
|
|
3733
|
+
state = "stale";
|
|
3734
|
+
reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
|
|
3735
|
+
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
|
|
3736
|
+
state = "stale";
|
|
3737
|
+
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
3519
3738
|
} else if (entry.status === "download-failed") {
|
|
3520
3739
|
state = "recoverable";
|
|
3521
3740
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -3544,6 +3763,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3544
3763
|
} else if (await anyOutputModified(entry, spec)) {
|
|
3545
3764
|
state = "orphaned";
|
|
3546
3765
|
reason = "output modified since download";
|
|
3766
|
+
} else if (spec.mirror && !postprocessCurrent(entry, spec)) {
|
|
3767
|
+
state = "stale";
|
|
3768
|
+
reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
|
|
3547
3769
|
} else if (!postprocessCurrent(entry, spec)) {
|
|
3548
3770
|
state = "recoverable";
|
|
3549
3771
|
reason = spec.enforcePalette ? "palette enforcement turned on; run pixelkiln fetch to snap the file (no generation cost)" : "palette enforcement turned off; run pixelkiln fetch to restore the provider's bytes (no generation cost)";
|
|
@@ -3555,15 +3777,19 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3555
3777
|
items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
|
|
3556
3778
|
}
|
|
3557
3779
|
const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
|
|
3558
|
-
const
|
|
3780
|
+
const stateOf = new Map(items.map((i) => [i.key, i.state]));
|
|
3559
3781
|
for (const item of items) {
|
|
3560
3782
|
if (!isActionable(item)) continue;
|
|
3561
|
-
const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
3783
|
+
const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
3562
3784
|
if (!parent) continue;
|
|
3563
3785
|
const parentKey = lockKey(parent.styleId, parent.assetId);
|
|
3564
|
-
|
|
3786
|
+
const parentState = stateOf.get(parentKey);
|
|
3787
|
+
if (parentState && isActionable({ state: parentState })) {
|
|
3565
3788
|
item.state = "blocked";
|
|
3566
3789
|
item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
|
|
3790
|
+
} else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
|
|
3791
|
+
item.state = "blocked";
|
|
3792
|
+
item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
|
|
3567
3793
|
}
|
|
3568
3794
|
}
|
|
3569
3795
|
const actionable = items.filter(isActionable);
|
|
@@ -3718,12 +3944,12 @@ async function confirm(question, auto) {
|
|
|
3718
3944
|
}
|
|
3719
3945
|
|
|
3720
3946
|
// src/cli/commands/account.ts
|
|
3721
|
-
import
|
|
3947
|
+
import path22 from "path";
|
|
3722
3948
|
import { existsSync as existsSync16 } from "fs";
|
|
3723
3949
|
|
|
3724
3950
|
// src/env.ts
|
|
3725
3951
|
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
3726
|
-
import
|
|
3952
|
+
import path10 from "path";
|
|
3727
3953
|
function loadEnvFiles(dir) {
|
|
3728
3954
|
const loaded = [];
|
|
3729
3955
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -3746,7 +3972,7 @@ function readEnvFiles(dir) {
|
|
|
3746
3972
|
function envFiles(dir) {
|
|
3747
3973
|
const out = [];
|
|
3748
3974
|
for (const name of [".env.local", ".env"]) {
|
|
3749
|
-
const file =
|
|
3975
|
+
const file = path10.join(dir, name);
|
|
3750
3976
|
if (!existsSync6(file)) continue;
|
|
3751
3977
|
try {
|
|
3752
3978
|
out.push([file, parseEnv(readFileSync(file, "utf8"))]);
|
|
@@ -3776,8 +4002,8 @@ function parseEnv(contents) {
|
|
|
3776
4002
|
|
|
3777
4003
|
// src/providers/comfyui.ts
|
|
3778
4004
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3779
|
-
import { readFile as
|
|
3780
|
-
import
|
|
4005
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4006
|
+
import path11 from "path";
|
|
3781
4007
|
|
|
3782
4008
|
// src/http.ts
|
|
3783
4009
|
var MAX_RETRIES = 4;
|
|
@@ -3964,10 +4190,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3964
4190
|
}
|
|
3965
4191
|
async resolveOptions(value, context) {
|
|
3966
4192
|
const options = parseOptions(value);
|
|
3967
|
-
const workflowPath =
|
|
4193
|
+
const workflowPath = path11.resolve(context.root, options.workflowFile);
|
|
3968
4194
|
let raw;
|
|
3969
4195
|
try {
|
|
3970
|
-
raw = await
|
|
4196
|
+
raw = await readFile5(workflowPath, "utf8");
|
|
3971
4197
|
} catch (err) {
|
|
3972
4198
|
throw new Error(
|
|
3973
4199
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4019,10 +4245,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4019
4245
|
if (typeof item !== "string") {
|
|
4020
4246
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
4021
4247
|
}
|
|
4022
|
-
const file =
|
|
4248
|
+
const file = path11.resolve(context.root, item);
|
|
4023
4249
|
let bytes;
|
|
4024
4250
|
try {
|
|
4025
|
-
bytes = await
|
|
4251
|
+
bytes = await readFile5(file);
|
|
4026
4252
|
} catch (error) {
|
|
4027
4253
|
throw new Error(
|
|
4028
4254
|
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -4154,7 +4380,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4154
4380
|
setBinding(workflow2, binding, input);
|
|
4155
4381
|
return;
|
|
4156
4382
|
}
|
|
4157
|
-
const bytes = await
|
|
4383
|
+
const bytes = await readFile5(input.path);
|
|
4158
4384
|
if (sha256(bytes) !== input.sha256) {
|
|
4159
4385
|
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
4160
4386
|
}
|
|
@@ -4221,7 +4447,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4221
4447
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
4222
4448
|
throw new Error("ComfyUI revision source is not ready");
|
|
4223
4449
|
}
|
|
4224
|
-
const sourceBytes = await
|
|
4450
|
+
const sourceBytes = await readFile5(spec.revision.sourceFile);
|
|
4225
4451
|
if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
|
|
4226
4452
|
throw new Error("ComfyUI revision source changed before upload");
|
|
4227
4453
|
}
|
|
@@ -4235,7 +4461,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4235
4461
|
if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
|
|
4236
4462
|
throw new Error("ComfyUI revision mask is not ready");
|
|
4237
4463
|
}
|
|
4238
|
-
const maskBytes = await
|
|
4464
|
+
const maskBytes = await readFile5(spec.revision.maskFile);
|
|
4239
4465
|
if (sha256(maskBytes) !== spec.revision.maskSha256) {
|
|
4240
4466
|
throw new Error("ComfyUI revision mask changed before upload");
|
|
4241
4467
|
}
|
|
@@ -4709,7 +4935,7 @@ function canonical2(value) {
|
|
|
4709
4935
|
import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4710
4936
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4711
4937
|
import os from "os";
|
|
4712
|
-
import
|
|
4938
|
+
import path12 from "path";
|
|
4713
4939
|
|
|
4714
4940
|
// src/client.ts
|
|
4715
4941
|
import { z as z2 } from "zod";
|
|
@@ -4839,6 +5065,17 @@ var PixelLabError = class extends ProviderError {
|
|
|
4839
5065
|
}
|
|
4840
5066
|
body;
|
|
4841
5067
|
};
|
|
5068
|
+
function proportionsPayload(proportions) {
|
|
5069
|
+
if (typeof proportions === "string") return { type: "preset", name: proportions };
|
|
5070
|
+
return {
|
|
5071
|
+
type: "custom",
|
|
5072
|
+
...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
|
|
5073
|
+
...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
|
|
5074
|
+
...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
|
|
5075
|
+
...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
|
|
5076
|
+
...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
4842
5079
|
var PixelLabClient = class {
|
|
4843
5080
|
constructor(apiKey, timeoutMs = 12e4) {
|
|
4844
5081
|
this.apiKey = apiKey;
|
|
@@ -4848,9 +5085,9 @@ var PixelLabClient = class {
|
|
|
4848
5085
|
apiKey;
|
|
4849
5086
|
http;
|
|
4850
5087
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
4851
|
-
async request(
|
|
5088
|
+
async request(path45, init) {
|
|
4852
5089
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
4853
|
-
const res = await this.http(`${BASE}${
|
|
5090
|
+
const res = await this.http(`${BASE}${path45}`, {
|
|
4854
5091
|
...init,
|
|
4855
5092
|
headers: {
|
|
4856
5093
|
Authorization: auth,
|
|
@@ -4860,13 +5097,13 @@ var PixelLabClient = class {
|
|
|
4860
5097
|
});
|
|
4861
5098
|
const text = await res.text();
|
|
4862
5099
|
if (!res.ok) {
|
|
4863
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
5100
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path45} \u2192 ${res.status}`, res.status, text);
|
|
4864
5101
|
}
|
|
4865
5102
|
if (!text) return {};
|
|
4866
5103
|
try {
|
|
4867
5104
|
return JSON.parse(text);
|
|
4868
5105
|
} catch {
|
|
4869
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
5106
|
+
throw new Error(`${init?.method ?? "GET"} ${path45} returned invalid JSON`);
|
|
4870
5107
|
}
|
|
4871
5108
|
}
|
|
4872
5109
|
async balance() {
|
|
@@ -5047,14 +5284,21 @@ var PixelLabClient = class {
|
|
|
5047
5284
|
* A base character. Standard mode picks the 4- or 8-direction template
|
|
5048
5285
|
* endpoint; v3 and pro have their own and always draw 8. Every one answers
|
|
5049
5286
|
* at once with the character id and a background job.
|
|
5287
|
+
*
|
|
5288
|
+
* A `reference` is the author's own sprite: standard takes one per
|
|
5289
|
+
* direction and draws the rest, v3 rotates the south one, and pro
|
|
5290
|
+
* switches to its rotate method for it. A `styleReference` is pro's style
|
|
5291
|
+
* anchor; the other engines have no such input.
|
|
5050
5292
|
*/
|
|
5051
5293
|
async createCharacter(args) {
|
|
5052
5294
|
const imageSize = { width: args.size, height: args.size };
|
|
5053
5295
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
5054
|
-
|
|
5296
|
+
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
5297
|
+
const south = args.reference?.south;
|
|
5298
|
+
let path45;
|
|
5055
5299
|
let body;
|
|
5056
5300
|
if (args.mode === "v3") {
|
|
5057
|
-
|
|
5301
|
+
path45 = "/create-character-v3";
|
|
5058
5302
|
body = {
|
|
5059
5303
|
description: args.description,
|
|
5060
5304
|
image_size: imageSize,
|
|
@@ -5063,20 +5307,23 @@ var PixelLabClient = class {
|
|
|
5063
5307
|
...args.view ? { view: args.view } : {},
|
|
5064
5308
|
...args.outline ? { outline: args.outline } : {},
|
|
5065
5309
|
...args.detail ? { detail: args.detail } : {},
|
|
5066
|
-
...args.seed != null ? { seed: args.seed } : {}
|
|
5310
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
5311
|
+
...south ? { reference_image: encode(south) } : {}
|
|
5067
5312
|
};
|
|
5068
5313
|
} else if (args.mode === "pro") {
|
|
5069
|
-
|
|
5314
|
+
path45 = "/create-character-pro";
|
|
5070
5315
|
body = {
|
|
5071
5316
|
description: args.description,
|
|
5072
5317
|
image_size: imageSize,
|
|
5073
5318
|
template_id: args.template,
|
|
5074
5319
|
no_background: args.noBackground ?? true,
|
|
5320
|
+
method: south ? "rotate_character" : "create_with_style",
|
|
5075
5321
|
...args.view ? { view: args.view } : {},
|
|
5076
|
-
...args.seed != null ? { seed: args.seed } : {}
|
|
5322
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
5323
|
+
...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
|
|
5077
5324
|
};
|
|
5078
5325
|
} else {
|
|
5079
|
-
|
|
5326
|
+
path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
5080
5327
|
body = {
|
|
5081
5328
|
description: args.description,
|
|
5082
5329
|
image_size: imageSize,
|
|
@@ -5086,11 +5333,15 @@ var PixelLabClient = class {
|
|
|
5086
5333
|
...args.shading ? { shading: args.shading } : {},
|
|
5087
5334
|
...args.detail ? { detail: args.detail } : {},
|
|
5088
5335
|
...args.seed != null ? { seed: args.seed } : {},
|
|
5336
|
+
...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
|
|
5337
|
+
...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
|
|
5338
|
+
...args.isometric !== void 0 ? { isometric: args.isometric } : {},
|
|
5339
|
+
...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
|
|
5089
5340
|
...palette
|
|
5090
5341
|
};
|
|
5091
5342
|
}
|
|
5092
|
-
const raw = await this.request(
|
|
5093
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
5343
|
+
const raw = await this.request(path45, { method: "POST", body: JSON.stringify(body) });
|
|
5344
|
+
return validateResponse(CharacterSubmitSchema, raw, path45.slice(1));
|
|
5094
5345
|
}
|
|
5095
5346
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
5096
5347
|
async createCharacterState(args) {
|
|
@@ -5125,6 +5376,8 @@ var PixelLabClient = class {
|
|
|
5125
5376
|
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
5126
5377
|
...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
5127
5378
|
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
5379
|
+
...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
|
|
5380
|
+
...args.isometric !== void 0 ? { isometric: args.isometric } : {},
|
|
5128
5381
|
...args.seed != null ? { seed: args.seed } : {}
|
|
5129
5382
|
})
|
|
5130
5383
|
});
|
|
@@ -5278,7 +5531,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5278
5531
|
* filename, polling is an existence check, and downloading is a file read.
|
|
5279
5532
|
*/
|
|
5280
5533
|
static cacheDir() {
|
|
5281
|
-
const dir =
|
|
5534
|
+
const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
|
|
5282
5535
|
mkdirSync(dir, { recursive: true });
|
|
5283
5536
|
return dir;
|
|
5284
5537
|
}
|
|
@@ -5321,8 +5574,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5321
5574
|
"high detail"
|
|
5322
5575
|
]);
|
|
5323
5576
|
}
|
|
5324
|
-
if (spec.generator === "character") this.validateCharacter(spec);
|
|
5325
|
-
if ((spec.generator === "map" || spec.generator === "pixflux"
|
|
5577
|
+
if (spec.generator === "character") this.validateCharacter(spec, styleImages);
|
|
5578
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
5326
5579
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
5327
5580
|
}
|
|
5328
5581
|
for (const image of styleImages) {
|
|
@@ -5338,9 +5591,61 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5338
5591
|
);
|
|
5339
5592
|
}
|
|
5340
5593
|
}
|
|
5341
|
-
validateCharacter(spec) {
|
|
5594
|
+
validateCharacter(spec, styleImages = []) {
|
|
5342
5595
|
const character = spec.character;
|
|
5343
5596
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
5597
|
+
const label = `${spec.styleId}/${spec.assetId}`;
|
|
5598
|
+
if (character.kind === "base" && character.proportions !== void 0) {
|
|
5599
|
+
if (character.mode !== "standard") {
|
|
5600
|
+
throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
|
|
5601
|
+
}
|
|
5602
|
+
if (character.template !== "mannequin") {
|
|
5603
|
+
throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
if (character.reference) {
|
|
5607
|
+
const given = Object.keys(character.reference);
|
|
5608
|
+
if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
|
|
5609
|
+
if (character.mode !== "standard") {
|
|
5610
|
+
if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
|
|
5611
|
+
const limit = character.mode === "v3" ? 256 : 168;
|
|
5612
|
+
const south = character.reference.south;
|
|
5613
|
+
if (south.width > limit || south.height > limit) {
|
|
5614
|
+
throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
|
|
5615
|
+
}
|
|
5616
|
+
} else {
|
|
5617
|
+
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5618
|
+
for (const direction of given) {
|
|
5619
|
+
if (!allowed.includes(direction)) {
|
|
5620
|
+
throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
|
|
5621
|
+
}
|
|
5622
|
+
const image = character.reference[direction];
|
|
5623
|
+
if (image.width !== spec.width || image.height !== spec.height) {
|
|
5624
|
+
throw new Error(
|
|
5625
|
+
`${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
|
|
5626
|
+
);
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
if (character.template !== "mannequin" && !character.reference.east) {
|
|
5630
|
+
throw new Error(`${label}: a ${character.template} reference needs south and east images`);
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
if (styleImages.length) {
|
|
5635
|
+
if (character.mode !== "pro") {
|
|
5636
|
+
throw new Error(
|
|
5637
|
+
`PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
|
|
5638
|
+
);
|
|
5639
|
+
}
|
|
5640
|
+
if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
|
|
5641
|
+
if (character.kind === "base" && character.reference) {
|
|
5642
|
+
throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
|
|
5643
|
+
}
|
|
5644
|
+
const image = styleImages[0];
|
|
5645
|
+
if (image.width > 168 || image.height > 168) {
|
|
5646
|
+
throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5344
5649
|
const maxSize = character.mode === "v3" ? 256 : 128;
|
|
5345
5650
|
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
5346
5651
|
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
@@ -5376,10 +5681,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5376
5681
|
* asset so a later run can find it, and clears its own earlier take for
|
|
5377
5682
|
* that direction first, since PixelLab skips a direction that exists.
|
|
5378
5683
|
*/
|
|
5379
|
-
async submitCharacter(spec, context) {
|
|
5684
|
+
async submitCharacter(spec, styleImages, context) {
|
|
5380
5685
|
const character = spec.character;
|
|
5381
5686
|
if (character.kind === "base") {
|
|
5382
5687
|
const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
5688
|
+
const styleImage = styleImages[0];
|
|
5383
5689
|
const res2 = await this.client.createCharacter({
|
|
5384
5690
|
mode: character.mode,
|
|
5385
5691
|
description: spec.prompt,
|
|
@@ -5392,7 +5698,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5392
5698
|
detail: spec.detail,
|
|
5393
5699
|
seed: spec.seed,
|
|
5394
5700
|
noBackground: spec.noBackground,
|
|
5395
|
-
paletteSwatchBase64: swatch
|
|
5701
|
+
paletteSwatchBase64: swatch,
|
|
5702
|
+
proportions: character.proportions,
|
|
5703
|
+
textGuidanceScale: character.textGuidanceScale,
|
|
5704
|
+
isometric: character.isometric,
|
|
5705
|
+
reference: character.reference ? readReference(spec, character.reference) : void 0,
|
|
5706
|
+
styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
|
|
5396
5707
|
});
|
|
5397
5708
|
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
5709
|
}
|
|
@@ -5434,7 +5745,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5434
5745
|
frameCount: animation.frames,
|
|
5435
5746
|
keepFirstFrame: animation.keepFirstFrame,
|
|
5436
5747
|
directions: [animation.direction],
|
|
5437
|
-
seed: spec.seed
|
|
5748
|
+
seed: spec.seed,
|
|
5749
|
+
textGuidanceScale: character.textGuidanceScale,
|
|
5750
|
+
isometric: character.isometric
|
|
5438
5751
|
});
|
|
5439
5752
|
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
|
|
5440
5753
|
return {
|
|
@@ -5524,7 +5837,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5524
5837
|
}
|
|
5525
5838
|
async submit(spec, styleImages, context) {
|
|
5526
5839
|
this.validate(spec, styleImages);
|
|
5527
|
-
if (spec.generator === "character") return this.submitCharacter(spec, context);
|
|
5840
|
+
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
5528
5841
|
if (spec.generator === "pixflux") {
|
|
5529
5842
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
5530
5843
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -5536,7 +5849,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5536
5849
|
seed: spec.seed
|
|
5537
5850
|
});
|
|
5538
5851
|
const jobId = randomUUID3();
|
|
5539
|
-
writeFileSync(
|
|
5852
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
5540
5853
|
return { jobId };
|
|
5541
5854
|
}
|
|
5542
5855
|
if (spec.generator === "tiles") {
|
|
@@ -5577,7 +5890,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5577
5890
|
}
|
|
5578
5891
|
async poll(jobId, generator, context) {
|
|
5579
5892
|
if (generator === "pixflux") {
|
|
5580
|
-
const file =
|
|
5893
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
5581
5894
|
if (existsSync7(file)) {
|
|
5582
5895
|
const sourceUrl = `file://${file}`;
|
|
5583
5896
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
@@ -5791,6 +6104,17 @@ function firstUrl(urls) {
|
|
|
5791
6104
|
if (!urls) return null;
|
|
5792
6105
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
5793
6106
|
}
|
|
6107
|
+
function readReference(spec, reference) {
|
|
6108
|
+
const images = {};
|
|
6109
|
+
for (const [direction, image] of Object.entries(reference)) {
|
|
6110
|
+
const bytes = readFileSync2(image.path);
|
|
6111
|
+
if (sha256(bytes) !== image.sha256) {
|
|
6112
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
|
|
6113
|
+
}
|
|
6114
|
+
images[direction] = { base64: bytes.toString("base64"), format: image.format };
|
|
6115
|
+
}
|
|
6116
|
+
return images;
|
|
6117
|
+
}
|
|
5794
6118
|
function rotationSources(character) {
|
|
5795
6119
|
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5796
6120
|
const sources = [];
|
|
@@ -5861,9 +6185,9 @@ var RetroDiffusionClient = class {
|
|
|
5861
6185
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
5862
6186
|
return Buffer.from(await response.arrayBuffer());
|
|
5863
6187
|
}
|
|
5864
|
-
async call(
|
|
6188
|
+
async call(path45, init = {}) {
|
|
5865
6189
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
5866
|
-
const response = await this.request(`${this.baseUrl}${
|
|
6190
|
+
const response = await this.request(`${this.baseUrl}${path45}`, {
|
|
5867
6191
|
...init,
|
|
5868
6192
|
headers: {
|
|
5869
6193
|
"Content-Type": "application/json",
|
|
@@ -6720,13 +7044,13 @@ registerProvider({
|
|
|
6720
7044
|
});
|
|
6721
7045
|
|
|
6722
7046
|
// src/manifest.ts
|
|
6723
|
-
import { readFile as
|
|
7047
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
6724
7048
|
import { existsSync as existsSync8 } from "fs";
|
|
6725
|
-
import
|
|
7049
|
+
import path13 from "path";
|
|
6726
7050
|
async function loadManifest(manifestPath) {
|
|
6727
|
-
const abs =
|
|
7051
|
+
const abs = path13.resolve(manifestPath);
|
|
6728
7052
|
if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
6729
|
-
const input = ManifestInputSchema.safeParse(JSON.parse(await
|
|
7053
|
+
const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
|
|
6730
7054
|
if (!input.success) {
|
|
6731
7055
|
const issues = formatManifestIssues(input.error.issues);
|
|
6732
7056
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -6811,7 +7135,7 @@ ${issues}`);
|
|
|
6811
7135
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6812
7136
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
6813
7137
|
}
|
|
6814
|
-
return { manifest: parsed.data, root:
|
|
7138
|
+
return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
|
|
6815
7139
|
}
|
|
6816
7140
|
function parentAssetId(asset) {
|
|
6817
7141
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -6910,16 +7234,16 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6910
7234
|
}
|
|
6911
7235
|
}
|
|
6912
7236
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
6913
|
-
async function loadStyleImage(rel) {
|
|
6914
|
-
const abs =
|
|
7237
|
+
async function loadStyleImage(rel, what = "Style image") {
|
|
7238
|
+
const abs = path13.resolve(root, rel);
|
|
6915
7239
|
let hit = styleImageCache.get(abs);
|
|
6916
7240
|
if (!hit) {
|
|
6917
|
-
if (!existsSync8(abs)) throw new Error(
|
|
6918
|
-
const buf = await
|
|
7241
|
+
if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
7242
|
+
const buf = await readFile6(abs);
|
|
6919
7243
|
const metadata = imageMetadata(buf);
|
|
6920
|
-
if (!metadata) throw new Error(
|
|
7244
|
+
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
6921
7245
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
6922
|
-
throw new Error(
|
|
7246
|
+
throw new Error(`${what} has invalid dimensions: ${abs}`);
|
|
6923
7247
|
}
|
|
6924
7248
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
6925
7249
|
styleImageCache.set(abs, hit);
|
|
@@ -6941,7 +7265,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6941
7265
|
styleImageDimensions.push(loadedImage);
|
|
6942
7266
|
}
|
|
6943
7267
|
const resolvedImages = style.styleImages.map((image) => {
|
|
6944
|
-
const hit = styleImageCache.get(
|
|
7268
|
+
const hit = styleImageCache.get(path13.resolve(root, image.path));
|
|
6945
7269
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
6946
7270
|
});
|
|
6947
7271
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -6994,11 +7318,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6994
7318
|
);
|
|
6995
7319
|
}
|
|
6996
7320
|
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
6997
|
-
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
7321
|
+
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
6998
7322
|
const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
6999
|
-
const relFile = asset.file ??
|
|
7000
|
-
const outFile =
|
|
7001
|
-
const qualityOutFile = style.quality ?
|
|
7323
|
+
const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
|
|
7324
|
+
const outFile = path13.resolve(root, style.outDir, relFile);
|
|
7325
|
+
const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
7002
7326
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
7003
7327
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
7004
7328
|
const base = {
|
|
@@ -7026,7 +7350,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7026
7350
|
tileView: generator === "tiles" ? style.tileView : void 0,
|
|
7027
7351
|
tileFeature: generator === "tiles" ? style.tileFeature : void 0,
|
|
7028
7352
|
outlineMode: generator === "tiles" ? style.outlineMode : void 0,
|
|
7029
|
-
...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
|
|
7353
|
+
...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7030
7354
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
|
|
7031
7355
|
costUnit: "generations",
|
|
7032
7356
|
candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
|
|
@@ -7051,7 +7375,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7051
7375
|
minGridConfidence: style.quality.minGridConfidence,
|
|
7052
7376
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
7053
7377
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
7054
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
7378
|
+
...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
|
|
7055
7379
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
7056
7380
|
}
|
|
7057
7381
|
} : {},
|
|
@@ -7069,11 +7393,42 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7069
7393
|
const resolved = styleSpecs.get(assetId);
|
|
7070
7394
|
if (!resolved) {
|
|
7071
7395
|
throw new Error(
|
|
7072
|
-
`Asset "${assetId}" is not available in style "${styleId}"; revision parents must
|
|
7396
|
+
`Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
|
|
7073
7397
|
);
|
|
7074
7398
|
}
|
|
7075
7399
|
if (finalized.has(assetId)) return resolved;
|
|
7076
7400
|
const asset = manifest.assets[assetId];
|
|
7401
|
+
if (asset.mirror) {
|
|
7402
|
+
if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
|
|
7403
|
+
const sourceSpec = await finalize(asset.mirror);
|
|
7404
|
+
if (sourceSpec.generator === "tiles") {
|
|
7405
|
+
throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
|
|
7406
|
+
}
|
|
7407
|
+
if (sourceSpec.character?.kind === "animation") {
|
|
7408
|
+
const facing = sourceSpec.character.animation.direction;
|
|
7409
|
+
const mirrored = MIRRORED_DIRECTION[facing];
|
|
7410
|
+
if (mirrored === facing) {
|
|
7411
|
+
throw new Error(
|
|
7412
|
+
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
7413
|
+
);
|
|
7414
|
+
}
|
|
7415
|
+
resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
7416
|
+
} else if (sourceSpec.character) {
|
|
7417
|
+
resolved.character = { ...sourceSpec.character };
|
|
7418
|
+
}
|
|
7419
|
+
resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
|
|
7420
|
+
resolved.generator = sourceSpec.generator;
|
|
7421
|
+
resolved.width = sourceSpec.width;
|
|
7422
|
+
resolved.height = sourceSpec.height;
|
|
7423
|
+
resolved.size = sourceSpec.size;
|
|
7424
|
+
resolved.prompt = "";
|
|
7425
|
+
resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
|
|
7426
|
+
resolved.cost = 0;
|
|
7427
|
+
resolved.costUnit = sourceSpec.costUnit;
|
|
7428
|
+
resolved.candidates = 1;
|
|
7429
|
+
finalized.add(assetId);
|
|
7430
|
+
return resolved;
|
|
7431
|
+
}
|
|
7077
7432
|
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
7078
7433
|
if (resolved.character && characterParent) {
|
|
7079
7434
|
const parentSpec = await finalize(characterParent);
|
|
@@ -7097,9 +7452,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7097
7452
|
);
|
|
7098
7453
|
}
|
|
7099
7454
|
const sourceSpec = await finalize(asset.revision.from);
|
|
7100
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
7455
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
7101
7456
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
7102
|
-
const maskFile = asset.revision.mask ?
|
|
7457
|
+
const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
|
|
7103
7458
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
7104
7459
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
7105
7460
|
throw new Error(
|
|
@@ -7180,12 +7535,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7180
7535
|
return specs;
|
|
7181
7536
|
}
|
|
7182
7537
|
function pngPath(file) {
|
|
7183
|
-
const extension =
|
|
7538
|
+
const extension = path13.extname(file);
|
|
7184
7539
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
7185
7540
|
}
|
|
7186
7541
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
7187
7542
|
if (!existsSync8(file)) return null;
|
|
7188
|
-
const bytes = await
|
|
7543
|
+
const bytes = await readFile6(file);
|
|
7189
7544
|
const metadata = imageMetadata(bytes);
|
|
7190
7545
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
7191
7546
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -7198,8 +7553,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
7198
7553
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
7199
7554
|
const out = [];
|
|
7200
7555
|
for (const img of style.styleImages) {
|
|
7201
|
-
const file =
|
|
7202
|
-
const buf = await
|
|
7556
|
+
const file = path13.resolve(loaded.root, img.path);
|
|
7557
|
+
const buf = await readFile6(file);
|
|
7203
7558
|
const metadata = imageMetadata(buf);
|
|
7204
7559
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
7205
7560
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -7237,15 +7592,33 @@ function imageMetadata(buf) {
|
|
|
7237
7592
|
}
|
|
7238
7593
|
return null;
|
|
7239
7594
|
}
|
|
7240
|
-
function resolveCharacterShape(asset, style, kind) {
|
|
7595
|
+
async function resolveCharacterShape(asset, style, kind, files) {
|
|
7241
7596
|
const mode2 = style.mode ?? "standard";
|
|
7242
7597
|
const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
|
|
7598
|
+
const proportions = asset.proportions ?? style.proportions;
|
|
7243
7599
|
const shape = {
|
|
7244
7600
|
kind,
|
|
7245
7601
|
mode: mode2,
|
|
7246
7602
|
directions,
|
|
7247
|
-
template: style.template ?? "mannequin"
|
|
7603
|
+
template: style.template ?? "mannequin",
|
|
7604
|
+
...proportions !== void 0 ? { proportions } : {},
|
|
7605
|
+
...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
|
|
7606
|
+
...style.isometric !== void 0 ? { isometric: style.isometric } : {}
|
|
7248
7607
|
};
|
|
7608
|
+
if (asset.reference) {
|
|
7609
|
+
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
7610
|
+
shape.reference = {};
|
|
7611
|
+
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
7612
|
+
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
7613
|
+
shape.reference[direction] = {
|
|
7614
|
+
path: path13.resolve(files.root, rel),
|
|
7615
|
+
sha256: image.hash,
|
|
7616
|
+
width: image.width,
|
|
7617
|
+
height: image.height,
|
|
7618
|
+
format: image.format
|
|
7619
|
+
};
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7249
7622
|
if (asset.state) {
|
|
7250
7623
|
shape.state = {
|
|
7251
7624
|
paletteFromReference: asset.state.paletteFromReference,
|
|
@@ -7268,8 +7641,8 @@ function resolveCharacterShape(asset, style, kind) {
|
|
|
7268
7641
|
|
|
7269
7642
|
// src/pipeline/fetch.ts
|
|
7270
7643
|
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
|
|
7271
|
-
import { mkdir as
|
|
7272
|
-
import
|
|
7644
|
+
import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
7645
|
+
import path14 from "path";
|
|
7273
7646
|
|
|
7274
7647
|
// src/source-url.ts
|
|
7275
7648
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -7315,9 +7688,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7315
7688
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
7316
7689
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
7317
7690
|
normalizeLockOutputPaths(lock, specs);
|
|
7318
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
7691
|
+
const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
|
|
7319
7692
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
7320
7693
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
7694
|
+
if (specByKey.get(key)?.mirror) return false;
|
|
7321
7695
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7322
7696
|
if (e.status !== "downloaded") return false;
|
|
7323
7697
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
@@ -7416,7 +7790,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7416
7790
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7417
7791
|
fresh = await provider.download(source.url);
|
|
7418
7792
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
7419
|
-
log2(` current ${
|
|
7793
|
+
log2(` current ${path14.relative(process.cwd(), target)}`);
|
|
7420
7794
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7421
7795
|
continue;
|
|
7422
7796
|
}
|
|
@@ -7430,7 +7804,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7430
7804
|
if (cacheDir) {
|
|
7431
7805
|
await cacheMedia(
|
|
7432
7806
|
cacheDir,
|
|
7433
|
-
await
|
|
7807
|
+
await readFile7(target),
|
|
7434
7808
|
recorded.mediaType ?? MediaType.PNG,
|
|
7435
7809
|
recorded.sha256
|
|
7436
7810
|
);
|
|
@@ -7439,10 +7813,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7439
7813
|
continue;
|
|
7440
7814
|
}
|
|
7441
7815
|
if (reapply && !fresh) {
|
|
7442
|
-
if (!recorded.raw) raw = await
|
|
7443
|
-
log2(` reapply ${
|
|
7816
|
+
if (!recorded.raw) raw = await readFile7(target);
|
|
7817
|
+
log2(` reapply ${path14.relative(process.cwd(), target)}`);
|
|
7444
7818
|
} else {
|
|
7445
|
-
log2(` changed ${
|
|
7819
|
+
log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
|
|
7446
7820
|
}
|
|
7447
7821
|
} else {
|
|
7448
7822
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -7461,7 +7835,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7461
7835
|
}
|
|
7462
7836
|
}
|
|
7463
7837
|
log2(
|
|
7464
|
-
` replace ${
|
|
7838
|
+
` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
7465
7839
|
);
|
|
7466
7840
|
}
|
|
7467
7841
|
}
|
|
@@ -7470,7 +7844,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7470
7844
|
} else if (!raw && recorded && cacheDir) {
|
|
7471
7845
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
7472
7846
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
7473
|
-
if (final || raw) log2(` cached ${
|
|
7847
|
+
if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
|
|
7474
7848
|
}
|
|
7475
7849
|
if (!final && !raw) {
|
|
7476
7850
|
if (!source.url) {
|
|
@@ -7497,13 +7871,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7497
7871
|
buf = processed.bytes;
|
|
7498
7872
|
if (processed.changed) {
|
|
7499
7873
|
rawHash = sha256(raw);
|
|
7500
|
-
log2(` snapped ${
|
|
7874
|
+
log2(` snapped ${path14.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
7501
7875
|
}
|
|
7502
7876
|
}
|
|
7503
7877
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
7504
|
-
await
|
|
7878
|
+
await mkdir4(path14.dirname(target), { recursive: true });
|
|
7505
7879
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
7506
|
-
await
|
|
7880
|
+
await writeFile4(tmp, buf);
|
|
7507
7881
|
outputs.push({
|
|
7508
7882
|
path: portableOutputPath(target, spec.root),
|
|
7509
7883
|
sha256: sha256(buf),
|
|
@@ -7513,9 +7887,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7513
7887
|
});
|
|
7514
7888
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7515
7889
|
await saveLock(lockPath, lock);
|
|
7516
|
-
await
|
|
7890
|
+
await rename4(tmp, target);
|
|
7517
7891
|
wrote = true;
|
|
7518
|
-
log2(` wrote ${
|
|
7892
|
+
log2(` wrote ${path14.relative(process.cwd(), target)}`);
|
|
7519
7893
|
}
|
|
7520
7894
|
if (opts.refresh && !wrote) {
|
|
7521
7895
|
if (rotated) {
|
|
@@ -7559,10 +7933,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7559
7933
|
return result;
|
|
7560
7934
|
}
|
|
7561
7935
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
7562
|
-
const file =
|
|
7936
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
7563
7937
|
if (!existsSync9(file)) return null;
|
|
7564
7938
|
try {
|
|
7565
|
-
const buf = await
|
|
7939
|
+
const buf = await readFile7(file);
|
|
7566
7940
|
if (sha256(buf) !== hash) return null;
|
|
7567
7941
|
validateMedia(buf, mediaType);
|
|
7568
7942
|
return buf;
|
|
@@ -7573,13 +7947,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
7573
7947
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
7574
7948
|
const hash = knownHash ?? sha256(buf);
|
|
7575
7949
|
validateMedia(buf, mediaType);
|
|
7576
|
-
const file =
|
|
7950
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
7577
7951
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
7578
|
-
await
|
|
7952
|
+
await mkdir4(cacheDir, { recursive: true });
|
|
7579
7953
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7580
|
-
await
|
|
7954
|
+
await writeFile4(tmp, buf);
|
|
7581
7955
|
try {
|
|
7582
|
-
await
|
|
7956
|
+
await rename4(tmp, file);
|
|
7583
7957
|
} catch (err) {
|
|
7584
7958
|
if (!existsSync9(file)) throw err;
|
|
7585
7959
|
} finally {
|
|
@@ -7615,11 +7989,11 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
7615
7989
|
|
|
7616
7990
|
// src/pipeline/adopt.ts
|
|
7617
7991
|
import { existsSync as existsSync11 } from "fs";
|
|
7618
|
-
import { readFile as
|
|
7619
|
-
import
|
|
7992
|
+
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
7993
|
+
import path15 from "path";
|
|
7620
7994
|
|
|
7621
7995
|
// src/cache.ts
|
|
7622
|
-
import { mkdir as
|
|
7996
|
+
import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
7623
7997
|
import { existsSync as existsSync10 } from "fs";
|
|
7624
7998
|
import pathModule from "path";
|
|
7625
7999
|
import { z as z3 } from "zod";
|
|
@@ -7635,10 +8009,10 @@ function isSha256Hash(value) {
|
|
|
7635
8009
|
function parseCache(value) {
|
|
7636
8010
|
return HashCacheSchema.parse(value);
|
|
7637
8011
|
}
|
|
7638
|
-
async function loadCache(
|
|
7639
|
-
if (!existsSync10(
|
|
8012
|
+
async function loadCache(path45) {
|
|
8013
|
+
if (!existsSync10(path45)) return { version: 1, hashes: {} };
|
|
7640
8014
|
try {
|
|
7641
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
8015
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path45, "utf8")));
|
|
7642
8016
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
7643
8017
|
return {
|
|
7644
8018
|
version: 1,
|
|
@@ -7650,18 +8024,18 @@ async function loadCache(path44) {
|
|
|
7650
8024
|
return { version: 1, hashes: {} };
|
|
7651
8025
|
}
|
|
7652
8026
|
}
|
|
7653
|
-
async function saveCache(
|
|
8027
|
+
async function saveCache(path45, cache) {
|
|
7654
8028
|
const sorted = {};
|
|
7655
8029
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
7656
8030
|
const hash = cache.hashes[key];
|
|
7657
8031
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
7658
8032
|
sorted[key] = hash;
|
|
7659
8033
|
}
|
|
7660
|
-
await
|
|
7661
|
-
const tmp = `${
|
|
8034
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path45)), { recursive: true });
|
|
8035
|
+
const tmp = `${path45}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7662
8036
|
try {
|
|
7663
|
-
await
|
|
7664
|
-
await
|
|
8037
|
+
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
8038
|
+
await rename5(tmp, path45);
|
|
7665
8039
|
} finally {
|
|
7666
8040
|
await rm5(tmp, { force: true });
|
|
7667
8041
|
}
|
|
@@ -7699,7 +8073,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7699
8073
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
7700
8074
|
continue;
|
|
7701
8075
|
}
|
|
7702
|
-
const bytes = await
|
|
8076
|
+
const bytes = await readFile9(spec.outFile);
|
|
7703
8077
|
try {
|
|
7704
8078
|
decodePng(bytes);
|
|
7705
8079
|
} catch (err) {
|
|
@@ -7819,7 +8193,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
7819
8193
|
const log2 = opts.onProgress ?? (() => {
|
|
7820
8194
|
});
|
|
7821
8195
|
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
7822
|
-
const raw = JSON.parse(await
|
|
8196
|
+
const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
|
|
7823
8197
|
const best = /* @__PURE__ */ new Map();
|
|
7824
8198
|
for (const entry of Object.values(lock.entries)) {
|
|
7825
8199
|
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
@@ -7841,7 +8215,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
7841
8215
|
stillEmpty.push(id);
|
|
7842
8216
|
}
|
|
7843
8217
|
}
|
|
7844
|
-
await
|
|
8218
|
+
await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7845
8219
|
log2(` filled ${filled} prompt(s) from adopted objects`);
|
|
7846
8220
|
return { filled, stillEmpty };
|
|
7847
8221
|
}
|
|
@@ -7853,14 +8227,14 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7853
8227
|
|
|
7854
8228
|
// src/pipeline/adopt-characters.ts
|
|
7855
8229
|
import { existsSync as existsSync12 } from "fs";
|
|
7856
|
-
import { mkdir as
|
|
7857
|
-
import
|
|
8230
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
8231
|
+
import path16 from "path";
|
|
7858
8232
|
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
7859
8233
|
const log2 = opts.onProgress ?? (() => {
|
|
7860
8234
|
});
|
|
7861
8235
|
const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
|
|
7862
8236
|
if (!provider.listCharacters || !provider.getCharacter) return result;
|
|
7863
|
-
const wanted = specs.filter((spec) => spec.character);
|
|
8237
|
+
const wanted = specs.filter((spec) => spec.character && !spec.mirror);
|
|
7864
8238
|
if (!wanted.length) return result;
|
|
7865
8239
|
const remote = [];
|
|
7866
8240
|
for await (const character of provider.listCharacters()) remote.push(character);
|
|
@@ -7906,11 +8280,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7906
8280
|
const hash = sha256(bytes);
|
|
7907
8281
|
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
7908
8282
|
if (existsSync12(file)) {
|
|
7909
|
-
if (sha256(await
|
|
8283
|
+
if (sha256(await readFile10(file)) !== hash) result.differing.push(path16.relative(process.cwd(), file));
|
|
7910
8284
|
} else {
|
|
7911
|
-
await
|
|
7912
|
-
await
|
|
7913
|
-
result.written.push(
|
|
8285
|
+
await mkdir6(path16.dirname(file), { recursive: true });
|
|
8286
|
+
await writeFile7(file, bytes);
|
|
8287
|
+
result.written.push(path16.relative(process.cwd(), file));
|
|
7914
8288
|
}
|
|
7915
8289
|
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
7916
8290
|
}
|
|
@@ -7949,10 +8323,10 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7949
8323
|
} else {
|
|
7950
8324
|
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
7951
8325
|
if (!existsSync12(south)) {
|
|
7952
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
8326
|
+
result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), south)} to match by`);
|
|
7953
8327
|
return false;
|
|
7954
8328
|
}
|
|
7955
|
-
const localHash = sha256(await
|
|
8329
|
+
const localHash = sha256(await readFile10(south));
|
|
7956
8330
|
for (const candidate of remote) {
|
|
7957
8331
|
if (claimed.has(candidate.id)) continue;
|
|
7958
8332
|
if (await southHash(candidate) === localHash) {
|
|
@@ -7961,7 +8335,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7961
8335
|
}
|
|
7962
8336
|
}
|
|
7963
8337
|
if (!match) {
|
|
7964
|
-
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${
|
|
8338
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path16.relative(process.cwd(), south)}`);
|
|
7965
8339
|
return false;
|
|
7966
8340
|
}
|
|
7967
8341
|
}
|
|
@@ -8031,12 +8405,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
8031
8405
|
if (!existsSync12(first)) {
|
|
8032
8406
|
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
8033
8407
|
if (!byName) {
|
|
8034
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
8408
|
+
result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), first)} to match by`);
|
|
8035
8409
|
return false;
|
|
8036
8410
|
}
|
|
8037
8411
|
match = byName;
|
|
8038
8412
|
} else {
|
|
8039
|
-
const localHash = sha256(await
|
|
8413
|
+
const localHash = sha256(await readFile10(first));
|
|
8040
8414
|
for (const candidate of candidates) {
|
|
8041
8415
|
if (sha256(await download(candidate.frames[0])) === localHash) {
|
|
8042
8416
|
match = candidate;
|
|
@@ -8044,7 +8418,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
8044
8418
|
}
|
|
8045
8419
|
}
|
|
8046
8420
|
if (!match) {
|
|
8047
|
-
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${
|
|
8421
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path16.relative(process.cwd(), first)}`);
|
|
8048
8422
|
return false;
|
|
8049
8423
|
}
|
|
8050
8424
|
}
|
|
@@ -8104,9 +8478,9 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
8104
8478
|
}
|
|
8105
8479
|
|
|
8106
8480
|
// src/pipeline/salvage.ts
|
|
8107
|
-
import { readFile as
|
|
8481
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
8108
8482
|
import { existsSync as existsSync13 } from "fs";
|
|
8109
|
-
import
|
|
8483
|
+
import path17 from "path";
|
|
8110
8484
|
function providerClaimId(provider, objectId) {
|
|
8111
8485
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
8112
8486
|
}
|
|
@@ -8116,7 +8490,7 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
8116
8490
|
if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
8117
8491
|
let parsed;
|
|
8118
8492
|
try {
|
|
8119
|
-
parsed = parseLock(JSON.parse(await
|
|
8493
|
+
parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
|
|
8120
8494
|
} catch {
|
|
8121
8495
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
8122
8496
|
}
|
|
@@ -8164,20 +8538,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
8164
8538
|
return null;
|
|
8165
8539
|
}
|
|
8166
8540
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
8167
|
-
const own =
|
|
8541
|
+
const own = path17.resolve(ownManifestPath);
|
|
8168
8542
|
const siblingManifestPaths = [
|
|
8169
8543
|
.../* @__PURE__ */ new Set([
|
|
8170
8544
|
...workspaceManifestPaths,
|
|
8171
|
-
...claimPaths.map((c) =>
|
|
8545
|
+
...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
|
|
8172
8546
|
])
|
|
8173
8547
|
];
|
|
8174
8548
|
const siblings = [];
|
|
8175
8549
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
8176
|
-
if (
|
|
8550
|
+
if (path17.resolve(siblingManifestPath) === own) continue;
|
|
8177
8551
|
if (!existsSync13(siblingManifestPath)) continue;
|
|
8178
8552
|
try {
|
|
8179
8553
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
8180
|
-
siblings.push({ label:
|
|
8554
|
+
siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
|
|
8181
8555
|
} catch {
|
|
8182
8556
|
}
|
|
8183
8557
|
}
|
|
@@ -8293,9 +8667,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
8293
8667
|
}
|
|
8294
8668
|
|
|
8295
8669
|
// src/workspace.ts
|
|
8296
|
-
import { mkdir as
|
|
8670
|
+
import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
8297
8671
|
import { existsSync as existsSync14 } from "fs";
|
|
8298
|
-
import
|
|
8672
|
+
import path18 from "path";
|
|
8299
8673
|
import { z as z4 } from "zod";
|
|
8300
8674
|
var WorkspaceProjectSchema = z4.object({
|
|
8301
8675
|
id: z4.string().min(1),
|
|
@@ -8323,7 +8697,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
8323
8697
|
if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
|
|
8324
8698
|
let raw;
|
|
8325
8699
|
try {
|
|
8326
|
-
raw = JSON.parse(await
|
|
8700
|
+
raw = JSON.parse(await readFile12(workspacePath, "utf8"));
|
|
8327
8701
|
} catch (err) {
|
|
8328
8702
|
throw new ProjectError(
|
|
8329
8703
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -8337,22 +8711,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
8337
8711
|
version: 1,
|
|
8338
8712
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
8339
8713
|
};
|
|
8340
|
-
await
|
|
8714
|
+
await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
|
|
8341
8715
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8342
8716
|
try {
|
|
8343
|
-
await
|
|
8344
|
-
await
|
|
8717
|
+
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
8718
|
+
await rename6(tmp, workspacePath);
|
|
8345
8719
|
} finally {
|
|
8346
8720
|
await rm6(tmp, { force: true });
|
|
8347
8721
|
}
|
|
8348
8722
|
}
|
|
8349
8723
|
function toPortablePath(dir, absolute) {
|
|
8350
|
-
return
|
|
8724
|
+
return path18.relative(dir, absolute).split(path18.sep).join("/");
|
|
8351
8725
|
}
|
|
8352
8726
|
function resolveProject(dir, project) {
|
|
8353
8727
|
return {
|
|
8354
|
-
manifestPath:
|
|
8355
|
-
lockPath:
|
|
8728
|
+
manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
|
|
8729
|
+
lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
|
|
8356
8730
|
};
|
|
8357
8731
|
}
|
|
8358
8732
|
function validateWorkspace(ws, dir) {
|
|
@@ -8365,7 +8739,7 @@ function validateWorkspace(ws, dir) {
|
|
|
8365
8739
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
8366
8740
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
8367
8741
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
8368
|
-
if (
|
|
8742
|
+
if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
|
|
8369
8743
|
diagnostics.push({
|
|
8370
8744
|
id: "absolute-path",
|
|
8371
8745
|
level: "warning",
|
|
@@ -8426,8 +8800,8 @@ function validateWorkspace(ws, dir) {
|
|
|
8426
8800
|
}
|
|
8427
8801
|
|
|
8428
8802
|
// src/pick/salvage-server.ts
|
|
8429
|
-
import { mkdir as
|
|
8430
|
-
import
|
|
8803
|
+
import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
|
|
8804
|
+
import path19 from "path";
|
|
8431
8805
|
|
|
8432
8806
|
// src/pick/salvage-sheet.ts
|
|
8433
8807
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -8587,7 +8961,7 @@ refresh();
|
|
|
8587
8961
|
|
|
8588
8962
|
// src/pick/review-server.ts
|
|
8589
8963
|
import { createServer } from "http";
|
|
8590
|
-
import { readFile as
|
|
8964
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
8591
8965
|
|
|
8592
8966
|
// src/open.ts
|
|
8593
8967
|
import { spawn } from "child_process";
|
|
@@ -8614,7 +8988,7 @@ function serveReviewPage(opts) {
|
|
|
8614
8988
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
8615
8989
|
try {
|
|
8616
8990
|
const asset = opts.assets.get(req.url);
|
|
8617
|
-
const bytes = await
|
|
8991
|
+
const bytes = await readFile13(asset.path);
|
|
8618
8992
|
res.writeHead(200, {
|
|
8619
8993
|
"Content-Type": asset.contentType,
|
|
8620
8994
|
"Cache-Control": "no-store",
|
|
@@ -8691,7 +9065,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8691
9065
|
});
|
|
8692
9066
|
const html = renderSalvageSheet(orphans, {
|
|
8693
9067
|
styleId: ctx.styleId,
|
|
8694
|
-
importDir:
|
|
9068
|
+
importDir: path19.relative(process.cwd(), ctx.importDir) || "."
|
|
8695
9069
|
});
|
|
8696
9070
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
8697
9071
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -8720,10 +9094,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8720
9094
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
8721
9095
|
decodePng(buf);
|
|
8722
9096
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
8723
|
-
const rel =
|
|
8724
|
-
const outFile =
|
|
8725
|
-
await
|
|
8726
|
-
await
|
|
9097
|
+
const rel = path19.join("_salvaged", `${assetId}.png`);
|
|
9098
|
+
const outFile = path19.resolve(ctx.importDir, rel);
|
|
9099
|
+
await mkdir8(path19.dirname(outFile), { recursive: true });
|
|
9100
|
+
await writeFile9(outFile, buf);
|
|
8727
9101
|
ctx.manifest.assets[assetId] = {
|
|
8728
9102
|
prompt: orphan.prompt,
|
|
8729
9103
|
promptByStyle: {},
|
|
@@ -8755,7 +9129,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8755
9129
|
sourceUrl: durableSource,
|
|
8756
9130
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
8757
9131
|
outputs: [{
|
|
8758
|
-
path: portableOutputPath(outFile,
|
|
9132
|
+
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
8759
9133
|
sha256: sha256(buf)
|
|
8760
9134
|
}],
|
|
8761
9135
|
submittedAt: orphan.createdAt,
|
|
@@ -8779,9 +9153,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8779
9153
|
}
|
|
8780
9154
|
}
|
|
8781
9155
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
8782
|
-
const raw = JSON.parse(await
|
|
9156
|
+
const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
|
|
8783
9157
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
8784
|
-
await
|
|
9158
|
+
await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
8785
9159
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
8786
9160
|
return result;
|
|
8787
9161
|
}
|
|
@@ -8789,19 +9163,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8789
9163
|
}
|
|
8790
9164
|
|
|
8791
9165
|
// src/cli/project.ts
|
|
8792
|
-
import
|
|
9166
|
+
import path21 from "path";
|
|
8793
9167
|
import { existsSync as existsSync15 } from "fs";
|
|
8794
9168
|
|
|
8795
9169
|
// src/project.ts
|
|
8796
|
-
import
|
|
9170
|
+
import path20 from "path";
|
|
8797
9171
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
8798
|
-
const absolute =
|
|
8799
|
-
const root =
|
|
9172
|
+
const absolute = path20.resolve(manifestPath);
|
|
9173
|
+
const root = path20.dirname(absolute);
|
|
8800
9174
|
if (opts.env !== false) {
|
|
8801
9175
|
loadEnvFiles(root);
|
|
8802
|
-
if (
|
|
9176
|
+
if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
8803
9177
|
}
|
|
8804
|
-
const lockPath =
|
|
9178
|
+
const lockPath = path20.resolve(opts.lockPath ?? path20.join(root, "pixelkiln.lock.json"));
|
|
8805
9179
|
const loaded = await loadManifest(absolute);
|
|
8806
9180
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
8807
9181
|
const lock = await loadLock(lockPath);
|
|
@@ -8932,7 +9306,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
8932
9306
|
|
|
8933
9307
|
// src/cli/project.ts
|
|
8934
9308
|
async function provenanceFile(id, file) {
|
|
8935
|
-
const absolute =
|
|
9309
|
+
const absolute = path21.resolve(file);
|
|
8936
9310
|
return {
|
|
8937
9311
|
id,
|
|
8938
9312
|
path: absolute,
|
|
@@ -9011,7 +9385,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
|
9011
9385
|
if (!existsSync15(workspacePath)) {
|
|
9012
9386
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
9013
9387
|
}
|
|
9014
|
-
const dir =
|
|
9388
|
+
const dir = path21.dirname(path21.resolve(workspacePath));
|
|
9015
9389
|
const ws = await loadWorkspace(workspacePath);
|
|
9016
9390
|
const diagnostics = validateWorkspace(ws, dir);
|
|
9017
9391
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -9034,9 +9408,9 @@ async function openAccountProject(args) {
|
|
|
9034
9408
|
return project;
|
|
9035
9409
|
}
|
|
9036
9410
|
async function open(args) {
|
|
9037
|
-
if (!existsSync15(
|
|
9411
|
+
if (!existsSync15(path21.resolve(args.manifest))) {
|
|
9038
9412
|
throw new ProjectError(
|
|
9039
|
-
`No manifest at ${
|
|
9413
|
+
`No manifest at ${path21.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
9040
9414
|
);
|
|
9041
9415
|
}
|
|
9042
9416
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -9078,7 +9452,7 @@ function providerCache(mode2) {
|
|
|
9078
9452
|
|
|
9079
9453
|
// src/cli/commands/account.ts
|
|
9080
9454
|
async function runBalance(args) {
|
|
9081
|
-
loadEnvFiles(
|
|
9455
|
+
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
9082
9456
|
loadEnvFiles(process.cwd());
|
|
9083
9457
|
const loaded = await loadManifest(args.manifest);
|
|
9084
9458
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -9151,12 +9525,12 @@ async function runAdopt(args) {
|
|
|
9151
9525
|
tagged ${n} object(s) upstream`);
|
|
9152
9526
|
}
|
|
9153
9527
|
if (args.writePrompts) {
|
|
9154
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
9528
|
+
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
9155
9529
|
onProgress: log,
|
|
9156
9530
|
provider: provider.id,
|
|
9157
9531
|
assetIds: specs.map((spec) => spec.assetId)
|
|
9158
9532
|
});
|
|
9159
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
9533
|
+
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
9160
9534
|
const reloaded = await loadManifest(args.manifest);
|
|
9161
9535
|
const rebased = (await resolveSpecs(reloaded, {
|
|
9162
9536
|
styles: args.styles,
|
|
@@ -9186,12 +9560,12 @@ async function runSalvage2(args) {
|
|
|
9186
9560
|
const provider = providerFor(accountProvider);
|
|
9187
9561
|
const jsonMode = args.dryRun && args.json;
|
|
9188
9562
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
9189
|
-
const ownLock =
|
|
9563
|
+
const ownLock = path22.resolve(args.lock);
|
|
9190
9564
|
let workspaceProjects = [];
|
|
9191
9565
|
let workspaceDir = "";
|
|
9192
9566
|
if (args.workspace) {
|
|
9193
|
-
const workspacePath =
|
|
9194
|
-
workspaceDir =
|
|
9567
|
+
const workspacePath = path22.resolve(args.workspace);
|
|
9568
|
+
workspaceDir = path22.dirname(workspacePath);
|
|
9195
9569
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
9196
9570
|
workspaceProjects = complete.ws.projects;
|
|
9197
9571
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -9201,11 +9575,11 @@ async function runSalvage2(args) {
|
|
|
9201
9575
|
.../* @__PURE__ */ new Set([
|
|
9202
9576
|
...workspaceLockPaths,
|
|
9203
9577
|
...existsSync16(ownLock) ? [ownLock] : [],
|
|
9204
|
-
...args.claims.map((c) =>
|
|
9578
|
+
...args.claims.map((c) => path22.resolve(c))
|
|
9205
9579
|
])
|
|
9206
9580
|
];
|
|
9207
9581
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
9208
|
-
for (const p of lockPaths) diag(` ${
|
|
9582
|
+
for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
|
|
9209
9583
|
if (!args.claims.length && !args.workspace) {
|
|
9210
9584
|
diag(
|
|
9211
9585
|
`
|
|
@@ -9213,7 +9587,7 @@ async function runSalvage2(args) {
|
|
|
9213
9587
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
9214
9588
|
register every project in a workspace catalog and pass --workspace.`
|
|
9215
9589
|
);
|
|
9216
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
9590
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
|
|
9217
9591
|
diag(
|
|
9218
9592
|
`
|
|
9219
9593
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -9297,7 +9671,7 @@ async function runSalvage2(args) {
|
|
|
9297
9671
|
manifestPath: loaded.path,
|
|
9298
9672
|
manifest: accountManifest,
|
|
9299
9673
|
styleId,
|
|
9300
|
-
importDir:
|
|
9674
|
+
importDir: path22.resolve(loaded.root, style.outDir),
|
|
9301
9675
|
lock,
|
|
9302
9676
|
lockPath: args.lock
|
|
9303
9677
|
},
|
|
@@ -9398,17 +9772,17 @@ async function runTag(args) {
|
|
|
9398
9772
|
}
|
|
9399
9773
|
|
|
9400
9774
|
// src/cli/commands/edit.ts
|
|
9401
|
-
import
|
|
9775
|
+
import path25 from "path";
|
|
9402
9776
|
|
|
9403
9777
|
// src/pipeline/hand-edit.ts
|
|
9404
9778
|
import { existsSync as existsSync17 } from "fs";
|
|
9405
|
-
import { copyFile, mkdir as
|
|
9406
|
-
import
|
|
9779
|
+
import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
|
|
9780
|
+
import path24 from "path";
|
|
9407
9781
|
import { z as z6 } from "zod";
|
|
9408
9782
|
|
|
9409
9783
|
// src/manifest-edit.ts
|
|
9410
|
-
import { readFile as
|
|
9411
|
-
import
|
|
9784
|
+
import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
|
|
9785
|
+
import path23 from "path";
|
|
9412
9786
|
import { z as z5 } from "zod";
|
|
9413
9787
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
9414
9788
|
var AssetPatchSchema = z5.object({
|
|
@@ -9623,8 +9997,8 @@ function applyEdit(raw, edit) {
|
|
|
9623
9997
|
}
|
|
9624
9998
|
}
|
|
9625
9999
|
async function applyManifestEdit(manifestPath, edit) {
|
|
9626
|
-
const absolute =
|
|
9627
|
-
const original = await
|
|
10000
|
+
const absolute = path23.resolve(manifestPath);
|
|
10001
|
+
const original = await readFile15(absolute, "utf8");
|
|
9628
10002
|
const before = sha256(original);
|
|
9629
10003
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
9630
10004
|
let raw;
|
|
@@ -9638,12 +10012,12 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9638
10012
|
applyEdit(raw, edit);
|
|
9639
10013
|
const next = serializeLike(original, raw);
|
|
9640
10014
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
9641
|
-
const tmp =
|
|
9642
|
-
|
|
9643
|
-
`.${
|
|
10015
|
+
const tmp = path23.join(
|
|
10016
|
+
path23.dirname(absolute),
|
|
10017
|
+
`.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
9644
10018
|
);
|
|
9645
10019
|
try {
|
|
9646
|
-
await
|
|
10020
|
+
await writeFile10(tmp, next);
|
|
9647
10021
|
try {
|
|
9648
10022
|
const loaded = await loadManifest(tmp);
|
|
9649
10023
|
await resolveSpecs(loaded);
|
|
@@ -9651,7 +10025,7 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9651
10025
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
9652
10026
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
9653
10027
|
}
|
|
9654
|
-
await
|
|
10028
|
+
await rename7(tmp, absolute);
|
|
9655
10029
|
} finally {
|
|
9656
10030
|
await rm7(tmp, { force: true });
|
|
9657
10031
|
}
|
|
@@ -9662,11 +10036,11 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9662
10036
|
var HAND_EDIT_DIR = "edits";
|
|
9663
10037
|
function handEditPath(loaded, spec) {
|
|
9664
10038
|
const style = loaded.manifest.styles[spec.styleId];
|
|
9665
|
-
const outDir =
|
|
9666
|
-
const relative =
|
|
9667
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
9668
|
-
const file = inside ? relative :
|
|
9669
|
-
return
|
|
10039
|
+
const outDir = path24.resolve(loaded.root, style?.outDir ?? ".");
|
|
10040
|
+
const relative = path24.relative(outDir, spec.outFile);
|
|
10041
|
+
const inside = relative && !relative.startsWith("..") && !path24.isAbsolute(relative);
|
|
10042
|
+
const file = inside ? relative : path24.basename(spec.outFile);
|
|
10043
|
+
return path24.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
9670
10044
|
}
|
|
9671
10045
|
function handEditBases(spec, lock) {
|
|
9672
10046
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -9708,7 +10082,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
9708
10082
|
`${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`
|
|
9709
10083
|
);
|
|
9710
10084
|
}
|
|
9711
|
-
const existing = spec.source ?
|
|
10085
|
+
const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
|
|
9712
10086
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
9713
10087
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
9714
10088
|
let created = false;
|
|
@@ -9719,7 +10093,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
9719
10093
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
9720
10094
|
);
|
|
9721
10095
|
}
|
|
9722
|
-
await
|
|
10096
|
+
await mkdir9(path24.dirname(member.path), { recursive: true });
|
|
9723
10097
|
await copyFile(member.base, member.path);
|
|
9724
10098
|
created = true;
|
|
9725
10099
|
}
|
|
@@ -9798,7 +10172,7 @@ function handEditProjectPath(editPath) {
|
|
|
9798
10172
|
async function readHandEditCompanion(editPath) {
|
|
9799
10173
|
let text;
|
|
9800
10174
|
try {
|
|
9801
|
-
text = await
|
|
10175
|
+
text = await readFile16(handEditCompanionPath(editPath), "utf8");
|
|
9802
10176
|
} catch {
|
|
9803
10177
|
return null;
|
|
9804
10178
|
}
|
|
@@ -9841,7 +10215,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
9841
10215
|
}
|
|
9842
10216
|
});
|
|
9843
10217
|
const bases = handEditBases(spec, lock);
|
|
9844
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
10218
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile16(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
9845
10219
|
for (const [index, image] of decoded.entries()) {
|
|
9846
10220
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
9847
10221
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -9857,7 +10231,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
9857
10231
|
editor: input.editor,
|
|
9858
10232
|
protocol: input.protocol,
|
|
9859
10233
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9860
|
-
project: projectPath ?
|
|
10234
|
+
project: projectPath ? path24.basename(projectPath) : null,
|
|
9861
10235
|
outputs: incoming.map((frame, index) => ({
|
|
9862
10236
|
role: frame.role,
|
|
9863
10237
|
basedOn: basedOnHash(entry, index),
|
|
@@ -9887,8 +10261,8 @@ function basedOnHash(entry, index) {
|
|
|
9887
10261
|
async function replaceFile(file, bytes) {
|
|
9888
10262
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9889
10263
|
try {
|
|
9890
|
-
await
|
|
9891
|
-
await
|
|
10264
|
+
await writeFile11(tmp, bytes);
|
|
10265
|
+
await rename8(tmp, file);
|
|
9892
10266
|
} finally {
|
|
9893
10267
|
await rm8(tmp, { force: true });
|
|
9894
10268
|
}
|
|
@@ -9917,7 +10291,7 @@ async function runEdit(args) {
|
|
|
9917
10291
|
const set = started.members.length > 1;
|
|
9918
10292
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
9919
10293
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
9920
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
10294
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path25.relative(process.cwd(), member.path)}`);
|
|
9921
10295
|
if (args.noOpen) {
|
|
9922
10296
|
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`);
|
|
9923
10297
|
} else {
|
|
@@ -9927,17 +10301,17 @@ async function runEdit(args) {
|
|
|
9927
10301
|
}
|
|
9928
10302
|
|
|
9929
10303
|
// src/cli/commands/gallery.ts
|
|
9930
|
-
import
|
|
10304
|
+
import path32 from "path";
|
|
9931
10305
|
import { existsSync as existsSync23 } from "fs";
|
|
9932
10306
|
|
|
9933
10307
|
// src/gallery/snapshot.ts
|
|
9934
10308
|
import { createHash as createHash3 } from "crypto";
|
|
9935
10309
|
import { existsSync as existsSync19 } from "fs";
|
|
9936
|
-
import { readFile as
|
|
9937
|
-
import
|
|
10310
|
+
import { readFile as readFile17, stat as stat2 } from "fs/promises";
|
|
10311
|
+
import path28 from "path";
|
|
9938
10312
|
|
|
9939
10313
|
// src/gallery/edit.ts
|
|
9940
|
-
import
|
|
10314
|
+
import path26 from "path";
|
|
9941
10315
|
import { z as z7 } from "zod";
|
|
9942
10316
|
var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
9943
10317
|
var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
@@ -10027,7 +10401,7 @@ function createGalleryEditHandler(opts) {
|
|
|
10027
10401
|
}
|
|
10028
10402
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
10029
10403
|
if (result.changed) {
|
|
10030
|
-
const relative =
|
|
10404
|
+
const relative = path26.relative(process.cwd(), result.manifestPath);
|
|
10031
10405
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
10032
10406
|
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;
|
|
10033
10407
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -10038,7 +10412,7 @@ function createGalleryEditHandler(opts) {
|
|
|
10038
10412
|
|
|
10039
10413
|
// src/pipeline/history.ts
|
|
10040
10414
|
import { existsSync as existsSync18 } from "fs";
|
|
10041
|
-
import
|
|
10415
|
+
import path27 from "path";
|
|
10042
10416
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
10043
10417
|
function historyLimit(manifest, env = process.env) {
|
|
10044
10418
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -10127,7 +10501,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
10127
10501
|
if (!existsSync18(file)) continue;
|
|
10128
10502
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
10129
10503
|
throw new OverwriteRefusedError(
|
|
10130
|
-
`${
|
|
10504
|
+
`${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
10131
10505
|
[file]
|
|
10132
10506
|
);
|
|
10133
10507
|
}
|
|
@@ -10179,7 +10553,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
10179
10553
|
// src/gallery/snapshot.ts
|
|
10180
10554
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
10181
10555
|
function galleryMediaId(absolutePath) {
|
|
10182
|
-
return createHash3("sha256").update(
|
|
10556
|
+
return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
10183
10557
|
}
|
|
10184
10558
|
function galleryMediaRoute(id) {
|
|
10185
10559
|
return `/media/${id}`;
|
|
@@ -10198,7 +10572,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10198
10572
|
return (entry.history ?? []).map((generation, i) => {
|
|
10199
10573
|
const outputs = generation.outputs.map((output) => {
|
|
10200
10574
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
10201
|
-
const file =
|
|
10575
|
+
const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
10202
10576
|
let url = null;
|
|
10203
10577
|
if (existsSync19(file)) {
|
|
10204
10578
|
const id = galleryMediaId(file);
|
|
@@ -10229,7 +10603,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10229
10603
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
10230
10604
|
async function samePixels(a, b) {
|
|
10231
10605
|
try {
|
|
10232
|
-
const [left, right] = await Promise.all([
|
|
10606
|
+
const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
|
|
10233
10607
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
10234
10608
|
const x = decodePng(left);
|
|
10235
10609
|
const y = decodePng(right);
|
|
@@ -10299,9 +10673,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
10299
10673
|
async function describeQuality(media, root, inspection) {
|
|
10300
10674
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
10301
10675
|
const outputs = await Promise.all(
|
|
10302
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
10676
|
+
outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
|
|
10303
10677
|
);
|
|
10304
|
-
const recordPath =
|
|
10678
|
+
const recordPath = path28.resolve(root, inspection.record);
|
|
10305
10679
|
const recordExists = existsSync19(recordPath);
|
|
10306
10680
|
let options = null;
|
|
10307
10681
|
let check = null;
|
|
@@ -10321,7 +10695,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
10321
10695
|
return {
|
|
10322
10696
|
state: inspection.state,
|
|
10323
10697
|
reason: inspection.reason,
|
|
10324
|
-
output: portableOutputPath(
|
|
10698
|
+
output: portableOutputPath(path28.resolve(root, inspection.output), root),
|
|
10325
10699
|
record: portableOutputPath(recordPath, root),
|
|
10326
10700
|
recordExists,
|
|
10327
10701
|
outputs,
|
|
@@ -10358,9 +10732,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
10358
10732
|
const media = /* @__PURE__ */ new Map();
|
|
10359
10733
|
const plan = await buildPlan(specs, lock);
|
|
10360
10734
|
const items = [];
|
|
10361
|
-
const manifestText = await
|
|
10735
|
+
const manifestText = await readFile17(loaded.path, "utf8");
|
|
10362
10736
|
const manifestSha256 = sha256(manifestText);
|
|
10363
|
-
const cacheDir =
|
|
10737
|
+
const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
10364
10738
|
let rawStyles = {};
|
|
10365
10739
|
try {
|
|
10366
10740
|
const raw = JSON.parse(manifestText);
|
|
@@ -10384,7 +10758,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10384
10758
|
)
|
|
10385
10759
|
);
|
|
10386
10760
|
if (spec.source) {
|
|
10387
|
-
const editPath =
|
|
10761
|
+
const editPath = path28.resolve(root, spec.source);
|
|
10388
10762
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
10389
10763
|
const editShas = await Promise.all(
|
|
10390
10764
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
@@ -10433,7 +10807,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10433
10807
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
10434
10808
|
}
|
|
10435
10809
|
} else if (spec.source) {
|
|
10436
|
-
outputs = [await describeOutput(media, root,
|
|
10810
|
+
outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
|
|
10437
10811
|
} else if (!entry && existsSync19(spec.outFile)) {
|
|
10438
10812
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
10439
10813
|
} else {
|
|
@@ -10473,6 +10847,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10473
10847
|
currentSpecHash: spec.specHash,
|
|
10474
10848
|
revision: entry?.revision ?? null,
|
|
10475
10849
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
10850
|
+
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
10476
10851
|
character: describeCharacter(spec, entry),
|
|
10477
10852
|
outputs,
|
|
10478
10853
|
quality,
|
|
@@ -10535,6 +10910,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10535
10910
|
currentSpecHash: null,
|
|
10536
10911
|
revision: entry.revision,
|
|
10537
10912
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
10913
|
+
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
10538
10914
|
character: describeCharacter(void 0, entry),
|
|
10539
10915
|
outputs,
|
|
10540
10916
|
quality: null,
|
|
@@ -10623,7 +10999,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10623
10999
|
id: loaded.manifest.name,
|
|
10624
11000
|
name: loaded.manifest.name,
|
|
10625
11001
|
manifest: loaded.path,
|
|
10626
|
-
lock:
|
|
11002
|
+
lock: path28.resolve(opts.lockPath),
|
|
10627
11003
|
root,
|
|
10628
11004
|
provider: loaded.manifest.provider,
|
|
10629
11005
|
account: null,
|
|
@@ -10649,7 +11025,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10649
11025
|
return { snapshot, media };
|
|
10650
11026
|
}
|
|
10651
11027
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
10652
|
-
const dir =
|
|
11028
|
+
const dir = path28.dirname(path28.resolve(opts.workspacePath));
|
|
10653
11029
|
const media = /* @__PURE__ */ new Map();
|
|
10654
11030
|
const projects = [];
|
|
10655
11031
|
const items = [];
|
|
@@ -10692,11 +11068,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10692
11068
|
id: project.id,
|
|
10693
11069
|
name: loaded.manifest.name,
|
|
10694
11070
|
manifest: manifestPath,
|
|
10695
|
-
lock:
|
|
11071
|
+
lock: path28.resolve(lockPath),
|
|
10696
11072
|
root: loaded.root,
|
|
10697
11073
|
provider: loaded.manifest.provider,
|
|
10698
11074
|
account: project.account ?? null,
|
|
10699
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
11075
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
|
|
10700
11076
|
historyLimit: historyLimit(loaded.manifest),
|
|
10701
11077
|
entries: Object.keys(lock.entries).length,
|
|
10702
11078
|
items: projectItems.length,
|
|
@@ -10708,8 +11084,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10708
11084
|
id: project.id,
|
|
10709
11085
|
name: project.id,
|
|
10710
11086
|
manifest: manifestPath,
|
|
10711
|
-
lock:
|
|
10712
|
-
root:
|
|
11087
|
+
lock: path28.resolve(lockPath),
|
|
11088
|
+
root: path28.dirname(manifestPath),
|
|
10713
11089
|
provider: project.provider,
|
|
10714
11090
|
account: project.account ?? null,
|
|
10715
11091
|
manifestSha256: null,
|
|
@@ -10736,7 +11112,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10736
11112
|
version: 1,
|
|
10737
11113
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
10738
11114
|
project: null,
|
|
10739
|
-
workspace: { path:
|
|
11115
|
+
workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
|
|
10740
11116
|
filter,
|
|
10741
11117
|
totals: {
|
|
10742
11118
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -10755,7 +11131,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10755
11131
|
// src/gallery/server.ts
|
|
10756
11132
|
import { randomBytes } from "crypto";
|
|
10757
11133
|
import { createServer as createServer2 } from "http";
|
|
10758
|
-
import { readFile as
|
|
11134
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
10759
11135
|
|
|
10760
11136
|
// src/gallery/page.ts
|
|
10761
11137
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -10996,7 +11372,7 @@ async function serveGallery(opts) {
|
|
|
10996
11372
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
10997
11373
|
if (!asset) return fail(404, "no such review asset");
|
|
10998
11374
|
try {
|
|
10999
|
-
const bytes = await
|
|
11375
|
+
const bytes = await readFile18(asset.path);
|
|
11000
11376
|
res.writeHead(200, {
|
|
11001
11377
|
"Content-Type": asset.contentType,
|
|
11002
11378
|
"Cache-Control": "no-store",
|
|
@@ -11072,7 +11448,7 @@ async function serveGallery(opts) {
|
|
|
11072
11448
|
if (!asset) return fail(404, "no such gallery media");
|
|
11073
11449
|
let bytes;
|
|
11074
11450
|
try {
|
|
11075
|
-
bytes = await
|
|
11451
|
+
bytes = await readFile18(asset.path);
|
|
11076
11452
|
} catch {
|
|
11077
11453
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
11078
11454
|
}
|
|
@@ -11216,7 +11592,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11216
11592
|
});
|
|
11217
11593
|
const estimates = new Map(items.map((item) => [
|
|
11218
11594
|
item.key,
|
|
11219
|
-
validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
11595
|
+
item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
11220
11596
|
]));
|
|
11221
11597
|
const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
|
|
11222
11598
|
if (units.size > 1) {
|
|
@@ -11266,6 +11642,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11266
11642
|
}
|
|
11267
11643
|
for (const { spec, key } of items) {
|
|
11268
11644
|
const estimate = estimates.get(key);
|
|
11645
|
+
if (spec.mirror) {
|
|
11646
|
+
try {
|
|
11647
|
+
await requireRevisionReady(spec, lock);
|
|
11648
|
+
await deriveMirror(spec, lock, lockPath, { onProgress: log2 });
|
|
11649
|
+
submitted++;
|
|
11650
|
+
} catch (err) {
|
|
11651
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
11652
|
+
failed++;
|
|
11653
|
+
upsert(lock, key, {
|
|
11654
|
+
styleId: spec.styleId,
|
|
11655
|
+
assetId: spec.assetId,
|
|
11656
|
+
specHash: spec.specHash,
|
|
11657
|
+
generator: spec.generator,
|
|
11658
|
+
prompt: "",
|
|
11659
|
+
width: spec.width,
|
|
11660
|
+
height: spec.height,
|
|
11661
|
+
status: "failed",
|
|
11662
|
+
error: message6,
|
|
11663
|
+
outputs: lock.entries[key]?.outputs ?? [],
|
|
11664
|
+
cost: 0,
|
|
11665
|
+
costUnit: spec.costUnit,
|
|
11666
|
+
provider: provider.id
|
|
11667
|
+
});
|
|
11668
|
+
await saveLock(lockPath, lock);
|
|
11669
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
11670
|
+
}
|
|
11671
|
+
continue;
|
|
11672
|
+
}
|
|
11269
11673
|
await waitForSlot();
|
|
11270
11674
|
const since = Date.now() - lastSubmitAt;
|
|
11271
11675
|
if (since < spacing) await sleep2(spacing - since);
|
|
@@ -11391,12 +11795,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11391
11795
|
|
|
11392
11796
|
// src/pick/server.ts
|
|
11393
11797
|
import { existsSync as existsSync21 } from "fs";
|
|
11394
|
-
import { readFile as
|
|
11798
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
11395
11799
|
|
|
11396
11800
|
// src/pipeline/init.ts
|
|
11397
|
-
import { readdir, readFile as
|
|
11801
|
+
import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
|
|
11398
11802
|
import { existsSync as existsSync20 } from "fs";
|
|
11399
|
-
import
|
|
11803
|
+
import path29 from "path";
|
|
11400
11804
|
function pngSize(buf) {
|
|
11401
11805
|
if (buf.length < 24) return null;
|
|
11402
11806
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -11410,7 +11814,7 @@ function slugify(value) {
|
|
|
11410
11814
|
async function walk(dir, exclude) {
|
|
11411
11815
|
const out = [];
|
|
11412
11816
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
11413
|
-
const full =
|
|
11817
|
+
const full = path29.join(dir, entry.name);
|
|
11414
11818
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
11415
11819
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
11416
11820
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -11424,14 +11828,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
11424
11828
|
const skipped = [];
|
|
11425
11829
|
const seen = /* @__PURE__ */ new Map();
|
|
11426
11830
|
for (const file of files.sort()) {
|
|
11427
|
-
const size = pngSize(await
|
|
11831
|
+
const size = pngSize(await readFile19(file));
|
|
11428
11832
|
if (!size) {
|
|
11429
11833
|
skipped.push(`${file} (not a readable PNG)`);
|
|
11430
11834
|
continue;
|
|
11431
11835
|
}
|
|
11432
|
-
const rel =
|
|
11433
|
-
const category =
|
|
11434
|
-
let id = slugify(
|
|
11836
|
+
const rel = path29.relative(root, file);
|
|
11837
|
+
const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
|
|
11838
|
+
let id = slugify(path29.basename(rel));
|
|
11435
11839
|
if (seen.has(id)) {
|
|
11436
11840
|
const n = seen.get(id) + 1;
|
|
11437
11841
|
seen.set(id, n);
|
|
@@ -11492,7 +11896,7 @@ async function writeManifestFile(target, manifest) {
|
|
|
11492
11896
|
if (existsSync20(target)) {
|
|
11493
11897
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
11494
11898
|
}
|
|
11495
|
-
await
|
|
11899
|
+
await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
11496
11900
|
}
|
|
11497
11901
|
|
|
11498
11902
|
// src/pick/sheet.ts
|
|
@@ -11898,7 +12302,7 @@ async function currentArt(entry, spec) {
|
|
|
11898
12302
|
let width = entry.width;
|
|
11899
12303
|
let height = entry.height;
|
|
11900
12304
|
if (contentType === "image/png") {
|
|
11901
|
-
const size = pngSize(await
|
|
12305
|
+
const size = pngSize(await readFile20(file));
|
|
11902
12306
|
if (size) ({ width, height } = size);
|
|
11903
12307
|
}
|
|
11904
12308
|
return { path: file, contentType, width, height };
|
|
@@ -12391,15 +12795,15 @@ function createGenerateHandlers(opts) {
|
|
|
12391
12795
|
}
|
|
12392
12796
|
|
|
12393
12797
|
// src/gallery/editor.ts
|
|
12394
|
-
import { readFile as
|
|
12395
|
-
import
|
|
12798
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
12799
|
+
import path31 from "path";
|
|
12396
12800
|
|
|
12397
12801
|
// src/editor/install.ts
|
|
12398
12802
|
import { createHash as createHash4 } from "crypto";
|
|
12399
12803
|
import { existsSync as existsSync22 } from "fs";
|
|
12400
|
-
import { mkdir as
|
|
12804
|
+
import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
|
|
12401
12805
|
import os2 from "os";
|
|
12402
|
-
import
|
|
12806
|
+
import path30 from "path";
|
|
12403
12807
|
|
|
12404
12808
|
// tools/pixelorama-bridge/pin.json
|
|
12405
12809
|
var pin_default = {
|
|
@@ -12463,15 +12867,15 @@ var EditorInstallError = class extends Error {
|
|
|
12463
12867
|
};
|
|
12464
12868
|
function editorToolsRoot() {
|
|
12465
12869
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
12466
|
-
if (override) return
|
|
12467
|
-
if (process.platform === "darwin") return
|
|
12870
|
+
if (override) return path30.resolve(override);
|
|
12871
|
+
if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
12468
12872
|
if (process.platform === "win32") {
|
|
12469
|
-
return
|
|
12873
|
+
return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
12470
12874
|
}
|
|
12471
|
-
return
|
|
12875
|
+
return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
12472
12876
|
}
|
|
12473
12877
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
12474
|
-
return
|
|
12878
|
+
return path30.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
12475
12879
|
}
|
|
12476
12880
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
12477
12881
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -12481,7 +12885,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
12481
12885
|
}
|
|
12482
12886
|
async function sha256Of(file) {
|
|
12483
12887
|
try {
|
|
12484
|
-
return createHash4("sha256").update(await
|
|
12888
|
+
return createHash4("sha256").update(await readFile21(file)).digest("hex");
|
|
12485
12889
|
} catch {
|
|
12486
12890
|
return null;
|
|
12487
12891
|
}
|
|
@@ -12494,7 +12898,7 @@ async function editorStatus(opts = {}) {
|
|
|
12494
12898
|
let totalBytes = 0;
|
|
12495
12899
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
12496
12900
|
totalBytes += expected.bytes;
|
|
12497
|
-
const file =
|
|
12901
|
+
const file = path30.join(dir, name);
|
|
12498
12902
|
const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
|
|
12499
12903
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
12500
12904
|
installedBytes += expected.bytes;
|
|
@@ -12524,7 +12928,7 @@ async function installEditor(opts = {}) {
|
|
|
12524
12928
|
);
|
|
12525
12929
|
}
|
|
12526
12930
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
12527
|
-
await
|
|
12931
|
+
await mkdir10(dir, { recursive: true });
|
|
12528
12932
|
const downloaded = [];
|
|
12529
12933
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
12530
12934
|
let fetchedBytes = before.installedBytes;
|
|
@@ -12552,11 +12956,11 @@ async function installEditor(opts = {}) {
|
|
|
12552
12956
|
if (digest2 !== expected.sha256) {
|
|
12553
12957
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
12554
12958
|
}
|
|
12555
|
-
const final =
|
|
12959
|
+
const final = path30.join(dir, name);
|
|
12556
12960
|
const tmp = `${final}.${process.pid}.part`;
|
|
12557
12961
|
try {
|
|
12558
|
-
await
|
|
12559
|
-
await
|
|
12962
|
+
await writeFile13(tmp, bytes);
|
|
12963
|
+
await rename9(tmp, final);
|
|
12560
12964
|
} finally {
|
|
12561
12965
|
await rm9(tmp, { force: true });
|
|
12562
12966
|
}
|
|
@@ -12598,7 +13002,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
12598
13002
|
".json": "application/json; charset=utf-8"
|
|
12599
13003
|
};
|
|
12600
13004
|
function editorContentType(name) {
|
|
12601
|
-
return EDITOR_CONTENT_TYPES[
|
|
13005
|
+
return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
|
|
12602
13006
|
}
|
|
12603
13007
|
|
|
12604
13008
|
// src/gallery/editor.ts
|
|
@@ -12674,7 +13078,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12674
13078
|
if (!status.installed) return null;
|
|
12675
13079
|
let bytes;
|
|
12676
13080
|
try {
|
|
12677
|
-
bytes = await
|
|
13081
|
+
bytes = await readFile22(path31.join(dir, name));
|
|
12678
13082
|
} catch {
|
|
12679
13083
|
verified = null;
|
|
12680
13084
|
return null;
|
|
@@ -12714,13 +13118,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
12714
13118
|
};
|
|
12715
13119
|
const loadProject = async (project) => {
|
|
12716
13120
|
const ctx = await access2.loadProject(project);
|
|
12717
|
-
const dir =
|
|
13121
|
+
const dir = path32.dirname(ctx.loaded.path);
|
|
12718
13122
|
const declared = readEnvFiles(dir);
|
|
12719
13123
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
12720
13124
|
for (const [name, value] of Object.entries(declared)) {
|
|
12721
13125
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
12722
13126
|
throw new Error(
|
|
12723
|
-
`${
|
|
13127
|
+
`${path32.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.`
|
|
12724
13128
|
);
|
|
12725
13129
|
}
|
|
12726
13130
|
}
|
|
@@ -12765,7 +13169,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
12765
13169
|
}
|
|
12766
13170
|
async function runGallery(args) {
|
|
12767
13171
|
if (args.workspace) {
|
|
12768
|
-
const workspacePath =
|
|
13172
|
+
const workspacePath = path32.resolve(args.workspace);
|
|
12769
13173
|
if (!existsSync23(workspacePath)) {
|
|
12770
13174
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
12771
13175
|
}
|
|
@@ -12786,7 +13190,7 @@ async function runGallery(args) {
|
|
|
12786
13190
|
const registered = async (projectId) => {
|
|
12787
13191
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
12788
13192
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
12789
|
-
return resolveProject(
|
|
13193
|
+
return resolveProject(path32.dirname(workspacePath), project);
|
|
12790
13194
|
};
|
|
12791
13195
|
await serveUntilStopped(initial, build2, args, {
|
|
12792
13196
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -12811,7 +13215,7 @@ async function runGallery(args) {
|
|
|
12811
13215
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
12812
13216
|
};
|
|
12813
13217
|
await serveUntilStopped(await build(), reload, args, {
|
|
12814
|
-
manifestFor: () =>
|
|
13218
|
+
manifestFor: () => path32.resolve(args.manifest),
|
|
12815
13219
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
12816
13220
|
});
|
|
12817
13221
|
}
|
|
@@ -13054,7 +13458,7 @@ async function runLifecycle(args, wave, spent) {
|
|
|
13054
13458
|
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
13055
13459
|
}
|
|
13056
13460
|
}
|
|
13057
|
-
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision);
|
|
13461
|
+
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision || spec.mirror);
|
|
13058
13462
|
}
|
|
13059
13463
|
return false;
|
|
13060
13464
|
}
|
|
@@ -13120,14 +13524,14 @@ import { existsSync as existsSync25 } from "fs";
|
|
|
13120
13524
|
|
|
13121
13525
|
// src/pipeline/cache-health.ts
|
|
13122
13526
|
import { existsSync as existsSync24 } from "fs";
|
|
13123
|
-
import { readFile as
|
|
13124
|
-
import
|
|
13527
|
+
import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
13528
|
+
import path33 from "path";
|
|
13125
13529
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
13126
13530
|
if (options.prune && !existsSync24(lockPath)) {
|
|
13127
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
13531
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
|
|
13128
13532
|
}
|
|
13129
|
-
const contentDir =
|
|
13130
|
-
const remotePath =
|
|
13533
|
+
const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
|
|
13534
|
+
const remotePath = path33.resolve(cachePathFor(lockPath));
|
|
13131
13535
|
const referenced = referencedHashes(lock);
|
|
13132
13536
|
let content = await inspectContentCache(contentDir, referenced);
|
|
13133
13537
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -13139,7 +13543,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
13139
13543
|
]);
|
|
13140
13544
|
for (const name of names) {
|
|
13141
13545
|
try {
|
|
13142
|
-
await rm10(
|
|
13546
|
+
await rm10(path33.join(contentDir, name), { force: true });
|
|
13143
13547
|
removed.contentFiles++;
|
|
13144
13548
|
} catch {
|
|
13145
13549
|
}
|
|
@@ -13148,7 +13552,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
13148
13552
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
13149
13553
|
removed.resetRemoteHashCache = true;
|
|
13150
13554
|
} else if (remoteHashes.invalidIds.length) {
|
|
13151
|
-
const cache = parseCache(JSON.parse(await
|
|
13555
|
+
const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
|
|
13152
13556
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
13153
13557
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
13154
13558
|
await saveCache(remotePath, cache);
|
|
@@ -13187,12 +13591,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
13187
13591
|
continue;
|
|
13188
13592
|
}
|
|
13189
13593
|
report.files++;
|
|
13190
|
-
const file =
|
|
13594
|
+
const file = path33.join(contentDir, entry.name);
|
|
13191
13595
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
13192
13596
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
13193
13597
|
let bytes;
|
|
13194
13598
|
try {
|
|
13195
|
-
bytes = await
|
|
13599
|
+
bytes = await readFile23(file);
|
|
13196
13600
|
report.bytes += bytes.length;
|
|
13197
13601
|
} catch (err) {
|
|
13198
13602
|
report.invalid.push({
|
|
@@ -13239,7 +13643,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
13239
13643
|
if (!report.exists) return report;
|
|
13240
13644
|
let cache;
|
|
13241
13645
|
try {
|
|
13242
|
-
cache = parseCache(JSON.parse(await
|
|
13646
|
+
cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
|
|
13243
13647
|
} catch (err) {
|
|
13244
13648
|
report.error = err instanceof Error ? err.message : String(err);
|
|
13245
13649
|
return report;
|
|
@@ -13358,42 +13762,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
13358
13762
|
}
|
|
13359
13763
|
|
|
13360
13764
|
// src/cli/commands/init.ts
|
|
13361
|
-
import
|
|
13765
|
+
import path34 from "path";
|
|
13362
13766
|
import { existsSync as existsSync26 } from "fs";
|
|
13363
13767
|
async function runInit(args) {
|
|
13364
13768
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13365
|
-
const root =
|
|
13769
|
+
const root = path34.resolve(args.from);
|
|
13366
13770
|
if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
|
|
13367
13771
|
const generator = args.generator ?? "map";
|
|
13368
13772
|
if (generator !== "1dir" && generator !== "map") {
|
|
13369
13773
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13370
13774
|
}
|
|
13371
|
-
const target =
|
|
13775
|
+
const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13372
13776
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13373
13777
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13374
13778
|
const manifest = buildManifest(
|
|
13375
|
-
args.name ??
|
|
13779
|
+
args.name ?? path34.basename(path34.dirname(target)),
|
|
13376
13780
|
args.styles[0] ?? "base",
|
|
13377
13781
|
generator,
|
|
13378
|
-
|
|
13782
|
+
path34.relative(path34.dirname(target), root) || ".",
|
|
13379
13783
|
assets
|
|
13380
13784
|
);
|
|
13381
13785
|
await writeManifestFile(target, manifest);
|
|
13382
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
13786
|
+
log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
|
|
13383
13787
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13384
|
-
log(` wrote ${
|
|
13788
|
+
log(` wrote ${path34.relative(process.cwd(), target)}`);
|
|
13385
13789
|
log(`
|
|
13386
13790
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13387
13791
|
log(` PixelLab account instead of inventing them:`);
|
|
13388
13792
|
log(`
|
|
13389
|
-
pixelkiln adopt --manifest ${
|
|
13793
|
+
pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
|
|
13390
13794
|
`);
|
|
13391
13795
|
}
|
|
13392
13796
|
|
|
13393
13797
|
// src/pipeline/doctor.ts
|
|
13394
13798
|
import { constants, existsSync as existsSync27 } from "fs";
|
|
13395
13799
|
import { access, stat as stat4 } from "fs/promises";
|
|
13396
|
-
import
|
|
13800
|
+
import path35 from "path";
|
|
13397
13801
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
13398
13802
|
const checks = [];
|
|
13399
13803
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -13404,8 +13808,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13404
13808
|
);
|
|
13405
13809
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
13406
13810
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
13407
|
-
|
|
13408
|
-
...spec.quality ? [
|
|
13811
|
+
path35.dirname(spec.outFile),
|
|
13812
|
+
...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
|
|
13409
13813
|
]))];
|
|
13410
13814
|
const unwritable = [];
|
|
13411
13815
|
for (const dir of dirs) {
|
|
@@ -13428,7 +13832,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13428
13832
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
13429
13833
|
const output = entry.outputs[index];
|
|
13430
13834
|
const spec = specByKey.get(key);
|
|
13431
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
13835
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
|
|
13432
13836
|
const owner = outputOwners.get(absolute);
|
|
13433
13837
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
13434
13838
|
outputOwners.set(absolute, key);
|
|
@@ -13443,7 +13847,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13443
13847
|
} else {
|
|
13444
13848
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
13445
13849
|
}
|
|
13446
|
-
const cacheDir =
|
|
13850
|
+
const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
|
|
13447
13851
|
const stranded = [];
|
|
13448
13852
|
const invalidState = [];
|
|
13449
13853
|
const staleMapUrls = [];
|
|
@@ -13459,7 +13863,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13459
13863
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
13460
13864
|
let hasCache = false;
|
|
13461
13865
|
for (const output of entry.outputs) {
|
|
13462
|
-
const cached =
|
|
13866
|
+
const cached = path35.join(
|
|
13463
13867
|
cacheDir,
|
|
13464
13868
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
13465
13869
|
);
|
|
@@ -13559,16 +13963,16 @@ async function nearestExistingDirectory(start) {
|
|
|
13559
13963
|
if ((await stat4(current)).isDirectory()) return current;
|
|
13560
13964
|
} catch {
|
|
13561
13965
|
}
|
|
13562
|
-
const parent =
|
|
13966
|
+
const parent = path35.dirname(current);
|
|
13563
13967
|
if (parent === current) return current;
|
|
13564
13968
|
current = parent;
|
|
13565
13969
|
}
|
|
13566
13970
|
}
|
|
13567
13971
|
|
|
13568
13972
|
// src/pipeline/audit.ts
|
|
13569
|
-
import { readFile as
|
|
13973
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
13570
13974
|
import { existsSync as existsSync28 } from "fs";
|
|
13571
|
-
import
|
|
13975
|
+
import path36 from "path";
|
|
13572
13976
|
function paletteDistance(asset, reference) {
|
|
13573
13977
|
if (!asset.length || !reference.length) return 0;
|
|
13574
13978
|
let total = 0;
|
|
@@ -13609,7 +14013,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
13609
14013
|
continue;
|
|
13610
14014
|
}
|
|
13611
14015
|
try {
|
|
13612
|
-
const png = decodePng(await
|
|
14016
|
+
const png = decodePng(await readFile24(output.absolutePath));
|
|
13613
14017
|
const palette = extractPalette(png, 12);
|
|
13614
14018
|
assets.push({
|
|
13615
14019
|
assetId: spec.assetId,
|
|
@@ -13633,10 +14037,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
13633
14037
|
let referenceFromStyleImages = false;
|
|
13634
14038
|
const refPalettes = [];
|
|
13635
14039
|
for (const img of style.styleImages) {
|
|
13636
|
-
const abs =
|
|
14040
|
+
const abs = path36.resolve(loaded.root, img.path);
|
|
13637
14041
|
if (!existsSync28(abs)) continue;
|
|
13638
14042
|
try {
|
|
13639
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
14043
|
+
refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
|
|
13640
14044
|
} catch {
|
|
13641
14045
|
}
|
|
13642
14046
|
}
|
|
@@ -13856,24 +14260,24 @@ async function runAudit(args) {
|
|
|
13856
14260
|
}
|
|
13857
14261
|
|
|
13858
14262
|
// src/cli/commands/pack.ts
|
|
13859
|
-
import
|
|
13860
|
-
import { readFile as
|
|
14263
|
+
import path38 from "path";
|
|
14264
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
13861
14265
|
|
|
13862
14266
|
// src/pipeline/pack.ts
|
|
13863
14267
|
import { readFileSync as readFileSync5 } from "fs";
|
|
13864
|
-
import
|
|
14268
|
+
import path37 from "path";
|
|
13865
14269
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
13866
14270
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
13867
14271
|
if (!Array.isArray(raw) || !raw.length) {
|
|
13868
14272
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
13869
14273
|
}
|
|
13870
|
-
const baseDir =
|
|
14274
|
+
const baseDir = path37.dirname(path37.resolve(inputsFilePath));
|
|
13871
14275
|
return raw.map((entry, i) => {
|
|
13872
14276
|
const e = entry;
|
|
13873
14277
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
13874
14278
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
13875
14279
|
}
|
|
13876
|
-
return { id: e.id, path:
|
|
14280
|
+
return { id: e.id, path: path37.resolve(baseDir, e.path) };
|
|
13877
14281
|
});
|
|
13878
14282
|
}
|
|
13879
14283
|
function packSprites(inputs, options = {}) {
|
|
@@ -13945,7 +14349,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
13945
14349
|
}
|
|
13946
14350
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
13947
14351
|
id,
|
|
13948
|
-
path:
|
|
14352
|
+
path: path37.resolve(source)
|
|
13949
14353
|
}));
|
|
13950
14354
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
13951
14355
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -13969,7 +14373,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
13969
14373
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
13970
14374
|
const source = sources[id];
|
|
13971
14375
|
if (source && outputs.length <= 1) {
|
|
13972
|
-
inputs.push({ id, path:
|
|
14376
|
+
inputs.push({ id, path: path37.resolve(manifestDir, source) });
|
|
13973
14377
|
continue;
|
|
13974
14378
|
}
|
|
13975
14379
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -14003,7 +14407,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
14003
14407
|
}
|
|
14004
14408
|
}
|
|
14005
14409
|
for (const [id, source] of Object.entries(sources)) {
|
|
14006
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
14410
|
+
if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
|
|
14007
14411
|
}
|
|
14008
14412
|
if (!inputs.length) {
|
|
14009
14413
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -14111,7 +14515,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14111
14515
|
const source = sources[id];
|
|
14112
14516
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
14113
14517
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
14114
|
-
placements.push({ id, path:
|
|
14518
|
+
placements.push({ id, path: path37.resolve(manifestDir, source), cell });
|
|
14115
14519
|
continue;
|
|
14116
14520
|
}
|
|
14117
14521
|
if (source && entry) {
|
|
@@ -14144,7 +14548,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14144
14548
|
`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}`
|
|
14145
14549
|
);
|
|
14146
14550
|
}
|
|
14147
|
-
const basePng = mount.base ? readFileSync5(
|
|
14551
|
+
const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
|
|
14148
14552
|
const mounted = mountSprites(placements, {
|
|
14149
14553
|
cellWidth: mount.cellWidth,
|
|
14150
14554
|
cellHeight: mount.cellHeight,
|
|
@@ -14154,7 +14558,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14154
14558
|
...mounted,
|
|
14155
14559
|
sources: mount.base ? [{
|
|
14156
14560
|
id: "$base",
|
|
14157
|
-
path:
|
|
14561
|
+
path: path37.resolve(manifestDir, mount.base),
|
|
14158
14562
|
sha256: sha256(basePng),
|
|
14159
14563
|
included: true
|
|
14160
14564
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -14521,12 +14925,12 @@ async function runPack(args) {
|
|
|
14521
14925
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
14522
14926
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
14523
14927
|
}
|
|
14524
|
-
const raw = JSON.parse(await
|
|
14928
|
+
const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
|
|
14525
14929
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
14526
14930
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
14527
14931
|
const format = sheetFormat(args);
|
|
14528
|
-
const base =
|
|
14529
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14932
|
+
const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
14933
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
|
|
14530
14934
|
const outputs = [
|
|
14531
14935
|
{ path: `${base}.png`, data: png },
|
|
14532
14936
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -14539,7 +14943,7 @@ async function runPack(args) {
|
|
|
14539
14943
|
log(
|
|
14540
14944
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
14541
14945
|
);
|
|
14542
|
-
log(` ${
|
|
14946
|
+
log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
14543
14947
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14544
14948
|
return;
|
|
14545
14949
|
}
|
|
@@ -14547,7 +14951,7 @@ async function runPack(args) {
|
|
|
14547
14951
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14548
14952
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14549
14953
|
}
|
|
14550
|
-
const manifestDir =
|
|
14954
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14551
14955
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14552
14956
|
for (const styleId of styleIds) {
|
|
14553
14957
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14560,9 +14964,9 @@ async function runPack(args) {
|
|
|
14560
14964
|
sources: manifestSources(loaded.manifest, styleId)
|
|
14561
14965
|
});
|
|
14562
14966
|
const style = loaded.manifest.styles[styleId];
|
|
14563
|
-
const base = args.out ?
|
|
14967
|
+
const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
14564
14968
|
const format = sheetFormat(args);
|
|
14565
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14969
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
|
|
14566
14970
|
const outputs = [
|
|
14567
14971
|
{ path: `${base}.png`, data: png },
|
|
14568
14972
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -14593,13 +14997,13 @@ async function runPack(args) {
|
|
|
14593
14997
|
log(
|
|
14594
14998
|
` ${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})`
|
|
14595
14999
|
);
|
|
14596
|
-
log(` ${
|
|
15000
|
+
log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
14597
15001
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14598
15002
|
}
|
|
14599
15003
|
}
|
|
14600
15004
|
async function runMount(args) {
|
|
14601
15005
|
const { loaded, lock } = await openProject2(args);
|
|
14602
|
-
const manifestDir =
|
|
15006
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14603
15007
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14604
15008
|
for (const styleId of styleIds) {
|
|
14605
15009
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14638,9 +15042,9 @@ async function runMount(args) {
|
|
|
14638
15042
|
sources,
|
|
14639
15043
|
outputRoles
|
|
14640
15044
|
);
|
|
14641
|
-
const out =
|
|
15045
|
+
const out = path38.resolve(manifestDir, style.mount.out);
|
|
14642
15046
|
const format = sheetFormat(args);
|
|
14643
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
15047
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
|
|
14644
15048
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
14645
15049
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14646
15050
|
const outputs = [
|
|
@@ -14660,7 +15064,7 @@ async function runMount(args) {
|
|
|
14660
15064
|
await provenanceFile("$lock", args.lock),
|
|
14661
15065
|
...qualityRecords,
|
|
14662
15066
|
...artifactSources.filter(
|
|
14663
|
-
(source) => source.id !== "$base" ||
|
|
15067
|
+
(source) => source.id !== "$base" || path38.resolve(source.path) !== out
|
|
14664
15068
|
)
|
|
14665
15069
|
],
|
|
14666
15070
|
options: {
|
|
@@ -14674,14 +15078,14 @@ async function runMount(args) {
|
|
|
14674
15078
|
log(
|
|
14675
15079
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14676
15080
|
);
|
|
14677
|
-
log(` ${
|
|
15081
|
+
log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14678
15082
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14679
15083
|
}
|
|
14680
15084
|
}
|
|
14681
15085
|
async function runExport(args) {
|
|
14682
15086
|
const { loaded, specs, lock } = await openProject2(args);
|
|
14683
15087
|
const format = args.format ?? "generic";
|
|
14684
|
-
const manifestDir =
|
|
15088
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14685
15089
|
const selected = specs.filter((spec) => {
|
|
14686
15090
|
if (spec.generator !== "tiles") return false;
|
|
14687
15091
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14697,12 +15101,12 @@ async function runExport(args) {
|
|
|
14697
15101
|
for (const spec of selected) {
|
|
14698
15102
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14699
15103
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14700
|
-
const defaultBase =
|
|
14701
|
-
const base = args.out ?
|
|
15104
|
+
const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
15105
|
+
const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14702
15106
|
const result = exportTileset(entry, spec, {
|
|
14703
15107
|
format,
|
|
14704
15108
|
manifestDir,
|
|
14705
|
-
imageName:
|
|
15109
|
+
imageName: path38.basename(`${base}.png`),
|
|
14706
15110
|
columns: args.columns
|
|
14707
15111
|
});
|
|
14708
15112
|
const outputs = [
|
|
@@ -14720,7 +15124,7 @@ async function runExport(args) {
|
|
|
14720
15124
|
asset: spec.assetId,
|
|
14721
15125
|
columns: args.columns ?? null,
|
|
14722
15126
|
format,
|
|
14723
|
-
image:
|
|
15127
|
+
image: path38.basename(`${base}.png`),
|
|
14724
15128
|
providerRules: result.generic.providerRules,
|
|
14725
15129
|
style: spec.styleId,
|
|
14726
15130
|
tileType: spec.tileType ?? null
|
|
@@ -14730,7 +15134,7 @@ async function runExport(args) {
|
|
|
14730
15134
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14731
15135
|
);
|
|
14732
15136
|
log(
|
|
14733
|
-
` ${
|
|
15137
|
+
` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14734
15138
|
);
|
|
14735
15139
|
}
|
|
14736
15140
|
}
|
|
@@ -14743,19 +15147,19 @@ function sheetFormat(args) {
|
|
|
14743
15147
|
}
|
|
14744
15148
|
|
|
14745
15149
|
// src/cli/commands/quality.ts
|
|
14746
|
-
import
|
|
14747
|
-
import { readFile as
|
|
15150
|
+
import path40 from "path";
|
|
15151
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
14748
15152
|
|
|
14749
15153
|
// src/pipeline/quality-regression.ts
|
|
14750
|
-
import
|
|
15154
|
+
import path39 from "path";
|
|
14751
15155
|
import { existsSync as existsSync29 } from "fs";
|
|
14752
|
-
import { readFile as
|
|
15156
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
14753
15157
|
import { z as z9 } from "zod";
|
|
14754
15158
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
14755
15159
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
14756
15160
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
14757
15161
|
var QualityPathSchema = z9.string().min(1).refine(
|
|
14758
|
-
(value) => !
|
|
15162
|
+
(value) => !path39.posix.isAbsolute(value) && !value.includes("\\"),
|
|
14759
15163
|
"expected a portable relative path"
|
|
14760
15164
|
);
|
|
14761
15165
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -14902,10 +15306,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
14902
15306
|
};
|
|
14903
15307
|
}
|
|
14904
15308
|
async function measureImageQuality(file) {
|
|
14905
|
-
const absolute =
|
|
15309
|
+
const absolute = path39.resolve(file);
|
|
14906
15310
|
let bytes;
|
|
14907
15311
|
try {
|
|
14908
|
-
bytes = await
|
|
15312
|
+
bytes = await readFile26(absolute);
|
|
14909
15313
|
} catch (error) {
|
|
14910
15314
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
14911
15315
|
}
|
|
@@ -14924,11 +15328,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
14924
15328
|
}
|
|
14925
15329
|
const ids = parsed.data.map((entry) => entry.id);
|
|
14926
15330
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
14927
|
-
const root =
|
|
15331
|
+
const root = path39.dirname(path39.resolve(inputsFilePath));
|
|
14928
15332
|
return parsed.data.map((entry) => ({
|
|
14929
15333
|
id: entry.id,
|
|
14930
|
-
file:
|
|
14931
|
-
...entry.record ? { record:
|
|
15334
|
+
file: path39.resolve(root, entry.path),
|
|
15335
|
+
...entry.record ? { record: path39.resolve(root, entry.record) } : {},
|
|
14932
15336
|
tolerances: QualityToleranceSchema.parse({
|
|
14933
15337
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
14934
15338
|
...entry.tolerances
|
|
@@ -14936,7 +15340,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
14936
15340
|
}));
|
|
14937
15341
|
}
|
|
14938
15342
|
function portableRelative2(from, to) {
|
|
14939
|
-
return
|
|
15343
|
+
return path39.relative(from, path39.resolve(to)).split(path39.sep).join("/") || ".";
|
|
14940
15344
|
}
|
|
14941
15345
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
14942
15346
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -14951,20 +15355,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
14951
15355
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
14952
15356
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
14953
15357
|
}
|
|
14954
|
-
const outputs = record.outputs.map((output) =>
|
|
14955
|
-
if (!outputs.includes(
|
|
15358
|
+
const outputs = record.outputs.map((output) => path39.resolve(path39.dirname(recordPath), output.path));
|
|
15359
|
+
if (!outputs.includes(path39.resolve(imagePath))) {
|
|
14956
15360
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
14957
15361
|
}
|
|
14958
15362
|
}
|
|
14959
15363
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
14960
15364
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
14961
|
-
const absolute =
|
|
15365
|
+
const absolute = path39.resolve(baselinePath);
|
|
14962
15366
|
for (const input of inputs) {
|
|
14963
|
-
if (
|
|
15367
|
+
if (path39.resolve(input.file) === absolute || input.record && path39.resolve(input.record) === absolute) {
|
|
14964
15368
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
14965
15369
|
}
|
|
14966
15370
|
}
|
|
14967
|
-
const root =
|
|
15371
|
+
const root = path39.dirname(absolute);
|
|
14968
15372
|
const cases = [];
|
|
14969
15373
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
14970
15374
|
const expected = await measureImageQuality(input.file);
|
|
@@ -14992,7 +15396,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
14992
15396
|
});
|
|
14993
15397
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
14994
15398
|
if (existsSync29(absolute) && !options.force) {
|
|
14995
|
-
const current = await
|
|
15399
|
+
const current = await readFile26(absolute);
|
|
14996
15400
|
if (!current.equals(data)) {
|
|
14997
15401
|
throw new OverwriteRefusedError(
|
|
14998
15402
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -15004,10 +15408,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
15004
15408
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
15005
15409
|
}
|
|
15006
15410
|
async function readQualityBaseline(baselinePath) {
|
|
15007
|
-
const absolute =
|
|
15411
|
+
const absolute = path39.resolve(baselinePath);
|
|
15008
15412
|
let raw;
|
|
15009
15413
|
try {
|
|
15010
|
-
raw = JSON.parse(await
|
|
15414
|
+
raw = JSON.parse(await readFile26(absolute, "utf8"));
|
|
15011
15415
|
} catch (error) {
|
|
15012
15416
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
15013
15417
|
}
|
|
@@ -15077,12 +15481,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
15077
15481
|
return { violations, warnings };
|
|
15078
15482
|
}
|
|
15079
15483
|
async function checkQualityBaseline(baselinePath) {
|
|
15080
|
-
const absolute =
|
|
15484
|
+
const absolute = path39.resolve(baselinePath);
|
|
15081
15485
|
const baseline = await readQualityBaseline(absolute);
|
|
15082
|
-
const root =
|
|
15486
|
+
const root = path39.dirname(absolute);
|
|
15083
15487
|
const cases = [];
|
|
15084
15488
|
for (const entry of baseline.cases) {
|
|
15085
|
-
const file =
|
|
15489
|
+
const file = path39.resolve(root, entry.file);
|
|
15086
15490
|
let actual = null;
|
|
15087
15491
|
const violations = [];
|
|
15088
15492
|
const warnings = [];
|
|
@@ -15095,7 +15499,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
15095
15499
|
violations.push(message4(error));
|
|
15096
15500
|
}
|
|
15097
15501
|
if (entry.record) {
|
|
15098
|
-
const recordPath =
|
|
15502
|
+
const recordPath = path39.resolve(root, entry.record.path);
|
|
15099
15503
|
try {
|
|
15100
15504
|
const recordHash = await sha256File(recordPath);
|
|
15101
15505
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -15137,15 +15541,15 @@ async function runQuality(args) {
|
|
|
15137
15541
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
15138
15542
|
let raw;
|
|
15139
15543
|
try {
|
|
15140
|
-
raw = JSON.parse(await
|
|
15544
|
+
raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
|
|
15141
15545
|
} catch (error) {
|
|
15142
15546
|
throw new Error(
|
|
15143
|
-
`Could not read quality inputs ${
|
|
15547
|
+
`Could not read quality inputs ${path40.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
15144
15548
|
{ cause: error }
|
|
15145
15549
|
);
|
|
15146
15550
|
}
|
|
15147
15551
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
15148
|
-
if (
|
|
15552
|
+
if (path40.resolve(args.inputs) === path40.resolve(args.out)) {
|
|
15149
15553
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
15150
15554
|
}
|
|
15151
15555
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -15158,7 +15562,7 @@ async function runQuality(args) {
|
|
|
15158
15562
|
}, null, 2));
|
|
15159
15563
|
} else {
|
|
15160
15564
|
log(
|
|
15161
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
15565
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path40.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
15162
15566
|
);
|
|
15163
15567
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
15164
15568
|
}
|
|
@@ -15184,12 +15588,12 @@ async function runQuality(args) {
|
|
|
15184
15588
|
}
|
|
15185
15589
|
|
|
15186
15590
|
// src/cli/commands/recipe.ts
|
|
15187
|
-
import
|
|
15591
|
+
import path42 from "path";
|
|
15188
15592
|
|
|
15189
15593
|
// src/recipes.ts
|
|
15190
|
-
import
|
|
15594
|
+
import path41 from "path";
|
|
15191
15595
|
import { existsSync as existsSync30 } from "fs";
|
|
15192
|
-
import { readdir as readdir3, readFile as
|
|
15596
|
+
import { readdir as readdir3, readFile as readFile28 } from "fs/promises";
|
|
15193
15597
|
import { fileURLToPath } from "url";
|
|
15194
15598
|
import { z as z10 } from "zod";
|
|
15195
15599
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -15198,7 +15602,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
15198
15602
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
15199
15603
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
15200
15604
|
var PortablePathSchema = z10.string().min(1).refine((value) => {
|
|
15201
|
-
if (value.includes("\\") ||
|
|
15605
|
+
if (value.includes("\\") || path41.posix.isAbsolute(value)) return false;
|
|
15202
15606
|
const parts = value.split("/");
|
|
15203
15607
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
15204
15608
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -15374,10 +15778,10 @@ function message5(error) {
|
|
|
15374
15778
|
return error instanceof Error ? error.message : String(error);
|
|
15375
15779
|
}
|
|
15376
15780
|
async function readRecipeFile(recipePath, bundled) {
|
|
15377
|
-
const absolute =
|
|
15781
|
+
const absolute = path41.resolve(recipePath);
|
|
15378
15782
|
let raw;
|
|
15379
15783
|
try {
|
|
15380
|
-
raw = JSON.parse(await
|
|
15784
|
+
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
15381
15785
|
} catch (error) {
|
|
15382
15786
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
15383
15787
|
}
|
|
@@ -15388,13 +15792,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
15388
15792
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
15389
15793
|
);
|
|
15390
15794
|
}
|
|
15391
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
15795
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path41.dirname(absolute), bundled };
|
|
15392
15796
|
}
|
|
15393
15797
|
async function walkRecipeFiles(root) {
|
|
15394
15798
|
if (!existsSync30(root)) return [];
|
|
15395
15799
|
const out = [];
|
|
15396
15800
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
15397
|
-
const resolved =
|
|
15801
|
+
const resolved = path41.join(root, entry.name);
|
|
15398
15802
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
15399
15803
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
15400
15804
|
}
|
|
@@ -15405,7 +15809,7 @@ function bundledRecipeRoot() {
|
|
|
15405
15809
|
}
|
|
15406
15810
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
15407
15811
|
const recipes = await Promise.all(
|
|
15408
|
-
(await walkRecipeFiles(
|
|
15812
|
+
(await walkRecipeFiles(path41.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
15409
15813
|
);
|
|
15410
15814
|
return recipes.sort(
|
|
15411
15815
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -15428,9 +15832,9 @@ function parseSelector(selector) {
|
|
|
15428
15832
|
return { id: match[1], version: match[2] };
|
|
15429
15833
|
}
|
|
15430
15834
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
15431
|
-
const local =
|
|
15835
|
+
const local = path41.resolve(target);
|
|
15432
15836
|
if (existsSync30(local)) {
|
|
15433
|
-
const recipePath =
|
|
15837
|
+
const recipePath = path41.basename(local) === RECIPE_FILE ? local : path41.join(local, RECIPE_FILE);
|
|
15434
15838
|
return readRecipeFile(recipePath, false);
|
|
15435
15839
|
}
|
|
15436
15840
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -15444,7 +15848,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
15444
15848
|
return matches[0];
|
|
15445
15849
|
}
|
|
15446
15850
|
async function verifyFile(root, file) {
|
|
15447
|
-
const absolute =
|
|
15851
|
+
const absolute = path41.resolve(root, ...file.path.split("/"));
|
|
15448
15852
|
if (!existsSync30(absolute)) {
|
|
15449
15853
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
15450
15854
|
}
|
|
@@ -15460,7 +15864,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
15460
15864
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
15461
15865
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
15462
15866
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
15463
|
-
const modelRoot = options.modelRoot ?
|
|
15867
|
+
const modelRoot = options.modelRoot ? path41.resolve(options.modelRoot) : null;
|
|
15464
15868
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
15465
15869
|
...await verifyFile(modelRoot, model),
|
|
15466
15870
|
source: model.source,
|
|
@@ -15494,7 +15898,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
15494
15898
|
};
|
|
15495
15899
|
}
|
|
15496
15900
|
function renderStyle(style, destination, cwd) {
|
|
15497
|
-
const portableDir =
|
|
15901
|
+
const portableDir = path41.relative(cwd, destination).split(path41.sep).join("/") || ".";
|
|
15498
15902
|
const replace = (value) => {
|
|
15499
15903
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
15500
15904
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -15506,25 +15910,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
15506
15910
|
return replace(style);
|
|
15507
15911
|
}
|
|
15508
15912
|
async function installRecipe(target, options = {}) {
|
|
15509
|
-
const cwd =
|
|
15913
|
+
const cwd = path41.resolve(options.cwd ?? process.cwd());
|
|
15510
15914
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
15511
15915
|
const verification = await verifyRecipe(loaded.path);
|
|
15512
15916
|
if (!verification.ok) {
|
|
15513
15917
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
15514
15918
|
}
|
|
15515
|
-
const destination = options.out ?
|
|
15919
|
+
const destination = options.out ? path41.resolve(cwd, options.out) : path41.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
15516
15920
|
const sources = [
|
|
15517
15921
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
15518
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
15922
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path41.join(loaded.dir, ...file.path.split("/")) }))
|
|
15519
15923
|
];
|
|
15520
15924
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
15521
|
-
path:
|
|
15522
|
-
data: await
|
|
15925
|
+
path: path41.join(destination, ...file.path.split("/")),
|
|
15926
|
+
data: await readFile28(file.source)
|
|
15523
15927
|
})));
|
|
15524
15928
|
if (!options.force) {
|
|
15525
15929
|
for (const file of files) {
|
|
15526
15930
|
if (!existsSync30(file.path)) continue;
|
|
15527
|
-
const current = await
|
|
15931
|
+
const current = await readFile28(file.path);
|
|
15528
15932
|
if (!current.equals(file.data)) {
|
|
15529
15933
|
throw new OverwriteRefusedError(
|
|
15530
15934
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -15618,14 +16022,14 @@ async function runRecipe(args) {
|
|
|
15618
16022
|
}, null, 2));
|
|
15619
16023
|
} else {
|
|
15620
16024
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
15621
|
-
log(` ${
|
|
16025
|
+
log(` ${path42.relative(process.cwd(), result.destination) || "."}`);
|
|
15622
16026
|
log(`
|
|
15623
16027
|
Add this entry under your manifest's styles object:`);
|
|
15624
16028
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
15625
16029
|
if (result.recipe.models.length) {
|
|
15626
16030
|
log(`
|
|
15627
16031
|
Models are not downloaded automatically. Verify them with:`);
|
|
15628
|
-
log(` pixelkiln recipe verify ${
|
|
16032
|
+
log(` pixelkiln recipe verify ${path42.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
15629
16033
|
}
|
|
15630
16034
|
}
|
|
15631
16035
|
return;
|
|
@@ -15633,7 +16037,7 @@ async function runRecipe(args) {
|
|
|
15633
16037
|
}
|
|
15634
16038
|
|
|
15635
16039
|
// src/cli/commands/refine.ts
|
|
15636
|
-
import
|
|
16040
|
+
import path43 from "path";
|
|
15637
16041
|
async function runRefine(args) {
|
|
15638
16042
|
if (args.from || args.subcommand === "approve") {
|
|
15639
16043
|
if (!args.from) {
|
|
@@ -15663,12 +16067,12 @@ async function runRefine(args) {
|
|
|
15663
16067
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
15664
16068
|
);
|
|
15665
16069
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
15666
|
-
log(` wrote ${
|
|
15667
|
-
log(` quality record: ${
|
|
16070
|
+
log(` wrote ${path43.relative(process.cwd(), result3.output)}`);
|
|
16071
|
+
log(` quality record: ${path43.relative(process.cwd(), result3.record)}`);
|
|
15668
16072
|
log(`
|
|
15669
16073
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
15670
16074
|
log(
|
|
15671
|
-
` pixelkiln refine approve --from ${
|
|
16075
|
+
` pixelkiln refine approve --from ${path43.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
15672
16076
|
);
|
|
15673
16077
|
}
|
|
15674
16078
|
return;
|
|
@@ -15696,7 +16100,7 @@ async function runRefine(args) {
|
|
|
15696
16100
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
15697
16101
|
else {
|
|
15698
16102
|
log(
|
|
15699
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
16103
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path43.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path43.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
15700
16104
|
);
|
|
15701
16105
|
}
|
|
15702
16106
|
return;
|
|
@@ -15705,7 +16109,7 @@ async function runRefine(args) {
|
|
|
15705
16109
|
if (args.json) {
|
|
15706
16110
|
log(JSON.stringify(result2, null, 2));
|
|
15707
16111
|
} else {
|
|
15708
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
16112
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path43.relative(process.cwd(), result2.output || result2.record)}`);
|
|
15709
16113
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
15710
16114
|
}
|
|
15711
16115
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -15751,7 +16155,7 @@ async function runRefine(args) {
|
|
|
15751
16155
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
15752
16156
|
if (item.state === "needs-approval") {
|
|
15753
16157
|
log(
|
|
15754
|
-
` pixelkiln refine approve --from ${
|
|
16158
|
+
` pixelkiln refine approve --from ${path43.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
15755
16159
|
);
|
|
15756
16160
|
}
|
|
15757
16161
|
}
|
|
@@ -15803,15 +16207,15 @@ async function runTools(args) {
|
|
|
15803
16207
|
}
|
|
15804
16208
|
|
|
15805
16209
|
// src/cli/commands/workspace.ts
|
|
15806
|
-
import
|
|
16210
|
+
import path44 from "path";
|
|
15807
16211
|
import { existsSync as existsSync31 } from "fs";
|
|
15808
16212
|
async function runWorkspace(args) {
|
|
15809
|
-
const workspacePath =
|
|
15810
|
-
const dir =
|
|
16213
|
+
const workspacePath = path44.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
16214
|
+
const dir = path44.dirname(workspacePath);
|
|
15811
16215
|
if (args.subcommand === "add") {
|
|
15812
|
-
const manifestPath =
|
|
16216
|
+
const manifestPath = path44.resolve(args.target);
|
|
15813
16217
|
const loadedTarget = await loadManifest(manifestPath);
|
|
15814
|
-
const lockPath = args.explicitLock ?
|
|
16218
|
+
const lockPath = args.explicitLock ? path44.resolve(args.explicitLock) : path44.join(path44.dirname(manifestPath), "pixelkiln.lock.json");
|
|
15815
16219
|
const ws = await loadWorkspace(workspacePath);
|
|
15816
16220
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
15817
16221
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -15831,7 +16235,7 @@ async function runWorkspace(args) {
|
|
|
15831
16235
|
...args.account ? { account: args.account } : {}
|
|
15832
16236
|
};
|
|
15833
16237
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
15834
|
-
log(` registered "${id}" in ${
|
|
16238
|
+
log(` registered "${id}" in ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15835
16239
|
log(` manifest: ${project.manifest}`);
|
|
15836
16240
|
log(` lock: ${project.lock}`);
|
|
15837
16241
|
if (!existsSync31(lockPath)) {
|
|
@@ -15843,7 +16247,7 @@ async function runWorkspace(args) {
|
|
|
15843
16247
|
}
|
|
15844
16248
|
if (args.subcommand === "remove") {
|
|
15845
16249
|
const ws = await loadWorkspace(workspacePath);
|
|
15846
|
-
const resolvedTarget =
|
|
16250
|
+
const resolvedTarget = path44.resolve(args.target);
|
|
15847
16251
|
const match = ws.projects.find(
|
|
15848
16252
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
15849
16253
|
);
|
|
@@ -15854,7 +16258,7 @@ async function runWorkspace(args) {
|
|
|
15854
16258
|
version: 1,
|
|
15855
16259
|
projects: ws.projects.filter((p) => p !== match)
|
|
15856
16260
|
});
|
|
15857
|
-
log(` removed "${match.id}" from ${
|
|
16261
|
+
log(` removed "${match.id}" from ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15858
16262
|
return;
|
|
15859
16263
|
}
|
|
15860
16264
|
if (args.subcommand === "list") {
|
|
@@ -15866,9 +16270,9 @@ async function runWorkspace(args) {
|
|
|
15866
16270
|
if (args.json) {
|
|
15867
16271
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
15868
16272
|
} else if (!ws.projects.length) {
|
|
15869
|
-
log(` no projects registered in ${
|
|
16273
|
+
log(` no projects registered in ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15870
16274
|
} else {
|
|
15871
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
16275
|
+
log(` ${ws.projects.length} project(s) in ${path44.relative(process.cwd(), workspacePath)}:`);
|
|
15872
16276
|
for (const p of ws.projects) {
|
|
15873
16277
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
15874
16278
|
}
|
|
@@ -15886,7 +16290,7 @@ async function runWorkspace(args) {
|
|
|
15886
16290
|
if (args.json) {
|
|
15887
16291
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
15888
16292
|
} else {
|
|
15889
|
-
log(` workspace: ${
|
|
16293
|
+
log(` workspace: ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15890
16294
|
for (const p of report.projects) {
|
|
15891
16295
|
if (p.error) {
|
|
15892
16296
|
log(`
|
|
@@ -15941,7 +16345,7 @@ async function runVersion() {
|
|
|
15941
16345
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
15942
16346
|
const url = new URL(up, import.meta.url);
|
|
15943
16347
|
if (!existsSync32(url)) continue;
|
|
15944
|
-
const pkg = JSON.parse(await
|
|
16348
|
+
const pkg = JSON.parse(await readFile29(url, "utf8"));
|
|
15945
16349
|
if (pkg.name === "pixelkiln") {
|
|
15946
16350
|
log(`${pkg.name} ${pkg.version}`);
|
|
15947
16351
|
return;
|