pixelkiln 0.40.0 → 0.42.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 +874 -540
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +1 -0
- package/dist/client/gallery.js +53 -0
- package/dist/index.cjs +789 -427
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -23
- package/dist/index.d.ts +180 -23
- package/dist/index.js +766 -405
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +11 -6
- package/docs/ENDPOINTS.md +7 -0
- package/docs/GENERATORS.md +9 -2
- package/docs/MANIFEST.md +48 -3
- package/docs/PIXELLAB.md +7 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +4 -3
- package/skills/pixelkiln/references/pixellab.md +7 -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")));
|
|
@@ -667,6 +667,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
667
667
|
state: spec.character.state,
|
|
668
668
|
animation: spec.character.animation
|
|
669
669
|
} : void 0,
|
|
670
|
+
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
671
|
+
// compares those directly, so only the choice of source is identity.
|
|
672
|
+
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
670
673
|
revision: spec.revision ? {
|
|
671
674
|
mode: spec.revision.mode,
|
|
672
675
|
from: spec.revision.sourceAssetId,
|
|
@@ -678,9 +681,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
678
681
|
);
|
|
679
682
|
}
|
|
680
683
|
|
|
681
|
-
// src/
|
|
682
|
-
import {
|
|
683
|
-
import
|
|
684
|
+
// src/pipeline/mirror.ts
|
|
685
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
686
|
+
import path4 from "path";
|
|
684
687
|
|
|
685
688
|
// src/lock.ts
|
|
686
689
|
import { readFile, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
@@ -769,6 +772,16 @@ var CHARACTER_DIRECTIONS_8 = [
|
|
|
769
772
|
"south-west"
|
|
770
773
|
];
|
|
771
774
|
var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
|
|
775
|
+
var MIRRORED_DIRECTION = {
|
|
776
|
+
south: "south",
|
|
777
|
+
"south-east": "south-west",
|
|
778
|
+
east: "west",
|
|
779
|
+
"north-east": "north-west",
|
|
780
|
+
north: "north",
|
|
781
|
+
"north-west": "north-east",
|
|
782
|
+
west: "east",
|
|
783
|
+
"south-west": "south-east"
|
|
784
|
+
};
|
|
772
785
|
var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
|
|
773
786
|
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
774
787
|
var CharacterStateSchema = z.object({
|
|
@@ -1028,8 +1041,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
|
1028
1041
|
}).strict();
|
|
1029
1042
|
var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
|
|
1030
1043
|
var AssetSchema = z.object({
|
|
1031
|
-
/** The subject. Style wrapping comes from the style's prefix/suffix. */
|
|
1032
|
-
prompt: z.string(),
|
|
1044
|
+
/** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
|
|
1045
|
+
prompt: z.string().optional(),
|
|
1033
1046
|
/** Subdirectory under the style's outDir. Optional. */
|
|
1034
1047
|
category: z.string().optional(),
|
|
1035
1048
|
/** Overrides the style default. `map` generator only. */
|
|
@@ -1087,6 +1100,19 @@ var AssetSchema = z.object({
|
|
|
1087
1100
|
state: CharacterStateSchema.optional(),
|
|
1088
1101
|
/** `character` styles: this asset is a loop of another character asset in one direction. */
|
|
1089
1102
|
animation: CharacterAnimationSchema.optional(),
|
|
1103
|
+
/**
|
|
1104
|
+
* Another asset of the same style flipped left to right, made locally
|
|
1105
|
+
* from that asset's downloaded files at no generation cost.
|
|
1106
|
+
*
|
|
1107
|
+
* The saving is in character loops, where each direction is its own
|
|
1108
|
+
* generation: a walk facing west mirrored is the walk facing east, and a
|
|
1109
|
+
* diagonal mirrors to the other diagonal. South and north mirror onto
|
|
1110
|
+
* themselves and are refused. A single image or a frame set from any
|
|
1111
|
+
* generator can be mirrored too; a tile set cannot, since its edges
|
|
1112
|
+
* carry meaning. Mirroring swaps handedness, so an asymmetric character
|
|
1113
|
+
* (a sword hand, a parted fringe) comes out the other way round.
|
|
1114
|
+
*/
|
|
1115
|
+
mirror: z.string().min(1).optional(),
|
|
1090
1116
|
/**
|
|
1091
1117
|
* Generated output role to place in `cell` when this asset expands to
|
|
1092
1118
|
* several files. Omit for ordinary single-output assets. A structural set
|
|
@@ -1126,7 +1152,7 @@ var AssetSchema = z.object({
|
|
|
1126
1152
|
path: ["revision"]
|
|
1127
1153
|
});
|
|
1128
1154
|
}
|
|
1129
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
|
|
1155
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
|
|
1130
1156
|
if (shapes.length > 1) {
|
|
1131
1157
|
context.addIssue({
|
|
1132
1158
|
code: z.ZodIssueCode.custom,
|
|
@@ -1134,6 +1160,20 @@ var AssetSchema = z.object({
|
|
|
1134
1160
|
path: [shapes[1]]
|
|
1135
1161
|
});
|
|
1136
1162
|
}
|
|
1163
|
+
if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
|
|
1164
|
+
context.addIssue({
|
|
1165
|
+
code: z.ZodIssueCode.custom,
|
|
1166
|
+
message: "a mirror is made from another asset's files; it cannot also declare a source",
|
|
1167
|
+
path: ["mirror"]
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
if (asset.prompt === void 0 && !asset.mirror) {
|
|
1171
|
+
context.addIssue({
|
|
1172
|
+
code: z.ZodIssueCode.custom,
|
|
1173
|
+
message: "Required",
|
|
1174
|
+
path: ["prompt"]
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1137
1177
|
});
|
|
1138
1178
|
var DEFAULT_HISTORY_LIMIT = 5;
|
|
1139
1179
|
var HistoryLimitSchema = z.number().int().min(0).max(100);
|
|
@@ -1223,6 +1263,11 @@ var LockEntrySchema = z.object({
|
|
|
1223
1263
|
maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1224
1264
|
strength: z.number().min(0).max(1).optional()
|
|
1225
1265
|
}).strict().nullable().default(null),
|
|
1266
|
+
/** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
|
|
1267
|
+
mirror: z.object({
|
|
1268
|
+
sourceAssetId: z.string().min(1),
|
|
1269
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1270
|
+
}).strict().nullable().default(null),
|
|
1226
1271
|
/**
|
|
1227
1272
|
* Output hashes owned by the previous generation while its replacement is
|
|
1228
1273
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1922,6 +1967,8 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
|
1922
1967
|
}
|
|
1923
1968
|
|
|
1924
1969
|
// src/outputs.ts
|
|
1970
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1971
|
+
import path3 from "path";
|
|
1925
1972
|
function portableOutputPath(file, manifestDir) {
|
|
1926
1973
|
const root = path3.resolve(manifestDir);
|
|
1927
1974
|
const absolute = path3.isAbsolute(file) ? path3.normalize(file) : path3.resolve(root, file);
|
|
@@ -2047,21 +2094,219 @@ function frameSetFps(entry) {
|
|
|
2047
2094
|
return null;
|
|
2048
2095
|
}
|
|
2049
2096
|
|
|
2097
|
+
// src/palette.ts
|
|
2098
|
+
function normalizePalette(values) {
|
|
2099
|
+
const colors = /* @__PURE__ */ new Map();
|
|
2100
|
+
for (const value of values) {
|
|
2101
|
+
const rgb = parseHex(value);
|
|
2102
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
2103
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
2104
|
+
}
|
|
2105
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
2106
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
2107
|
+
}
|
|
2108
|
+
return [...colors.values()];
|
|
2109
|
+
}
|
|
2110
|
+
function colorDistance(a, b) {
|
|
2111
|
+
const rmean = (a.r + b.r) / 2;
|
|
2112
|
+
const dr = a.r - b.r;
|
|
2113
|
+
const dg = a.g - b.g;
|
|
2114
|
+
const db = a.b - b.b;
|
|
2115
|
+
return Math.sqrt(
|
|
2116
|
+
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
function quantize(png, palette) {
|
|
2120
|
+
const colors = normalizePalette(palette);
|
|
2121
|
+
const pixels = Buffer.from(png.pixels);
|
|
2122
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
2123
|
+
let changed = 0;
|
|
2124
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
2125
|
+
const alpha = pixels[i + 3];
|
|
2126
|
+
if (alpha === 0) {
|
|
2127
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
2128
|
+
pixels[i] = 0;
|
|
2129
|
+
pixels[i + 1] = 0;
|
|
2130
|
+
pixels[i + 2] = 0;
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
2134
|
+
let nearest = nearestOf.get(key);
|
|
2135
|
+
if (!nearest) {
|
|
2136
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
2137
|
+
nearest = colors[0];
|
|
2138
|
+
let distance = colorDistance(pixel, nearest);
|
|
2139
|
+
for (let j = 1; j < colors.length; j++) {
|
|
2140
|
+
const candidate = colors[j];
|
|
2141
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
2142
|
+
if (candidateDistance < distance) {
|
|
2143
|
+
nearest = candidate;
|
|
2144
|
+
distance = candidateDistance;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
nearestOf.set(key, nearest);
|
|
2148
|
+
}
|
|
2149
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
2150
|
+
pixels[i] = nearest.r;
|
|
2151
|
+
pixels[i + 1] = nearest.g;
|
|
2152
|
+
pixels[i + 2] = nearest.b;
|
|
2153
|
+
}
|
|
2154
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
2155
|
+
}
|
|
2156
|
+
function quantizeToPalette(png, palette) {
|
|
2157
|
+
return quantize(png, palette).png;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/pipeline/postprocess.ts
|
|
2161
|
+
function postprocessFor(spec) {
|
|
2162
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
2163
|
+
return {
|
|
2164
|
+
palette: {
|
|
2165
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
2166
|
+
dither: "none",
|
|
2167
|
+
distance: "redmean"
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
function samePostprocess(a, b) {
|
|
2172
|
+
const pa = a?.palette;
|
|
2173
|
+
const pb = b?.palette;
|
|
2174
|
+
if (!pa && !pb) return true;
|
|
2175
|
+
if (!pa || !pb) return false;
|
|
2176
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
2177
|
+
}
|
|
2178
|
+
function postprocessCurrent(entry, spec) {
|
|
2179
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
2180
|
+
}
|
|
2181
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
2182
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
2183
|
+
const png = decodePng(bytes);
|
|
2184
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
2185
|
+
if (!changed) return { bytes, changed: false };
|
|
2186
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/pipeline/mirror.ts
|
|
2190
|
+
function mirrorSourceHash(source) {
|
|
2191
|
+
return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
|
|
2192
|
+
}
|
|
2193
|
+
function mirrorCurrent(entry, source) {
|
|
2194
|
+
return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
|
|
2195
|
+
}
|
|
2196
|
+
function mirroredRole(role) {
|
|
2197
|
+
if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
|
|
2198
|
+
return role;
|
|
2199
|
+
}
|
|
2200
|
+
function flipHorizontal(png) {
|
|
2201
|
+
const { width, height, pixels } = decodePng(png);
|
|
2202
|
+
const flipped = Buffer.alloc(pixels.length);
|
|
2203
|
+
const stride = width * 4;
|
|
2204
|
+
for (let y = 0; y < height; y++) {
|
|
2205
|
+
const row = y * stride;
|
|
2206
|
+
for (let x = 0; x < width; x++) {
|
|
2207
|
+
pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return encodeRgbaPng(width, height, flipped);
|
|
2211
|
+
}
|
|
2212
|
+
async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
2213
|
+
const log2 = opts.onProgress ?? (() => {
|
|
2214
|
+
});
|
|
2215
|
+
const mirror = spec.mirror;
|
|
2216
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
2217
|
+
const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
|
|
2218
|
+
const source = lock.entries[sourceKey];
|
|
2219
|
+
if (!source || source.status !== "downloaded" || !source.outputs.length) {
|
|
2220
|
+
throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
|
|
2221
|
+
}
|
|
2222
|
+
const members = source.outputs.map((output, index) => ({
|
|
2223
|
+
file: currentEntryOutputPath(source, mirror.sourceSpec, index),
|
|
2224
|
+
role: mirroredRole(output.role)
|
|
2225
|
+
}));
|
|
2226
|
+
if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
|
|
2227
|
+
members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
|
|
2228
|
+
}
|
|
2229
|
+
const outputs = [];
|
|
2230
|
+
const written = [];
|
|
2231
|
+
for (const [index, member] of members.entries()) {
|
|
2232
|
+
const bytes = flipHorizontal(await readFile2(member.file));
|
|
2233
|
+
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
2234
|
+
await mkdir2(path4.dirname(target), { recursive: true });
|
|
2235
|
+
const tmp = `${target}.pixelkiln.tmp`;
|
|
2236
|
+
await writeFile2(tmp, bytes);
|
|
2237
|
+
await rename2(tmp, target);
|
|
2238
|
+
written.push(target);
|
|
2239
|
+
outputs.push({
|
|
2240
|
+
path: portableOutputPath(target, spec.root),
|
|
2241
|
+
sha256: sha256(bytes),
|
|
2242
|
+
...member.role ? { role: member.role } : {},
|
|
2243
|
+
mediaType: MediaType.PNG
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
|
|
2247
|
+
const character = spec.character;
|
|
2248
|
+
const entry = upsert(lock, key, {
|
|
2249
|
+
styleId: spec.styleId,
|
|
2250
|
+
assetId: spec.assetId,
|
|
2251
|
+
specHash: spec.specHash,
|
|
2252
|
+
generator: spec.generator,
|
|
2253
|
+
tileFeature: null,
|
|
2254
|
+
prompt: "",
|
|
2255
|
+
width: spec.width,
|
|
2256
|
+
height: spec.height,
|
|
2257
|
+
revision: null,
|
|
2258
|
+
mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
|
|
2259
|
+
supersededOutputs: [],
|
|
2260
|
+
jobId: null,
|
|
2261
|
+
submissionComplete: void 0,
|
|
2262
|
+
reviewObjectId: null,
|
|
2263
|
+
objectId: null,
|
|
2264
|
+
candidateIndex: null,
|
|
2265
|
+
status: "downloaded",
|
|
2266
|
+
error: null,
|
|
2267
|
+
outputs,
|
|
2268
|
+
providerMetadata: {
|
|
2269
|
+
[source.provider]: {
|
|
2270
|
+
...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
|
|
2271
|
+
...character ? {
|
|
2272
|
+
character: {
|
|
2273
|
+
kind: character.kind,
|
|
2274
|
+
mirrorOf: mirror.sourceAssetId,
|
|
2275
|
+
...character.animation ? { direction: character.animation.direction } : {}
|
|
2276
|
+
}
|
|
2277
|
+
} : {}
|
|
2278
|
+
}
|
|
2279
|
+
},
|
|
2280
|
+
sourceUrl: null,
|
|
2281
|
+
sourceUrls: [],
|
|
2282
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2283
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2284
|
+
history: [],
|
|
2285
|
+
cost: 0,
|
|
2286
|
+
costUnit: spec.costUnit,
|
|
2287
|
+
provider: source.provider,
|
|
2288
|
+
postprocess: postprocessFor(spec)
|
|
2289
|
+
});
|
|
2290
|
+
await saveLock(lockPath, lock);
|
|
2291
|
+
log2(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
|
|
2292
|
+
return entry;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2050
2295
|
// src/pipeline/quality-profile.ts
|
|
2051
2296
|
import { existsSync as existsSync3 } from "fs";
|
|
2052
|
-
import
|
|
2297
|
+
import path7 from "path";
|
|
2053
2298
|
|
|
2054
2299
|
// src/pipeline/refine.ts
|
|
2055
|
-
import
|
|
2300
|
+
import path6 from "path";
|
|
2056
2301
|
import { execFile } from "child_process";
|
|
2057
|
-
import { mkdtemp, readFile as
|
|
2302
|
+
import { mkdtemp, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
2058
2303
|
import { tmpdir } from "os";
|
|
2059
2304
|
import { promisify } from "util";
|
|
2060
2305
|
|
|
2061
2306
|
// src/artifacts.ts
|
|
2062
|
-
import
|
|
2307
|
+
import path5 from "path";
|
|
2063
2308
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2064
|
-
import { mkdir as
|
|
2309
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
2065
2310
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
2066
2311
|
function message(error) {
|
|
2067
2312
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2070,7 +2315,7 @@ function digest(data) {
|
|
|
2070
2315
|
return createHash2("sha256").update(data).digest("hex");
|
|
2071
2316
|
}
|
|
2072
2317
|
function portableRelative(from, to) {
|
|
2073
|
-
return
|
|
2318
|
+
return path5.relative(from, path5.resolve(to)).split(path5.sep).join("/") || ".";
|
|
2074
2319
|
}
|
|
2075
2320
|
function canonical(value) {
|
|
2076
2321
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -2120,12 +2365,12 @@ function parseArtifactManifest(absolute, data) {
|
|
|
2120
2365
|
return raw;
|
|
2121
2366
|
}
|
|
2122
2367
|
async function readArtifactBundleManifest(manifestPath) {
|
|
2123
|
-
const absolute =
|
|
2124
|
-
return parseArtifactManifest(absolute, await
|
|
2368
|
+
const absolute = path5.resolve(manifestPath);
|
|
2369
|
+
return parseArtifactManifest(absolute, await readFile3(absolute, "utf8"));
|
|
2125
2370
|
}
|
|
2126
2371
|
async function readOptional(file) {
|
|
2127
2372
|
try {
|
|
2128
|
-
return await
|
|
2373
|
+
return await readFile3(file);
|
|
2129
2374
|
} catch (error) {
|
|
2130
2375
|
if (isCode(error, "ENOENT")) return null;
|
|
2131
2376
|
throw error;
|
|
@@ -2144,8 +2389,8 @@ function processIsAlive(pid) {
|
|
|
2144
2389
|
}
|
|
2145
2390
|
}
|
|
2146
2391
|
function validTemporaryPath(candidate, destination, type) {
|
|
2147
|
-
return
|
|
2148
|
-
`.${
|
|
2392
|
+
return path5.dirname(candidate) === path5.dirname(destination) && path5.basename(candidate).startsWith(
|
|
2393
|
+
`.${path5.basename(destination)}.pixelkiln-${type}-`
|
|
2149
2394
|
);
|
|
2150
2395
|
}
|
|
2151
2396
|
function parseTransaction(journal, data) {
|
|
@@ -2165,7 +2410,7 @@ async function removeTransactionFiles(journal) {
|
|
|
2165
2410
|
await rm2(transactionMarker(journal), { force: true });
|
|
2166
2411
|
}
|
|
2167
2412
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
2168
|
-
const journal =
|
|
2413
|
+
const journal = path5.resolve(recoveryFile);
|
|
2169
2414
|
const bytes = await readOptional(journal);
|
|
2170
2415
|
if (!bytes) {
|
|
2171
2416
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -2181,7 +2426,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2181
2426
|
);
|
|
2182
2427
|
}
|
|
2183
2428
|
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(
|
|
2429
|
+
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
2430
|
throw new Error(
|
|
2186
2431
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
2187
2432
|
);
|
|
@@ -2207,7 +2452,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2207
2452
|
}
|
|
2208
2453
|
await remove2(entry.destination, errors);
|
|
2209
2454
|
try {
|
|
2210
|
-
await
|
|
2455
|
+
await rename3(entry.backup, entry.destination);
|
|
2211
2456
|
} catch (error) {
|
|
2212
2457
|
errors.push(
|
|
2213
2458
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -2227,7 +2472,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2227
2472
|
await removeTransactionFiles(journal);
|
|
2228
2473
|
}
|
|
2229
2474
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
2230
|
-
const root =
|
|
2475
|
+
const root = path5.dirname(path5.resolve(manifestPath));
|
|
2231
2476
|
const sources = provenance.sources.map((source) => ({
|
|
2232
2477
|
...source,
|
|
2233
2478
|
path: portableRelative(root, source.path)
|
|
@@ -2257,11 +2502,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
2257
2502
|
];
|
|
2258
2503
|
}
|
|
2259
2504
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
2260
|
-
const absoluteManifest =
|
|
2505
|
+
const absoluteManifest = path5.resolve(manifestPath);
|
|
2261
2506
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
2262
2507
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
2263
2508
|
absoluteManifest,
|
|
2264
|
-
...outputs.map((output) =>
|
|
2509
|
+
...outputs.map((output) => path5.resolve(output.path))
|
|
2265
2510
|
]);
|
|
2266
2511
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
2267
2512
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -2279,13 +2524,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
2279
2524
|
}
|
|
2280
2525
|
}
|
|
2281
2526
|
if (!options.force) {
|
|
2282
|
-
const root =
|
|
2527
|
+
const root = path5.dirname(absoluteManifest);
|
|
2283
2528
|
const recorded = new Map(
|
|
2284
|
-
previous?.outputs.map((output) => [
|
|
2529
|
+
previous?.outputs.map((output) => [path5.resolve(root, output.path), output.sha256]) ?? []
|
|
2285
2530
|
);
|
|
2286
2531
|
const conflicts = [];
|
|
2287
2532
|
for (const output of outputs) {
|
|
2288
|
-
const destination =
|
|
2533
|
+
const destination = path5.resolve(output.path);
|
|
2289
2534
|
const current = await readOptional(destination);
|
|
2290
2535
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
2291
2536
|
const expected = recorded.get(destination);
|
|
@@ -2307,15 +2552,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
2307
2552
|
);
|
|
2308
2553
|
}
|
|
2309
2554
|
async function verifyArtifactBundle(manifestPath) {
|
|
2310
|
-
const absolute =
|
|
2555
|
+
const absolute = path5.resolve(manifestPath);
|
|
2311
2556
|
const raw = await readArtifactBundleManifest(absolute);
|
|
2312
|
-
const root =
|
|
2557
|
+
const root = path5.dirname(absolute);
|
|
2313
2558
|
const changedSources = [];
|
|
2314
2559
|
const changedOutputs = [];
|
|
2315
2560
|
for (const source of raw.sources) {
|
|
2316
2561
|
let actual = null;
|
|
2317
2562
|
try {
|
|
2318
|
-
actual = digest(await
|
|
2563
|
+
actual = digest(await readFile3(path5.resolve(root, source.path)));
|
|
2319
2564
|
} catch (error) {
|
|
2320
2565
|
if (!isCode(error, "ENOENT")) throw error;
|
|
2321
2566
|
}
|
|
@@ -2323,7 +2568,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
2323
2568
|
}
|
|
2324
2569
|
for (const output of raw.outputs) {
|
|
2325
2570
|
try {
|
|
2326
|
-
if (digest(await
|
|
2571
|
+
if (digest(await readFile3(path5.resolve(root, output.path))) !== output.sha256) {
|
|
2327
2572
|
changedOutputs.push(output.path);
|
|
2328
2573
|
}
|
|
2329
2574
|
} catch (error) {
|
|
@@ -2362,7 +2607,7 @@ async function rollback(prepared) {
|
|
|
2362
2607
|
for (const artifact of [...prepared].reverse()) {
|
|
2363
2608
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
2364
2609
|
try {
|
|
2365
|
-
await
|
|
2610
|
+
await rename3(artifact.backup, artifact.destination);
|
|
2366
2611
|
artifact.backedUp = false;
|
|
2367
2612
|
} catch (error) {
|
|
2368
2613
|
errors.push(
|
|
@@ -2379,7 +2624,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2379
2624
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
2380
2625
|
const normalized = files.map((file) => {
|
|
2381
2626
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
2382
|
-
return { destination:
|
|
2627
|
+
return { destination: path5.resolve(file.path), data: Buffer.from(file.data) };
|
|
2383
2628
|
});
|
|
2384
2629
|
const destinations = /* @__PURE__ */ new Set();
|
|
2385
2630
|
for (const artifact of normalized) {
|
|
@@ -2395,7 +2640,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2395
2640
|
const unchanged = [];
|
|
2396
2641
|
for (const artifact of normalized) {
|
|
2397
2642
|
try {
|
|
2398
|
-
const current = await
|
|
2643
|
+
const current = await readFile3(artifact.destination);
|
|
2399
2644
|
if (current.equals(artifact.data)) {
|
|
2400
2645
|
unchanged.push(artifact.destination);
|
|
2401
2646
|
} else {
|
|
@@ -2409,21 +2654,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2409
2654
|
if (!changed.length) return { changed: [], unchanged };
|
|
2410
2655
|
const token = randomUUID();
|
|
2411
2656
|
for (const [index, artifact] of changed.entries()) {
|
|
2412
|
-
const basename =
|
|
2413
|
-
artifact.stage =
|
|
2414
|
-
|
|
2657
|
+
const basename = path5.basename(artifact.destination);
|
|
2658
|
+
artifact.stage = path5.join(
|
|
2659
|
+
path5.dirname(artifact.destination),
|
|
2415
2660
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
2416
2661
|
);
|
|
2417
2662
|
if (artifact.existed) {
|
|
2418
|
-
artifact.backup =
|
|
2419
|
-
|
|
2663
|
+
artifact.backup = path5.join(
|
|
2664
|
+
path5.dirname(artifact.destination),
|
|
2420
2665
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
2421
2666
|
);
|
|
2422
2667
|
}
|
|
2423
2668
|
}
|
|
2424
|
-
const journal = options.recoveryFile ?
|
|
2669
|
+
const journal = options.recoveryFile ? path5.resolve(options.recoveryFile) : null;
|
|
2425
2670
|
if (journal) {
|
|
2426
|
-
await
|
|
2671
|
+
await mkdir3(path5.dirname(journal), { recursive: true });
|
|
2427
2672
|
const transaction = {
|
|
2428
2673
|
format: "pixelkiln-artifact-transaction",
|
|
2429
2674
|
version: 1,
|
|
@@ -2437,7 +2682,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2437
2682
|
}))
|
|
2438
2683
|
};
|
|
2439
2684
|
try {
|
|
2440
|
-
await
|
|
2685
|
+
await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
2441
2686
|
activeTransactions.add(journal);
|
|
2442
2687
|
} catch (error) {
|
|
2443
2688
|
throw new Error(
|
|
@@ -2448,9 +2693,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2448
2693
|
}
|
|
2449
2694
|
try {
|
|
2450
2695
|
for (const [index, artifact] of changed.entries()) {
|
|
2451
|
-
await
|
|
2696
|
+
await mkdir3(path5.dirname(artifact.destination), { recursive: true });
|
|
2452
2697
|
await options.beforeStage?.(artifact.destination, index);
|
|
2453
|
-
await
|
|
2698
|
+
await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
|
|
2454
2699
|
}
|
|
2455
2700
|
} catch (error) {
|
|
2456
2701
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -2463,17 +2708,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2463
2708
|
try {
|
|
2464
2709
|
for (const artifact of changed) {
|
|
2465
2710
|
if (!artifact.existed) continue;
|
|
2466
|
-
await
|
|
2711
|
+
await rename3(artifact.destination, artifact.backup);
|
|
2467
2712
|
artifact.backedUp = true;
|
|
2468
2713
|
}
|
|
2469
2714
|
for (const [index, artifact] of changed.entries()) {
|
|
2470
2715
|
await options.beforePromote?.(artifact.destination, index);
|
|
2471
|
-
await
|
|
2716
|
+
await rename3(artifact.stage, artifact.destination);
|
|
2472
2717
|
artifact.stage = void 0;
|
|
2473
2718
|
artifact.promoted = true;
|
|
2474
2719
|
}
|
|
2475
2720
|
if (journal) {
|
|
2476
|
-
await
|
|
2721
|
+
await writeFile3(transactionMarker(journal), `${token}
|
|
2477
2722
|
`, { flag: "wx" });
|
|
2478
2723
|
durableCommit = true;
|
|
2479
2724
|
await options.afterCommit?.();
|
|
@@ -2515,69 +2760,6 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2515
2760
|
};
|
|
2516
2761
|
}
|
|
2517
2762
|
|
|
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
2763
|
// src/pipeline/refine.ts
|
|
2582
2764
|
var execFileAsync = promisify(execFile);
|
|
2583
2765
|
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
@@ -2777,16 +2959,16 @@ function sameFrameGrid(first, next) {
|
|
|
2777
2959
|
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
2960
|
}
|
|
2779
2961
|
async function refineAsset(options) {
|
|
2780
|
-
const source =
|
|
2781
|
-
const output =
|
|
2962
|
+
const source = path6.resolve(options.source);
|
|
2963
|
+
const output = path6.resolve(options.output);
|
|
2782
2964
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
2783
|
-
const record =
|
|
2965
|
+
const record = path6.resolve(qualityRecordPath(output));
|
|
2784
2966
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
2785
2967
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
2786
2968
|
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
2969
|
let sourcePng;
|
|
2788
2970
|
try {
|
|
2789
|
-
sourcePng = decodePng(await
|
|
2971
|
+
sourcePng = decodePng(await readFile4(source));
|
|
2790
2972
|
} catch (error) {
|
|
2791
2973
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
2792
2974
|
}
|
|
@@ -2803,8 +2985,8 @@ async function refineAsset(options) {
|
|
|
2803
2985
|
);
|
|
2804
2986
|
}
|
|
2805
2987
|
}
|
|
2806
|
-
const temp = await mkdtemp(
|
|
2807
|
-
const recovered =
|
|
2988
|
+
const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-"));
|
|
2989
|
+
const recovered = path6.join(temp, "native.png");
|
|
2808
2990
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
2809
2991
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
2810
2992
|
try {
|
|
@@ -2825,7 +3007,7 @@ async function refineAsset(options) {
|
|
|
2825
3007
|
);
|
|
2826
3008
|
}
|
|
2827
3009
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
2828
|
-
const native = decodePng(await
|
|
3010
|
+
const native = decodePng(await readFile4(recovered));
|
|
2829
3011
|
const quantized = quantizeToPalette(native, palette);
|
|
2830
3012
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
2831
3013
|
const audit = auditRefinement(
|
|
@@ -2889,18 +3071,18 @@ async function refineFrameSet(options) {
|
|
|
2889
3071
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
2890
3072
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
2891
3073
|
}
|
|
2892
|
-
const output =
|
|
2893
|
-
const record =
|
|
3074
|
+
const output = path6.resolve(options.output);
|
|
3075
|
+
const record = path6.resolve(qualityRecordPath(output));
|
|
2894
3076
|
const palette = normalizeRefinementPalette(options.palette);
|
|
2895
3077
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
2896
3078
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
2897
3079
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
2898
3080
|
}
|
|
2899
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
3081
|
+
const sourceSet = new Set(options.sources.map((source) => path6.resolve(source.path)));
|
|
2900
3082
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
2901
3083
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
2902
3084
|
}
|
|
2903
|
-
const temp = await mkdtemp(
|
|
3085
|
+
const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
2904
3086
|
try {
|
|
2905
3087
|
const frames = [];
|
|
2906
3088
|
const outputs = [];
|
|
@@ -2908,8 +3090,8 @@ async function refineFrameSet(options) {
|
|
|
2908
3090
|
const sourcePaths = [];
|
|
2909
3091
|
let firstOptions = null;
|
|
2910
3092
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
2911
|
-
const source =
|
|
2912
|
-
const temporaryOutput =
|
|
3093
|
+
const source = path6.resolve(sourceEntry.path);
|
|
3094
|
+
const temporaryOutput = path6.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
2913
3095
|
const refined = await refineAsset({
|
|
2914
3096
|
source,
|
|
2915
3097
|
output: temporaryOutput,
|
|
@@ -2941,7 +3123,7 @@ async function refineFrameSet(options) {
|
|
|
2941
3123
|
});
|
|
2942
3124
|
outputs.push({
|
|
2943
3125
|
path: finalOutputs[index],
|
|
2944
|
-
data: await
|
|
3126
|
+
data: await readFile4(temporaryOutput)
|
|
2945
3127
|
});
|
|
2946
3128
|
}
|
|
2947
3129
|
const first = frames[0];
|
|
@@ -2984,7 +3166,7 @@ async function refineFrameSet(options) {
|
|
|
2984
3166
|
);
|
|
2985
3167
|
return {
|
|
2986
3168
|
sources: sourcePaths,
|
|
2987
|
-
outputs: outputs.map((item) =>
|
|
3169
|
+
outputs: outputs.map((item) => path6.resolve(item.path)),
|
|
2988
3170
|
record,
|
|
2989
3171
|
frames,
|
|
2990
3172
|
palette,
|
|
@@ -2995,12 +3177,12 @@ async function refineFrameSet(options) {
|
|
|
2995
3177
|
}
|
|
2996
3178
|
}
|
|
2997
3179
|
async function checkQualityRecord(recordPath) {
|
|
2998
|
-
const record =
|
|
3180
|
+
const record = path6.resolve(recordPath);
|
|
2999
3181
|
const manifest = await readArtifactBundleManifest(record);
|
|
3000
3182
|
const options = parseRecordOptions(manifest, record);
|
|
3001
3183
|
const verification = await verifyArtifactBundle(record);
|
|
3002
|
-
const sources = manifest.sources.map((source2) =>
|
|
3003
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
3184
|
+
const sources = manifest.sources.map((source2) => path6.resolve(path6.dirname(record), source2.path));
|
|
3185
|
+
const outputs = manifest.outputs.map((output2) => path6.resolve(path6.dirname(record), output2.path));
|
|
3004
3186
|
const source = sources[0] ?? "";
|
|
3005
3187
|
const output = outputs[0] ?? "";
|
|
3006
3188
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -3033,7 +3215,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
3033
3215
|
async function approveQualityRecord(recordPath, approval) {
|
|
3034
3216
|
const reviewer = approval.reviewer.trim();
|
|
3035
3217
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
3036
|
-
const record =
|
|
3218
|
+
const record = path6.resolve(recordPath);
|
|
3037
3219
|
const manifest = await readArtifactBundleManifest(record);
|
|
3038
3220
|
const options = parseRecordOptions(manifest, record);
|
|
3039
3221
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -3054,14 +3236,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
3054
3236
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
3055
3237
|
);
|
|
3056
3238
|
}
|
|
3057
|
-
const root =
|
|
3239
|
+
const root = path6.dirname(record);
|
|
3058
3240
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
3059
|
-
const outputPath =
|
|
3060
|
-
return { path: outputPath, data: await
|
|
3241
|
+
const outputPath = path6.resolve(root, entry.path);
|
|
3242
|
+
return { path: outputPath, data: await readFile4(outputPath) };
|
|
3061
3243
|
}));
|
|
3062
3244
|
const sources = manifest.sources.map((source) => ({
|
|
3063
3245
|
...source,
|
|
3064
|
-
path:
|
|
3246
|
+
path: path6.resolve(root, source.path)
|
|
3065
3247
|
}));
|
|
3066
3248
|
const approved = {
|
|
3067
3249
|
...options,
|
|
@@ -3093,7 +3275,7 @@ async function qualitySource(spec, lock) {
|
|
|
3093
3275
|
if (spec.generator === "frames") {
|
|
3094
3276
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
3095
3277
|
}
|
|
3096
|
-
const source =
|
|
3278
|
+
const source = path7.resolve(spec.root, spec.source);
|
|
3097
3279
|
return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
3098
3280
|
}
|
|
3099
3281
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -3160,7 +3342,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
3160
3342
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
3161
3343
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
3162
3344
|
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) =>
|
|
3345
|
+
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
3346
|
if (contractChanged) {
|
|
3165
3347
|
return {
|
|
3166
3348
|
key,
|
|
@@ -3322,43 +3504,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
3322
3504
|
return sources;
|
|
3323
3505
|
}
|
|
3324
3506
|
|
|
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
3507
|
// src/pipeline/revision.ts
|
|
3355
3508
|
import { existsSync as existsSync4 } from "fs";
|
|
3356
|
-
import
|
|
3509
|
+
import path8 from "path";
|
|
3357
3510
|
async function inspectRevisionReadiness(spec, lock) {
|
|
3511
|
+
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
3358
3512
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
3359
3513
|
if (!spec.revision) return null;
|
|
3360
3514
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
3361
3515
|
}
|
|
3516
|
+
async function inspectMirror(spec, lock, seen) {
|
|
3517
|
+
const source = spec.mirror.sourceSpec;
|
|
3518
|
+
const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
|
|
3519
|
+
const dependency = await inspectParent(source, lock, seen);
|
|
3520
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
3521
|
+
}
|
|
3362
3522
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
3363
3523
|
const character = spec.character;
|
|
3364
3524
|
const parentSpec = character.parentSpec;
|
|
@@ -3412,6 +3572,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3412
3572
|
if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
|
|
3413
3573
|
const nextSeen = new Set(seen);
|
|
3414
3574
|
nextSeen.add(key);
|
|
3575
|
+
if (spec.mirror) {
|
|
3576
|
+
const inputs = await inspectMirror(spec, lock, nextSeen);
|
|
3577
|
+
if (!inputs.ready) return inputs;
|
|
3578
|
+
}
|
|
3415
3579
|
if (spec.revision) {
|
|
3416
3580
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
3417
3581
|
if (!inputs.ready) return inputs;
|
|
@@ -3428,7 +3592,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3428
3592
|
};
|
|
3429
3593
|
}
|
|
3430
3594
|
if (spec.source) {
|
|
3431
|
-
const source =
|
|
3595
|
+
const source = path8.resolve(spec.root, spec.source);
|
|
3432
3596
|
return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
3433
3597
|
}
|
|
3434
3598
|
const entry = lock.entries[key];
|
|
@@ -3498,7 +3662,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3498
3662
|
state = "missing";
|
|
3499
3663
|
reason = "--force";
|
|
3500
3664
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
3501
|
-
if (existsSync5(
|
|
3665
|
+
if (existsSync5(path9.resolve(spec.root, spec.source))) {
|
|
3502
3666
|
state = "ok";
|
|
3503
3667
|
reason = `placed from ${spec.source}; not generated`;
|
|
3504
3668
|
} else {
|
|
@@ -3511,11 +3675,17 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3511
3675
|
reason = "file on disk, no provenance recorded";
|
|
3512
3676
|
} else {
|
|
3513
3677
|
state = "missing";
|
|
3514
|
-
reason = "not in lockfile";
|
|
3678
|
+
reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
|
|
3515
3679
|
}
|
|
3516
3680
|
} else if (entry.specHash !== spec.specHash) {
|
|
3517
3681
|
state = "stale";
|
|
3518
3682
|
reason = "prompt, size, or style changed";
|
|
3683
|
+
} else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
|
|
3684
|
+
state = "stale";
|
|
3685
|
+
reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
|
|
3686
|
+
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
|
|
3687
|
+
state = "stale";
|
|
3688
|
+
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
3519
3689
|
} else if (entry.status === "download-failed") {
|
|
3520
3690
|
state = "recoverable";
|
|
3521
3691
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -3544,6 +3714,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3544
3714
|
} else if (await anyOutputModified(entry, spec)) {
|
|
3545
3715
|
state = "orphaned";
|
|
3546
3716
|
reason = "output modified since download";
|
|
3717
|
+
} else if (spec.mirror && !postprocessCurrent(entry, spec)) {
|
|
3718
|
+
state = "stale";
|
|
3719
|
+
reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
|
|
3547
3720
|
} else if (!postprocessCurrent(entry, spec)) {
|
|
3548
3721
|
state = "recoverable";
|
|
3549
3722
|
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 +3728,19 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3555
3728
|
items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
|
|
3556
3729
|
}
|
|
3557
3730
|
const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
|
|
3558
|
-
const
|
|
3731
|
+
const stateOf = new Map(items.map((i) => [i.key, i.state]));
|
|
3559
3732
|
for (const item of items) {
|
|
3560
3733
|
if (!isActionable(item)) continue;
|
|
3561
|
-
const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
3734
|
+
const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
3562
3735
|
if (!parent) continue;
|
|
3563
3736
|
const parentKey = lockKey(parent.styleId, parent.assetId);
|
|
3564
|
-
|
|
3737
|
+
const parentState = stateOf.get(parentKey);
|
|
3738
|
+
if (parentState && isActionable({ state: parentState })) {
|
|
3565
3739
|
item.state = "blocked";
|
|
3566
3740
|
item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
|
|
3741
|
+
} else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
|
|
3742
|
+
item.state = "blocked";
|
|
3743
|
+
item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
|
|
3567
3744
|
}
|
|
3568
3745
|
}
|
|
3569
3746
|
const actionable = items.filter(isActionable);
|
|
@@ -3718,12 +3895,12 @@ async function confirm(question, auto) {
|
|
|
3718
3895
|
}
|
|
3719
3896
|
|
|
3720
3897
|
// src/cli/commands/account.ts
|
|
3721
|
-
import
|
|
3898
|
+
import path22 from "path";
|
|
3722
3899
|
import { existsSync as existsSync16 } from "fs";
|
|
3723
3900
|
|
|
3724
3901
|
// src/env.ts
|
|
3725
3902
|
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
3726
|
-
import
|
|
3903
|
+
import path10 from "path";
|
|
3727
3904
|
function loadEnvFiles(dir) {
|
|
3728
3905
|
const loaded = [];
|
|
3729
3906
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -3746,7 +3923,7 @@ function readEnvFiles(dir) {
|
|
|
3746
3923
|
function envFiles(dir) {
|
|
3747
3924
|
const out = [];
|
|
3748
3925
|
for (const name of [".env.local", ".env"]) {
|
|
3749
|
-
const file =
|
|
3926
|
+
const file = path10.join(dir, name);
|
|
3750
3927
|
if (!existsSync6(file)) continue;
|
|
3751
3928
|
try {
|
|
3752
3929
|
out.push([file, parseEnv(readFileSync(file, "utf8"))]);
|
|
@@ -3776,8 +3953,8 @@ function parseEnv(contents) {
|
|
|
3776
3953
|
|
|
3777
3954
|
// src/providers/comfyui.ts
|
|
3778
3955
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3779
|
-
import { readFile as
|
|
3780
|
-
import
|
|
3956
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3957
|
+
import path11 from "path";
|
|
3781
3958
|
|
|
3782
3959
|
// src/http.ts
|
|
3783
3960
|
var MAX_RETRIES = 4;
|
|
@@ -3964,10 +4141,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3964
4141
|
}
|
|
3965
4142
|
async resolveOptions(value, context) {
|
|
3966
4143
|
const options = parseOptions(value);
|
|
3967
|
-
const workflowPath =
|
|
4144
|
+
const workflowPath = path11.resolve(context.root, options.workflowFile);
|
|
3968
4145
|
let raw;
|
|
3969
4146
|
try {
|
|
3970
|
-
raw = await
|
|
4147
|
+
raw = await readFile5(workflowPath, "utf8");
|
|
3971
4148
|
} catch (err) {
|
|
3972
4149
|
throw new Error(
|
|
3973
4150
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4019,10 +4196,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4019
4196
|
if (typeof item !== "string") {
|
|
4020
4197
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
4021
4198
|
}
|
|
4022
|
-
const file =
|
|
4199
|
+
const file = path11.resolve(context.root, item);
|
|
4023
4200
|
let bytes;
|
|
4024
4201
|
try {
|
|
4025
|
-
bytes = await
|
|
4202
|
+
bytes = await readFile5(file);
|
|
4026
4203
|
} catch (error) {
|
|
4027
4204
|
throw new Error(
|
|
4028
4205
|
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -4154,7 +4331,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4154
4331
|
setBinding(workflow2, binding, input);
|
|
4155
4332
|
return;
|
|
4156
4333
|
}
|
|
4157
|
-
const bytes = await
|
|
4334
|
+
const bytes = await readFile5(input.path);
|
|
4158
4335
|
if (sha256(bytes) !== input.sha256) {
|
|
4159
4336
|
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
4160
4337
|
}
|
|
@@ -4221,7 +4398,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4221
4398
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
4222
4399
|
throw new Error("ComfyUI revision source is not ready");
|
|
4223
4400
|
}
|
|
4224
|
-
const sourceBytes = await
|
|
4401
|
+
const sourceBytes = await readFile5(spec.revision.sourceFile);
|
|
4225
4402
|
if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
|
|
4226
4403
|
throw new Error("ComfyUI revision source changed before upload");
|
|
4227
4404
|
}
|
|
@@ -4235,7 +4412,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4235
4412
|
if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
|
|
4236
4413
|
throw new Error("ComfyUI revision mask is not ready");
|
|
4237
4414
|
}
|
|
4238
|
-
const maskBytes = await
|
|
4415
|
+
const maskBytes = await readFile5(spec.revision.maskFile);
|
|
4239
4416
|
if (sha256(maskBytes) !== spec.revision.maskSha256) {
|
|
4240
4417
|
throw new Error("ComfyUI revision mask changed before upload");
|
|
4241
4418
|
}
|
|
@@ -4709,7 +4886,7 @@ function canonical2(value) {
|
|
|
4709
4886
|
import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4710
4887
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4711
4888
|
import os from "os";
|
|
4712
|
-
import
|
|
4889
|
+
import path12 from "path";
|
|
4713
4890
|
|
|
4714
4891
|
// src/client.ts
|
|
4715
4892
|
import { z as z2 } from "zod";
|
|
@@ -4848,9 +5025,9 @@ var PixelLabClient = class {
|
|
|
4848
5025
|
apiKey;
|
|
4849
5026
|
http;
|
|
4850
5027
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
4851
|
-
async request(
|
|
5028
|
+
async request(path45, init) {
|
|
4852
5029
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
4853
|
-
const res = await this.http(`${BASE}${
|
|
5030
|
+
const res = await this.http(`${BASE}${path45}`, {
|
|
4854
5031
|
...init,
|
|
4855
5032
|
headers: {
|
|
4856
5033
|
Authorization: auth,
|
|
@@ -4860,13 +5037,13 @@ var PixelLabClient = class {
|
|
|
4860
5037
|
});
|
|
4861
5038
|
const text = await res.text();
|
|
4862
5039
|
if (!res.ok) {
|
|
4863
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
5040
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path45} \u2192 ${res.status}`, res.status, text);
|
|
4864
5041
|
}
|
|
4865
5042
|
if (!text) return {};
|
|
4866
5043
|
try {
|
|
4867
5044
|
return JSON.parse(text);
|
|
4868
5045
|
} catch {
|
|
4869
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
5046
|
+
throw new Error(`${init?.method ?? "GET"} ${path45} returned invalid JSON`);
|
|
4870
5047
|
}
|
|
4871
5048
|
}
|
|
4872
5049
|
async balance() {
|
|
@@ -5051,10 +5228,10 @@ var PixelLabClient = class {
|
|
|
5051
5228
|
async createCharacter(args) {
|
|
5052
5229
|
const imageSize = { width: args.size, height: args.size };
|
|
5053
5230
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
5054
|
-
let
|
|
5231
|
+
let path45;
|
|
5055
5232
|
let body;
|
|
5056
5233
|
if (args.mode === "v3") {
|
|
5057
|
-
|
|
5234
|
+
path45 = "/create-character-v3";
|
|
5058
5235
|
body = {
|
|
5059
5236
|
description: args.description,
|
|
5060
5237
|
image_size: imageSize,
|
|
@@ -5066,7 +5243,7 @@ var PixelLabClient = class {
|
|
|
5066
5243
|
...args.seed != null ? { seed: args.seed } : {}
|
|
5067
5244
|
};
|
|
5068
5245
|
} else if (args.mode === "pro") {
|
|
5069
|
-
|
|
5246
|
+
path45 = "/create-character-pro";
|
|
5070
5247
|
body = {
|
|
5071
5248
|
description: args.description,
|
|
5072
5249
|
image_size: imageSize,
|
|
@@ -5076,7 +5253,7 @@ var PixelLabClient = class {
|
|
|
5076
5253
|
...args.seed != null ? { seed: args.seed } : {}
|
|
5077
5254
|
};
|
|
5078
5255
|
} else {
|
|
5079
|
-
|
|
5256
|
+
path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
5080
5257
|
body = {
|
|
5081
5258
|
description: args.description,
|
|
5082
5259
|
image_size: imageSize,
|
|
@@ -5089,8 +5266,8 @@ var PixelLabClient = class {
|
|
|
5089
5266
|
...palette
|
|
5090
5267
|
};
|
|
5091
5268
|
}
|
|
5092
|
-
const raw = await this.request(
|
|
5093
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
5269
|
+
const raw = await this.request(path45, { method: "POST", body: JSON.stringify(body) });
|
|
5270
|
+
return validateResponse(CharacterSubmitSchema, raw, path45.slice(1));
|
|
5094
5271
|
}
|
|
5095
5272
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
5096
5273
|
async createCharacterState(args) {
|
|
@@ -5278,7 +5455,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5278
5455
|
* filename, polling is an existence check, and downloading is a file read.
|
|
5279
5456
|
*/
|
|
5280
5457
|
static cacheDir() {
|
|
5281
|
-
const dir =
|
|
5458
|
+
const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
|
|
5282
5459
|
mkdirSync(dir, { recursive: true });
|
|
5283
5460
|
return dir;
|
|
5284
5461
|
}
|
|
@@ -5448,12 +5625,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5448
5625
|
const character = await this.client.getCharacter(jobId);
|
|
5449
5626
|
if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
|
|
5450
5627
|
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
5451
|
-
const
|
|
5452
|
-
const sources = [];
|
|
5453
|
-
for (const direction of order) {
|
|
5454
|
-
const url = character.rotation_urls[direction];
|
|
5455
|
-
if (url) sources.push({ url, role: direction });
|
|
5456
|
-
}
|
|
5628
|
+
const sources = rotationSources(character);
|
|
5457
5629
|
if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
|
|
5458
5630
|
return {
|
|
5459
5631
|
status: "ready",
|
|
@@ -5541,7 +5713,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5541
5713
|
seed: spec.seed
|
|
5542
5714
|
});
|
|
5543
5715
|
const jobId = randomUUID3();
|
|
5544
|
-
writeFileSync(
|
|
5716
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
5545
5717
|
return { jobId };
|
|
5546
5718
|
}
|
|
5547
5719
|
if (spec.generator === "tiles") {
|
|
@@ -5582,7 +5754,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5582
5754
|
}
|
|
5583
5755
|
async poll(jobId, generator, context) {
|
|
5584
5756
|
if (generator === "pixflux") {
|
|
5585
|
-
const file =
|
|
5757
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
5586
5758
|
if (existsSync7(file)) {
|
|
5587
5759
|
const sourceUrl = `file://${file}`;
|
|
5588
5760
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
@@ -5745,6 +5917,27 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5745
5917
|
})))
|
|
5746
5918
|
};
|
|
5747
5919
|
}
|
|
5920
|
+
/**
|
|
5921
|
+
* A character's rotation and frame URLs carry its last edit time as a
|
|
5922
|
+
* cache-buster, so the record's URLs can name bytes a cache still holds
|
|
5923
|
+
* after an edit in PixelLab's editor. Objects keep stable URLs.
|
|
5924
|
+
*/
|
|
5925
|
+
async refreshSources(objectId, context) {
|
|
5926
|
+
if (context.generator !== "character") return void 0;
|
|
5927
|
+
const [characterId, groupId] = objectId.split("#");
|
|
5928
|
+
const character = await this.client.getCharacter(characterId).catch((err) => {
|
|
5929
|
+
if (err instanceof PixelLabError && err.status === 404) return null;
|
|
5930
|
+
throw err;
|
|
5931
|
+
});
|
|
5932
|
+
if (!character) return null;
|
|
5933
|
+
if (!groupId) return character.status === "completed" ? rotationSources(character) : void 0;
|
|
5934
|
+
const recorded = context.metadata?.character ?? {};
|
|
5935
|
+
const direction = recorded.direction ?? "south";
|
|
5936
|
+
const group = character.animations.find((g) => g.animation_group_id === groupId);
|
|
5937
|
+
const frames = group?.directions.find((d) => d.direction === direction && d.frames.length)?.frames ?? (recorded.animationName ? findAnimation(character, recorded.animationName, direction, recorded.animationId ?? null)?.frames : void 0);
|
|
5938
|
+
if (!frames) return null;
|
|
5939
|
+
return frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
5940
|
+
}
|
|
5748
5941
|
};
|
|
5749
5942
|
function remoteCharacter(character) {
|
|
5750
5943
|
return {
|
|
@@ -5775,6 +5968,15 @@ function firstUrl(urls) {
|
|
|
5775
5968
|
if (!urls) return null;
|
|
5776
5969
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
5777
5970
|
}
|
|
5971
|
+
function rotationSources(character) {
|
|
5972
|
+
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
5973
|
+
const sources = [];
|
|
5974
|
+
for (const direction of order) {
|
|
5975
|
+
const url = character.rotation_urls?.[direction];
|
|
5976
|
+
if (url) sources.push({ url, role: direction });
|
|
5977
|
+
}
|
|
5978
|
+
return sources;
|
|
5979
|
+
}
|
|
5778
5980
|
function findAnimation(character, name, direction, animationId) {
|
|
5779
5981
|
for (const group of character.animations) {
|
|
5780
5982
|
const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
|
|
@@ -5836,9 +6038,9 @@ var RetroDiffusionClient = class {
|
|
|
5836
6038
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
5837
6039
|
return Buffer.from(await response.arrayBuffer());
|
|
5838
6040
|
}
|
|
5839
|
-
async call(
|
|
6041
|
+
async call(path45, init = {}) {
|
|
5840
6042
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
5841
|
-
const response = await this.request(`${this.baseUrl}${
|
|
6043
|
+
const response = await this.request(`${this.baseUrl}${path45}`, {
|
|
5842
6044
|
...init,
|
|
5843
6045
|
headers: {
|
|
5844
6046
|
"Content-Type": "application/json",
|
|
@@ -6695,13 +6897,13 @@ registerProvider({
|
|
|
6695
6897
|
});
|
|
6696
6898
|
|
|
6697
6899
|
// src/manifest.ts
|
|
6698
|
-
import { readFile as
|
|
6900
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
6699
6901
|
import { existsSync as existsSync8 } from "fs";
|
|
6700
|
-
import
|
|
6902
|
+
import path13 from "path";
|
|
6701
6903
|
async function loadManifest(manifestPath) {
|
|
6702
|
-
const abs =
|
|
6904
|
+
const abs = path13.resolve(manifestPath);
|
|
6703
6905
|
if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
6704
|
-
const input = ManifestInputSchema.safeParse(JSON.parse(await
|
|
6906
|
+
const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
|
|
6705
6907
|
if (!input.success) {
|
|
6706
6908
|
const issues = formatManifestIssues(input.error.issues);
|
|
6707
6909
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -6786,7 +6988,7 @@ ${issues}`);
|
|
|
6786
6988
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6787
6989
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
6788
6990
|
}
|
|
6789
|
-
return { manifest: parsed.data, root:
|
|
6991
|
+
return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
|
|
6790
6992
|
}
|
|
6791
6993
|
function parentAssetId(asset) {
|
|
6792
6994
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -6886,11 +7088,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6886
7088
|
}
|
|
6887
7089
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
6888
7090
|
async function loadStyleImage(rel) {
|
|
6889
|
-
const abs =
|
|
7091
|
+
const abs = path13.resolve(root, rel);
|
|
6890
7092
|
let hit = styleImageCache.get(abs);
|
|
6891
7093
|
if (!hit) {
|
|
6892
7094
|
if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
6893
|
-
const buf = await
|
|
7095
|
+
const buf = await readFile6(abs);
|
|
6894
7096
|
const metadata = imageMetadata(buf);
|
|
6895
7097
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
6896
7098
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -6916,7 +7118,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6916
7118
|
styleImageDimensions.push(loadedImage);
|
|
6917
7119
|
}
|
|
6918
7120
|
const resolvedImages = style.styleImages.map((image) => {
|
|
6919
|
-
const hit = styleImageCache.get(
|
|
7121
|
+
const hit = styleImageCache.get(path13.resolve(root, image.path));
|
|
6920
7122
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
6921
7123
|
});
|
|
6922
7124
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -6969,11 +7171,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6969
7171
|
);
|
|
6970
7172
|
}
|
|
6971
7173
|
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
6972
|
-
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
7174
|
+
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
6973
7175
|
const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
6974
|
-
const relFile = asset.file ??
|
|
6975
|
-
const outFile =
|
|
6976
|
-
const qualityOutFile = style.quality ?
|
|
7176
|
+
const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
|
|
7177
|
+
const outFile = path13.resolve(root, style.outDir, relFile);
|
|
7178
|
+
const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
6977
7179
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
6978
7180
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
6979
7181
|
const base = {
|
|
@@ -7026,7 +7228,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7026
7228
|
minGridConfidence: style.quality.minGridConfidence,
|
|
7027
7229
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
7028
7230
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
7029
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
7231
|
+
...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
|
|
7030
7232
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
7031
7233
|
}
|
|
7032
7234
|
} : {},
|
|
@@ -7044,11 +7246,42 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7044
7246
|
const resolved = styleSpecs.get(assetId);
|
|
7045
7247
|
if (!resolved) {
|
|
7046
7248
|
throw new Error(
|
|
7047
|
-
`Asset "${assetId}" is not available in style "${styleId}"; revision parents must
|
|
7249
|
+
`Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
|
|
7048
7250
|
);
|
|
7049
7251
|
}
|
|
7050
7252
|
if (finalized.has(assetId)) return resolved;
|
|
7051
7253
|
const asset = manifest.assets[assetId];
|
|
7254
|
+
if (asset.mirror) {
|
|
7255
|
+
if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
|
|
7256
|
+
const sourceSpec = await finalize(asset.mirror);
|
|
7257
|
+
if (sourceSpec.generator === "tiles") {
|
|
7258
|
+
throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
|
|
7259
|
+
}
|
|
7260
|
+
if (sourceSpec.character?.kind === "animation") {
|
|
7261
|
+
const facing = sourceSpec.character.animation.direction;
|
|
7262
|
+
const mirrored = MIRRORED_DIRECTION[facing];
|
|
7263
|
+
if (mirrored === facing) {
|
|
7264
|
+
throw new Error(
|
|
7265
|
+
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
7266
|
+
);
|
|
7267
|
+
}
|
|
7268
|
+
resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
7269
|
+
} else if (sourceSpec.character) {
|
|
7270
|
+
resolved.character = { ...sourceSpec.character };
|
|
7271
|
+
}
|
|
7272
|
+
resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
|
|
7273
|
+
resolved.generator = sourceSpec.generator;
|
|
7274
|
+
resolved.width = sourceSpec.width;
|
|
7275
|
+
resolved.height = sourceSpec.height;
|
|
7276
|
+
resolved.size = sourceSpec.size;
|
|
7277
|
+
resolved.prompt = "";
|
|
7278
|
+
resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
|
|
7279
|
+
resolved.cost = 0;
|
|
7280
|
+
resolved.costUnit = sourceSpec.costUnit;
|
|
7281
|
+
resolved.candidates = 1;
|
|
7282
|
+
finalized.add(assetId);
|
|
7283
|
+
return resolved;
|
|
7284
|
+
}
|
|
7052
7285
|
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
7053
7286
|
if (resolved.character && characterParent) {
|
|
7054
7287
|
const parentSpec = await finalize(characterParent);
|
|
@@ -7072,9 +7305,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7072
7305
|
);
|
|
7073
7306
|
}
|
|
7074
7307
|
const sourceSpec = await finalize(asset.revision.from);
|
|
7075
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
7308
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
7076
7309
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
7077
|
-
const maskFile = asset.revision.mask ?
|
|
7310
|
+
const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
|
|
7078
7311
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
7079
7312
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
7080
7313
|
throw new Error(
|
|
@@ -7155,12 +7388,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7155
7388
|
return specs;
|
|
7156
7389
|
}
|
|
7157
7390
|
function pngPath(file) {
|
|
7158
|
-
const extension =
|
|
7391
|
+
const extension = path13.extname(file);
|
|
7159
7392
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
7160
7393
|
}
|
|
7161
7394
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
7162
7395
|
if (!existsSync8(file)) return null;
|
|
7163
|
-
const bytes = await
|
|
7396
|
+
const bytes = await readFile6(file);
|
|
7164
7397
|
const metadata = imageMetadata(bytes);
|
|
7165
7398
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
7166
7399
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -7173,8 +7406,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
7173
7406
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
7174
7407
|
const out = [];
|
|
7175
7408
|
for (const img of style.styleImages) {
|
|
7176
|
-
const file =
|
|
7177
|
-
const buf = await
|
|
7409
|
+
const file = path13.resolve(loaded.root, img.path);
|
|
7410
|
+
const buf = await readFile6(file);
|
|
7178
7411
|
const metadata = imageMetadata(buf);
|
|
7179
7412
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
7180
7413
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -7243,8 +7476,8 @@ function resolveCharacterShape(asset, style, kind) {
|
|
|
7243
7476
|
|
|
7244
7477
|
// src/pipeline/fetch.ts
|
|
7245
7478
|
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
|
|
7246
|
-
import { mkdir as
|
|
7247
|
-
import
|
|
7479
|
+
import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
7480
|
+
import path14 from "path";
|
|
7248
7481
|
|
|
7249
7482
|
// src/source-url.ts
|
|
7250
7483
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -7290,9 +7523,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7290
7523
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
7291
7524
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
7292
7525
|
normalizeLockOutputPaths(lock, specs);
|
|
7293
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
7526
|
+
const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
|
|
7294
7527
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
7295
7528
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
7529
|
+
if (specByKey.get(key)?.mirror) return false;
|
|
7296
7530
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7297
7531
|
if (e.status !== "downloaded") return false;
|
|
7298
7532
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
@@ -7331,7 +7565,40 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7331
7565
|
result.skipped++;
|
|
7332
7566
|
continue;
|
|
7333
7567
|
}
|
|
7334
|
-
const
|
|
7568
|
+
const recordedSources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
|
|
7569
|
+
let sources = recordedSources;
|
|
7570
|
+
let rotated = false;
|
|
7571
|
+
if (opts.refresh && entry.status === "downloaded" && entry.objectId && provider.refreshSources) {
|
|
7572
|
+
let current;
|
|
7573
|
+
try {
|
|
7574
|
+
current = await provider.refreshSources(entry.objectId, {
|
|
7575
|
+
generator: entry.generator,
|
|
7576
|
+
metadata: entry.providerMetadata?.[provider.id],
|
|
7577
|
+
sources: recordedSources
|
|
7578
|
+
});
|
|
7579
|
+
} catch (err) {
|
|
7580
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
7581
|
+
upsert(lock, key, { status: "download-failed", error: `download failed: ${message6}` });
|
|
7582
|
+
result.failed++;
|
|
7583
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
7584
|
+
await saveLock(lockPath, lock);
|
|
7585
|
+
continue;
|
|
7586
|
+
}
|
|
7587
|
+
if (current === null) {
|
|
7588
|
+
upsert(lock, key, { status: "download-failed", error: `download failed: ${entry.objectId} no longer exists upstream` });
|
|
7589
|
+
result.failed++;
|
|
7590
|
+
log2(` FAILED ${key}: ${entry.objectId} no longer exists upstream`);
|
|
7591
|
+
await saveLock(lockPath, lock);
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
if (current) {
|
|
7595
|
+
sources = current.map((source) => ({
|
|
7596
|
+
...source,
|
|
7597
|
+
mediaType: source.mediaType ?? recordedSources.find((r) => r.role === source.role)?.mediaType
|
|
7598
|
+
}));
|
|
7599
|
+
rotated = sources.length !== recordedSources.length || sources.some((source, index) => source.url !== recordedSources[index]?.url || source.role !== recordedSources[index]?.role);
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7335
7602
|
if (!sources.length) {
|
|
7336
7603
|
upsert(lock, key, {
|
|
7337
7604
|
status: "download-failed",
|
|
@@ -7358,7 +7625,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7358
7625
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7359
7626
|
fresh = await provider.download(source.url);
|
|
7360
7627
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
7361
|
-
log2(` current ${
|
|
7628
|
+
log2(` current ${path14.relative(process.cwd(), target)}`);
|
|
7362
7629
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7363
7630
|
continue;
|
|
7364
7631
|
}
|
|
@@ -7372,7 +7639,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7372
7639
|
if (cacheDir) {
|
|
7373
7640
|
await cacheMedia(
|
|
7374
7641
|
cacheDir,
|
|
7375
|
-
await
|
|
7642
|
+
await readFile7(target),
|
|
7376
7643
|
recorded.mediaType ?? MediaType.PNG,
|
|
7377
7644
|
recorded.sha256
|
|
7378
7645
|
);
|
|
@@ -7381,10 +7648,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7381
7648
|
continue;
|
|
7382
7649
|
}
|
|
7383
7650
|
if (reapply && !fresh) {
|
|
7384
|
-
if (!recorded.raw) raw = await
|
|
7385
|
-
log2(` reapply ${
|
|
7651
|
+
if (!recorded.raw) raw = await readFile7(target);
|
|
7652
|
+
log2(` reapply ${path14.relative(process.cwd(), target)}`);
|
|
7386
7653
|
} else {
|
|
7387
|
-
log2(` changed ${
|
|
7654
|
+
log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
|
|
7388
7655
|
}
|
|
7389
7656
|
} else {
|
|
7390
7657
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -7403,7 +7670,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7403
7670
|
}
|
|
7404
7671
|
}
|
|
7405
7672
|
log2(
|
|
7406
|
-
` replace ${
|
|
7673
|
+
` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
7407
7674
|
);
|
|
7408
7675
|
}
|
|
7409
7676
|
}
|
|
@@ -7412,7 +7679,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7412
7679
|
} else if (!raw && recorded && cacheDir) {
|
|
7413
7680
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
7414
7681
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
7415
|
-
if (final || raw) log2(` cached ${
|
|
7682
|
+
if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
|
|
7416
7683
|
}
|
|
7417
7684
|
if (!final && !raw) {
|
|
7418
7685
|
if (!source.url) {
|
|
@@ -7439,13 +7706,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7439
7706
|
buf = processed.bytes;
|
|
7440
7707
|
if (processed.changed) {
|
|
7441
7708
|
rawHash = sha256(raw);
|
|
7442
|
-
log2(` snapped ${
|
|
7709
|
+
log2(` snapped ${path14.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
7443
7710
|
}
|
|
7444
7711
|
}
|
|
7445
7712
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
7446
|
-
await
|
|
7713
|
+
await mkdir4(path14.dirname(target), { recursive: true });
|
|
7447
7714
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
7448
|
-
await
|
|
7715
|
+
await writeFile4(tmp, buf);
|
|
7449
7716
|
outputs.push({
|
|
7450
7717
|
path: portableOutputPath(target, spec.root),
|
|
7451
7718
|
sha256: sha256(buf),
|
|
@@ -7455,11 +7722,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7455
7722
|
});
|
|
7456
7723
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7457
7724
|
await saveLock(lockPath, lock);
|
|
7458
|
-
await
|
|
7725
|
+
await rename4(tmp, target);
|
|
7459
7726
|
wrote = true;
|
|
7460
|
-
log2(` wrote ${
|
|
7727
|
+
log2(` wrote ${path14.relative(process.cwd(), target)}`);
|
|
7461
7728
|
}
|
|
7462
7729
|
if (opts.refresh && !wrote) {
|
|
7730
|
+
if (rotated) {
|
|
7731
|
+
const persistent = sources.filter((source) => shouldPersistSourceUrl(source.url));
|
|
7732
|
+
upsert(lock, key, { sourceUrl: persistent[0]?.url ?? null, sourceUrls: persistent });
|
|
7733
|
+
}
|
|
7463
7734
|
result.unchanged = (result.unchanged ?? 0) + 1;
|
|
7464
7735
|
continue;
|
|
7465
7736
|
}
|
|
@@ -7497,10 +7768,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7497
7768
|
return result;
|
|
7498
7769
|
}
|
|
7499
7770
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
7500
|
-
const file =
|
|
7771
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
7501
7772
|
if (!existsSync9(file)) return null;
|
|
7502
7773
|
try {
|
|
7503
|
-
const buf = await
|
|
7774
|
+
const buf = await readFile7(file);
|
|
7504
7775
|
if (sha256(buf) !== hash) return null;
|
|
7505
7776
|
validateMedia(buf, mediaType);
|
|
7506
7777
|
return buf;
|
|
@@ -7511,13 +7782,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
7511
7782
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
7512
7783
|
const hash = knownHash ?? sha256(buf);
|
|
7513
7784
|
validateMedia(buf, mediaType);
|
|
7514
|
-
const file =
|
|
7785
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
7515
7786
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
7516
|
-
await
|
|
7787
|
+
await mkdir4(cacheDir, { recursive: true });
|
|
7517
7788
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7518
|
-
await
|
|
7789
|
+
await writeFile4(tmp, buf);
|
|
7519
7790
|
try {
|
|
7520
|
-
await
|
|
7791
|
+
await rename4(tmp, file);
|
|
7521
7792
|
} catch (err) {
|
|
7522
7793
|
if (!existsSync9(file)) throw err;
|
|
7523
7794
|
} finally {
|
|
@@ -7553,11 +7824,11 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
7553
7824
|
|
|
7554
7825
|
// src/pipeline/adopt.ts
|
|
7555
7826
|
import { existsSync as existsSync11 } from "fs";
|
|
7556
|
-
import { readFile as
|
|
7557
|
-
import
|
|
7827
|
+
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
7828
|
+
import path15 from "path";
|
|
7558
7829
|
|
|
7559
7830
|
// src/cache.ts
|
|
7560
|
-
import { mkdir as
|
|
7831
|
+
import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
7561
7832
|
import { existsSync as existsSync10 } from "fs";
|
|
7562
7833
|
import pathModule from "path";
|
|
7563
7834
|
import { z as z3 } from "zod";
|
|
@@ -7573,10 +7844,10 @@ function isSha256Hash(value) {
|
|
|
7573
7844
|
function parseCache(value) {
|
|
7574
7845
|
return HashCacheSchema.parse(value);
|
|
7575
7846
|
}
|
|
7576
|
-
async function loadCache(
|
|
7577
|
-
if (!existsSync10(
|
|
7847
|
+
async function loadCache(path45) {
|
|
7848
|
+
if (!existsSync10(path45)) return { version: 1, hashes: {} };
|
|
7578
7849
|
try {
|
|
7579
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
7850
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path45, "utf8")));
|
|
7580
7851
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
7581
7852
|
return {
|
|
7582
7853
|
version: 1,
|
|
@@ -7588,18 +7859,18 @@ async function loadCache(path44) {
|
|
|
7588
7859
|
return { version: 1, hashes: {} };
|
|
7589
7860
|
}
|
|
7590
7861
|
}
|
|
7591
|
-
async function saveCache(
|
|
7862
|
+
async function saveCache(path45, cache) {
|
|
7592
7863
|
const sorted = {};
|
|
7593
7864
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
7594
7865
|
const hash = cache.hashes[key];
|
|
7595
7866
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
7596
7867
|
sorted[key] = hash;
|
|
7597
7868
|
}
|
|
7598
|
-
await
|
|
7599
|
-
const tmp = `${
|
|
7869
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path45)), { recursive: true });
|
|
7870
|
+
const tmp = `${path45}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7600
7871
|
try {
|
|
7601
|
-
await
|
|
7602
|
-
await
|
|
7872
|
+
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
7873
|
+
await rename5(tmp, path45);
|
|
7603
7874
|
} finally {
|
|
7604
7875
|
await rm5(tmp, { force: true });
|
|
7605
7876
|
}
|
|
@@ -7637,7 +7908,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7637
7908
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
7638
7909
|
continue;
|
|
7639
7910
|
}
|
|
7640
|
-
const bytes = await
|
|
7911
|
+
const bytes = await readFile9(spec.outFile);
|
|
7641
7912
|
try {
|
|
7642
7913
|
decodePng(bytes);
|
|
7643
7914
|
} catch (err) {
|
|
@@ -7757,7 +8028,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
7757
8028
|
const log2 = opts.onProgress ?? (() => {
|
|
7758
8029
|
});
|
|
7759
8030
|
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
7760
|
-
const raw = JSON.parse(await
|
|
8031
|
+
const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
|
|
7761
8032
|
const best = /* @__PURE__ */ new Map();
|
|
7762
8033
|
for (const entry of Object.values(lock.entries)) {
|
|
7763
8034
|
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
@@ -7779,7 +8050,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
7779
8050
|
stillEmpty.push(id);
|
|
7780
8051
|
}
|
|
7781
8052
|
}
|
|
7782
|
-
await
|
|
8053
|
+
await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7783
8054
|
log2(` filled ${filled} prompt(s) from adopted objects`);
|
|
7784
8055
|
return { filled, stillEmpty };
|
|
7785
8056
|
}
|
|
@@ -7791,14 +8062,14 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7791
8062
|
|
|
7792
8063
|
// src/pipeline/adopt-characters.ts
|
|
7793
8064
|
import { existsSync as existsSync12 } from "fs";
|
|
7794
|
-
import { mkdir as
|
|
7795
|
-
import
|
|
8065
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
8066
|
+
import path16 from "path";
|
|
7796
8067
|
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
7797
8068
|
const log2 = opts.onProgress ?? (() => {
|
|
7798
8069
|
});
|
|
7799
8070
|
const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
|
|
7800
8071
|
if (!provider.listCharacters || !provider.getCharacter) return result;
|
|
7801
|
-
const wanted = specs.filter((spec) => spec.character);
|
|
8072
|
+
const wanted = specs.filter((spec) => spec.character && !spec.mirror);
|
|
7802
8073
|
if (!wanted.length) return result;
|
|
7803
8074
|
const remote = [];
|
|
7804
8075
|
for await (const character of provider.listCharacters()) remote.push(character);
|
|
@@ -7844,11 +8115,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7844
8115
|
const hash = sha256(bytes);
|
|
7845
8116
|
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
7846
8117
|
if (existsSync12(file)) {
|
|
7847
|
-
if (sha256(await
|
|
8118
|
+
if (sha256(await readFile10(file)) !== hash) result.differing.push(path16.relative(process.cwd(), file));
|
|
7848
8119
|
} else {
|
|
7849
|
-
await
|
|
7850
|
-
await
|
|
7851
|
-
result.written.push(
|
|
8120
|
+
await mkdir6(path16.dirname(file), { recursive: true });
|
|
8121
|
+
await writeFile7(file, bytes);
|
|
8122
|
+
result.written.push(path16.relative(process.cwd(), file));
|
|
7852
8123
|
}
|
|
7853
8124
|
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
7854
8125
|
}
|
|
@@ -7887,10 +8158,10 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7887
8158
|
} else {
|
|
7888
8159
|
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
7889
8160
|
if (!existsSync12(south)) {
|
|
7890
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
8161
|
+
result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), south)} to match by`);
|
|
7891
8162
|
return false;
|
|
7892
8163
|
}
|
|
7893
|
-
const localHash = sha256(await
|
|
8164
|
+
const localHash = sha256(await readFile10(south));
|
|
7894
8165
|
for (const candidate of remote) {
|
|
7895
8166
|
if (claimed.has(candidate.id)) continue;
|
|
7896
8167
|
if (await southHash(candidate) === localHash) {
|
|
@@ -7899,7 +8170,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7899
8170
|
}
|
|
7900
8171
|
}
|
|
7901
8172
|
if (!match) {
|
|
7902
|
-
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${
|
|
8173
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path16.relative(process.cwd(), south)}`);
|
|
7903
8174
|
return false;
|
|
7904
8175
|
}
|
|
7905
8176
|
}
|
|
@@ -7969,12 +8240,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7969
8240
|
if (!existsSync12(first)) {
|
|
7970
8241
|
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
7971
8242
|
if (!byName) {
|
|
7972
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
8243
|
+
result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), first)} to match by`);
|
|
7973
8244
|
return false;
|
|
7974
8245
|
}
|
|
7975
8246
|
match = byName;
|
|
7976
8247
|
} else {
|
|
7977
|
-
const localHash = sha256(await
|
|
8248
|
+
const localHash = sha256(await readFile10(first));
|
|
7978
8249
|
for (const candidate of candidates) {
|
|
7979
8250
|
if (sha256(await download(candidate.frames[0])) === localHash) {
|
|
7980
8251
|
match = candidate;
|
|
@@ -7982,7 +8253,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
7982
8253
|
}
|
|
7983
8254
|
}
|
|
7984
8255
|
if (!match) {
|
|
7985
|
-
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${
|
|
8256
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path16.relative(process.cwd(), first)}`);
|
|
7986
8257
|
return false;
|
|
7987
8258
|
}
|
|
7988
8259
|
}
|
|
@@ -8042,9 +8313,9 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
8042
8313
|
}
|
|
8043
8314
|
|
|
8044
8315
|
// src/pipeline/salvage.ts
|
|
8045
|
-
import { readFile as
|
|
8316
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
8046
8317
|
import { existsSync as existsSync13 } from "fs";
|
|
8047
|
-
import
|
|
8318
|
+
import path17 from "path";
|
|
8048
8319
|
function providerClaimId(provider, objectId) {
|
|
8049
8320
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
8050
8321
|
}
|
|
@@ -8054,7 +8325,7 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
8054
8325
|
if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
8055
8326
|
let parsed;
|
|
8056
8327
|
try {
|
|
8057
|
-
parsed = parseLock(JSON.parse(await
|
|
8328
|
+
parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
|
|
8058
8329
|
} catch {
|
|
8059
8330
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
8060
8331
|
}
|
|
@@ -8102,20 +8373,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
8102
8373
|
return null;
|
|
8103
8374
|
}
|
|
8104
8375
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
8105
|
-
const own =
|
|
8376
|
+
const own = path17.resolve(ownManifestPath);
|
|
8106
8377
|
const siblingManifestPaths = [
|
|
8107
8378
|
.../* @__PURE__ */ new Set([
|
|
8108
8379
|
...workspaceManifestPaths,
|
|
8109
|
-
...claimPaths.map((c) =>
|
|
8380
|
+
...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
|
|
8110
8381
|
])
|
|
8111
8382
|
];
|
|
8112
8383
|
const siblings = [];
|
|
8113
8384
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
8114
|
-
if (
|
|
8385
|
+
if (path17.resolve(siblingManifestPath) === own) continue;
|
|
8115
8386
|
if (!existsSync13(siblingManifestPath)) continue;
|
|
8116
8387
|
try {
|
|
8117
8388
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
8118
|
-
siblings.push({ label:
|
|
8389
|
+
siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
|
|
8119
8390
|
} catch {
|
|
8120
8391
|
}
|
|
8121
8392
|
}
|
|
@@ -8231,9 +8502,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
8231
8502
|
}
|
|
8232
8503
|
|
|
8233
8504
|
// src/workspace.ts
|
|
8234
|
-
import { mkdir as
|
|
8505
|
+
import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
8235
8506
|
import { existsSync as existsSync14 } from "fs";
|
|
8236
|
-
import
|
|
8507
|
+
import path18 from "path";
|
|
8237
8508
|
import { z as z4 } from "zod";
|
|
8238
8509
|
var WorkspaceProjectSchema = z4.object({
|
|
8239
8510
|
id: z4.string().min(1),
|
|
@@ -8261,7 +8532,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
8261
8532
|
if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
|
|
8262
8533
|
let raw;
|
|
8263
8534
|
try {
|
|
8264
|
-
raw = JSON.parse(await
|
|
8535
|
+
raw = JSON.parse(await readFile12(workspacePath, "utf8"));
|
|
8265
8536
|
} catch (err) {
|
|
8266
8537
|
throw new ProjectError(
|
|
8267
8538
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -8275,22 +8546,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
8275
8546
|
version: 1,
|
|
8276
8547
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
8277
8548
|
};
|
|
8278
|
-
await
|
|
8549
|
+
await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
|
|
8279
8550
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8280
8551
|
try {
|
|
8281
|
-
await
|
|
8282
|
-
await
|
|
8552
|
+
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
8553
|
+
await rename6(tmp, workspacePath);
|
|
8283
8554
|
} finally {
|
|
8284
8555
|
await rm6(tmp, { force: true });
|
|
8285
8556
|
}
|
|
8286
8557
|
}
|
|
8287
8558
|
function toPortablePath(dir, absolute) {
|
|
8288
|
-
return
|
|
8559
|
+
return path18.relative(dir, absolute).split(path18.sep).join("/");
|
|
8289
8560
|
}
|
|
8290
8561
|
function resolveProject(dir, project) {
|
|
8291
8562
|
return {
|
|
8292
|
-
manifestPath:
|
|
8293
|
-
lockPath:
|
|
8563
|
+
manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
|
|
8564
|
+
lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
|
|
8294
8565
|
};
|
|
8295
8566
|
}
|
|
8296
8567
|
function validateWorkspace(ws, dir) {
|
|
@@ -8303,7 +8574,7 @@ function validateWorkspace(ws, dir) {
|
|
|
8303
8574
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
8304
8575
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
8305
8576
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
8306
|
-
if (
|
|
8577
|
+
if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
|
|
8307
8578
|
diagnostics.push({
|
|
8308
8579
|
id: "absolute-path",
|
|
8309
8580
|
level: "warning",
|
|
@@ -8364,8 +8635,8 @@ function validateWorkspace(ws, dir) {
|
|
|
8364
8635
|
}
|
|
8365
8636
|
|
|
8366
8637
|
// src/pick/salvage-server.ts
|
|
8367
|
-
import { mkdir as
|
|
8368
|
-
import
|
|
8638
|
+
import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
|
|
8639
|
+
import path19 from "path";
|
|
8369
8640
|
|
|
8370
8641
|
// src/pick/salvage-sheet.ts
|
|
8371
8642
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -8525,7 +8796,7 @@ refresh();
|
|
|
8525
8796
|
|
|
8526
8797
|
// src/pick/review-server.ts
|
|
8527
8798
|
import { createServer } from "http";
|
|
8528
|
-
import { readFile as
|
|
8799
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
8529
8800
|
|
|
8530
8801
|
// src/open.ts
|
|
8531
8802
|
import { spawn } from "child_process";
|
|
@@ -8552,7 +8823,7 @@ function serveReviewPage(opts) {
|
|
|
8552
8823
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
8553
8824
|
try {
|
|
8554
8825
|
const asset = opts.assets.get(req.url);
|
|
8555
|
-
const bytes = await
|
|
8826
|
+
const bytes = await readFile13(asset.path);
|
|
8556
8827
|
res.writeHead(200, {
|
|
8557
8828
|
"Content-Type": asset.contentType,
|
|
8558
8829
|
"Cache-Control": "no-store",
|
|
@@ -8629,7 +8900,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8629
8900
|
});
|
|
8630
8901
|
const html = renderSalvageSheet(orphans, {
|
|
8631
8902
|
styleId: ctx.styleId,
|
|
8632
|
-
importDir:
|
|
8903
|
+
importDir: path19.relative(process.cwd(), ctx.importDir) || "."
|
|
8633
8904
|
});
|
|
8634
8905
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
8635
8906
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -8658,10 +8929,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8658
8929
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
8659
8930
|
decodePng(buf);
|
|
8660
8931
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
8661
|
-
const rel =
|
|
8662
|
-
const outFile =
|
|
8663
|
-
await
|
|
8664
|
-
await
|
|
8932
|
+
const rel = path19.join("_salvaged", `${assetId}.png`);
|
|
8933
|
+
const outFile = path19.resolve(ctx.importDir, rel);
|
|
8934
|
+
await mkdir8(path19.dirname(outFile), { recursive: true });
|
|
8935
|
+
await writeFile9(outFile, buf);
|
|
8665
8936
|
ctx.manifest.assets[assetId] = {
|
|
8666
8937
|
prompt: orphan.prompt,
|
|
8667
8938
|
promptByStyle: {},
|
|
@@ -8693,7 +8964,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8693
8964
|
sourceUrl: durableSource,
|
|
8694
8965
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
8695
8966
|
outputs: [{
|
|
8696
|
-
path: portableOutputPath(outFile,
|
|
8967
|
+
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
8697
8968
|
sha256: sha256(buf)
|
|
8698
8969
|
}],
|
|
8699
8970
|
submittedAt: orphan.createdAt,
|
|
@@ -8717,9 +8988,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8717
8988
|
}
|
|
8718
8989
|
}
|
|
8719
8990
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
8720
|
-
const raw = JSON.parse(await
|
|
8991
|
+
const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
|
|
8721
8992
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
8722
|
-
await
|
|
8993
|
+
await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
8723
8994
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
8724
8995
|
return result;
|
|
8725
8996
|
}
|
|
@@ -8727,19 +8998,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
8727
8998
|
}
|
|
8728
8999
|
|
|
8729
9000
|
// src/cli/project.ts
|
|
8730
|
-
import
|
|
9001
|
+
import path21 from "path";
|
|
8731
9002
|
import { existsSync as existsSync15 } from "fs";
|
|
8732
9003
|
|
|
8733
9004
|
// src/project.ts
|
|
8734
|
-
import
|
|
9005
|
+
import path20 from "path";
|
|
8735
9006
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
8736
|
-
const absolute =
|
|
8737
|
-
const root =
|
|
9007
|
+
const absolute = path20.resolve(manifestPath);
|
|
9008
|
+
const root = path20.dirname(absolute);
|
|
8738
9009
|
if (opts.env !== false) {
|
|
8739
9010
|
loadEnvFiles(root);
|
|
8740
|
-
if (
|
|
9011
|
+
if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
8741
9012
|
}
|
|
8742
|
-
const lockPath =
|
|
9013
|
+
const lockPath = path20.resolve(opts.lockPath ?? path20.join(root, "pixelkiln.lock.json"));
|
|
8743
9014
|
const loaded = await loadManifest(absolute);
|
|
8744
9015
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
8745
9016
|
const lock = await loadLock(lockPath);
|
|
@@ -8870,7 +9141,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
8870
9141
|
|
|
8871
9142
|
// src/cli/project.ts
|
|
8872
9143
|
async function provenanceFile(id, file) {
|
|
8873
|
-
const absolute =
|
|
9144
|
+
const absolute = path21.resolve(file);
|
|
8874
9145
|
return {
|
|
8875
9146
|
id,
|
|
8876
9147
|
path: absolute,
|
|
@@ -8949,7 +9220,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
|
8949
9220
|
if (!existsSync15(workspacePath)) {
|
|
8950
9221
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
8951
9222
|
}
|
|
8952
|
-
const dir =
|
|
9223
|
+
const dir = path21.dirname(path21.resolve(workspacePath));
|
|
8953
9224
|
const ws = await loadWorkspace(workspacePath);
|
|
8954
9225
|
const diagnostics = validateWorkspace(ws, dir);
|
|
8955
9226
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -8972,9 +9243,9 @@ async function openAccountProject(args) {
|
|
|
8972
9243
|
return project;
|
|
8973
9244
|
}
|
|
8974
9245
|
async function open(args) {
|
|
8975
|
-
if (!existsSync15(
|
|
9246
|
+
if (!existsSync15(path21.resolve(args.manifest))) {
|
|
8976
9247
|
throw new ProjectError(
|
|
8977
|
-
`No manifest at ${
|
|
9248
|
+
`No manifest at ${path21.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
8978
9249
|
);
|
|
8979
9250
|
}
|
|
8980
9251
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -9016,7 +9287,7 @@ function providerCache(mode2) {
|
|
|
9016
9287
|
|
|
9017
9288
|
// src/cli/commands/account.ts
|
|
9018
9289
|
async function runBalance(args) {
|
|
9019
|
-
loadEnvFiles(
|
|
9290
|
+
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
9020
9291
|
loadEnvFiles(process.cwd());
|
|
9021
9292
|
const loaded = await loadManifest(args.manifest);
|
|
9022
9293
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -9089,12 +9360,12 @@ async function runAdopt(args) {
|
|
|
9089
9360
|
tagged ${n} object(s) upstream`);
|
|
9090
9361
|
}
|
|
9091
9362
|
if (args.writePrompts) {
|
|
9092
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
9363
|
+
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
9093
9364
|
onProgress: log,
|
|
9094
9365
|
provider: provider.id,
|
|
9095
9366
|
assetIds: specs.map((spec) => spec.assetId)
|
|
9096
9367
|
});
|
|
9097
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
9368
|
+
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
9098
9369
|
const reloaded = await loadManifest(args.manifest);
|
|
9099
9370
|
const rebased = (await resolveSpecs(reloaded, {
|
|
9100
9371
|
styles: args.styles,
|
|
@@ -9124,12 +9395,12 @@ async function runSalvage2(args) {
|
|
|
9124
9395
|
const provider = providerFor(accountProvider);
|
|
9125
9396
|
const jsonMode = args.dryRun && args.json;
|
|
9126
9397
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
9127
|
-
const ownLock =
|
|
9398
|
+
const ownLock = path22.resolve(args.lock);
|
|
9128
9399
|
let workspaceProjects = [];
|
|
9129
9400
|
let workspaceDir = "";
|
|
9130
9401
|
if (args.workspace) {
|
|
9131
|
-
const workspacePath =
|
|
9132
|
-
workspaceDir =
|
|
9402
|
+
const workspacePath = path22.resolve(args.workspace);
|
|
9403
|
+
workspaceDir = path22.dirname(workspacePath);
|
|
9133
9404
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
9134
9405
|
workspaceProjects = complete.ws.projects;
|
|
9135
9406
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -9139,11 +9410,11 @@ async function runSalvage2(args) {
|
|
|
9139
9410
|
.../* @__PURE__ */ new Set([
|
|
9140
9411
|
...workspaceLockPaths,
|
|
9141
9412
|
...existsSync16(ownLock) ? [ownLock] : [],
|
|
9142
|
-
...args.claims.map((c) =>
|
|
9413
|
+
...args.claims.map((c) => path22.resolve(c))
|
|
9143
9414
|
])
|
|
9144
9415
|
];
|
|
9145
9416
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
9146
|
-
for (const p of lockPaths) diag(` ${
|
|
9417
|
+
for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
|
|
9147
9418
|
if (!args.claims.length && !args.workspace) {
|
|
9148
9419
|
diag(
|
|
9149
9420
|
`
|
|
@@ -9151,7 +9422,7 @@ async function runSalvage2(args) {
|
|
|
9151
9422
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
9152
9423
|
register every project in a workspace catalog and pass --workspace.`
|
|
9153
9424
|
);
|
|
9154
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
9425
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
|
|
9155
9426
|
diag(
|
|
9156
9427
|
`
|
|
9157
9428
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -9235,7 +9506,7 @@ async function runSalvage2(args) {
|
|
|
9235
9506
|
manifestPath: loaded.path,
|
|
9236
9507
|
manifest: accountManifest,
|
|
9237
9508
|
styleId,
|
|
9238
|
-
importDir:
|
|
9509
|
+
importDir: path22.resolve(loaded.root, style.outDir),
|
|
9239
9510
|
lock,
|
|
9240
9511
|
lockPath: args.lock
|
|
9241
9512
|
},
|
|
@@ -9336,17 +9607,17 @@ async function runTag(args) {
|
|
|
9336
9607
|
}
|
|
9337
9608
|
|
|
9338
9609
|
// src/cli/commands/edit.ts
|
|
9339
|
-
import
|
|
9610
|
+
import path25 from "path";
|
|
9340
9611
|
|
|
9341
9612
|
// src/pipeline/hand-edit.ts
|
|
9342
9613
|
import { existsSync as existsSync17 } from "fs";
|
|
9343
|
-
import { copyFile, mkdir as
|
|
9344
|
-
import
|
|
9614
|
+
import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
|
|
9615
|
+
import path24 from "path";
|
|
9345
9616
|
import { z as z6 } from "zod";
|
|
9346
9617
|
|
|
9347
9618
|
// src/manifest-edit.ts
|
|
9348
|
-
import { readFile as
|
|
9349
|
-
import
|
|
9619
|
+
import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
|
|
9620
|
+
import path23 from "path";
|
|
9350
9621
|
import { z as z5 } from "zod";
|
|
9351
9622
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
9352
9623
|
var AssetPatchSchema = z5.object({
|
|
@@ -9561,8 +9832,8 @@ function applyEdit(raw, edit) {
|
|
|
9561
9832
|
}
|
|
9562
9833
|
}
|
|
9563
9834
|
async function applyManifestEdit(manifestPath, edit) {
|
|
9564
|
-
const absolute =
|
|
9565
|
-
const original = await
|
|
9835
|
+
const absolute = path23.resolve(manifestPath);
|
|
9836
|
+
const original = await readFile15(absolute, "utf8");
|
|
9566
9837
|
const before = sha256(original);
|
|
9567
9838
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
9568
9839
|
let raw;
|
|
@@ -9576,12 +9847,12 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9576
9847
|
applyEdit(raw, edit);
|
|
9577
9848
|
const next = serializeLike(original, raw);
|
|
9578
9849
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
9579
|
-
const tmp =
|
|
9580
|
-
|
|
9581
|
-
`.${
|
|
9850
|
+
const tmp = path23.join(
|
|
9851
|
+
path23.dirname(absolute),
|
|
9852
|
+
`.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
9582
9853
|
);
|
|
9583
9854
|
try {
|
|
9584
|
-
await
|
|
9855
|
+
await writeFile10(tmp, next);
|
|
9585
9856
|
try {
|
|
9586
9857
|
const loaded = await loadManifest(tmp);
|
|
9587
9858
|
await resolveSpecs(loaded);
|
|
@@ -9589,7 +9860,7 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9589
9860
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
9590
9861
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
9591
9862
|
}
|
|
9592
|
-
await
|
|
9863
|
+
await rename7(tmp, absolute);
|
|
9593
9864
|
} finally {
|
|
9594
9865
|
await rm7(tmp, { force: true });
|
|
9595
9866
|
}
|
|
@@ -9600,11 +9871,11 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9600
9871
|
var HAND_EDIT_DIR = "edits";
|
|
9601
9872
|
function handEditPath(loaded, spec) {
|
|
9602
9873
|
const style = loaded.manifest.styles[spec.styleId];
|
|
9603
|
-
const outDir =
|
|
9604
|
-
const relative =
|
|
9605
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
9606
|
-
const file = inside ? relative :
|
|
9607
|
-
return
|
|
9874
|
+
const outDir = path24.resolve(loaded.root, style?.outDir ?? ".");
|
|
9875
|
+
const relative = path24.relative(outDir, spec.outFile);
|
|
9876
|
+
const inside = relative && !relative.startsWith("..") && !path24.isAbsolute(relative);
|
|
9877
|
+
const file = inside ? relative : path24.basename(spec.outFile);
|
|
9878
|
+
return path24.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
9608
9879
|
}
|
|
9609
9880
|
function handEditBases(spec, lock) {
|
|
9610
9881
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -9646,7 +9917,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
9646
9917
|
`${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`
|
|
9647
9918
|
);
|
|
9648
9919
|
}
|
|
9649
|
-
const existing = spec.source ?
|
|
9920
|
+
const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
|
|
9650
9921
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
9651
9922
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
9652
9923
|
let created = false;
|
|
@@ -9657,7 +9928,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
9657
9928
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
9658
9929
|
);
|
|
9659
9930
|
}
|
|
9660
|
-
await
|
|
9931
|
+
await mkdir9(path24.dirname(member.path), { recursive: true });
|
|
9661
9932
|
await copyFile(member.base, member.path);
|
|
9662
9933
|
created = true;
|
|
9663
9934
|
}
|
|
@@ -9736,7 +10007,7 @@ function handEditProjectPath(editPath) {
|
|
|
9736
10007
|
async function readHandEditCompanion(editPath) {
|
|
9737
10008
|
let text;
|
|
9738
10009
|
try {
|
|
9739
|
-
text = await
|
|
10010
|
+
text = await readFile16(handEditCompanionPath(editPath), "utf8");
|
|
9740
10011
|
} catch {
|
|
9741
10012
|
return null;
|
|
9742
10013
|
}
|
|
@@ -9779,7 +10050,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
9779
10050
|
}
|
|
9780
10051
|
});
|
|
9781
10052
|
const bases = handEditBases(spec, lock);
|
|
9782
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
10053
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile16(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
9783
10054
|
for (const [index, image] of decoded.entries()) {
|
|
9784
10055
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
9785
10056
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -9795,7 +10066,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
9795
10066
|
editor: input.editor,
|
|
9796
10067
|
protocol: input.protocol,
|
|
9797
10068
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9798
|
-
project: projectPath ?
|
|
10069
|
+
project: projectPath ? path24.basename(projectPath) : null,
|
|
9799
10070
|
outputs: incoming.map((frame, index) => ({
|
|
9800
10071
|
role: frame.role,
|
|
9801
10072
|
basedOn: basedOnHash(entry, index),
|
|
@@ -9825,8 +10096,8 @@ function basedOnHash(entry, index) {
|
|
|
9825
10096
|
async function replaceFile(file, bytes) {
|
|
9826
10097
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9827
10098
|
try {
|
|
9828
|
-
await
|
|
9829
|
-
await
|
|
10099
|
+
await writeFile11(tmp, bytes);
|
|
10100
|
+
await rename8(tmp, file);
|
|
9830
10101
|
} finally {
|
|
9831
10102
|
await rm8(tmp, { force: true });
|
|
9832
10103
|
}
|
|
@@ -9855,7 +10126,7 @@ async function runEdit(args) {
|
|
|
9855
10126
|
const set = started.members.length > 1;
|
|
9856
10127
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
9857
10128
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
9858
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
10129
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path25.relative(process.cwd(), member.path)}`);
|
|
9859
10130
|
if (args.noOpen) {
|
|
9860
10131
|
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`);
|
|
9861
10132
|
} else {
|
|
@@ -9865,17 +10136,17 @@ async function runEdit(args) {
|
|
|
9865
10136
|
}
|
|
9866
10137
|
|
|
9867
10138
|
// src/cli/commands/gallery.ts
|
|
9868
|
-
import
|
|
10139
|
+
import path32 from "path";
|
|
9869
10140
|
import { existsSync as existsSync23 } from "fs";
|
|
9870
10141
|
|
|
9871
10142
|
// src/gallery/snapshot.ts
|
|
9872
10143
|
import { createHash as createHash3 } from "crypto";
|
|
9873
10144
|
import { existsSync as existsSync19 } from "fs";
|
|
9874
|
-
import { readFile as
|
|
9875
|
-
import
|
|
10145
|
+
import { readFile as readFile17, stat as stat2 } from "fs/promises";
|
|
10146
|
+
import path28 from "path";
|
|
9876
10147
|
|
|
9877
10148
|
// src/gallery/edit.ts
|
|
9878
|
-
import
|
|
10149
|
+
import path26 from "path";
|
|
9879
10150
|
import { z as z7 } from "zod";
|
|
9880
10151
|
var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
9881
10152
|
var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
@@ -9965,7 +10236,7 @@ function createGalleryEditHandler(opts) {
|
|
|
9965
10236
|
}
|
|
9966
10237
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
9967
10238
|
if (result.changed) {
|
|
9968
|
-
const relative =
|
|
10239
|
+
const relative = path26.relative(process.cwd(), result.manifestPath);
|
|
9969
10240
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
9970
10241
|
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;
|
|
9971
10242
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -9976,7 +10247,7 @@ function createGalleryEditHandler(opts) {
|
|
|
9976
10247
|
|
|
9977
10248
|
// src/pipeline/history.ts
|
|
9978
10249
|
import { existsSync as existsSync18 } from "fs";
|
|
9979
|
-
import
|
|
10250
|
+
import path27 from "path";
|
|
9980
10251
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
9981
10252
|
function historyLimit(manifest, env = process.env) {
|
|
9982
10253
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -10065,7 +10336,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
10065
10336
|
if (!existsSync18(file)) continue;
|
|
10066
10337
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
10067
10338
|
throw new OverwriteRefusedError(
|
|
10068
|
-
`${
|
|
10339
|
+
`${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
10069
10340
|
[file]
|
|
10070
10341
|
);
|
|
10071
10342
|
}
|
|
@@ -10117,7 +10388,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
10117
10388
|
// src/gallery/snapshot.ts
|
|
10118
10389
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
10119
10390
|
function galleryMediaId(absolutePath) {
|
|
10120
|
-
return createHash3("sha256").update(
|
|
10391
|
+
return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
10121
10392
|
}
|
|
10122
10393
|
function galleryMediaRoute(id) {
|
|
10123
10394
|
return `/media/${id}`;
|
|
@@ -10136,7 +10407,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10136
10407
|
return (entry.history ?? []).map((generation, i) => {
|
|
10137
10408
|
const outputs = generation.outputs.map((output) => {
|
|
10138
10409
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
10139
|
-
const file =
|
|
10410
|
+
const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
10140
10411
|
let url = null;
|
|
10141
10412
|
if (existsSync19(file)) {
|
|
10142
10413
|
const id = galleryMediaId(file);
|
|
@@ -10167,7 +10438,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10167
10438
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
10168
10439
|
async function samePixels(a, b) {
|
|
10169
10440
|
try {
|
|
10170
|
-
const [left, right] = await Promise.all([
|
|
10441
|
+
const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
|
|
10171
10442
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
10172
10443
|
const x = decodePng(left);
|
|
10173
10444
|
const y = decodePng(right);
|
|
@@ -10177,6 +10448,32 @@ async function samePixels(a, b) {
|
|
|
10177
10448
|
}
|
|
10178
10449
|
}
|
|
10179
10450
|
var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
|
|
10451
|
+
function describeCharacter(spec, entry) {
|
|
10452
|
+
const recorded = entry?.providerMetadata?.[entry.provider]?.character;
|
|
10453
|
+
if (spec?.character) {
|
|
10454
|
+
const character = spec.character;
|
|
10455
|
+
return {
|
|
10456
|
+
kind: character.kind,
|
|
10457
|
+
parentKey: character.parentAssetId ? lockKey(spec.styleId, character.parentAssetId) : null,
|
|
10458
|
+
mode: character.kind === "animation" ? character.animation.mode : character.mode,
|
|
10459
|
+
directions: character.directions,
|
|
10460
|
+
direction: character.animation?.direction ?? null,
|
|
10461
|
+
characterId: recorded?.characterId ?? entry?.objectId?.split("#")[0] ?? null
|
|
10462
|
+
};
|
|
10463
|
+
}
|
|
10464
|
+
if (entry?.generator === "character") {
|
|
10465
|
+
const kind = recorded?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
|
|
10466
|
+
return {
|
|
10467
|
+
kind,
|
|
10468
|
+
parentKey: null,
|
|
10469
|
+
mode: recorded?.mode ?? "standard",
|
|
10470
|
+
directions: recorded?.directions ?? entry.outputs.length,
|
|
10471
|
+
direction: recorded?.direction ?? null,
|
|
10472
|
+
characterId: recorded?.characterId ?? entry.objectId?.split("#")[0] ?? null
|
|
10473
|
+
};
|
|
10474
|
+
}
|
|
10475
|
+
return null;
|
|
10476
|
+
}
|
|
10180
10477
|
async function fileInfo(absolutePath) {
|
|
10181
10478
|
try {
|
|
10182
10479
|
const info = await stat2(absolutePath);
|
|
@@ -10211,9 +10508,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
10211
10508
|
async function describeQuality(media, root, inspection) {
|
|
10212
10509
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
10213
10510
|
const outputs = await Promise.all(
|
|
10214
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
10511
|
+
outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
|
|
10215
10512
|
);
|
|
10216
|
-
const recordPath =
|
|
10513
|
+
const recordPath = path28.resolve(root, inspection.record);
|
|
10217
10514
|
const recordExists = existsSync19(recordPath);
|
|
10218
10515
|
let options = null;
|
|
10219
10516
|
let check = null;
|
|
@@ -10233,7 +10530,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
10233
10530
|
return {
|
|
10234
10531
|
state: inspection.state,
|
|
10235
10532
|
reason: inspection.reason,
|
|
10236
|
-
output: portableOutputPath(
|
|
10533
|
+
output: portableOutputPath(path28.resolve(root, inspection.output), root),
|
|
10237
10534
|
record: portableOutputPath(recordPath, root),
|
|
10238
10535
|
recordExists,
|
|
10239
10536
|
outputs,
|
|
@@ -10270,9 +10567,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
10270
10567
|
const media = /* @__PURE__ */ new Map();
|
|
10271
10568
|
const plan = await buildPlan(specs, lock);
|
|
10272
10569
|
const items = [];
|
|
10273
|
-
const manifestText = await
|
|
10570
|
+
const manifestText = await readFile17(loaded.path, "utf8");
|
|
10274
10571
|
const manifestSha256 = sha256(manifestText);
|
|
10275
|
-
const cacheDir =
|
|
10572
|
+
const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
10276
10573
|
let rawStyles = {};
|
|
10277
10574
|
try {
|
|
10278
10575
|
const raw = JSON.parse(manifestText);
|
|
@@ -10296,7 +10593,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10296
10593
|
)
|
|
10297
10594
|
);
|
|
10298
10595
|
if (spec.source) {
|
|
10299
|
-
const editPath =
|
|
10596
|
+
const editPath = path28.resolve(root, spec.source);
|
|
10300
10597
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
10301
10598
|
const editShas = await Promise.all(
|
|
10302
10599
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
@@ -10345,7 +10642,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10345
10642
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
10346
10643
|
}
|
|
10347
10644
|
} else if (spec.source) {
|
|
10348
|
-
outputs = [await describeOutput(media, root,
|
|
10645
|
+
outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
|
|
10349
10646
|
} else if (!entry && existsSync19(spec.outFile)) {
|
|
10350
10647
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
10351
10648
|
} else {
|
|
@@ -10385,6 +10682,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
10385
10682
|
currentSpecHash: spec.specHash,
|
|
10386
10683
|
revision: entry?.revision ?? null,
|
|
10387
10684
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
10685
|
+
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
10686
|
+
character: describeCharacter(spec, entry),
|
|
10388
10687
|
outputs,
|
|
10389
10688
|
quality,
|
|
10390
10689
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
@@ -10446,6 +10745,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
10446
10745
|
currentSpecHash: null,
|
|
10447
10746
|
revision: entry.revision,
|
|
10448
10747
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
10748
|
+
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
10749
|
+
character: describeCharacter(void 0, entry),
|
|
10449
10750
|
outputs,
|
|
10450
10751
|
quality: null,
|
|
10451
10752
|
providerMetadata: entry.providerMetadata ?? {},
|
|
@@ -10502,6 +10803,11 @@ async function buildGallerySnapshot(opts) {
|
|
|
10502
10803
|
view: style?.view ?? null,
|
|
10503
10804
|
noBackground: style?.noBackground ?? true,
|
|
10504
10805
|
quality: Boolean(style?.quality),
|
|
10806
|
+
characters: style?.generator === "character" ? {
|
|
10807
|
+
bases: styleItems.filter((item) => item.character?.kind === "base").length,
|
|
10808
|
+
states: styleItems.filter((item) => item.character?.kind === "state").length,
|
|
10809
|
+
animations: styleItems.filter((item) => item.character?.kind === "animation").length
|
|
10810
|
+
} : null,
|
|
10505
10811
|
tags: style?.tags ?? [],
|
|
10506
10812
|
extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
|
|
10507
10813
|
ownFields: rawStyle ? Object.keys(rawStyle).filter((key) => key !== "extends") : [],
|
|
@@ -10528,7 +10834,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10528
10834
|
id: loaded.manifest.name,
|
|
10529
10835
|
name: loaded.manifest.name,
|
|
10530
10836
|
manifest: loaded.path,
|
|
10531
|
-
lock:
|
|
10837
|
+
lock: path28.resolve(opts.lockPath),
|
|
10532
10838
|
root,
|
|
10533
10839
|
provider: loaded.manifest.provider,
|
|
10534
10840
|
account: null,
|
|
@@ -10554,7 +10860,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10554
10860
|
return { snapshot, media };
|
|
10555
10861
|
}
|
|
10556
10862
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
10557
|
-
const dir =
|
|
10863
|
+
const dir = path28.dirname(path28.resolve(opts.workspacePath));
|
|
10558
10864
|
const media = /* @__PURE__ */ new Map();
|
|
10559
10865
|
const projects = [];
|
|
10560
10866
|
const items = [];
|
|
@@ -10597,11 +10903,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10597
10903
|
id: project.id,
|
|
10598
10904
|
name: loaded.manifest.name,
|
|
10599
10905
|
manifest: manifestPath,
|
|
10600
|
-
lock:
|
|
10906
|
+
lock: path28.resolve(lockPath),
|
|
10601
10907
|
root: loaded.root,
|
|
10602
10908
|
provider: loaded.manifest.provider,
|
|
10603
10909
|
account: project.account ?? null,
|
|
10604
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
10910
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
|
|
10605
10911
|
historyLimit: historyLimit(loaded.manifest),
|
|
10606
10912
|
entries: Object.keys(lock.entries).length,
|
|
10607
10913
|
items: projectItems.length,
|
|
@@ -10613,8 +10919,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10613
10919
|
id: project.id,
|
|
10614
10920
|
name: project.id,
|
|
10615
10921
|
manifest: manifestPath,
|
|
10616
|
-
lock:
|
|
10617
|
-
root:
|
|
10922
|
+
lock: path28.resolve(lockPath),
|
|
10923
|
+
root: path28.dirname(manifestPath),
|
|
10618
10924
|
provider: project.provider,
|
|
10619
10925
|
account: project.account ?? null,
|
|
10620
10926
|
manifestSha256: null,
|
|
@@ -10641,7 +10947,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10641
10947
|
version: 1,
|
|
10642
10948
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
10643
10949
|
project: null,
|
|
10644
|
-
workspace: { path:
|
|
10950
|
+
workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
|
|
10645
10951
|
filter,
|
|
10646
10952
|
totals: {
|
|
10647
10953
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -10660,7 +10966,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
10660
10966
|
// src/gallery/server.ts
|
|
10661
10967
|
import { randomBytes } from "crypto";
|
|
10662
10968
|
import { createServer as createServer2 } from "http";
|
|
10663
|
-
import { readFile as
|
|
10969
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
10664
10970
|
|
|
10665
10971
|
// src/gallery/page.ts
|
|
10666
10972
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -10901,7 +11207,7 @@ async function serveGallery(opts) {
|
|
|
10901
11207
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
10902
11208
|
if (!asset) return fail(404, "no such review asset");
|
|
10903
11209
|
try {
|
|
10904
|
-
const bytes = await
|
|
11210
|
+
const bytes = await readFile18(asset.path);
|
|
10905
11211
|
res.writeHead(200, {
|
|
10906
11212
|
"Content-Type": asset.contentType,
|
|
10907
11213
|
"Cache-Control": "no-store",
|
|
@@ -10977,7 +11283,7 @@ async function serveGallery(opts) {
|
|
|
10977
11283
|
if (!asset) return fail(404, "no such gallery media");
|
|
10978
11284
|
let bytes;
|
|
10979
11285
|
try {
|
|
10980
|
-
bytes = await
|
|
11286
|
+
bytes = await readFile18(asset.path);
|
|
10981
11287
|
} catch {
|
|
10982
11288
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
10983
11289
|
}
|
|
@@ -11121,7 +11427,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11121
11427
|
});
|
|
11122
11428
|
const estimates = new Map(items.map((item) => [
|
|
11123
11429
|
item.key,
|
|
11124
|
-
validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
11430
|
+
item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
11125
11431
|
]));
|
|
11126
11432
|
const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
|
|
11127
11433
|
if (units.size > 1) {
|
|
@@ -11171,6 +11477,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11171
11477
|
}
|
|
11172
11478
|
for (const { spec, key } of items) {
|
|
11173
11479
|
const estimate = estimates.get(key);
|
|
11480
|
+
if (spec.mirror) {
|
|
11481
|
+
try {
|
|
11482
|
+
await requireRevisionReady(spec, lock);
|
|
11483
|
+
await deriveMirror(spec, lock, lockPath, { onProgress: log2 });
|
|
11484
|
+
submitted++;
|
|
11485
|
+
} catch (err) {
|
|
11486
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
11487
|
+
failed++;
|
|
11488
|
+
upsert(lock, key, {
|
|
11489
|
+
styleId: spec.styleId,
|
|
11490
|
+
assetId: spec.assetId,
|
|
11491
|
+
specHash: spec.specHash,
|
|
11492
|
+
generator: spec.generator,
|
|
11493
|
+
prompt: "",
|
|
11494
|
+
width: spec.width,
|
|
11495
|
+
height: spec.height,
|
|
11496
|
+
status: "failed",
|
|
11497
|
+
error: message6,
|
|
11498
|
+
outputs: lock.entries[key]?.outputs ?? [],
|
|
11499
|
+
cost: 0,
|
|
11500
|
+
costUnit: spec.costUnit,
|
|
11501
|
+
provider: provider.id
|
|
11502
|
+
});
|
|
11503
|
+
await saveLock(lockPath, lock);
|
|
11504
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
11505
|
+
}
|
|
11506
|
+
continue;
|
|
11507
|
+
}
|
|
11174
11508
|
await waitForSlot();
|
|
11175
11509
|
const since = Date.now() - lastSubmitAt;
|
|
11176
11510
|
if (since < spacing) await sleep2(spacing - since);
|
|
@@ -11296,12 +11630,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
11296
11630
|
|
|
11297
11631
|
// src/pick/server.ts
|
|
11298
11632
|
import { existsSync as existsSync21 } from "fs";
|
|
11299
|
-
import { readFile as
|
|
11633
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
11300
11634
|
|
|
11301
11635
|
// src/pipeline/init.ts
|
|
11302
|
-
import { readdir, readFile as
|
|
11636
|
+
import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
|
|
11303
11637
|
import { existsSync as existsSync20 } from "fs";
|
|
11304
|
-
import
|
|
11638
|
+
import path29 from "path";
|
|
11305
11639
|
function pngSize(buf) {
|
|
11306
11640
|
if (buf.length < 24) return null;
|
|
11307
11641
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -11315,7 +11649,7 @@ function slugify(value) {
|
|
|
11315
11649
|
async function walk(dir, exclude) {
|
|
11316
11650
|
const out = [];
|
|
11317
11651
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
11318
|
-
const full =
|
|
11652
|
+
const full = path29.join(dir, entry.name);
|
|
11319
11653
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
11320
11654
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
11321
11655
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -11329,14 +11663,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
11329
11663
|
const skipped = [];
|
|
11330
11664
|
const seen = /* @__PURE__ */ new Map();
|
|
11331
11665
|
for (const file of files.sort()) {
|
|
11332
|
-
const size = pngSize(await
|
|
11666
|
+
const size = pngSize(await readFile19(file));
|
|
11333
11667
|
if (!size) {
|
|
11334
11668
|
skipped.push(`${file} (not a readable PNG)`);
|
|
11335
11669
|
continue;
|
|
11336
11670
|
}
|
|
11337
|
-
const rel =
|
|
11338
|
-
const category =
|
|
11339
|
-
let id = slugify(
|
|
11671
|
+
const rel = path29.relative(root, file);
|
|
11672
|
+
const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
|
|
11673
|
+
let id = slugify(path29.basename(rel));
|
|
11340
11674
|
if (seen.has(id)) {
|
|
11341
11675
|
const n = seen.get(id) + 1;
|
|
11342
11676
|
seen.set(id, n);
|
|
@@ -11397,7 +11731,7 @@ async function writeManifestFile(target, manifest) {
|
|
|
11397
11731
|
if (existsSync20(target)) {
|
|
11398
11732
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
11399
11733
|
}
|
|
11400
|
-
await
|
|
11734
|
+
await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
11401
11735
|
}
|
|
11402
11736
|
|
|
11403
11737
|
// src/pick/sheet.ts
|
|
@@ -11803,7 +12137,7 @@ async function currentArt(entry, spec) {
|
|
|
11803
12137
|
let width = entry.width;
|
|
11804
12138
|
let height = entry.height;
|
|
11805
12139
|
if (contentType === "image/png") {
|
|
11806
|
-
const size = pngSize(await
|
|
12140
|
+
const size = pngSize(await readFile20(file));
|
|
11807
12141
|
if (size) ({ width, height } = size);
|
|
11808
12142
|
}
|
|
11809
12143
|
return { path: file, contentType, width, height };
|
|
@@ -12296,15 +12630,15 @@ function createGenerateHandlers(opts) {
|
|
|
12296
12630
|
}
|
|
12297
12631
|
|
|
12298
12632
|
// src/gallery/editor.ts
|
|
12299
|
-
import { readFile as
|
|
12300
|
-
import
|
|
12633
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
12634
|
+
import path31 from "path";
|
|
12301
12635
|
|
|
12302
12636
|
// src/editor/install.ts
|
|
12303
12637
|
import { createHash as createHash4 } from "crypto";
|
|
12304
12638
|
import { existsSync as existsSync22 } from "fs";
|
|
12305
|
-
import { mkdir as
|
|
12639
|
+
import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
|
|
12306
12640
|
import os2 from "os";
|
|
12307
|
-
import
|
|
12641
|
+
import path30 from "path";
|
|
12308
12642
|
|
|
12309
12643
|
// tools/pixelorama-bridge/pin.json
|
|
12310
12644
|
var pin_default = {
|
|
@@ -12368,15 +12702,15 @@ var EditorInstallError = class extends Error {
|
|
|
12368
12702
|
};
|
|
12369
12703
|
function editorToolsRoot() {
|
|
12370
12704
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
12371
|
-
if (override) return
|
|
12372
|
-
if (process.platform === "darwin") return
|
|
12705
|
+
if (override) return path30.resolve(override);
|
|
12706
|
+
if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
12373
12707
|
if (process.platform === "win32") {
|
|
12374
|
-
return
|
|
12708
|
+
return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
12375
12709
|
}
|
|
12376
|
-
return
|
|
12710
|
+
return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
12377
12711
|
}
|
|
12378
12712
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
12379
|
-
return
|
|
12713
|
+
return path30.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
12380
12714
|
}
|
|
12381
12715
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
12382
12716
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -12386,7 +12720,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
12386
12720
|
}
|
|
12387
12721
|
async function sha256Of(file) {
|
|
12388
12722
|
try {
|
|
12389
|
-
return createHash4("sha256").update(await
|
|
12723
|
+
return createHash4("sha256").update(await readFile21(file)).digest("hex");
|
|
12390
12724
|
} catch {
|
|
12391
12725
|
return null;
|
|
12392
12726
|
}
|
|
@@ -12399,7 +12733,7 @@ async function editorStatus(opts = {}) {
|
|
|
12399
12733
|
let totalBytes = 0;
|
|
12400
12734
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
12401
12735
|
totalBytes += expected.bytes;
|
|
12402
|
-
const file =
|
|
12736
|
+
const file = path30.join(dir, name);
|
|
12403
12737
|
const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
|
|
12404
12738
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
12405
12739
|
installedBytes += expected.bytes;
|
|
@@ -12429,7 +12763,7 @@ async function installEditor(opts = {}) {
|
|
|
12429
12763
|
);
|
|
12430
12764
|
}
|
|
12431
12765
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
12432
|
-
await
|
|
12766
|
+
await mkdir10(dir, { recursive: true });
|
|
12433
12767
|
const downloaded = [];
|
|
12434
12768
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
12435
12769
|
let fetchedBytes = before.installedBytes;
|
|
@@ -12457,11 +12791,11 @@ async function installEditor(opts = {}) {
|
|
|
12457
12791
|
if (digest2 !== expected.sha256) {
|
|
12458
12792
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
12459
12793
|
}
|
|
12460
|
-
const final =
|
|
12794
|
+
const final = path30.join(dir, name);
|
|
12461
12795
|
const tmp = `${final}.${process.pid}.part`;
|
|
12462
12796
|
try {
|
|
12463
|
-
await
|
|
12464
|
-
await
|
|
12797
|
+
await writeFile13(tmp, bytes);
|
|
12798
|
+
await rename9(tmp, final);
|
|
12465
12799
|
} finally {
|
|
12466
12800
|
await rm9(tmp, { force: true });
|
|
12467
12801
|
}
|
|
@@ -12503,7 +12837,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
12503
12837
|
".json": "application/json; charset=utf-8"
|
|
12504
12838
|
};
|
|
12505
12839
|
function editorContentType(name) {
|
|
12506
|
-
return EDITOR_CONTENT_TYPES[
|
|
12840
|
+
return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
|
|
12507
12841
|
}
|
|
12508
12842
|
|
|
12509
12843
|
// src/gallery/editor.ts
|
|
@@ -12579,7 +12913,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12579
12913
|
if (!status.installed) return null;
|
|
12580
12914
|
let bytes;
|
|
12581
12915
|
try {
|
|
12582
|
-
bytes = await
|
|
12916
|
+
bytes = await readFile22(path31.join(dir, name));
|
|
12583
12917
|
} catch {
|
|
12584
12918
|
verified = null;
|
|
12585
12919
|
return null;
|
|
@@ -12619,13 +12953,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
12619
12953
|
};
|
|
12620
12954
|
const loadProject = async (project) => {
|
|
12621
12955
|
const ctx = await access2.loadProject(project);
|
|
12622
|
-
const dir =
|
|
12956
|
+
const dir = path32.dirname(ctx.loaded.path);
|
|
12623
12957
|
const declared = readEnvFiles(dir);
|
|
12624
12958
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
12625
12959
|
for (const [name, value] of Object.entries(declared)) {
|
|
12626
12960
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
12627
12961
|
throw new Error(
|
|
12628
|
-
`${
|
|
12962
|
+
`${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.`
|
|
12629
12963
|
);
|
|
12630
12964
|
}
|
|
12631
12965
|
}
|
|
@@ -12670,7 +13004,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
12670
13004
|
}
|
|
12671
13005
|
async function runGallery(args) {
|
|
12672
13006
|
if (args.workspace) {
|
|
12673
|
-
const workspacePath =
|
|
13007
|
+
const workspacePath = path32.resolve(args.workspace);
|
|
12674
13008
|
if (!existsSync23(workspacePath)) {
|
|
12675
13009
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
12676
13010
|
}
|
|
@@ -12691,7 +13025,7 @@ async function runGallery(args) {
|
|
|
12691
13025
|
const registered = async (projectId) => {
|
|
12692
13026
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
12693
13027
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
12694
|
-
return resolveProject(
|
|
13028
|
+
return resolveProject(path32.dirname(workspacePath), project);
|
|
12695
13029
|
};
|
|
12696
13030
|
await serveUntilStopped(initial, build2, args, {
|
|
12697
13031
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -12716,7 +13050,7 @@ async function runGallery(args) {
|
|
|
12716
13050
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
12717
13051
|
};
|
|
12718
13052
|
await serveUntilStopped(await build(), reload, args, {
|
|
12719
|
-
manifestFor: () =>
|
|
13053
|
+
manifestFor: () => path32.resolve(args.manifest),
|
|
12720
13054
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
12721
13055
|
});
|
|
12722
13056
|
}
|
|
@@ -12959,7 +13293,7 @@ async function runLifecycle(args, wave, spent) {
|
|
|
12959
13293
|
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
12960
13294
|
}
|
|
12961
13295
|
}
|
|
12962
|
-
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision);
|
|
13296
|
+
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision || spec.mirror);
|
|
12963
13297
|
}
|
|
12964
13298
|
return false;
|
|
12965
13299
|
}
|
|
@@ -13025,14 +13359,14 @@ import { existsSync as existsSync25 } from "fs";
|
|
|
13025
13359
|
|
|
13026
13360
|
// src/pipeline/cache-health.ts
|
|
13027
13361
|
import { existsSync as existsSync24 } from "fs";
|
|
13028
|
-
import { readFile as
|
|
13029
|
-
import
|
|
13362
|
+
import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
13363
|
+
import path33 from "path";
|
|
13030
13364
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
13031
13365
|
if (options.prune && !existsSync24(lockPath)) {
|
|
13032
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
13366
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
|
|
13033
13367
|
}
|
|
13034
|
-
const contentDir =
|
|
13035
|
-
const remotePath =
|
|
13368
|
+
const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
|
|
13369
|
+
const remotePath = path33.resolve(cachePathFor(lockPath));
|
|
13036
13370
|
const referenced = referencedHashes(lock);
|
|
13037
13371
|
let content = await inspectContentCache(contentDir, referenced);
|
|
13038
13372
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -13044,7 +13378,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
13044
13378
|
]);
|
|
13045
13379
|
for (const name of names) {
|
|
13046
13380
|
try {
|
|
13047
|
-
await rm10(
|
|
13381
|
+
await rm10(path33.join(contentDir, name), { force: true });
|
|
13048
13382
|
removed.contentFiles++;
|
|
13049
13383
|
} catch {
|
|
13050
13384
|
}
|
|
@@ -13053,7 +13387,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
13053
13387
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
13054
13388
|
removed.resetRemoteHashCache = true;
|
|
13055
13389
|
} else if (remoteHashes.invalidIds.length) {
|
|
13056
|
-
const cache = parseCache(JSON.parse(await
|
|
13390
|
+
const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
|
|
13057
13391
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
13058
13392
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
13059
13393
|
await saveCache(remotePath, cache);
|
|
@@ -13092,12 +13426,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
13092
13426
|
continue;
|
|
13093
13427
|
}
|
|
13094
13428
|
report.files++;
|
|
13095
|
-
const file =
|
|
13429
|
+
const file = path33.join(contentDir, entry.name);
|
|
13096
13430
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
13097
13431
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
13098
13432
|
let bytes;
|
|
13099
13433
|
try {
|
|
13100
|
-
bytes = await
|
|
13434
|
+
bytes = await readFile23(file);
|
|
13101
13435
|
report.bytes += bytes.length;
|
|
13102
13436
|
} catch (err) {
|
|
13103
13437
|
report.invalid.push({
|
|
@@ -13144,7 +13478,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
13144
13478
|
if (!report.exists) return report;
|
|
13145
13479
|
let cache;
|
|
13146
13480
|
try {
|
|
13147
|
-
cache = parseCache(JSON.parse(await
|
|
13481
|
+
cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
|
|
13148
13482
|
} catch (err) {
|
|
13149
13483
|
report.error = err instanceof Error ? err.message : String(err);
|
|
13150
13484
|
return report;
|
|
@@ -13263,42 +13597,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
13263
13597
|
}
|
|
13264
13598
|
|
|
13265
13599
|
// src/cli/commands/init.ts
|
|
13266
|
-
import
|
|
13600
|
+
import path34 from "path";
|
|
13267
13601
|
import { existsSync as existsSync26 } from "fs";
|
|
13268
13602
|
async function runInit(args) {
|
|
13269
13603
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13270
|
-
const root =
|
|
13604
|
+
const root = path34.resolve(args.from);
|
|
13271
13605
|
if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
|
|
13272
13606
|
const generator = args.generator ?? "map";
|
|
13273
13607
|
if (generator !== "1dir" && generator !== "map") {
|
|
13274
13608
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13275
13609
|
}
|
|
13276
|
-
const target =
|
|
13610
|
+
const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13277
13611
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13278
13612
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13279
13613
|
const manifest = buildManifest(
|
|
13280
|
-
args.name ??
|
|
13614
|
+
args.name ?? path34.basename(path34.dirname(target)),
|
|
13281
13615
|
args.styles[0] ?? "base",
|
|
13282
13616
|
generator,
|
|
13283
|
-
|
|
13617
|
+
path34.relative(path34.dirname(target), root) || ".",
|
|
13284
13618
|
assets
|
|
13285
13619
|
);
|
|
13286
13620
|
await writeManifestFile(target, manifest);
|
|
13287
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
13621
|
+
log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
|
|
13288
13622
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13289
|
-
log(` wrote ${
|
|
13623
|
+
log(` wrote ${path34.relative(process.cwd(), target)}`);
|
|
13290
13624
|
log(`
|
|
13291
13625
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13292
13626
|
log(` PixelLab account instead of inventing them:`);
|
|
13293
13627
|
log(`
|
|
13294
|
-
pixelkiln adopt --manifest ${
|
|
13628
|
+
pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
|
|
13295
13629
|
`);
|
|
13296
13630
|
}
|
|
13297
13631
|
|
|
13298
13632
|
// src/pipeline/doctor.ts
|
|
13299
13633
|
import { constants, existsSync as existsSync27 } from "fs";
|
|
13300
13634
|
import { access, stat as stat4 } from "fs/promises";
|
|
13301
|
-
import
|
|
13635
|
+
import path35 from "path";
|
|
13302
13636
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
13303
13637
|
const checks = [];
|
|
13304
13638
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -13309,8 +13643,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13309
13643
|
);
|
|
13310
13644
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
13311
13645
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
13312
|
-
|
|
13313
|
-
...spec.quality ? [
|
|
13646
|
+
path35.dirname(spec.outFile),
|
|
13647
|
+
...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
|
|
13314
13648
|
]))];
|
|
13315
13649
|
const unwritable = [];
|
|
13316
13650
|
for (const dir of dirs) {
|
|
@@ -13333,7 +13667,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13333
13667
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
13334
13668
|
const output = entry.outputs[index];
|
|
13335
13669
|
const spec = specByKey.get(key);
|
|
13336
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
13670
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
|
|
13337
13671
|
const owner = outputOwners.get(absolute);
|
|
13338
13672
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
13339
13673
|
outputOwners.set(absolute, key);
|
|
@@ -13348,7 +13682,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13348
13682
|
} else {
|
|
13349
13683
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
13350
13684
|
}
|
|
13351
|
-
const cacheDir =
|
|
13685
|
+
const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
|
|
13352
13686
|
const stranded = [];
|
|
13353
13687
|
const invalidState = [];
|
|
13354
13688
|
const staleMapUrls = [];
|
|
@@ -13364,7 +13698,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13364
13698
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
13365
13699
|
let hasCache = false;
|
|
13366
13700
|
for (const output of entry.outputs) {
|
|
13367
|
-
const cached =
|
|
13701
|
+
const cached = path35.join(
|
|
13368
13702
|
cacheDir,
|
|
13369
13703
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
13370
13704
|
);
|
|
@@ -13464,16 +13798,16 @@ async function nearestExistingDirectory(start) {
|
|
|
13464
13798
|
if ((await stat4(current)).isDirectory()) return current;
|
|
13465
13799
|
} catch {
|
|
13466
13800
|
}
|
|
13467
|
-
const parent =
|
|
13801
|
+
const parent = path35.dirname(current);
|
|
13468
13802
|
if (parent === current) return current;
|
|
13469
13803
|
current = parent;
|
|
13470
13804
|
}
|
|
13471
13805
|
}
|
|
13472
13806
|
|
|
13473
13807
|
// src/pipeline/audit.ts
|
|
13474
|
-
import { readFile as
|
|
13808
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
13475
13809
|
import { existsSync as existsSync28 } from "fs";
|
|
13476
|
-
import
|
|
13810
|
+
import path36 from "path";
|
|
13477
13811
|
function paletteDistance(asset, reference) {
|
|
13478
13812
|
if (!asset.length || !reference.length) return 0;
|
|
13479
13813
|
let total = 0;
|
|
@@ -13514,7 +13848,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
13514
13848
|
continue;
|
|
13515
13849
|
}
|
|
13516
13850
|
try {
|
|
13517
|
-
const png = decodePng(await
|
|
13851
|
+
const png = decodePng(await readFile24(output.absolutePath));
|
|
13518
13852
|
const palette = extractPalette(png, 12);
|
|
13519
13853
|
assets.push({
|
|
13520
13854
|
assetId: spec.assetId,
|
|
@@ -13538,10 +13872,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
13538
13872
|
let referenceFromStyleImages = false;
|
|
13539
13873
|
const refPalettes = [];
|
|
13540
13874
|
for (const img of style.styleImages) {
|
|
13541
|
-
const abs =
|
|
13875
|
+
const abs = path36.resolve(loaded.root, img.path);
|
|
13542
13876
|
if (!existsSync28(abs)) continue;
|
|
13543
13877
|
try {
|
|
13544
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
13878
|
+
refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
|
|
13545
13879
|
} catch {
|
|
13546
13880
|
}
|
|
13547
13881
|
}
|
|
@@ -13761,24 +14095,24 @@ async function runAudit(args) {
|
|
|
13761
14095
|
}
|
|
13762
14096
|
|
|
13763
14097
|
// src/cli/commands/pack.ts
|
|
13764
|
-
import
|
|
13765
|
-
import { readFile as
|
|
14098
|
+
import path38 from "path";
|
|
14099
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
13766
14100
|
|
|
13767
14101
|
// src/pipeline/pack.ts
|
|
13768
14102
|
import { readFileSync as readFileSync5 } from "fs";
|
|
13769
|
-
import
|
|
14103
|
+
import path37 from "path";
|
|
13770
14104
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
13771
14105
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
13772
14106
|
if (!Array.isArray(raw) || !raw.length) {
|
|
13773
14107
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
13774
14108
|
}
|
|
13775
|
-
const baseDir =
|
|
14109
|
+
const baseDir = path37.dirname(path37.resolve(inputsFilePath));
|
|
13776
14110
|
return raw.map((entry, i) => {
|
|
13777
14111
|
const e = entry;
|
|
13778
14112
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
13779
14113
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
13780
14114
|
}
|
|
13781
|
-
return { id: e.id, path:
|
|
14115
|
+
return { id: e.id, path: path37.resolve(baseDir, e.path) };
|
|
13782
14116
|
});
|
|
13783
14117
|
}
|
|
13784
14118
|
function packSprites(inputs, options = {}) {
|
|
@@ -13850,7 +14184,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
13850
14184
|
}
|
|
13851
14185
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
13852
14186
|
id,
|
|
13853
|
-
path:
|
|
14187
|
+
path: path37.resolve(source)
|
|
13854
14188
|
}));
|
|
13855
14189
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
13856
14190
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -13874,7 +14208,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
13874
14208
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
13875
14209
|
const source = sources[id];
|
|
13876
14210
|
if (source && outputs.length <= 1) {
|
|
13877
|
-
inputs.push({ id, path:
|
|
14211
|
+
inputs.push({ id, path: path37.resolve(manifestDir, source) });
|
|
13878
14212
|
continue;
|
|
13879
14213
|
}
|
|
13880
14214
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -13908,7 +14242,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
13908
14242
|
}
|
|
13909
14243
|
}
|
|
13910
14244
|
for (const [id, source] of Object.entries(sources)) {
|
|
13911
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
14245
|
+
if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
|
|
13912
14246
|
}
|
|
13913
14247
|
if (!inputs.length) {
|
|
13914
14248
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -14016,7 +14350,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14016
14350
|
const source = sources[id];
|
|
14017
14351
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
14018
14352
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
14019
|
-
placements.push({ id, path:
|
|
14353
|
+
placements.push({ id, path: path37.resolve(manifestDir, source), cell });
|
|
14020
14354
|
continue;
|
|
14021
14355
|
}
|
|
14022
14356
|
if (source && entry) {
|
|
@@ -14049,7 +14383,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14049
14383
|
`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}`
|
|
14050
14384
|
);
|
|
14051
14385
|
}
|
|
14052
|
-
const basePng = mount.base ? readFileSync5(
|
|
14386
|
+
const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
|
|
14053
14387
|
const mounted = mountSprites(placements, {
|
|
14054
14388
|
cellWidth: mount.cellWidth,
|
|
14055
14389
|
cellHeight: mount.cellHeight,
|
|
@@ -14059,7 +14393,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
14059
14393
|
...mounted,
|
|
14060
14394
|
sources: mount.base ? [{
|
|
14061
14395
|
id: "$base",
|
|
14062
|
-
path:
|
|
14396
|
+
path: path37.resolve(manifestDir, mount.base),
|
|
14063
14397
|
sha256: sha256(basePng),
|
|
14064
14398
|
included: true
|
|
14065
14399
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -14426,12 +14760,12 @@ async function runPack(args) {
|
|
|
14426
14760
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
14427
14761
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
14428
14762
|
}
|
|
14429
|
-
const raw = JSON.parse(await
|
|
14763
|
+
const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
|
|
14430
14764
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
14431
14765
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
14432
14766
|
const format = sheetFormat(args);
|
|
14433
|
-
const base =
|
|
14434
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14767
|
+
const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
14768
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
|
|
14435
14769
|
const outputs = [
|
|
14436
14770
|
{ path: `${base}.png`, data: png },
|
|
14437
14771
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -14444,7 +14778,7 @@ async function runPack(args) {
|
|
|
14444
14778
|
log(
|
|
14445
14779
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
14446
14780
|
);
|
|
14447
|
-
log(` ${
|
|
14781
|
+
log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
14448
14782
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14449
14783
|
return;
|
|
14450
14784
|
}
|
|
@@ -14452,7 +14786,7 @@ async function runPack(args) {
|
|
|
14452
14786
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14453
14787
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14454
14788
|
}
|
|
14455
|
-
const manifestDir =
|
|
14789
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14456
14790
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14457
14791
|
for (const styleId of styleIds) {
|
|
14458
14792
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14465,9 +14799,9 @@ async function runPack(args) {
|
|
|
14465
14799
|
sources: manifestSources(loaded.manifest, styleId)
|
|
14466
14800
|
});
|
|
14467
14801
|
const style = loaded.manifest.styles[styleId];
|
|
14468
|
-
const base = args.out ?
|
|
14802
|
+
const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
14469
14803
|
const format = sheetFormat(args);
|
|
14470
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14804
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
|
|
14471
14805
|
const outputs = [
|
|
14472
14806
|
{ path: `${base}.png`, data: png },
|
|
14473
14807
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -14498,13 +14832,13 @@ async function runPack(args) {
|
|
|
14498
14832
|
log(
|
|
14499
14833
|
` ${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})`
|
|
14500
14834
|
);
|
|
14501
|
-
log(` ${
|
|
14835
|
+
log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
14502
14836
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14503
14837
|
}
|
|
14504
14838
|
}
|
|
14505
14839
|
async function runMount(args) {
|
|
14506
14840
|
const { loaded, lock } = await openProject2(args);
|
|
14507
|
-
const manifestDir =
|
|
14841
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14508
14842
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14509
14843
|
for (const styleId of styleIds) {
|
|
14510
14844
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14543,9 +14877,9 @@ async function runMount(args) {
|
|
|
14543
14877
|
sources,
|
|
14544
14878
|
outputRoles
|
|
14545
14879
|
);
|
|
14546
|
-
const out =
|
|
14880
|
+
const out = path38.resolve(manifestDir, style.mount.out);
|
|
14547
14881
|
const format = sheetFormat(args);
|
|
14548
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
14882
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
|
|
14549
14883
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
14550
14884
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14551
14885
|
const outputs = [
|
|
@@ -14565,7 +14899,7 @@ async function runMount(args) {
|
|
|
14565
14899
|
await provenanceFile("$lock", args.lock),
|
|
14566
14900
|
...qualityRecords,
|
|
14567
14901
|
...artifactSources.filter(
|
|
14568
|
-
(source) => source.id !== "$base" ||
|
|
14902
|
+
(source) => source.id !== "$base" || path38.resolve(source.path) !== out
|
|
14569
14903
|
)
|
|
14570
14904
|
],
|
|
14571
14905
|
options: {
|
|
@@ -14579,14 +14913,14 @@ async function runMount(args) {
|
|
|
14579
14913
|
log(
|
|
14580
14914
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14581
14915
|
);
|
|
14582
|
-
log(` ${
|
|
14916
|
+
log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14583
14917
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14584
14918
|
}
|
|
14585
14919
|
}
|
|
14586
14920
|
async function runExport(args) {
|
|
14587
14921
|
const { loaded, specs, lock } = await openProject2(args);
|
|
14588
14922
|
const format = args.format ?? "generic";
|
|
14589
|
-
const manifestDir =
|
|
14923
|
+
const manifestDir = path38.dirname(path38.resolve(args.manifest));
|
|
14590
14924
|
const selected = specs.filter((spec) => {
|
|
14591
14925
|
if (spec.generator !== "tiles") return false;
|
|
14592
14926
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14602,12 +14936,12 @@ async function runExport(args) {
|
|
|
14602
14936
|
for (const spec of selected) {
|
|
14603
14937
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14604
14938
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14605
|
-
const defaultBase =
|
|
14606
|
-
const base = args.out ?
|
|
14939
|
+
const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
14940
|
+
const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14607
14941
|
const result = exportTileset(entry, spec, {
|
|
14608
14942
|
format,
|
|
14609
14943
|
manifestDir,
|
|
14610
|
-
imageName:
|
|
14944
|
+
imageName: path38.basename(`${base}.png`),
|
|
14611
14945
|
columns: args.columns
|
|
14612
14946
|
});
|
|
14613
14947
|
const outputs = [
|
|
@@ -14625,7 +14959,7 @@ async function runExport(args) {
|
|
|
14625
14959
|
asset: spec.assetId,
|
|
14626
14960
|
columns: args.columns ?? null,
|
|
14627
14961
|
format,
|
|
14628
|
-
image:
|
|
14962
|
+
image: path38.basename(`${base}.png`),
|
|
14629
14963
|
providerRules: result.generic.providerRules,
|
|
14630
14964
|
style: spec.styleId,
|
|
14631
14965
|
tileType: spec.tileType ?? null
|
|
@@ -14635,7 +14969,7 @@ async function runExport(args) {
|
|
|
14635
14969
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14636
14970
|
);
|
|
14637
14971
|
log(
|
|
14638
|
-
` ${
|
|
14972
|
+
` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14639
14973
|
);
|
|
14640
14974
|
}
|
|
14641
14975
|
}
|
|
@@ -14648,19 +14982,19 @@ function sheetFormat(args) {
|
|
|
14648
14982
|
}
|
|
14649
14983
|
|
|
14650
14984
|
// src/cli/commands/quality.ts
|
|
14651
|
-
import
|
|
14652
|
-
import { readFile as
|
|
14985
|
+
import path40 from "path";
|
|
14986
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
14653
14987
|
|
|
14654
14988
|
// src/pipeline/quality-regression.ts
|
|
14655
|
-
import
|
|
14989
|
+
import path39 from "path";
|
|
14656
14990
|
import { existsSync as existsSync29 } from "fs";
|
|
14657
|
-
import { readFile as
|
|
14991
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
14658
14992
|
import { z as z9 } from "zod";
|
|
14659
14993
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
14660
14994
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
14661
14995
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
14662
14996
|
var QualityPathSchema = z9.string().min(1).refine(
|
|
14663
|
-
(value) => !
|
|
14997
|
+
(value) => !path39.posix.isAbsolute(value) && !value.includes("\\"),
|
|
14664
14998
|
"expected a portable relative path"
|
|
14665
14999
|
);
|
|
14666
15000
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -14807,10 +15141,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
14807
15141
|
};
|
|
14808
15142
|
}
|
|
14809
15143
|
async function measureImageQuality(file) {
|
|
14810
|
-
const absolute =
|
|
15144
|
+
const absolute = path39.resolve(file);
|
|
14811
15145
|
let bytes;
|
|
14812
15146
|
try {
|
|
14813
|
-
bytes = await
|
|
15147
|
+
bytes = await readFile26(absolute);
|
|
14814
15148
|
} catch (error) {
|
|
14815
15149
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
14816
15150
|
}
|
|
@@ -14829,11 +15163,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
14829
15163
|
}
|
|
14830
15164
|
const ids = parsed.data.map((entry) => entry.id);
|
|
14831
15165
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
14832
|
-
const root =
|
|
15166
|
+
const root = path39.dirname(path39.resolve(inputsFilePath));
|
|
14833
15167
|
return parsed.data.map((entry) => ({
|
|
14834
15168
|
id: entry.id,
|
|
14835
|
-
file:
|
|
14836
|
-
...entry.record ? { record:
|
|
15169
|
+
file: path39.resolve(root, entry.path),
|
|
15170
|
+
...entry.record ? { record: path39.resolve(root, entry.record) } : {},
|
|
14837
15171
|
tolerances: QualityToleranceSchema.parse({
|
|
14838
15172
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
14839
15173
|
...entry.tolerances
|
|
@@ -14841,7 +15175,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
14841
15175
|
}));
|
|
14842
15176
|
}
|
|
14843
15177
|
function portableRelative2(from, to) {
|
|
14844
|
-
return
|
|
15178
|
+
return path39.relative(from, path39.resolve(to)).split(path39.sep).join("/") || ".";
|
|
14845
15179
|
}
|
|
14846
15180
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
14847
15181
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -14856,20 +15190,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
14856
15190
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
14857
15191
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
14858
15192
|
}
|
|
14859
|
-
const outputs = record.outputs.map((output) =>
|
|
14860
|
-
if (!outputs.includes(
|
|
15193
|
+
const outputs = record.outputs.map((output) => path39.resolve(path39.dirname(recordPath), output.path));
|
|
15194
|
+
if (!outputs.includes(path39.resolve(imagePath))) {
|
|
14861
15195
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
14862
15196
|
}
|
|
14863
15197
|
}
|
|
14864
15198
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
14865
15199
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
14866
|
-
const absolute =
|
|
15200
|
+
const absolute = path39.resolve(baselinePath);
|
|
14867
15201
|
for (const input of inputs) {
|
|
14868
|
-
if (
|
|
15202
|
+
if (path39.resolve(input.file) === absolute || input.record && path39.resolve(input.record) === absolute) {
|
|
14869
15203
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
14870
15204
|
}
|
|
14871
15205
|
}
|
|
14872
|
-
const root =
|
|
15206
|
+
const root = path39.dirname(absolute);
|
|
14873
15207
|
const cases = [];
|
|
14874
15208
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
14875
15209
|
const expected = await measureImageQuality(input.file);
|
|
@@ -14897,7 +15231,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
14897
15231
|
});
|
|
14898
15232
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
14899
15233
|
if (existsSync29(absolute) && !options.force) {
|
|
14900
|
-
const current = await
|
|
15234
|
+
const current = await readFile26(absolute);
|
|
14901
15235
|
if (!current.equals(data)) {
|
|
14902
15236
|
throw new OverwriteRefusedError(
|
|
14903
15237
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -14909,10 +15243,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
14909
15243
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
14910
15244
|
}
|
|
14911
15245
|
async function readQualityBaseline(baselinePath) {
|
|
14912
|
-
const absolute =
|
|
15246
|
+
const absolute = path39.resolve(baselinePath);
|
|
14913
15247
|
let raw;
|
|
14914
15248
|
try {
|
|
14915
|
-
raw = JSON.parse(await
|
|
15249
|
+
raw = JSON.parse(await readFile26(absolute, "utf8"));
|
|
14916
15250
|
} catch (error) {
|
|
14917
15251
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
14918
15252
|
}
|
|
@@ -14982,12 +15316,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
14982
15316
|
return { violations, warnings };
|
|
14983
15317
|
}
|
|
14984
15318
|
async function checkQualityBaseline(baselinePath) {
|
|
14985
|
-
const absolute =
|
|
15319
|
+
const absolute = path39.resolve(baselinePath);
|
|
14986
15320
|
const baseline = await readQualityBaseline(absolute);
|
|
14987
|
-
const root =
|
|
15321
|
+
const root = path39.dirname(absolute);
|
|
14988
15322
|
const cases = [];
|
|
14989
15323
|
for (const entry of baseline.cases) {
|
|
14990
|
-
const file =
|
|
15324
|
+
const file = path39.resolve(root, entry.file);
|
|
14991
15325
|
let actual = null;
|
|
14992
15326
|
const violations = [];
|
|
14993
15327
|
const warnings = [];
|
|
@@ -15000,7 +15334,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
15000
15334
|
violations.push(message4(error));
|
|
15001
15335
|
}
|
|
15002
15336
|
if (entry.record) {
|
|
15003
|
-
const recordPath =
|
|
15337
|
+
const recordPath = path39.resolve(root, entry.record.path);
|
|
15004
15338
|
try {
|
|
15005
15339
|
const recordHash = await sha256File(recordPath);
|
|
15006
15340
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -15042,15 +15376,15 @@ async function runQuality(args) {
|
|
|
15042
15376
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
15043
15377
|
let raw;
|
|
15044
15378
|
try {
|
|
15045
|
-
raw = JSON.parse(await
|
|
15379
|
+
raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
|
|
15046
15380
|
} catch (error) {
|
|
15047
15381
|
throw new Error(
|
|
15048
|
-
`Could not read quality inputs ${
|
|
15382
|
+
`Could not read quality inputs ${path40.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
15049
15383
|
{ cause: error }
|
|
15050
15384
|
);
|
|
15051
15385
|
}
|
|
15052
15386
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
15053
|
-
if (
|
|
15387
|
+
if (path40.resolve(args.inputs) === path40.resolve(args.out)) {
|
|
15054
15388
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
15055
15389
|
}
|
|
15056
15390
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -15063,7 +15397,7 @@ async function runQuality(args) {
|
|
|
15063
15397
|
}, null, 2));
|
|
15064
15398
|
} else {
|
|
15065
15399
|
log(
|
|
15066
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
15400
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path40.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
15067
15401
|
);
|
|
15068
15402
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
15069
15403
|
}
|
|
@@ -15089,12 +15423,12 @@ async function runQuality(args) {
|
|
|
15089
15423
|
}
|
|
15090
15424
|
|
|
15091
15425
|
// src/cli/commands/recipe.ts
|
|
15092
|
-
import
|
|
15426
|
+
import path42 from "path";
|
|
15093
15427
|
|
|
15094
15428
|
// src/recipes.ts
|
|
15095
|
-
import
|
|
15429
|
+
import path41 from "path";
|
|
15096
15430
|
import { existsSync as existsSync30 } from "fs";
|
|
15097
|
-
import { readdir as readdir3, readFile as
|
|
15431
|
+
import { readdir as readdir3, readFile as readFile28 } from "fs/promises";
|
|
15098
15432
|
import { fileURLToPath } from "url";
|
|
15099
15433
|
import { z as z10 } from "zod";
|
|
15100
15434
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -15103,7 +15437,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
15103
15437
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
15104
15438
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
15105
15439
|
var PortablePathSchema = z10.string().min(1).refine((value) => {
|
|
15106
|
-
if (value.includes("\\") ||
|
|
15440
|
+
if (value.includes("\\") || path41.posix.isAbsolute(value)) return false;
|
|
15107
15441
|
const parts = value.split("/");
|
|
15108
15442
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
15109
15443
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -15279,10 +15613,10 @@ function message5(error) {
|
|
|
15279
15613
|
return error instanceof Error ? error.message : String(error);
|
|
15280
15614
|
}
|
|
15281
15615
|
async function readRecipeFile(recipePath, bundled) {
|
|
15282
|
-
const absolute =
|
|
15616
|
+
const absolute = path41.resolve(recipePath);
|
|
15283
15617
|
let raw;
|
|
15284
15618
|
try {
|
|
15285
|
-
raw = JSON.parse(await
|
|
15619
|
+
raw = JSON.parse(await readFile28(absolute, "utf8"));
|
|
15286
15620
|
} catch (error) {
|
|
15287
15621
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
15288
15622
|
}
|
|
@@ -15293,13 +15627,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
15293
15627
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
15294
15628
|
);
|
|
15295
15629
|
}
|
|
15296
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
15630
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path41.dirname(absolute), bundled };
|
|
15297
15631
|
}
|
|
15298
15632
|
async function walkRecipeFiles(root) {
|
|
15299
15633
|
if (!existsSync30(root)) return [];
|
|
15300
15634
|
const out = [];
|
|
15301
15635
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
15302
|
-
const resolved =
|
|
15636
|
+
const resolved = path41.join(root, entry.name);
|
|
15303
15637
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
15304
15638
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
15305
15639
|
}
|
|
@@ -15310,7 +15644,7 @@ function bundledRecipeRoot() {
|
|
|
15310
15644
|
}
|
|
15311
15645
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
15312
15646
|
const recipes = await Promise.all(
|
|
15313
|
-
(await walkRecipeFiles(
|
|
15647
|
+
(await walkRecipeFiles(path41.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
15314
15648
|
);
|
|
15315
15649
|
return recipes.sort(
|
|
15316
15650
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -15333,9 +15667,9 @@ function parseSelector(selector) {
|
|
|
15333
15667
|
return { id: match[1], version: match[2] };
|
|
15334
15668
|
}
|
|
15335
15669
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
15336
|
-
const local =
|
|
15670
|
+
const local = path41.resolve(target);
|
|
15337
15671
|
if (existsSync30(local)) {
|
|
15338
|
-
const recipePath =
|
|
15672
|
+
const recipePath = path41.basename(local) === RECIPE_FILE ? local : path41.join(local, RECIPE_FILE);
|
|
15339
15673
|
return readRecipeFile(recipePath, false);
|
|
15340
15674
|
}
|
|
15341
15675
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -15349,7 +15683,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
15349
15683
|
return matches[0];
|
|
15350
15684
|
}
|
|
15351
15685
|
async function verifyFile(root, file) {
|
|
15352
|
-
const absolute =
|
|
15686
|
+
const absolute = path41.resolve(root, ...file.path.split("/"));
|
|
15353
15687
|
if (!existsSync30(absolute)) {
|
|
15354
15688
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
15355
15689
|
}
|
|
@@ -15365,7 +15699,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
15365
15699
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
15366
15700
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
15367
15701
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
15368
|
-
const modelRoot = options.modelRoot ?
|
|
15702
|
+
const modelRoot = options.modelRoot ? path41.resolve(options.modelRoot) : null;
|
|
15369
15703
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
15370
15704
|
...await verifyFile(modelRoot, model),
|
|
15371
15705
|
source: model.source,
|
|
@@ -15399,7 +15733,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
15399
15733
|
};
|
|
15400
15734
|
}
|
|
15401
15735
|
function renderStyle(style, destination, cwd) {
|
|
15402
|
-
const portableDir =
|
|
15736
|
+
const portableDir = path41.relative(cwd, destination).split(path41.sep).join("/") || ".";
|
|
15403
15737
|
const replace = (value) => {
|
|
15404
15738
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
15405
15739
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -15411,25 +15745,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
15411
15745
|
return replace(style);
|
|
15412
15746
|
}
|
|
15413
15747
|
async function installRecipe(target, options = {}) {
|
|
15414
|
-
const cwd =
|
|
15748
|
+
const cwd = path41.resolve(options.cwd ?? process.cwd());
|
|
15415
15749
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
15416
15750
|
const verification = await verifyRecipe(loaded.path);
|
|
15417
15751
|
if (!verification.ok) {
|
|
15418
15752
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
15419
15753
|
}
|
|
15420
|
-
const destination = options.out ?
|
|
15754
|
+
const destination = options.out ? path41.resolve(cwd, options.out) : path41.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
15421
15755
|
const sources = [
|
|
15422
15756
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
15423
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
15757
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path41.join(loaded.dir, ...file.path.split("/")) }))
|
|
15424
15758
|
];
|
|
15425
15759
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
15426
|
-
path:
|
|
15427
|
-
data: await
|
|
15760
|
+
path: path41.join(destination, ...file.path.split("/")),
|
|
15761
|
+
data: await readFile28(file.source)
|
|
15428
15762
|
})));
|
|
15429
15763
|
if (!options.force) {
|
|
15430
15764
|
for (const file of files) {
|
|
15431
15765
|
if (!existsSync30(file.path)) continue;
|
|
15432
|
-
const current = await
|
|
15766
|
+
const current = await readFile28(file.path);
|
|
15433
15767
|
if (!current.equals(file.data)) {
|
|
15434
15768
|
throw new OverwriteRefusedError(
|
|
15435
15769
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -15523,14 +15857,14 @@ async function runRecipe(args) {
|
|
|
15523
15857
|
}, null, 2));
|
|
15524
15858
|
} else {
|
|
15525
15859
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
15526
|
-
log(` ${
|
|
15860
|
+
log(` ${path42.relative(process.cwd(), result.destination) || "."}`);
|
|
15527
15861
|
log(`
|
|
15528
15862
|
Add this entry under your manifest's styles object:`);
|
|
15529
15863
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
15530
15864
|
if (result.recipe.models.length) {
|
|
15531
15865
|
log(`
|
|
15532
15866
|
Models are not downloaded automatically. Verify them with:`);
|
|
15533
|
-
log(` pixelkiln recipe verify ${
|
|
15867
|
+
log(` pixelkiln recipe verify ${path42.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
15534
15868
|
}
|
|
15535
15869
|
}
|
|
15536
15870
|
return;
|
|
@@ -15538,7 +15872,7 @@ async function runRecipe(args) {
|
|
|
15538
15872
|
}
|
|
15539
15873
|
|
|
15540
15874
|
// src/cli/commands/refine.ts
|
|
15541
|
-
import
|
|
15875
|
+
import path43 from "path";
|
|
15542
15876
|
async function runRefine(args) {
|
|
15543
15877
|
if (args.from || args.subcommand === "approve") {
|
|
15544
15878
|
if (!args.from) {
|
|
@@ -15568,12 +15902,12 @@ async function runRefine(args) {
|
|
|
15568
15902
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
15569
15903
|
);
|
|
15570
15904
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
15571
|
-
log(` wrote ${
|
|
15572
|
-
log(` quality record: ${
|
|
15905
|
+
log(` wrote ${path43.relative(process.cwd(), result3.output)}`);
|
|
15906
|
+
log(` quality record: ${path43.relative(process.cwd(), result3.record)}`);
|
|
15573
15907
|
log(`
|
|
15574
15908
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
15575
15909
|
log(
|
|
15576
|
-
` pixelkiln refine approve --from ${
|
|
15910
|
+
` pixelkiln refine approve --from ${path43.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
15577
15911
|
);
|
|
15578
15912
|
}
|
|
15579
15913
|
return;
|
|
@@ -15601,7 +15935,7 @@ async function runRefine(args) {
|
|
|
15601
15935
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
15602
15936
|
else {
|
|
15603
15937
|
log(
|
|
15604
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
15938
|
+
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()}`
|
|
15605
15939
|
);
|
|
15606
15940
|
}
|
|
15607
15941
|
return;
|
|
@@ -15610,7 +15944,7 @@ async function runRefine(args) {
|
|
|
15610
15944
|
if (args.json) {
|
|
15611
15945
|
log(JSON.stringify(result2, null, 2));
|
|
15612
15946
|
} else {
|
|
15613
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
15947
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path43.relative(process.cwd(), result2.output || result2.record)}`);
|
|
15614
15948
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
15615
15949
|
}
|
|
15616
15950
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -15656,7 +15990,7 @@ async function runRefine(args) {
|
|
|
15656
15990
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
15657
15991
|
if (item.state === "needs-approval") {
|
|
15658
15992
|
log(
|
|
15659
|
-
` pixelkiln refine approve --from ${
|
|
15993
|
+
` pixelkiln refine approve --from ${path43.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
15660
15994
|
);
|
|
15661
15995
|
}
|
|
15662
15996
|
}
|
|
@@ -15708,15 +16042,15 @@ async function runTools(args) {
|
|
|
15708
16042
|
}
|
|
15709
16043
|
|
|
15710
16044
|
// src/cli/commands/workspace.ts
|
|
15711
|
-
import
|
|
16045
|
+
import path44 from "path";
|
|
15712
16046
|
import { existsSync as existsSync31 } from "fs";
|
|
15713
16047
|
async function runWorkspace(args) {
|
|
15714
|
-
const workspacePath =
|
|
15715
|
-
const dir =
|
|
16048
|
+
const workspacePath = path44.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
16049
|
+
const dir = path44.dirname(workspacePath);
|
|
15716
16050
|
if (args.subcommand === "add") {
|
|
15717
|
-
const manifestPath =
|
|
16051
|
+
const manifestPath = path44.resolve(args.target);
|
|
15718
16052
|
const loadedTarget = await loadManifest(manifestPath);
|
|
15719
|
-
const lockPath = args.explicitLock ?
|
|
16053
|
+
const lockPath = args.explicitLock ? path44.resolve(args.explicitLock) : path44.join(path44.dirname(manifestPath), "pixelkiln.lock.json");
|
|
15720
16054
|
const ws = await loadWorkspace(workspacePath);
|
|
15721
16055
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
15722
16056
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -15736,7 +16070,7 @@ async function runWorkspace(args) {
|
|
|
15736
16070
|
...args.account ? { account: args.account } : {}
|
|
15737
16071
|
};
|
|
15738
16072
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
15739
|
-
log(` registered "${id}" in ${
|
|
16073
|
+
log(` registered "${id}" in ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15740
16074
|
log(` manifest: ${project.manifest}`);
|
|
15741
16075
|
log(` lock: ${project.lock}`);
|
|
15742
16076
|
if (!existsSync31(lockPath)) {
|
|
@@ -15748,7 +16082,7 @@ async function runWorkspace(args) {
|
|
|
15748
16082
|
}
|
|
15749
16083
|
if (args.subcommand === "remove") {
|
|
15750
16084
|
const ws = await loadWorkspace(workspacePath);
|
|
15751
|
-
const resolvedTarget =
|
|
16085
|
+
const resolvedTarget = path44.resolve(args.target);
|
|
15752
16086
|
const match = ws.projects.find(
|
|
15753
16087
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
15754
16088
|
);
|
|
@@ -15759,7 +16093,7 @@ async function runWorkspace(args) {
|
|
|
15759
16093
|
version: 1,
|
|
15760
16094
|
projects: ws.projects.filter((p) => p !== match)
|
|
15761
16095
|
});
|
|
15762
|
-
log(` removed "${match.id}" from ${
|
|
16096
|
+
log(` removed "${match.id}" from ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15763
16097
|
return;
|
|
15764
16098
|
}
|
|
15765
16099
|
if (args.subcommand === "list") {
|
|
@@ -15771,9 +16105,9 @@ async function runWorkspace(args) {
|
|
|
15771
16105
|
if (args.json) {
|
|
15772
16106
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
15773
16107
|
} else if (!ws.projects.length) {
|
|
15774
|
-
log(` no projects registered in ${
|
|
16108
|
+
log(` no projects registered in ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15775
16109
|
} else {
|
|
15776
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
16110
|
+
log(` ${ws.projects.length} project(s) in ${path44.relative(process.cwd(), workspacePath)}:`);
|
|
15777
16111
|
for (const p of ws.projects) {
|
|
15778
16112
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
15779
16113
|
}
|
|
@@ -15791,7 +16125,7 @@ async function runWorkspace(args) {
|
|
|
15791
16125
|
if (args.json) {
|
|
15792
16126
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
15793
16127
|
} else {
|
|
15794
|
-
log(` workspace: ${
|
|
16128
|
+
log(` workspace: ${path44.relative(process.cwd(), workspacePath)}`);
|
|
15795
16129
|
for (const p of report.projects) {
|
|
15796
16130
|
if (p.error) {
|
|
15797
16131
|
log(`
|
|
@@ -15846,7 +16180,7 @@ async function runVersion() {
|
|
|
15846
16180
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
15847
16181
|
const url = new URL(up, import.meta.url);
|
|
15848
16182
|
if (!existsSync32(url)) continue;
|
|
15849
|
-
const pkg = JSON.parse(await
|
|
16183
|
+
const pkg = JSON.parse(await readFile29(url, "utf8"));
|
|
15850
16184
|
if (pkg.name === "pixelkiln") {
|
|
15851
16185
|
log(`${pkg.name} ${pkg.version}`);
|
|
15852
16186
|
return;
|