pixelkiln 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +7 -0
- package/README.md +1 -1
- package/dist/cli.js +800 -589
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +42 -5
- package/dist/index.cjs +551 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +616 -71
- package/dist/index.d.ts +616 -71
- package/dist/index.js +538 -320
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/CLI.md +62 -2
- package/docs/LIBRARY.md +32 -0
- package/docs/MANIFEST.md +50 -2
- package/docs/QUALITY.md +16 -0
- package/docs/RECIPES.md +6 -2
- package/package.json +3 -1
- package/recipes/comfyui/pixel-art-xl-environment/1.0.0/pixelkiln.recipe.json +1 -1
- package/recipes/comfyui/pixel-art-xl-img2img/1.0.0/pixelkiln.recipe.json +1 -1
- package/schema/manifest.schema.json +8 -0
- package/schema/recipe.schema.json +4 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var EXIT_CODES = {
|
|
5
|
+
usage: 2,
|
|
6
|
+
project: 3,
|
|
7
|
+
provider: 4,
|
|
8
|
+
"refused-overwrite": 5,
|
|
9
|
+
budget: 6,
|
|
10
|
+
capability: 7
|
|
11
|
+
};
|
|
12
|
+
var PixelKilnError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
hint;
|
|
15
|
+
constructor(message6, code, opts = {}) {
|
|
16
|
+
super(message6, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
17
|
+
this.name = "PixelKilnError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
if (opts.hint) this.hint = opts.hint;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var UsageError = class extends PixelKilnError {
|
|
23
|
+
constructor(message6, opts) {
|
|
24
|
+
super(message6, "usage", opts);
|
|
25
|
+
this.name = "UsageError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var ProjectError = class extends PixelKilnError {
|
|
29
|
+
constructor(message6, opts) {
|
|
30
|
+
super(message6, "project", opts);
|
|
31
|
+
this.name = "ProjectError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var ProviderError = class extends PixelKilnError {
|
|
35
|
+
/** Registry id of the provider that failed. */
|
|
36
|
+
provider;
|
|
37
|
+
/** HTTP status when the failure was a response, otherwise undefined. */
|
|
38
|
+
status;
|
|
39
|
+
constructor(provider, message6, opts = {}) {
|
|
40
|
+
super(message6, "provider", opts);
|
|
41
|
+
this.name = "ProviderError";
|
|
42
|
+
this.provider = provider;
|
|
43
|
+
if (opts.status !== void 0) this.status = opts.status;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var OverwriteRefusedError = class extends PixelKilnError {
|
|
47
|
+
/** The file(s) left untouched. */
|
|
48
|
+
files;
|
|
49
|
+
constructor(message6, files, opts) {
|
|
50
|
+
super(message6, "refused-overwrite", opts);
|
|
51
|
+
this.name = "OverwriteRefusedError";
|
|
52
|
+
this.files = files;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var BudgetError = class extends PixelKilnError {
|
|
56
|
+
constructor(message6, opts) {
|
|
57
|
+
super(message6, "budget", opts);
|
|
58
|
+
this.name = "BudgetError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function errorCode(err) {
|
|
62
|
+
return err instanceof PixelKilnError ? err.code : void 0;
|
|
63
|
+
}
|
|
64
|
+
function exitCodeFor(err) {
|
|
65
|
+
const code = errorCode(err);
|
|
66
|
+
return code ? EXIT_CODES[code] : 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
3
69
|
// src/cli/main.ts
|
|
4
70
|
import { existsSync as existsSync31 } from "fs";
|
|
5
71
|
import { readFile as readFile27 } from "fs/promises";
|
|
@@ -103,7 +169,7 @@ var TOOLS = ["editor"];
|
|
|
103
169
|
function parseArgs(argv) {
|
|
104
170
|
const [command = "help"] = argv;
|
|
105
171
|
if (!COMMANDS.includes(command)) {
|
|
106
|
-
throw new
|
|
172
|
+
throw new UsageError(`Unknown command "${command}". Run \`pixelkiln help\` for the list.`);
|
|
107
173
|
}
|
|
108
174
|
let rest = argv.slice(1);
|
|
109
175
|
let subcommand;
|
|
@@ -111,10 +177,10 @@ function parseArgs(argv) {
|
|
|
111
177
|
if (command === "quality") {
|
|
112
178
|
subcommand = rest[0];
|
|
113
179
|
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
114
|
-
throw new
|
|
180
|
+
throw new UsageError(`quality needs a subcommand: ${QUALITY_SUBCOMMANDS.join(", ")}`);
|
|
115
181
|
}
|
|
116
182
|
if (!QUALITY_SUBCOMMANDS.includes(subcommand)) {
|
|
117
|
-
throw new
|
|
183
|
+
throw new UsageError(
|
|
118
184
|
`Unknown quality subcommand "${subcommand}". Known: ${QUALITY_SUBCOMMANDS.join(", ")}`
|
|
119
185
|
);
|
|
120
186
|
}
|
|
@@ -122,10 +188,10 @@ function parseArgs(argv) {
|
|
|
122
188
|
} else if (command === "recipe") {
|
|
123
189
|
subcommand = rest[0];
|
|
124
190
|
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
125
|
-
throw new
|
|
191
|
+
throw new UsageError(`recipe needs a subcommand: ${RECIPE_SUBCOMMANDS.join(", ")}`);
|
|
126
192
|
}
|
|
127
193
|
if (!RECIPE_SUBCOMMANDS.includes(subcommand)) {
|
|
128
|
-
throw new
|
|
194
|
+
throw new UsageError(
|
|
129
195
|
`Unknown recipe subcommand "${subcommand}". Known: ${RECIPE_SUBCOMMANDS.join(", ")}`
|
|
130
196
|
);
|
|
131
197
|
}
|
|
@@ -133,17 +199,17 @@ function parseArgs(argv) {
|
|
|
133
199
|
if (subcommand !== "list") {
|
|
134
200
|
target = rest[0];
|
|
135
201
|
if (target === void 0 || target.startsWith("-")) {
|
|
136
|
-
throw new
|
|
202
|
+
throw new UsageError(`recipe ${subcommand} needs a recipe id, selector, or path.`);
|
|
137
203
|
}
|
|
138
204
|
rest = rest.slice(1);
|
|
139
205
|
}
|
|
140
206
|
} else if (command === "workspace") {
|
|
141
207
|
subcommand = rest[0];
|
|
142
208
|
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
143
|
-
throw new
|
|
209
|
+
throw new UsageError(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
|
|
144
210
|
}
|
|
145
211
|
if (!WORKSPACE_SUBCOMMANDS.includes(subcommand)) {
|
|
146
|
-
throw new
|
|
212
|
+
throw new UsageError(
|
|
147
213
|
`Unknown workspace subcommand "${subcommand}". Known: ${WORKSPACE_SUBCOMMANDS.join(", ")}`
|
|
148
214
|
);
|
|
149
215
|
}
|
|
@@ -151,7 +217,7 @@ function parseArgs(argv) {
|
|
|
151
217
|
if (subcommand === "add" || subcommand === "remove") {
|
|
152
218
|
target = rest[0];
|
|
153
219
|
if (target === void 0 || target.startsWith("-")) {
|
|
154
|
-
throw new
|
|
220
|
+
throw new UsageError(
|
|
155
221
|
subcommand === "add" ? "workspace add needs a manifest path." : "workspace remove needs a project id or manifest path."
|
|
156
222
|
);
|
|
157
223
|
}
|
|
@@ -160,29 +226,29 @@ function parseArgs(argv) {
|
|
|
160
226
|
} else if (command === "edit") {
|
|
161
227
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "start" : rest[0];
|
|
162
228
|
if (!EDIT_SUBCOMMANDS.includes(subcommand)) {
|
|
163
|
-
throw new
|
|
229
|
+
throw new UsageError(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
|
|
164
230
|
}
|
|
165
231
|
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
166
232
|
} else if (command === "tools") {
|
|
167
233
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "status" : rest[0];
|
|
168
234
|
if (!TOOLS_SUBCOMMANDS.includes(subcommand)) {
|
|
169
|
-
throw new
|
|
235
|
+
throw new UsageError(`Unknown tools subcommand "${subcommand}". Known: ${TOOLS_SUBCOMMANDS.join(", ")}`);
|
|
170
236
|
}
|
|
171
237
|
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
172
238
|
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
173
239
|
if (subcommand === "install" && target === void 0) {
|
|
174
|
-
throw new
|
|
240
|
+
throw new UsageError(`tools install needs a tool name: ${TOOLS.join(", ")}`);
|
|
175
241
|
}
|
|
176
242
|
if (target !== void 0) {
|
|
177
243
|
if (!TOOLS.includes(target)) {
|
|
178
|
-
throw new
|
|
244
|
+
throw new UsageError(`Unknown tool "${target}". Known: ${TOOLS.join(", ")}`);
|
|
179
245
|
}
|
|
180
246
|
rest = rest.slice(1);
|
|
181
247
|
}
|
|
182
248
|
} else if (command === "refine") {
|
|
183
249
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
184
250
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
185
|
-
throw new
|
|
251
|
+
throw new UsageError(
|
|
186
252
|
`Unknown refine subcommand "${subcommand}". Known: ${REFINE_SUBCOMMANDS.join(", ")}`
|
|
187
253
|
);
|
|
188
254
|
}
|
|
@@ -191,19 +257,19 @@ function parseArgs(argv) {
|
|
|
191
257
|
for (let i = 0; i < rest.length; i++) {
|
|
192
258
|
const token = rest[i];
|
|
193
259
|
if (!token.startsWith("-")) {
|
|
194
|
-
throw new
|
|
260
|
+
throw new UsageError(`Unexpected argument "${token}". Options must be passed with a named flag.`);
|
|
195
261
|
}
|
|
196
262
|
if (BOOL_FLAGS.includes(token)) continue;
|
|
197
263
|
if (VALUE_FLAGS.includes(token)) {
|
|
198
264
|
const value = rest[i + 1];
|
|
199
265
|
const looksLikeFlag = value !== void 0 && value.startsWith("-") && !Number.isFinite(Number(value));
|
|
200
266
|
if (value === void 0 || looksLikeFlag) {
|
|
201
|
-
throw new
|
|
267
|
+
throw new UsageError(`${token} needs a value.`);
|
|
202
268
|
}
|
|
203
269
|
i++;
|
|
204
270
|
continue;
|
|
205
271
|
}
|
|
206
|
-
throw new
|
|
272
|
+
throw new UsageError(
|
|
207
273
|
`Unknown flag "${token}". Known flags: ${[...VALUE_FLAGS, ...BOOL_FLAGS].join(", ")}`
|
|
208
274
|
);
|
|
209
275
|
}
|
|
@@ -227,7 +293,7 @@ function parseArgs(argv) {
|
|
|
227
293
|
if (rawColumns !== void 0) {
|
|
228
294
|
columns = Number(rawColumns);
|
|
229
295
|
if (!Number.isInteger(columns) || columns < 1 || columns > 1024) {
|
|
230
|
-
throw new
|
|
296
|
+
throw new UsageError(`--columns must be a whole number between 1 and 1024, got "${rawColumns}"`);
|
|
231
297
|
}
|
|
232
298
|
}
|
|
233
299
|
const rawPort = get("--port");
|
|
@@ -235,7 +301,7 @@ function parseArgs(argv) {
|
|
|
235
301
|
if (rawPort !== void 0) {
|
|
236
302
|
port = Number(rawPort);
|
|
237
303
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
238
|
-
throw new
|
|
304
|
+
throw new UsageError(`--port must be a whole number between 1 and 65535, got "${rawPort}"`);
|
|
239
305
|
}
|
|
240
306
|
}
|
|
241
307
|
let budget;
|
|
@@ -248,30 +314,30 @@ function parseArgs(argv) {
|
|
|
248
314
|
const rawAmount = rawBudget.slice(separator + 1).trim();
|
|
249
315
|
const amount2 = Number(rawAmount);
|
|
250
316
|
if (!providerId || /[=\s]/.test(providerId) || !Number.isFinite(amount2) || amount2 < 0) {
|
|
251
|
-
throw new
|
|
317
|
+
throw new UsageError(
|
|
252
318
|
`--budget must be a non-negative number or provider=number, got "${rawBudget}".`
|
|
253
319
|
);
|
|
254
320
|
}
|
|
255
321
|
if (Object.hasOwn(providerBudgets, providerId)) {
|
|
256
|
-
throw new
|
|
322
|
+
throw new UsageError(`--budget repeats provider "${providerId}".`);
|
|
257
323
|
}
|
|
258
324
|
providerBudgets[providerId] = amount2;
|
|
259
325
|
continue;
|
|
260
326
|
}
|
|
261
327
|
const amount = Number(rawBudget);
|
|
262
328
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
263
|
-
throw new
|
|
329
|
+
throw new UsageError(`--budget must be a non-negative number, got "${rawBudget}".`);
|
|
264
330
|
}
|
|
265
|
-
if (budget !== void 0) throw new
|
|
331
|
+
if (budget !== void 0) throw new UsageError("Only one unkeyed --budget may be passed.");
|
|
266
332
|
budget = amount;
|
|
267
333
|
}
|
|
268
334
|
if (budget !== void 0 && Object.keys(providerBudgets).length) {
|
|
269
|
-
throw new
|
|
335
|
+
throw new UsageError("Do not mix an unkeyed --budget with provider-keyed budgets.");
|
|
270
336
|
}
|
|
271
337
|
const manifest = get("--manifest") ?? "pixelkiln.manifest.json";
|
|
272
338
|
const rawFormat = get("--format");
|
|
273
339
|
if (rawFormat && rawFormat !== "generic" && rawFormat !== "tiled" && rawFormat !== "godot") {
|
|
274
|
-
throw new
|
|
340
|
+
throw new UsageError(`--format must be generic, tiled, or godot, got "${rawFormat}"`);
|
|
275
341
|
}
|
|
276
342
|
const numberOption = (flag, opts) => {
|
|
277
343
|
const raw = get(flag);
|
|
@@ -279,13 +345,13 @@ function parseArgs(argv) {
|
|
|
279
345
|
const value = Number(raw);
|
|
280
346
|
if (!Number.isFinite(value) || value < opts.min || opts.max !== void 0 && value > opts.max || opts.integer && !Number.isInteger(value)) {
|
|
281
347
|
const range = opts.max === void 0 ? `at least ${opts.min}` : `${opts.min} to ${opts.max}`;
|
|
282
|
-
throw new
|
|
348
|
+
throw new UsageError(`${flag} must be ${opts.integer ? "a whole number " : "a number "}${range}, got "${raw}"`);
|
|
283
349
|
}
|
|
284
350
|
return value;
|
|
285
351
|
};
|
|
286
352
|
const rawGridConfidence = get("--min-grid-confidence");
|
|
287
353
|
if (rawGridConfidence !== void 0 && rawGridConfidence !== "low" && rawGridConfidence !== "medium" && rawGridConfidence !== "high") {
|
|
288
|
-
throw new
|
|
354
|
+
throw new UsageError(
|
|
289
355
|
`--min-grid-confidence must be low, medium, or high, got "${rawGridConfidence}"`
|
|
290
356
|
);
|
|
291
357
|
}
|
|
@@ -505,10 +571,11 @@ function measureBalanceChange(before, after) {
|
|
|
505
571
|
};
|
|
506
572
|
}
|
|
507
573
|
var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
|
|
508
|
-
var UnsupportedCapabilityError = class extends
|
|
574
|
+
var UnsupportedCapabilityError = class extends PixelKilnError {
|
|
509
575
|
constructor(providerId, capability) {
|
|
510
576
|
super(
|
|
511
|
-
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend
|
|
577
|
+
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`,
|
|
578
|
+
"capability"
|
|
512
579
|
);
|
|
513
580
|
this.name = "UnsupportedCapabilityError";
|
|
514
581
|
}
|
|
@@ -539,8 +606,8 @@ function formatCost(unit, amount) {
|
|
|
539
606
|
}
|
|
540
607
|
|
|
541
608
|
// src/pipeline/plan.ts
|
|
542
|
-
import { existsSync as
|
|
543
|
-
import
|
|
609
|
+
import { existsSync as existsSync5 } from "fs";
|
|
610
|
+
import path8 from "path";
|
|
544
611
|
|
|
545
612
|
// src/hash.ts
|
|
546
613
|
import { createHash } from "crypto";
|
|
@@ -776,6 +843,18 @@ var StyleObjectSchema = z.object({
|
|
|
776
843
|
* chocolate bar.
|
|
777
844
|
*/
|
|
778
845
|
palette: z.array(HexColorSchema).default([]),
|
|
846
|
+
/**
|
|
847
|
+
* Snap every visible pixel of the downloaded art to the nearest `palette`
|
|
848
|
+
* colour, without dithering. The palette is sent to providers that take
|
|
849
|
+
* one, and most honour it most of the time; this makes it a guarantee
|
|
850
|
+
* for every generator, including the ones with no palette parameter.
|
|
851
|
+
* Applied by `fetch` as the file is written and recorded on the lock
|
|
852
|
+
* entry, so a restore reproduces the same bytes. Turning it on or off
|
|
853
|
+
* does not change the spec hash: `plan` reports the entry as recoverable
|
|
854
|
+
* and the next `fetch` or `restore` re-applies it from the art already
|
|
855
|
+
* on disk or in the cache, spending nothing.
|
|
856
|
+
*/
|
|
857
|
+
enforcePalette: z.boolean().default(false),
|
|
779
858
|
/** Fail-closed native-grid, palette, and human-approval policy for derived art. */
|
|
780
859
|
quality: QualityProfileSchema.optional(),
|
|
781
860
|
/**
|
|
@@ -817,6 +896,13 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
817
896
|
path: ["quality"]
|
|
818
897
|
});
|
|
819
898
|
}
|
|
899
|
+
if (style.enforcePalette && style.palette.length < 2) {
|
|
900
|
+
ctx.addIssue({
|
|
901
|
+
code: z.ZodIssueCode.custom,
|
|
902
|
+
message: "enforcePalette needs a palette of at least two colours to snap to",
|
|
903
|
+
path: ["enforcePalette"]
|
|
904
|
+
});
|
|
905
|
+
}
|
|
820
906
|
});
|
|
821
907
|
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
822
908
|
extends: z.string().min(1),
|
|
@@ -939,7 +1025,21 @@ var LockOutputSchema = z.object({
|
|
|
939
1025
|
path: z.string(),
|
|
940
1026
|
sha256: z.string(),
|
|
941
1027
|
role: z.string().optional(),
|
|
942
|
-
mediaType: MediaTypeSchema.optional()
|
|
1028
|
+
mediaType: MediaTypeSchema.optional(),
|
|
1029
|
+
/**
|
|
1030
|
+
* Hash of the provider's bytes before post-processing changed them, when
|
|
1031
|
+
* it did. The content cache keeps both, so the post-processing can be
|
|
1032
|
+
* undone or redone without contacting the provider.
|
|
1033
|
+
*/
|
|
1034
|
+
raw: z.string().optional()
|
|
1035
|
+
});
|
|
1036
|
+
var PostprocessSchema = z.object({
|
|
1037
|
+
palette: z.object({
|
|
1038
|
+
/** Canonical lowercase `#rrggbb` values, in manifest order. */
|
|
1039
|
+
colors: z.array(z.string()).min(2).max(256),
|
|
1040
|
+
dither: z.literal("none"),
|
|
1041
|
+
distance: z.literal("redmean")
|
|
1042
|
+
}).optional()
|
|
943
1043
|
});
|
|
944
1044
|
var LockSourceSchema = z.object({
|
|
945
1045
|
url: z.string(),
|
|
@@ -965,6 +1065,7 @@ var LockHistoryEntrySchema = z.object({
|
|
|
965
1065
|
cost: z.number().finite().nonnegative().default(0),
|
|
966
1066
|
costUnit: z.string().min(1).default("generations"),
|
|
967
1067
|
provider: z.string().default("pixellab"),
|
|
1068
|
+
postprocess: PostprocessSchema.optional(),
|
|
968
1069
|
/** When the replacement was submitted. */
|
|
969
1070
|
retiredAt: z.string()
|
|
970
1071
|
});
|
|
@@ -1036,14 +1137,7 @@ var LockEntrySchema = z.object({
|
|
|
1036
1137
|
* `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
|
|
1037
1138
|
* consumer can find the one it wants without pattern-matching on paths.
|
|
1038
1139
|
*/
|
|
1039
|
-
outputs: z.array(
|
|
1040
|
-
z.object({
|
|
1041
|
-
path: z.string(),
|
|
1042
|
-
sha256: z.string(),
|
|
1043
|
-
role: z.string().optional(),
|
|
1044
|
-
mediaType: MediaTypeSchema.optional()
|
|
1045
|
-
})
|
|
1046
|
-
).default([]),
|
|
1140
|
+
outputs: z.array(LockOutputSchema).default([]),
|
|
1047
1141
|
/**
|
|
1048
1142
|
* Provider-owned data retained for downstream consumers, namespaced by
|
|
1049
1143
|
* provider id. PixelLab connectable sets keep their exact `tileRules` here
|
|
@@ -1059,7 +1153,9 @@ var LockEntrySchema = z.object({
|
|
|
1059
1153
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
1060
1154
|
provider: z.string().default("pixellab"),
|
|
1061
1155
|
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
1062
|
-
history: z.array(LockHistoryEntrySchema).optional()
|
|
1156
|
+
history: z.array(LockHistoryEntrySchema).optional(),
|
|
1157
|
+
/** Post-processing applied to the recorded outputs; absent when the bytes are the provider's. */
|
|
1158
|
+
postprocess: PostprocessSchema.optional()
|
|
1063
1159
|
});
|
|
1064
1160
|
var LockSchema = z.object({
|
|
1065
1161
|
version: z.literal(2),
|
|
@@ -1086,9 +1182,10 @@ async function loadLock(lockPath) {
|
|
|
1086
1182
|
try {
|
|
1087
1183
|
return parseLock(JSON.parse(await readFile(lockPath, "utf8")));
|
|
1088
1184
|
} catch (err) {
|
|
1089
|
-
throw new
|
|
1185
|
+
throw new ProjectError(
|
|
1090
1186
|
`Lockfile at ${lockPath} is malformed:
|
|
1091
|
-
${err instanceof Error ? err.message : String(err)}
|
|
1187
|
+
${err instanceof Error ? err.message : String(err)}`,
|
|
1188
|
+
{ cause: err }
|
|
1092
1189
|
);
|
|
1093
1190
|
}
|
|
1094
1191
|
}
|
|
@@ -1122,8 +1219,9 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
1122
1219
|
try {
|
|
1123
1220
|
disk = parseLock(JSON.parse(await readFile(file, "utf8")));
|
|
1124
1221
|
} catch (err) {
|
|
1125
|
-
throw new
|
|
1126
|
-
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}
|
|
1222
|
+
throw new ProjectError(
|
|
1223
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1224
|
+
{ cause: err }
|
|
1127
1225
|
);
|
|
1128
1226
|
}
|
|
1129
1227
|
}
|
|
@@ -1807,13 +1905,13 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
1807
1905
|
}
|
|
1808
1906
|
|
|
1809
1907
|
// src/pipeline/quality-profile.ts
|
|
1810
|
-
import { existsSync as
|
|
1811
|
-
import
|
|
1908
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1909
|
+
import path6 from "path";
|
|
1812
1910
|
|
|
1813
1911
|
// src/pipeline/refine.ts
|
|
1814
|
-
import
|
|
1912
|
+
import path5 from "path";
|
|
1815
1913
|
import { execFile } from "child_process";
|
|
1816
|
-
import { mkdtemp, readFile as
|
|
1914
|
+
import { mkdtemp, readFile as readFile3, rm as rm3 } from "fs/promises";
|
|
1817
1915
|
import { tmpdir } from "os";
|
|
1818
1916
|
import { promisify } from "util";
|
|
1819
1917
|
|
|
@@ -2051,10 +2149,11 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
2051
2149
|
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
2052
2150
|
}
|
|
2053
2151
|
if (conflicts.length) {
|
|
2054
|
-
throw new
|
|
2152
|
+
throw new OverwriteRefusedError(
|
|
2055
2153
|
`Refusing to overwrite modified or unowned artifact file(s):
|
|
2056
2154
|
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
2057
|
-
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle
|
|
2155
|
+
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`,
|
|
2156
|
+
conflicts
|
|
2058
2157
|
);
|
|
2059
2158
|
}
|
|
2060
2159
|
}
|
|
@@ -2273,10 +2372,19 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2273
2372
|
};
|
|
2274
2373
|
}
|
|
2275
2374
|
|
|
2276
|
-
// src/
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2375
|
+
// src/palette.ts
|
|
2376
|
+
function normalizePalette(values) {
|
|
2377
|
+
const colors = /* @__PURE__ */ new Map();
|
|
2378
|
+
for (const value of values) {
|
|
2379
|
+
const rgb = parseHex(value);
|
|
2380
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
2381
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
2382
|
+
}
|
|
2383
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
2384
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
2385
|
+
}
|
|
2386
|
+
return [...colors.values()];
|
|
2387
|
+
}
|
|
2280
2388
|
function colorDistance(a, b) {
|
|
2281
2389
|
const rmean = (a.r + b.r) / 2;
|
|
2282
2390
|
const dr = a.r - b.r;
|
|
@@ -2286,146 +2394,45 @@ function colorDistance(a, b) {
|
|
|
2286
2394
|
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
2287
2395
|
);
|
|
2288
2396
|
}
|
|
2289
|
-
function
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const totals = /* @__PURE__ */ new Map();
|
|
2303
|
-
for (const palette of palettes) {
|
|
2304
|
-
for (const color of palette) {
|
|
2305
|
-
const key = `${color.r},${color.g},${color.b}`;
|
|
2306
|
-
const existing = totals.get(key);
|
|
2307
|
-
if (existing) existing.weight += color.weight;
|
|
2308
|
-
else totals.set(key, { ...color });
|
|
2397
|
+
function quantize(png, palette) {
|
|
2398
|
+
const colors = normalizePalette(palette);
|
|
2399
|
+
const pixels = Buffer.from(png.pixels);
|
|
2400
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
2401
|
+
let changed = 0;
|
|
2402
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
2403
|
+
const alpha = pixels[i + 3];
|
|
2404
|
+
if (alpha === 0) {
|
|
2405
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
2406
|
+
pixels[i] = 0;
|
|
2407
|
+
pixels[i + 1] = 0;
|
|
2408
|
+
pixels[i + 2] = 0;
|
|
2409
|
+
continue;
|
|
2309
2410
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
2324
|
-
if (!existsSync3(output.absolutePath)) {
|
|
2325
|
-
missing.push(output.id);
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
try {
|
|
2329
|
-
const png = decodePng(await readFile3(output.absolutePath));
|
|
2330
|
-
const palette = extractPalette(png, 12);
|
|
2331
|
-
assets.push({
|
|
2332
|
-
assetId: spec.assetId,
|
|
2333
|
-
...output.role ? { outputRole: output.role } : {},
|
|
2334
|
-
id: output.id,
|
|
2335
|
-
file: output.absolutePath,
|
|
2336
|
-
width: png.width,
|
|
2337
|
-
height: png.height,
|
|
2338
|
-
paletteDistance: 0,
|
|
2339
|
-
// filled once the reference is known
|
|
2340
|
-
transparency: transparencyRatio(png),
|
|
2341
|
-
colorCount: countColors(png),
|
|
2342
|
-
palette
|
|
2343
|
-
});
|
|
2344
|
-
} catch (err) {
|
|
2345
|
-
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2411
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
2412
|
+
let nearest = nearestOf.get(key);
|
|
2413
|
+
if (!nearest) {
|
|
2414
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
2415
|
+
nearest = colors[0];
|
|
2416
|
+
let distance = colorDistance(pixel, nearest);
|
|
2417
|
+
for (let j = 1; j < colors.length; j++) {
|
|
2418
|
+
const candidate = colors[j];
|
|
2419
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
2420
|
+
if (candidateDistance < distance) {
|
|
2421
|
+
nearest = candidate;
|
|
2422
|
+
distance = candidateDistance;
|
|
2423
|
+
}
|
|
2346
2424
|
}
|
|
2425
|
+
nearestOf.set(key, nearest);
|
|
2347
2426
|
}
|
|
2427
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
2428
|
+
pixels[i] = nearest.r;
|
|
2429
|
+
pixels[i + 1] = nearest.g;
|
|
2430
|
+
pixels[i + 2] = nearest.b;
|
|
2348
2431
|
}
|
|
2349
|
-
|
|
2350
|
-
let referenceFromStyleImages = false;
|
|
2351
|
-
const refPalettes = [];
|
|
2352
|
-
for (const img of style.styleImages) {
|
|
2353
|
-
const abs = path5.resolve(loaded.root, img.path);
|
|
2354
|
-
if (!existsSync3(abs)) continue;
|
|
2355
|
-
try {
|
|
2356
|
-
refPalettes.push(extractPalette(decodePng(await readFile3(abs)), 12));
|
|
2357
|
-
} catch {
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
if (refPalettes.length) {
|
|
2361
|
-
reference = mergePalettes(refPalettes);
|
|
2362
|
-
referenceFromStyleImages = true;
|
|
2363
|
-
for (const asset of assets) {
|
|
2364
|
-
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
2365
|
-
}
|
|
2366
|
-
} else {
|
|
2367
|
-
reference = mergePalettes(assets.map((a) => a.palette));
|
|
2368
|
-
for (const asset of assets) {
|
|
2369
|
-
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
2370
|
-
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
2374
|
-
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
2375
|
-
}
|
|
2376
|
-
function countColors(png) {
|
|
2377
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2378
|
-
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
2379
|
-
if (png.pixels[i + 3] < 128) continue;
|
|
2380
|
-
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
2381
|
-
}
|
|
2382
|
-
return seen.size;
|
|
2383
|
-
}
|
|
2384
|
-
function outliers(audit, sigma = 1.5) {
|
|
2385
|
-
const n = audit.assets.length;
|
|
2386
|
-
if (n < 3) return [];
|
|
2387
|
-
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
2388
|
-
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
2389
|
-
const sd = Math.sqrt(variance);
|
|
2390
|
-
if (sd === 0) return [];
|
|
2391
|
-
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
2392
|
-
}
|
|
2393
|
-
function evaluateAudit(audit, thresholds = {}) {
|
|
2394
|
-
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
2395
|
-
const relative = outliers(audit, resolved.sigma);
|
|
2396
|
-
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
2397
|
-
const violations = [];
|
|
2398
|
-
for (const asset of audit.assets) {
|
|
2399
|
-
const reasons = [];
|
|
2400
|
-
if (relativeIds.has(asset.id)) {
|
|
2401
|
-
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
2402
|
-
}
|
|
2403
|
-
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
2404
|
-
reasons.push(
|
|
2405
|
-
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
2406
|
-
);
|
|
2407
|
-
}
|
|
2408
|
-
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
2409
|
-
reasons.push(
|
|
2410
|
-
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
2411
|
-
);
|
|
2412
|
-
}
|
|
2413
|
-
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
2414
|
-
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
2415
|
-
}
|
|
2416
|
-
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
2417
|
-
}
|
|
2418
|
-
return {
|
|
2419
|
-
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
2420
|
-
thresholds: resolved,
|
|
2421
|
-
outliers: [...relativeIds],
|
|
2422
|
-
violations,
|
|
2423
|
-
missing: [...audit.missing],
|
|
2424
|
-
unreadable: [...audit.unreadable]
|
|
2425
|
-
};
|
|
2432
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
2426
2433
|
}
|
|
2427
|
-
function
|
|
2428
|
-
return
|
|
2434
|
+
function quantizeToPalette(png, palette) {
|
|
2435
|
+
return quantize(png, palette).png;
|
|
2429
2436
|
}
|
|
2430
2437
|
|
|
2431
2438
|
// src/pipeline/refine.ts
|
|
@@ -2512,48 +2519,12 @@ function parseDetection(raw) {
|
|
|
2512
2519
|
confidence: confidenceFor(consensus)
|
|
2513
2520
|
};
|
|
2514
2521
|
}
|
|
2515
|
-
function normalizePalette(values) {
|
|
2516
|
-
const colors = /* @__PURE__ */ new Map();
|
|
2517
|
-
for (const value of values) {
|
|
2518
|
-
const rgb = parseHex(value);
|
|
2519
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
2520
|
-
colors.set(hex2, { hex: hex2, ...rgb });
|
|
2521
|
-
}
|
|
2522
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
2523
|
-
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
2524
|
-
}
|
|
2525
|
-
return [...colors.values()];
|
|
2526
|
-
}
|
|
2527
2522
|
function normalizeRefinementPalette(values) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
const pixels = Buffer.from(png.pixels);
|
|
2533
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
2534
|
-
const alpha = pixels[i + 3];
|
|
2535
|
-
if (alpha === 0) {
|
|
2536
|
-
pixels[i] = 0;
|
|
2537
|
-
pixels[i + 1] = 0;
|
|
2538
|
-
pixels[i + 2] = 0;
|
|
2539
|
-
continue;
|
|
2540
|
-
}
|
|
2541
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
2542
|
-
let nearest = colors[0];
|
|
2543
|
-
let distance = colorDistance(pixel, nearest);
|
|
2544
|
-
for (let j = 1; j < colors.length; j++) {
|
|
2545
|
-
const candidate = colors[j];
|
|
2546
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
2547
|
-
if (candidateDistance < distance) {
|
|
2548
|
-
nearest = candidate;
|
|
2549
|
-
distance = candidateDistance;
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
pixels[i] = nearest.r;
|
|
2553
|
-
pixels[i + 1] = nearest.g;
|
|
2554
|
-
pixels[i + 2] = nearest.b;
|
|
2523
|
+
try {
|
|
2524
|
+
return normalizePalette(values).map((color) => color.hex);
|
|
2525
|
+
} catch (err) {
|
|
2526
|
+
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${new Set(values.map((v) => v.toLowerCase())).size}.`, { cause: err });
|
|
2555
2527
|
}
|
|
2556
|
-
return { width: png.width, height: png.height, pixels };
|
|
2557
2528
|
}
|
|
2558
2529
|
function colorCount(png) {
|
|
2559
2530
|
const colors = /* @__PURE__ */ new Set();
|
|
@@ -2663,16 +2634,16 @@ function sameFrameGrid(first, next) {
|
|
|
2663
2634
|
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;
|
|
2664
2635
|
}
|
|
2665
2636
|
async function refineAsset(options) {
|
|
2666
|
-
const source =
|
|
2667
|
-
const output =
|
|
2637
|
+
const source = path5.resolve(options.source);
|
|
2638
|
+
const output = path5.resolve(options.output);
|
|
2668
2639
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
2669
|
-
const record =
|
|
2640
|
+
const record = path5.resolve(qualityRecordPath(output));
|
|
2670
2641
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
2671
2642
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
2672
2643
|
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.");
|
|
2673
2644
|
let sourcePng;
|
|
2674
2645
|
try {
|
|
2675
|
-
sourcePng = decodePng(await
|
|
2646
|
+
sourcePng = decodePng(await readFile3(source));
|
|
2676
2647
|
} catch (error) {
|
|
2677
2648
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
2678
2649
|
}
|
|
@@ -2689,8 +2660,8 @@ async function refineAsset(options) {
|
|
|
2689
2660
|
);
|
|
2690
2661
|
}
|
|
2691
2662
|
}
|
|
2692
|
-
const temp = await mkdtemp(
|
|
2693
|
-
const recovered =
|
|
2663
|
+
const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-"));
|
|
2664
|
+
const recovered = path5.join(temp, "native.png");
|
|
2694
2665
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
2695
2666
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
2696
2667
|
try {
|
|
@@ -2711,7 +2682,7 @@ async function refineAsset(options) {
|
|
|
2711
2682
|
);
|
|
2712
2683
|
}
|
|
2713
2684
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
2714
|
-
const native = decodePng(await
|
|
2685
|
+
const native = decodePng(await readFile3(recovered));
|
|
2715
2686
|
const quantized = quantizeToPalette(native, palette);
|
|
2716
2687
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
2717
2688
|
const audit = auditRefinement(
|
|
@@ -2775,18 +2746,18 @@ async function refineFrameSet(options) {
|
|
|
2775
2746
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
2776
2747
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
2777
2748
|
}
|
|
2778
|
-
const output =
|
|
2779
|
-
const record =
|
|
2749
|
+
const output = path5.resolve(options.output);
|
|
2750
|
+
const record = path5.resolve(qualityRecordPath(output));
|
|
2780
2751
|
const palette = normalizeRefinementPalette(options.palette);
|
|
2781
2752
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
2782
2753
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
2783
2754
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
2784
2755
|
}
|
|
2785
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
2756
|
+
const sourceSet = new Set(options.sources.map((source) => path5.resolve(source.path)));
|
|
2786
2757
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
2787
2758
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
2788
2759
|
}
|
|
2789
|
-
const temp = await mkdtemp(
|
|
2760
|
+
const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
2790
2761
|
try {
|
|
2791
2762
|
const frames = [];
|
|
2792
2763
|
const outputs = [];
|
|
@@ -2794,8 +2765,8 @@ async function refineFrameSet(options) {
|
|
|
2794
2765
|
const sourcePaths = [];
|
|
2795
2766
|
let firstOptions = null;
|
|
2796
2767
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
2797
|
-
const source =
|
|
2798
|
-
const temporaryOutput =
|
|
2768
|
+
const source = path5.resolve(sourceEntry.path);
|
|
2769
|
+
const temporaryOutput = path5.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
2799
2770
|
const refined = await refineAsset({
|
|
2800
2771
|
source,
|
|
2801
2772
|
output: temporaryOutput,
|
|
@@ -2827,7 +2798,7 @@ async function refineFrameSet(options) {
|
|
|
2827
2798
|
});
|
|
2828
2799
|
outputs.push({
|
|
2829
2800
|
path: finalOutputs[index],
|
|
2830
|
-
data: await
|
|
2801
|
+
data: await readFile3(temporaryOutput)
|
|
2831
2802
|
});
|
|
2832
2803
|
}
|
|
2833
2804
|
const first = frames[0];
|
|
@@ -2870,7 +2841,7 @@ async function refineFrameSet(options) {
|
|
|
2870
2841
|
);
|
|
2871
2842
|
return {
|
|
2872
2843
|
sources: sourcePaths,
|
|
2873
|
-
outputs: outputs.map((item) =>
|
|
2844
|
+
outputs: outputs.map((item) => path5.resolve(item.path)),
|
|
2874
2845
|
record,
|
|
2875
2846
|
frames,
|
|
2876
2847
|
palette,
|
|
@@ -2881,12 +2852,12 @@ async function refineFrameSet(options) {
|
|
|
2881
2852
|
}
|
|
2882
2853
|
}
|
|
2883
2854
|
async function checkQualityRecord(recordPath) {
|
|
2884
|
-
const record =
|
|
2855
|
+
const record = path5.resolve(recordPath);
|
|
2885
2856
|
const manifest = await readArtifactBundleManifest(record);
|
|
2886
2857
|
const options = parseRecordOptions(manifest, record);
|
|
2887
2858
|
const verification = await verifyArtifactBundle(record);
|
|
2888
|
-
const sources = manifest.sources.map((source2) =>
|
|
2889
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
2859
|
+
const sources = manifest.sources.map((source2) => path5.resolve(path5.dirname(record), source2.path));
|
|
2860
|
+
const outputs = manifest.outputs.map((output2) => path5.resolve(path5.dirname(record), output2.path));
|
|
2890
2861
|
const source = sources[0] ?? "";
|
|
2891
2862
|
const output = outputs[0] ?? "";
|
|
2892
2863
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -2919,7 +2890,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
2919
2890
|
async function approveQualityRecord(recordPath, approval) {
|
|
2920
2891
|
const reviewer = approval.reviewer.trim();
|
|
2921
2892
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
2922
|
-
const record =
|
|
2893
|
+
const record = path5.resolve(recordPath);
|
|
2923
2894
|
const manifest = await readArtifactBundleManifest(record);
|
|
2924
2895
|
const options = parseRecordOptions(manifest, record);
|
|
2925
2896
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -2940,14 +2911,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
2940
2911
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
2941
2912
|
);
|
|
2942
2913
|
}
|
|
2943
|
-
const root =
|
|
2914
|
+
const root = path5.dirname(record);
|
|
2944
2915
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
2945
|
-
const outputPath =
|
|
2946
|
-
return { path: outputPath, data: await
|
|
2916
|
+
const outputPath = path5.resolve(root, entry.path);
|
|
2917
|
+
return { path: outputPath, data: await readFile3(outputPath) };
|
|
2947
2918
|
}));
|
|
2948
2919
|
const sources = manifest.sources.map((source) => ({
|
|
2949
2920
|
...source,
|
|
2950
|
-
path:
|
|
2921
|
+
path: path5.resolve(root, source.path)
|
|
2951
2922
|
}));
|
|
2952
2923
|
const approved = {
|
|
2953
2924
|
...options,
|
|
@@ -2979,8 +2950,8 @@ async function qualitySource(spec, lock) {
|
|
|
2979
2950
|
if (spec.generator === "frames") {
|
|
2980
2951
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
2981
2952
|
}
|
|
2982
|
-
const source =
|
|
2983
|
-
return
|
|
2953
|
+
const source = path6.resolve(spec.root, spec.source);
|
|
2954
|
+
return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
2984
2955
|
}
|
|
2985
2956
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
2986
2957
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -3009,7 +2980,7 @@ async function qualitySource(spec, lock) {
|
|
|
3009
2980
|
}
|
|
3010
2981
|
if (role) roles.add(role);
|
|
3011
2982
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
3012
|
-
if (!
|
|
2983
|
+
if (!existsSync3(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
3013
2984
|
if (await sha256File(source) !== output.sha256) {
|
|
3014
2985
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
3015
2986
|
}
|
|
@@ -3028,7 +2999,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
3028
2999
|
}
|
|
3029
3000
|
const sourcePaths = source.items.map((item) => item.path);
|
|
3030
3001
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
3031
|
-
if (!
|
|
3002
|
+
if (!existsSync3(record)) {
|
|
3032
3003
|
return {
|
|
3033
3004
|
key,
|
|
3034
3005
|
state: "needs-refinement",
|
|
@@ -3046,7 +3017,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
3046
3017
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
3047
3018
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
3048
3019
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
3049
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
3020
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path6.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path6.resolve(item))) || JSON.stringify(check.outputs.map((item) => path6.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path6.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);
|
|
3050
3021
|
if (contractChanged) {
|
|
3051
3022
|
return {
|
|
3052
3023
|
key,
|
|
@@ -3208,9 +3179,38 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
3208
3179
|
return sources;
|
|
3209
3180
|
}
|
|
3210
3181
|
|
|
3182
|
+
// src/pipeline/postprocess.ts
|
|
3183
|
+
function postprocessFor(spec) {
|
|
3184
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
3185
|
+
return {
|
|
3186
|
+
palette: {
|
|
3187
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
3188
|
+
dither: "none",
|
|
3189
|
+
distance: "redmean"
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
function samePostprocess(a, b) {
|
|
3194
|
+
const pa = a?.palette;
|
|
3195
|
+
const pb = b?.palette;
|
|
3196
|
+
if (!pa && !pb) return true;
|
|
3197
|
+
if (!pa || !pb) return false;
|
|
3198
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
3199
|
+
}
|
|
3200
|
+
function postprocessCurrent(entry, spec) {
|
|
3201
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
3202
|
+
}
|
|
3203
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
3204
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
3205
|
+
const png = decodePng(bytes);
|
|
3206
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
3207
|
+
if (!changed) return { bytes, changed: false };
|
|
3208
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
3211
|
// src/pipeline/revision.ts
|
|
3212
|
-
import { existsSync as
|
|
3213
|
-
import
|
|
3212
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3213
|
+
import path7 from "path";
|
|
3214
3214
|
async function inspectRevisionReadiness(spec, lock) {
|
|
3215
3215
|
if (!spec.revision) return null;
|
|
3216
3216
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -3225,14 +3225,14 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
3225
3225
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
3226
3226
|
};
|
|
3227
3227
|
}
|
|
3228
|
-
if (!revision.sourceSha256 || !
|
|
3228
|
+
if (!revision.sourceSha256 || !existsSync4(revision.sourceFile)) {
|
|
3229
3229
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
3230
3230
|
}
|
|
3231
3231
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
3232
3232
|
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
3233
3233
|
}
|
|
3234
3234
|
if (revision.maskFile) {
|
|
3235
|
-
if (!revision.maskSha256 || !
|
|
3235
|
+
if (!revision.maskSha256 || !existsSync4(revision.maskFile)) {
|
|
3236
3236
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
3237
3237
|
}
|
|
3238
3238
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -3262,8 +3262,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3262
3262
|
};
|
|
3263
3263
|
}
|
|
3264
3264
|
if (spec.source) {
|
|
3265
|
-
const source =
|
|
3266
|
-
return
|
|
3265
|
+
const source = path7.resolve(spec.root, spec.source);
|
|
3266
|
+
return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
3267
3267
|
}
|
|
3268
3268
|
const entry = lock.entries[key];
|
|
3269
3269
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -3277,7 +3277,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
3277
3277
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
3278
3278
|
const output = entry.outputs[index];
|
|
3279
3279
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
3280
|
-
if (!
|
|
3280
|
+
if (!existsSync4(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
3281
3281
|
if (await sha256File(file) !== output.sha256) {
|
|
3282
3282
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
3283
3283
|
}
|
|
@@ -3306,7 +3306,7 @@ function resumeActions(specs, lock) {
|
|
|
3306
3306
|
const entry = lock.entries[key];
|
|
3307
3307
|
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
3308
3308
|
if (entry.submissionComplete === false) continue;
|
|
3309
|
-
const command = resumeCommandForStatus(entry.status);
|
|
3309
|
+
const command = resumeCommandForStatus(entry.status) ?? (entry.status === "downloaded" && entry.outputs.length && !postprocessCurrent(entry, spec) ? "fetch" : null);
|
|
3310
3310
|
if (!command) continue;
|
|
3311
3311
|
if (command === "poll" && !entry.jobId) continue;
|
|
3312
3312
|
if (command === "pick" && !entry.reviewObjectId) continue;
|
|
@@ -3332,7 +3332,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3332
3332
|
state = "missing";
|
|
3333
3333
|
reason = "--force";
|
|
3334
3334
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
3335
|
-
if (
|
|
3335
|
+
if (existsSync5(path8.resolve(spec.root, spec.source))) {
|
|
3336
3336
|
state = "ok";
|
|
3337
3337
|
reason = `placed from ${spec.source}; not generated`;
|
|
3338
3338
|
} else {
|
|
@@ -3340,7 +3340,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3340
3340
|
reason = `declared source is missing: ${spec.source}`;
|
|
3341
3341
|
}
|
|
3342
3342
|
} else if (!entry) {
|
|
3343
|
-
if (
|
|
3343
|
+
if (existsSync5(spec.outFile)) {
|
|
3344
3344
|
state = "untracked";
|
|
3345
3345
|
reason = "file on disk, no provenance recorded";
|
|
3346
3346
|
} else {
|
|
@@ -3372,12 +3372,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3372
3372
|
} else if (entry.status !== "downloaded") {
|
|
3373
3373
|
state = "in-flight";
|
|
3374
3374
|
reason = `awaiting ${entry.status}`;
|
|
3375
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !
|
|
3375
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
|
|
3376
3376
|
state = "orphaned";
|
|
3377
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !
|
|
3377
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
3378
3378
|
} else if (await anyOutputModified(entry, spec)) {
|
|
3379
3379
|
state = "orphaned";
|
|
3380
3380
|
reason = "output modified since download";
|
|
3381
|
+
} else if (!postprocessCurrent(entry, spec)) {
|
|
3382
|
+
state = "recoverable";
|
|
3383
|
+
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)";
|
|
3381
3384
|
} else {
|
|
3382
3385
|
state = "ok";
|
|
3383
3386
|
reason = "up to date";
|
|
@@ -3537,12 +3540,12 @@ async function confirm(question, auto) {
|
|
|
3537
3540
|
}
|
|
3538
3541
|
|
|
3539
3542
|
// src/cli/commands/account.ts
|
|
3540
|
-
import
|
|
3541
|
-
import { existsSync as
|
|
3543
|
+
import path20 from "path";
|
|
3544
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3542
3545
|
|
|
3543
3546
|
// src/env.ts
|
|
3544
|
-
import { readFileSync, existsSync as
|
|
3545
|
-
import
|
|
3547
|
+
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
3548
|
+
import path9 from "path";
|
|
3546
3549
|
function loadEnvFiles(dir) {
|
|
3547
3550
|
const loaded = [];
|
|
3548
3551
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -3565,8 +3568,8 @@ function readEnvFiles(dir) {
|
|
|
3565
3568
|
function envFiles(dir) {
|
|
3566
3569
|
const out = [];
|
|
3567
3570
|
for (const name of [".env.local", ".env"]) {
|
|
3568
|
-
const file =
|
|
3569
|
-
if (!
|
|
3571
|
+
const file = path9.join(dir, name);
|
|
3572
|
+
if (!existsSync6(file)) continue;
|
|
3570
3573
|
try {
|
|
3571
3574
|
out.push([file, parseEnv(readFileSync(file, "utf8"))]);
|
|
3572
3575
|
} catch {
|
|
@@ -3595,8 +3598,8 @@ function parseEnv(contents) {
|
|
|
3595
3598
|
|
|
3596
3599
|
// src/providers/comfyui.ts
|
|
3597
3600
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3598
|
-
import { readFile as
|
|
3599
|
-
import
|
|
3601
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3602
|
+
import path10 from "path";
|
|
3600
3603
|
|
|
3601
3604
|
// src/http.ts
|
|
3602
3605
|
var MAX_RETRIES = 4;
|
|
@@ -3706,8 +3709,10 @@ var ComfyUIClient = class {
|
|
|
3706
3709
|
value = null;
|
|
3707
3710
|
}
|
|
3708
3711
|
if (!response.ok) {
|
|
3709
|
-
throw new
|
|
3710
|
-
|
|
3712
|
+
throw new ProviderError(
|
|
3713
|
+
"comfyui",
|
|
3714
|
+
`ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : ""),
|
|
3715
|
+
{ status: response.status }
|
|
3711
3716
|
);
|
|
3712
3717
|
}
|
|
3713
3718
|
if (!isObject(value) || typeof value.name !== "string" || !value.name) {
|
|
@@ -3730,7 +3735,7 @@ var ComfyUIClient = class {
|
|
|
3730
3735
|
let target = source;
|
|
3731
3736
|
if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
|
|
3732
3737
|
const response = await this.request(target);
|
|
3733
|
-
if (!response.ok) throw new
|
|
3738
|
+
if (!response.ok) throw new ProviderError("comfyui", `ComfyUI download failed (${response.status})`, { status: response.status });
|
|
3734
3739
|
return Buffer.from(await response.arrayBuffer());
|
|
3735
3740
|
}
|
|
3736
3741
|
endpoint(route) {
|
|
@@ -3755,8 +3760,10 @@ var ComfyUIClient = class {
|
|
|
3755
3760
|
}
|
|
3756
3761
|
if (!response.ok) {
|
|
3757
3762
|
const detail = apiError(value);
|
|
3758
|
-
throw new
|
|
3759
|
-
|
|
3763
|
+
throw new ProviderError(
|
|
3764
|
+
"comfyui",
|
|
3765
|
+
`ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : ""),
|
|
3766
|
+
{ status: response.status }
|
|
3760
3767
|
);
|
|
3761
3768
|
}
|
|
3762
3769
|
return value;
|
|
@@ -3779,10 +3786,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3779
3786
|
}
|
|
3780
3787
|
async resolveOptions(value, context) {
|
|
3781
3788
|
const options = parseOptions(value);
|
|
3782
|
-
const workflowPath =
|
|
3789
|
+
const workflowPath = path10.resolve(context.root, options.workflowFile);
|
|
3783
3790
|
let raw;
|
|
3784
3791
|
try {
|
|
3785
|
-
raw = await
|
|
3792
|
+
raw = await readFile4(workflowPath, "utf8");
|
|
3786
3793
|
} catch (err) {
|
|
3787
3794
|
throw new Error(
|
|
3788
3795
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3834,10 +3841,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3834
3841
|
if (typeof item !== "string") {
|
|
3835
3842
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
3836
3843
|
}
|
|
3837
|
-
const file =
|
|
3844
|
+
const file = path10.resolve(context.root, item);
|
|
3838
3845
|
let bytes;
|
|
3839
3846
|
try {
|
|
3840
|
-
bytes = await
|
|
3847
|
+
bytes = await readFile4(file);
|
|
3841
3848
|
} catch (error) {
|
|
3842
3849
|
throw new Error(
|
|
3843
3850
|
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -3969,7 +3976,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3969
3976
|
setBinding(workflow2, binding, input);
|
|
3970
3977
|
return;
|
|
3971
3978
|
}
|
|
3972
|
-
const bytes = await
|
|
3979
|
+
const bytes = await readFile4(input.path);
|
|
3973
3980
|
if (sha256(bytes) !== input.sha256) {
|
|
3974
3981
|
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
3975
3982
|
}
|
|
@@ -4036,7 +4043,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4036
4043
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
4037
4044
|
throw new Error("ComfyUI revision source is not ready");
|
|
4038
4045
|
}
|
|
4039
|
-
const sourceBytes = await
|
|
4046
|
+
const sourceBytes = await readFile4(spec.revision.sourceFile);
|
|
4040
4047
|
if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
|
|
4041
4048
|
throw new Error("ComfyUI revision source changed before upload");
|
|
4042
4049
|
}
|
|
@@ -4050,7 +4057,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4050
4057
|
if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
|
|
4051
4058
|
throw new Error("ComfyUI revision mask is not ready");
|
|
4052
4059
|
}
|
|
4053
|
-
const maskBytes = await
|
|
4060
|
+
const maskBytes = await readFile4(spec.revision.maskFile);
|
|
4054
4061
|
if (sha256(maskBytes) !== spec.revision.maskSha256) {
|
|
4055
4062
|
throw new Error("ComfyUI revision mask changed before upload");
|
|
4056
4063
|
}
|
|
@@ -4521,10 +4528,10 @@ function canonical2(value) {
|
|
|
4521
4528
|
}
|
|
4522
4529
|
|
|
4523
4530
|
// src/providers/pixellab.ts
|
|
4524
|
-
import { mkdirSync, existsSync as
|
|
4531
|
+
import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4525
4532
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4526
4533
|
import os from "os";
|
|
4527
|
-
import
|
|
4534
|
+
import path11 from "path";
|
|
4528
4535
|
|
|
4529
4536
|
// src/client.ts
|
|
4530
4537
|
import { z as z2 } from "zod";
|
|
@@ -4592,14 +4599,12 @@ function validateResponse(schema, raw, operation) {
|
|
|
4592
4599
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
4593
4600
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
4594
4601
|
}
|
|
4595
|
-
var PixelLabError = class extends
|
|
4602
|
+
var PixelLabError = class extends ProviderError {
|
|
4596
4603
|
constructor(message6, status, body) {
|
|
4597
|
-
super(message6);
|
|
4598
|
-
this.status = status;
|
|
4604
|
+
super("pixellab", message6, { status });
|
|
4599
4605
|
this.body = body;
|
|
4600
4606
|
this.name = "PixelLabError";
|
|
4601
4607
|
}
|
|
4602
|
-
status;
|
|
4603
4608
|
body;
|
|
4604
4609
|
};
|
|
4605
4610
|
var PixelLabClient = class {
|
|
@@ -4878,7 +4883,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4878
4883
|
* filename, polling is an existence check, and downloading is a file read.
|
|
4879
4884
|
*/
|
|
4880
4885
|
static cacheDir() {
|
|
4881
|
-
const dir =
|
|
4886
|
+
const dir = path11.join(os.tmpdir(), "pixelkiln-pixflux");
|
|
4882
4887
|
mkdirSync(dir, { recursive: true });
|
|
4883
4888
|
return dir;
|
|
4884
4889
|
}
|
|
@@ -4947,7 +4952,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4947
4952
|
seed: spec.seed
|
|
4948
4953
|
});
|
|
4949
4954
|
const jobId = randomUUID3();
|
|
4950
|
-
writeFileSync(
|
|
4955
|
+
writeFileSync(path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
4951
4956
|
return { jobId };
|
|
4952
4957
|
}
|
|
4953
4958
|
if (spec.generator === "tiles") {
|
|
@@ -4988,8 +4993,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4988
4993
|
}
|
|
4989
4994
|
async poll(jobId, generator, context) {
|
|
4990
4995
|
if (generator === "pixflux") {
|
|
4991
|
-
const file =
|
|
4992
|
-
if (
|
|
4996
|
+
const file = path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
4997
|
+
if (existsSync7(file)) {
|
|
4993
4998
|
const sourceUrl = `file://${file}`;
|
|
4994
4999
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
4995
5000
|
}
|
|
@@ -5180,7 +5185,7 @@ var RetroDiffusionClient = class {
|
|
|
5180
5185
|
/** Result URLs are signed; no token header, and the same retry policy as the API. */
|
|
5181
5186
|
async download(url) {
|
|
5182
5187
|
const response = await this.request(url);
|
|
5183
|
-
if (!response.ok) throw new
|
|
5188
|
+
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
5184
5189
|
return Buffer.from(await response.arrayBuffer());
|
|
5185
5190
|
}
|
|
5186
5191
|
async call(path43, init = {}) {
|
|
@@ -5203,8 +5208,10 @@ var RetroDiffusionClient = class {
|
|
|
5203
5208
|
if (!response.ok) {
|
|
5204
5209
|
const retry = response.headers.get("retry-after");
|
|
5205
5210
|
const detail = retroError(value);
|
|
5206
|
-
throw new
|
|
5207
|
-
|
|
5211
|
+
throw new ProviderError(
|
|
5212
|
+
"retrodiffusion",
|
|
5213
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
5214
|
+
{ status: response.status }
|
|
5208
5215
|
);
|
|
5209
5216
|
}
|
|
5210
5217
|
return value;
|
|
@@ -5596,7 +5603,7 @@ var ScenarioClient = class {
|
|
|
5596
5603
|
throw new Error("Scenario downloads must use HTTP or HTTPS");
|
|
5597
5604
|
}
|
|
5598
5605
|
const response = await this.request(parsed);
|
|
5599
|
-
if (!response.ok) throw new
|
|
5606
|
+
if (!response.ok) throw new ProviderError("scenario", `Scenario download failed (${response.status})`, { status: response.status });
|
|
5600
5607
|
const declared = Number(response.headers.get("content-length"));
|
|
5601
5608
|
if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
|
|
5602
5609
|
throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
@@ -5643,8 +5650,10 @@ var ScenarioClient = class {
|
|
|
5643
5650
|
if (!response.ok) {
|
|
5644
5651
|
const retry = response.headers.get("retry-after");
|
|
5645
5652
|
const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
|
|
5646
|
-
throw new
|
|
5647
|
-
|
|
5653
|
+
throw new ProviderError(
|
|
5654
|
+
"scenario",
|
|
5655
|
+
`Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
5656
|
+
{ status: response.status }
|
|
5648
5657
|
);
|
|
5649
5658
|
}
|
|
5650
5659
|
return value;
|
|
@@ -6038,23 +6047,23 @@ registerProvider({
|
|
|
6038
6047
|
});
|
|
6039
6048
|
|
|
6040
6049
|
// src/manifest.ts
|
|
6041
|
-
import { readFile as
|
|
6042
|
-
import { existsSync as
|
|
6043
|
-
import
|
|
6050
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6051
|
+
import { existsSync as existsSync8 } from "fs";
|
|
6052
|
+
import path12 from "path";
|
|
6044
6053
|
async function loadManifest(manifestPath) {
|
|
6045
|
-
const abs =
|
|
6046
|
-
if (!
|
|
6047
|
-
const input = ManifestInputSchema.safeParse(JSON.parse(await
|
|
6054
|
+
const abs = path12.resolve(manifestPath);
|
|
6055
|
+
if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
6056
|
+
const input = ManifestInputSchema.safeParse(JSON.parse(await readFile5(abs, "utf8")));
|
|
6048
6057
|
if (!input.success) {
|
|
6049
6058
|
const issues = formatManifestIssues(input.error.issues);
|
|
6050
|
-
throw new
|
|
6059
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6051
6060
|
${issues}`);
|
|
6052
6061
|
}
|
|
6053
6062
|
let styles;
|
|
6054
6063
|
try {
|
|
6055
6064
|
styles = resolveStyleInheritance(input.data.styles);
|
|
6056
6065
|
} catch (error) {
|
|
6057
|
-
throw new
|
|
6066
|
+
throw new ProjectError(
|
|
6058
6067
|
`Manifest at ${abs} is invalid:
|
|
6059
6068
|
${error instanceof Error ? error.message : String(error)}`
|
|
6060
6069
|
);
|
|
@@ -6062,7 +6071,7 @@ ${issues}`);
|
|
|
6062
6071
|
const parsed = ManifestSchema.safeParse({ ...input.data, styles });
|
|
6063
6072
|
if (!parsed.success) {
|
|
6064
6073
|
const issues = formatManifestIssues(parsed.error.issues);
|
|
6065
|
-
throw new
|
|
6074
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6066
6075
|
${issues}`);
|
|
6067
6076
|
}
|
|
6068
6077
|
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
@@ -6112,10 +6121,10 @@ ${issues}`);
|
|
|
6112
6121
|
};
|
|
6113
6122
|
for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
|
|
6114
6123
|
if (unknownReferences.length) {
|
|
6115
|
-
throw new
|
|
6124
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
6116
6125
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
6117
6126
|
}
|
|
6118
|
-
return { manifest: parsed.data, root:
|
|
6127
|
+
return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
|
|
6119
6128
|
}
|
|
6120
6129
|
function formatManifestIssues(issues) {
|
|
6121
6130
|
return issues.flatMap((issue) => {
|
|
@@ -6191,14 +6200,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6191
6200
|
);
|
|
6192
6201
|
for (const unknownStyle of filter?.styles ?? []) {
|
|
6193
6202
|
if (!manifest.styles[unknownStyle]) {
|
|
6194
|
-
throw new
|
|
6203
|
+
throw new UsageError(
|
|
6195
6204
|
`Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
|
|
6196
6205
|
);
|
|
6197
6206
|
}
|
|
6198
6207
|
}
|
|
6199
6208
|
for (const unknownAsset of filter?.assets ?? []) {
|
|
6200
6209
|
if (!manifest.assets[unknownAsset]) {
|
|
6201
|
-
throw new
|
|
6210
|
+
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
6202
6211
|
}
|
|
6203
6212
|
}
|
|
6204
6213
|
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
@@ -6212,11 +6221,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6212
6221
|
}
|
|
6213
6222
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
6214
6223
|
async function loadStyleImage(rel) {
|
|
6215
|
-
const abs =
|
|
6224
|
+
const abs = path12.resolve(root, rel);
|
|
6216
6225
|
let hit = styleImageCache.get(abs);
|
|
6217
6226
|
if (!hit) {
|
|
6218
|
-
if (!
|
|
6219
|
-
const buf = await
|
|
6227
|
+
if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
6228
|
+
const buf = await readFile5(abs);
|
|
6220
6229
|
const metadata = imageMetadata(buf);
|
|
6221
6230
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
6222
6231
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -6242,7 +6251,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6242
6251
|
styleImageDimensions.push(loadedImage);
|
|
6243
6252
|
}
|
|
6244
6253
|
const resolvedImages = style.styleImages.map((image) => {
|
|
6245
|
-
const hit = styleImageCache.get(
|
|
6254
|
+
const hit = styleImageCache.get(path12.resolve(root, image.path));
|
|
6246
6255
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
6247
6256
|
});
|
|
6248
6257
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -6287,9 +6296,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6287
6296
|
}
|
|
6288
6297
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
6289
6298
|
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
6290
|
-
const relFile = asset.file ??
|
|
6291
|
-
const outFile =
|
|
6292
|
-
const qualityOutFile = style.quality ?
|
|
6299
|
+
const relFile = asset.file ?? path12.join(asset.category ?? "", `${assetId}.png`);
|
|
6300
|
+
const outFile = path12.resolve(root, style.outDir, relFile);
|
|
6301
|
+
const qualityOutFile = style.quality ? path12.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
6293
6302
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
6294
6303
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
6295
6304
|
const base = {
|
|
@@ -6310,6 +6319,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6310
6319
|
detail: style.detail,
|
|
6311
6320
|
seed: style.seed,
|
|
6312
6321
|
palette: style.palette,
|
|
6322
|
+
enforcePalette: style.enforcePalette,
|
|
6313
6323
|
noBackground: style.noBackground,
|
|
6314
6324
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
6315
6325
|
tileType: generator === "tiles" ? style.tileType : void 0,
|
|
@@ -6340,7 +6350,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6340
6350
|
minGridConfidence: style.quality.minGridConfidence,
|
|
6341
6351
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
6342
6352
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
6343
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
6353
|
+
...style.quality.fixerPython ? { fixerPython: path12.resolve(root, style.quality.fixerPython) } : {},
|
|
6344
6354
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
6345
6355
|
}
|
|
6346
6356
|
} : {},
|
|
@@ -6369,9 +6379,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6369
6379
|
);
|
|
6370
6380
|
}
|
|
6371
6381
|
const sourceSpec = await finalize(asset.revision.from);
|
|
6372
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
6382
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path12.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
6373
6383
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
6374
|
-
const maskFile = asset.revision.mask ?
|
|
6384
|
+
const maskFile = asset.revision.mask ? path12.resolve(root, asset.revision.mask) : void 0;
|
|
6375
6385
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
6376
6386
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
6377
6387
|
throw new Error(
|
|
@@ -6452,12 +6462,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
6452
6462
|
return specs;
|
|
6453
6463
|
}
|
|
6454
6464
|
function pngPath(file) {
|
|
6455
|
-
const extension =
|
|
6465
|
+
const extension = path12.extname(file);
|
|
6456
6466
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
6457
6467
|
}
|
|
6458
6468
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
6459
|
-
if (!
|
|
6460
|
-
const bytes = await
|
|
6469
|
+
if (!existsSync8(file)) return null;
|
|
6470
|
+
const bytes = await readFile5(file);
|
|
6461
6471
|
const metadata = imageMetadata(bytes);
|
|
6462
6472
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
6463
6473
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -6470,8 +6480,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
6470
6480
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
6471
6481
|
const out = [];
|
|
6472
6482
|
for (const img of style.styleImages) {
|
|
6473
|
-
const file =
|
|
6474
|
-
const buf = await
|
|
6483
|
+
const file = path12.resolve(loaded.root, img.path);
|
|
6484
|
+
const buf = await readFile5(file);
|
|
6475
6485
|
const metadata = imageMetadata(buf);
|
|
6476
6486
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
6477
6487
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -6511,9 +6521,9 @@ function imageMetadata(buf) {
|
|
|
6511
6521
|
}
|
|
6512
6522
|
|
|
6513
6523
|
// src/pipeline/fetch.ts
|
|
6514
|
-
import { existsSync as
|
|
6515
|
-
import { mkdir as mkdir3, readFile as
|
|
6516
|
-
import
|
|
6524
|
+
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
|
|
6525
|
+
import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6526
|
+
import path13 from "path";
|
|
6517
6527
|
|
|
6518
6528
|
// src/source-url.ts
|
|
6519
6529
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -6559,18 +6569,24 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6559
6569
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
6560
6570
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6561
6571
|
normalizeLockOutputPaths(lock, specs);
|
|
6562
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
6572
|
+
const cacheDir = opts.cacheDir === false ? null : path13.resolve(opts.cacheDir ?? path13.join(path13.dirname(lockPath), ".pixelkiln", "cache"));
|
|
6563
6573
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
6564
6574
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
6565
6575
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
6566
6576
|
if (e.status !== "downloaded") return false;
|
|
6567
6577
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
6568
|
-
if (!opts.repair) return false;
|
|
6569
6578
|
const spec = specByKey.get(key);
|
|
6579
|
+
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
6580
|
+
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
6581
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
6582
|
+
return existsSync9(file) && sha256(readFileSync3(file)) === output.sha256;
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
if (!opts.repair) return false;
|
|
6570
6586
|
if (e.outputs.length === 0 || !spec) return true;
|
|
6571
6587
|
return e.outputs.some((output) => {
|
|
6572
6588
|
const file = resolveOutputPath(output.path, spec.root);
|
|
6573
|
-
if (!
|
|
6589
|
+
if (!existsSync9(file)) return true;
|
|
6574
6590
|
const current = sha256(readFileSync3(file));
|
|
6575
6591
|
if (current === output.sha256) return false;
|
|
6576
6592
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
@@ -6605,6 +6621,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6605
6621
|
}
|
|
6606
6622
|
const outputs = [];
|
|
6607
6623
|
let wrote = false;
|
|
6624
|
+
const wanted = postprocessFor(spec);
|
|
6625
|
+
const reapply = entry.status === "downloaded" && !postprocessCurrent(entry, spec);
|
|
6608
6626
|
try {
|
|
6609
6627
|
for (let index = 0; index < sources.length; index++) {
|
|
6610
6628
|
const source = sources[index];
|
|
@@ -6612,24 +6630,28 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6612
6630
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
6613
6631
|
);
|
|
6614
6632
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
6633
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
6634
|
+
const recordedRaw = recorded?.raw ?? recorded?.sha256;
|
|
6615
6635
|
let fresh = null;
|
|
6616
6636
|
if (opts.refresh) {
|
|
6617
6637
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
6618
6638
|
fresh = await provider.download(source.url);
|
|
6619
|
-
if (recorded && sha256(fresh) ===
|
|
6620
|
-
log2(` current ${
|
|
6639
|
+
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
6640
|
+
log2(` current ${path13.relative(process.cwd(), target)}`);
|
|
6621
6641
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
6622
6642
|
continue;
|
|
6623
6643
|
}
|
|
6624
6644
|
}
|
|
6625
|
-
|
|
6645
|
+
let raw = null;
|
|
6646
|
+
let final = null;
|
|
6647
|
+
if (existsSync9(target)) {
|
|
6626
6648
|
const currentHash = await sha256File(target);
|
|
6627
6649
|
if (recorded && currentHash === recorded.sha256) {
|
|
6628
|
-
if (!fresh) {
|
|
6650
|
+
if (!fresh && !reapply) {
|
|
6629
6651
|
if (cacheDir) {
|
|
6630
6652
|
await cacheMedia(
|
|
6631
6653
|
cacheDir,
|
|
6632
|
-
await
|
|
6654
|
+
await readFile6(target),
|
|
6633
6655
|
recorded.mediaType ?? MediaType.PNG,
|
|
6634
6656
|
recorded.sha256
|
|
6635
6657
|
);
|
|
@@ -6637,7 +6659,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6637
6659
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
6638
6660
|
continue;
|
|
6639
6661
|
}
|
|
6640
|
-
|
|
6662
|
+
if (reapply && !fresh) {
|
|
6663
|
+
if (!recorded.raw) raw = await readFile6(target);
|
|
6664
|
+
log2(` reapply ${path13.relative(process.cwd(), target)}`);
|
|
6665
|
+
} else {
|
|
6666
|
+
log2(` changed ${path13.relative(process.cwd(), target)} (upstream)`);
|
|
6667
|
+
}
|
|
6641
6668
|
} else {
|
|
6642
6669
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
6643
6670
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
@@ -6655,45 +6682,61 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6655
6682
|
}
|
|
6656
6683
|
}
|
|
6657
6684
|
log2(
|
|
6658
|
-
` replace ${
|
|
6685
|
+
` replace ${path13.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
6659
6686
|
);
|
|
6660
6687
|
}
|
|
6661
6688
|
}
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
if (
|
|
6665
|
-
|
|
6666
|
-
|
|
6689
|
+
if (fresh) {
|
|
6690
|
+
raw = fresh;
|
|
6691
|
+
} else if (!raw && recorded && cacheDir) {
|
|
6692
|
+
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
6693
|
+
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
6694
|
+
if (final || raw) log2(` cached ${path13.relative(process.cwd(), target)}`);
|
|
6695
|
+
}
|
|
6696
|
+
if (!final && !raw) {
|
|
6667
6697
|
if (!source.url) {
|
|
6668
6698
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
6669
6699
|
}
|
|
6670
|
-
|
|
6700
|
+
raw = await provider.download(source.url);
|
|
6671
6701
|
}
|
|
6672
6702
|
let mediaType;
|
|
6703
|
+
const bytes = final ?? raw;
|
|
6673
6704
|
try {
|
|
6674
|
-
mediaType = validateMedia(
|
|
6705
|
+
mediaType = validateMedia(bytes, expectedMediaType);
|
|
6675
6706
|
} catch (err) {
|
|
6676
6707
|
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
6677
|
-
const mismatch = detectMediaType(
|
|
6708
|
+
const mismatch = detectMediaType(bytes) !== expectedMediaType;
|
|
6678
6709
|
throw new Error(
|
|
6679
|
-
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${
|
|
6710
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${bytes.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
6680
6711
|
);
|
|
6681
6712
|
}
|
|
6713
|
+
let rawHash = final ? recorded?.raw : void 0;
|
|
6714
|
+
let buf = bytes;
|
|
6715
|
+
if (!final) {
|
|
6716
|
+
if (cacheDir) await cacheMedia(cacheDir, raw, mediaType);
|
|
6717
|
+
const processed = applyPostprocess(raw, mediaType, wanted);
|
|
6718
|
+
buf = processed.bytes;
|
|
6719
|
+
if (processed.changed) {
|
|
6720
|
+
rawHash = sha256(raw);
|
|
6721
|
+
log2(` snapped ${path13.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6682
6724
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
6683
|
-
await mkdir3(
|
|
6725
|
+
await mkdir3(path13.dirname(target), { recursive: true });
|
|
6684
6726
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
6685
6727
|
await writeFile3(tmp, buf);
|
|
6686
6728
|
outputs.push({
|
|
6687
6729
|
path: portableOutputPath(target, spec.root),
|
|
6688
6730
|
sha256: sha256(buf),
|
|
6689
6731
|
...source.role ? { role: source.role } : {},
|
|
6690
|
-
mediaType
|
|
6732
|
+
mediaType,
|
|
6733
|
+
...rawHash ? { raw: rawHash } : {}
|
|
6691
6734
|
});
|
|
6692
6735
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
6693
6736
|
await saveLock(lockPath, lock);
|
|
6694
6737
|
await rename3(tmp, target);
|
|
6695
6738
|
wrote = true;
|
|
6696
|
-
log2(` wrote ${
|
|
6739
|
+
log2(` wrote ${path13.relative(process.cwd(), target)}`);
|
|
6697
6740
|
}
|
|
6698
6741
|
if (opts.refresh && !wrote) {
|
|
6699
6742
|
result.unchanged = (result.unchanged ?? 0) + 1;
|
|
@@ -6711,7 +6754,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6711
6754
|
sourceUrls: persistentSources,
|
|
6712
6755
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6713
6756
|
error: null,
|
|
6714
|
-
supersededOutputs: []
|
|
6757
|
+
supersededOutputs: [],
|
|
6758
|
+
postprocess: wanted
|
|
6715
6759
|
});
|
|
6716
6760
|
result.downloaded++;
|
|
6717
6761
|
} catch (err) {
|
|
@@ -6732,10 +6776,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6732
6776
|
return result;
|
|
6733
6777
|
}
|
|
6734
6778
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
6735
|
-
const file =
|
|
6736
|
-
if (!
|
|
6779
|
+
const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6780
|
+
if (!existsSync9(file)) return null;
|
|
6737
6781
|
try {
|
|
6738
|
-
const buf = await
|
|
6782
|
+
const buf = await readFile6(file);
|
|
6739
6783
|
if (sha256(buf) !== hash) return null;
|
|
6740
6784
|
validateMedia(buf, mediaType);
|
|
6741
6785
|
return buf;
|
|
@@ -6746,7 +6790,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
6746
6790
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
6747
6791
|
const hash = knownHash ?? sha256(buf);
|
|
6748
6792
|
validateMedia(buf, mediaType);
|
|
6749
|
-
const file =
|
|
6793
|
+
const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6750
6794
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
6751
6795
|
await mkdir3(cacheDir, { recursive: true });
|
|
6752
6796
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -6754,7 +6798,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
|
6754
6798
|
try {
|
|
6755
6799
|
await rename3(tmp, file);
|
|
6756
6800
|
} catch (err) {
|
|
6757
|
-
if (!
|
|
6801
|
+
if (!existsSync9(file)) throw err;
|
|
6758
6802
|
} finally {
|
|
6759
6803
|
await rm4(tmp, { force: true });
|
|
6760
6804
|
}
|
|
@@ -6787,13 +6831,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
6787
6831
|
}
|
|
6788
6832
|
|
|
6789
6833
|
// src/pipeline/adopt.ts
|
|
6790
|
-
import { existsSync as
|
|
6791
|
-
import { readFile as
|
|
6792
|
-
import
|
|
6834
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6835
|
+
import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
6836
|
+
import path14 from "path";
|
|
6793
6837
|
|
|
6794
6838
|
// src/cache.ts
|
|
6795
|
-
import { mkdir as mkdir4, readFile as
|
|
6796
|
-
import { existsSync as
|
|
6839
|
+
import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
6840
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6797
6841
|
import pathModule from "path";
|
|
6798
6842
|
import { z as z3 } from "zod";
|
|
6799
6843
|
var HashCacheSchema = z3.object({
|
|
@@ -6809,9 +6853,9 @@ function parseCache(value) {
|
|
|
6809
6853
|
return HashCacheSchema.parse(value);
|
|
6810
6854
|
}
|
|
6811
6855
|
async function loadCache(path43) {
|
|
6812
|
-
if (!
|
|
6856
|
+
if (!existsSync10(path43)) return { version: 1, hashes: {} };
|
|
6813
6857
|
try {
|
|
6814
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
6858
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path43, "utf8")));
|
|
6815
6859
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6816
6860
|
return {
|
|
6817
6861
|
version: 1,
|
|
@@ -6865,11 +6909,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6865
6909
|
const localByHash = /* @__PURE__ */ new Map();
|
|
6866
6910
|
const unmatchedLocal = [];
|
|
6867
6911
|
for (const spec of specs) {
|
|
6868
|
-
if (!
|
|
6912
|
+
if (!existsSync11(spec.outFile)) {
|
|
6869
6913
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
6870
6914
|
continue;
|
|
6871
6915
|
}
|
|
6872
|
-
const bytes = await
|
|
6916
|
+
const bytes = await readFile8(spec.outFile);
|
|
6873
6917
|
try {
|
|
6874
6918
|
decodePng(bytes);
|
|
6875
6919
|
} catch (err) {
|
|
@@ -6989,7 +7033,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
6989
7033
|
const log2 = opts.onProgress ?? (() => {
|
|
6990
7034
|
});
|
|
6991
7035
|
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
6992
|
-
const raw = JSON.parse(await
|
|
7036
|
+
const raw = JSON.parse(await readFile8(manifestPath, "utf8"));
|
|
6993
7037
|
const best = /* @__PURE__ */ new Map();
|
|
6994
7038
|
for (const entry of Object.values(lock.entries)) {
|
|
6995
7039
|
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
@@ -7022,19 +7066,19 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7022
7066
|
}
|
|
7023
7067
|
|
|
7024
7068
|
// src/pipeline/salvage.ts
|
|
7025
|
-
import { readFile as
|
|
7026
|
-
import { existsSync as
|
|
7027
|
-
import
|
|
7069
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
7070
|
+
import { existsSync as existsSync12 } from "fs";
|
|
7071
|
+
import path15 from "path";
|
|
7028
7072
|
function providerClaimId(provider, objectId) {
|
|
7029
7073
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
7030
7074
|
}
|
|
7031
7075
|
async function loadClaims(lockPaths, opts = {}) {
|
|
7032
7076
|
const claimed = /* @__PURE__ */ new Set();
|
|
7033
7077
|
for (const p of lockPaths) {
|
|
7034
|
-
if (!
|
|
7078
|
+
if (!existsSync12(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
7035
7079
|
let parsed;
|
|
7036
7080
|
try {
|
|
7037
|
-
parsed = parseLock(JSON.parse(await
|
|
7081
|
+
parsed = parseLock(JSON.parse(await readFile9(p, "utf8")));
|
|
7038
7082
|
} catch {
|
|
7039
7083
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
7040
7084
|
}
|
|
@@ -7082,20 +7126,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
7082
7126
|
return null;
|
|
7083
7127
|
}
|
|
7084
7128
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
7085
|
-
const own =
|
|
7129
|
+
const own = path15.resolve(ownManifestPath);
|
|
7086
7130
|
const siblingManifestPaths = [
|
|
7087
7131
|
.../* @__PURE__ */ new Set([
|
|
7088
7132
|
...workspaceManifestPaths,
|
|
7089
|
-
...claimPaths.map((c) =>
|
|
7133
|
+
...claimPaths.map((c) => path15.join(path15.dirname(path15.resolve(c)), "pixelkiln.manifest.json"))
|
|
7090
7134
|
])
|
|
7091
7135
|
];
|
|
7092
7136
|
const siblings = [];
|
|
7093
7137
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
7094
|
-
if (
|
|
7095
|
-
if (!
|
|
7138
|
+
if (path15.resolve(siblingManifestPath) === own) continue;
|
|
7139
|
+
if (!existsSync12(siblingManifestPath)) continue;
|
|
7096
7140
|
try {
|
|
7097
7141
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
7098
|
-
siblings.push({ label:
|
|
7142
|
+
siblings.push({ label: path15.basename(path15.dirname(siblingManifestPath)), manifest });
|
|
7099
7143
|
} catch {
|
|
7100
7144
|
}
|
|
7101
7145
|
}
|
|
@@ -7211,9 +7255,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
7211
7255
|
}
|
|
7212
7256
|
|
|
7213
7257
|
// src/workspace.ts
|
|
7214
|
-
import { mkdir as mkdir5, readFile as
|
|
7215
|
-
import { existsSync as
|
|
7216
|
-
import
|
|
7258
|
+
import { mkdir as mkdir5, readFile as readFile10, rename as rename5, rm as rm6, writeFile as writeFile6 } from "fs/promises";
|
|
7259
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7260
|
+
import path16 from "path";
|
|
7217
7261
|
import { z as z4 } from "zod";
|
|
7218
7262
|
var WorkspaceProjectSchema = z4.object({
|
|
7219
7263
|
id: z4.string().min(1),
|
|
@@ -7232,18 +7276,18 @@ var WorkspaceSchema = z4.object({
|
|
|
7232
7276
|
function parseWorkspace(raw) {
|
|
7233
7277
|
const parsed = WorkspaceSchema.safeParse(raw);
|
|
7234
7278
|
if (parsed.success) return parsed.data;
|
|
7235
|
-
throw new
|
|
7279
|
+
throw new ProjectError(
|
|
7236
7280
|
`Workspace catalog is not valid v1:
|
|
7237
7281
|
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
7238
7282
|
);
|
|
7239
7283
|
}
|
|
7240
7284
|
async function loadWorkspace(workspacePath) {
|
|
7241
|
-
if (!
|
|
7285
|
+
if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
|
|
7242
7286
|
let raw;
|
|
7243
7287
|
try {
|
|
7244
|
-
raw = JSON.parse(await
|
|
7288
|
+
raw = JSON.parse(await readFile10(workspacePath, "utf8"));
|
|
7245
7289
|
} catch (err) {
|
|
7246
|
-
throw new
|
|
7290
|
+
throw new ProjectError(
|
|
7247
7291
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
7248
7292
|
${err instanceof Error ? err.message : String(err)}`
|
|
7249
7293
|
);
|
|
@@ -7255,7 +7299,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7255
7299
|
version: 1,
|
|
7256
7300
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
7257
7301
|
};
|
|
7258
|
-
await mkdir5(
|
|
7302
|
+
await mkdir5(path16.dirname(path16.resolve(workspacePath)), { recursive: true });
|
|
7259
7303
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7260
7304
|
try {
|
|
7261
7305
|
await writeFile6(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -7265,12 +7309,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7265
7309
|
}
|
|
7266
7310
|
}
|
|
7267
7311
|
function toPortablePath(dir, absolute) {
|
|
7268
|
-
return
|
|
7312
|
+
return path16.relative(dir, absolute).split(path16.sep).join("/");
|
|
7269
7313
|
}
|
|
7270
7314
|
function resolveProject(dir, project) {
|
|
7271
7315
|
return {
|
|
7272
|
-
manifestPath:
|
|
7273
|
-
lockPath:
|
|
7316
|
+
manifestPath: path16.resolve(dir, project.manifest.split("/").join(path16.sep)),
|
|
7317
|
+
lockPath: path16.resolve(dir, project.lock.split("/").join(path16.sep))
|
|
7274
7318
|
};
|
|
7275
7319
|
}
|
|
7276
7320
|
function validateWorkspace(ws, dir) {
|
|
@@ -7283,21 +7327,21 @@ function validateWorkspace(ws, dir) {
|
|
|
7283
7327
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
7284
7328
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
7285
7329
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
7286
|
-
if (
|
|
7330
|
+
if (path16.isAbsolute(project.manifest) || path16.isAbsolute(project.lock)) {
|
|
7287
7331
|
diagnostics.push({
|
|
7288
7332
|
id: "absolute-path",
|
|
7289
7333
|
level: "warning",
|
|
7290
7334
|
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
7291
7335
|
});
|
|
7292
7336
|
}
|
|
7293
|
-
if (!
|
|
7337
|
+
if (!existsSync13(manifestPath)) {
|
|
7294
7338
|
diagnostics.push({
|
|
7295
7339
|
id: "missing-manifest",
|
|
7296
7340
|
level: "error",
|
|
7297
7341
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
7298
7342
|
});
|
|
7299
7343
|
}
|
|
7300
|
-
if (!
|
|
7344
|
+
if (!existsSync13(lockPath)) {
|
|
7301
7345
|
diagnostics.push({
|
|
7302
7346
|
id: "missing-lock",
|
|
7303
7347
|
level: "error",
|
|
@@ -7344,8 +7388,8 @@ function validateWorkspace(ws, dir) {
|
|
|
7344
7388
|
}
|
|
7345
7389
|
|
|
7346
7390
|
// src/pick/salvage-server.ts
|
|
7347
|
-
import { mkdir as mkdir6, writeFile as writeFile7, readFile as
|
|
7348
|
-
import
|
|
7391
|
+
import { mkdir as mkdir6, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
|
|
7392
|
+
import path17 from "path";
|
|
7349
7393
|
|
|
7350
7394
|
// src/pick/salvage-sheet.ts
|
|
7351
7395
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -7505,7 +7549,7 @@ refresh();
|
|
|
7505
7549
|
|
|
7506
7550
|
// src/pick/review-server.ts
|
|
7507
7551
|
import { createServer } from "http";
|
|
7508
|
-
import { readFile as
|
|
7552
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
7509
7553
|
|
|
7510
7554
|
// src/open.ts
|
|
7511
7555
|
import { spawn } from "child_process";
|
|
@@ -7532,7 +7576,7 @@ function serveReviewPage(opts) {
|
|
|
7532
7576
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
7533
7577
|
try {
|
|
7534
7578
|
const asset = opts.assets.get(req.url);
|
|
7535
|
-
const bytes = await
|
|
7579
|
+
const bytes = await readFile11(asset.path);
|
|
7536
7580
|
res.writeHead(200, {
|
|
7537
7581
|
"Content-Type": asset.contentType,
|
|
7538
7582
|
"Cache-Control": "no-store",
|
|
@@ -7609,7 +7653,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7609
7653
|
});
|
|
7610
7654
|
const html = renderSalvageSheet(orphans, {
|
|
7611
7655
|
styleId: ctx.styleId,
|
|
7612
|
-
importDir:
|
|
7656
|
+
importDir: path17.relative(process.cwd(), ctx.importDir) || "."
|
|
7613
7657
|
});
|
|
7614
7658
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
7615
7659
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -7638,9 +7682,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7638
7682
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
7639
7683
|
decodePng(buf);
|
|
7640
7684
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
7641
|
-
const rel =
|
|
7642
|
-
const outFile =
|
|
7643
|
-
await mkdir6(
|
|
7685
|
+
const rel = path17.join("_salvaged", `${assetId}.png`);
|
|
7686
|
+
const outFile = path17.resolve(ctx.importDir, rel);
|
|
7687
|
+
await mkdir6(path17.dirname(outFile), { recursive: true });
|
|
7644
7688
|
await writeFile7(outFile, buf);
|
|
7645
7689
|
ctx.manifest.assets[assetId] = {
|
|
7646
7690
|
prompt: orphan.prompt,
|
|
@@ -7673,7 +7717,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7673
7717
|
sourceUrl: durableSource,
|
|
7674
7718
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
7675
7719
|
outputs: [{
|
|
7676
|
-
path: portableOutputPath(outFile,
|
|
7720
|
+
path: portableOutputPath(outFile, path17.dirname(ctx.manifestPath)),
|
|
7677
7721
|
sha256: sha256(buf)
|
|
7678
7722
|
}],
|
|
7679
7723
|
submittedAt: orphan.createdAt,
|
|
@@ -7697,7 +7741,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7697
7741
|
}
|
|
7698
7742
|
}
|
|
7699
7743
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
7700
|
-
const raw = JSON.parse(await
|
|
7744
|
+
const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
|
|
7701
7745
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
7702
7746
|
await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7703
7747
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
@@ -7707,19 +7751,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7707
7751
|
}
|
|
7708
7752
|
|
|
7709
7753
|
// src/cli/project.ts
|
|
7710
|
-
import
|
|
7711
|
-
import { existsSync as
|
|
7754
|
+
import path19 from "path";
|
|
7755
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7712
7756
|
|
|
7713
7757
|
// src/project.ts
|
|
7714
|
-
import
|
|
7758
|
+
import path18 from "path";
|
|
7715
7759
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
7716
|
-
const absolute =
|
|
7717
|
-
const root =
|
|
7760
|
+
const absolute = path18.resolve(manifestPath);
|
|
7761
|
+
const root = path18.dirname(absolute);
|
|
7718
7762
|
if (opts.env !== false) {
|
|
7719
7763
|
loadEnvFiles(root);
|
|
7720
|
-
if (
|
|
7764
|
+
if (path18.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
7721
7765
|
}
|
|
7722
|
-
const lockPath =
|
|
7766
|
+
const lockPath = path18.resolve(opts.lockPath ?? path18.join(root, "pixelkiln.lock.json"));
|
|
7723
7767
|
const loaded = await loadManifest(absolute);
|
|
7724
7768
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
7725
7769
|
const lock = await loadLock(lockPath);
|
|
@@ -7850,11 +7894,11 @@ async function workspaceStatus(ws, dir) {
|
|
|
7850
7894
|
|
|
7851
7895
|
// src/cli/project.ts
|
|
7852
7896
|
async function provenanceFile(id, file) {
|
|
7853
|
-
const absolute =
|
|
7897
|
+
const absolute = path19.resolve(file);
|
|
7854
7898
|
return {
|
|
7855
7899
|
id,
|
|
7856
7900
|
path: absolute,
|
|
7857
|
-
sha256:
|
|
7901
|
+
sha256: existsSync14(absolute) ? await sha256File(absolute) : null,
|
|
7858
7902
|
included: true
|
|
7859
7903
|
};
|
|
7860
7904
|
}
|
|
@@ -7885,14 +7929,14 @@ function accountProviderId(manifest, requested, command) {
|
|
|
7885
7929
|
const providers = manifestProviderIds(manifest);
|
|
7886
7930
|
if (requested) {
|
|
7887
7931
|
if (!providers.includes(requested)) {
|
|
7888
|
-
throw new
|
|
7932
|
+
throw new UsageError(
|
|
7889
7933
|
`Provider "${requested}" is not used by this manifest. Used: ${providers.join(", ")}.`
|
|
7890
7934
|
);
|
|
7891
7935
|
}
|
|
7892
7936
|
return requested;
|
|
7893
7937
|
}
|
|
7894
7938
|
if (providers.length > 1) {
|
|
7895
|
-
throw new
|
|
7939
|
+
throw new UsageError(
|
|
7896
7940
|
`${command} is account-scoped and this manifest uses ${providers.join(", ")}. Pass --provider <id>.`
|
|
7897
7941
|
);
|
|
7898
7942
|
}
|
|
@@ -7903,13 +7947,13 @@ function budgetsForPlan(plan, args) {
|
|
|
7903
7947
|
const keyed = Object.entries(args.providerBudgets);
|
|
7904
7948
|
for (const [provider] of keyed) {
|
|
7905
7949
|
if (!providerIds.has(provider)) {
|
|
7906
|
-
throw new
|
|
7950
|
+
throw new BudgetError(
|
|
7907
7951
|
`Budget names provider "${provider}", but this run has work only for ${[...providerIds].join(", ") || "no providers"}.`
|
|
7908
7952
|
);
|
|
7909
7953
|
}
|
|
7910
7954
|
}
|
|
7911
7955
|
if (plan.groups.length > 1 && args.budget !== void 0) {
|
|
7912
|
-
throw new
|
|
7956
|
+
throw new BudgetError(
|
|
7913
7957
|
"A mixed-provider run needs provider-keyed budgets, for example --budget pixellab=40 --budget retrodiffusion=1.25."
|
|
7914
7958
|
);
|
|
7915
7959
|
}
|
|
@@ -7917,7 +7961,7 @@ function budgetsForPlan(plan, args) {
|
|
|
7917
7961
|
for (const group of plan.groups) {
|
|
7918
7962
|
const ceiling = args.providerBudgets[group.provider];
|
|
7919
7963
|
if (plan.groups.length > 1 && group.costUnit !== "free" && group.cost > 0 && ceiling === void 0) {
|
|
7920
|
-
throw new
|
|
7964
|
+
throw new BudgetError(
|
|
7921
7965
|
`Mixed-provider run is missing --budget ${group.provider}=<amount> for ${formatCost(group.costUnit, group.cost)} of planned work.`
|
|
7922
7966
|
);
|
|
7923
7967
|
}
|
|
@@ -7926,15 +7970,15 @@ function budgetsForPlan(plan, args) {
|
|
|
7926
7970
|
return out;
|
|
7927
7971
|
}
|
|
7928
7972
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
7929
|
-
if (!
|
|
7930
|
-
throw new
|
|
7973
|
+
if (!existsSync14(workspacePath)) {
|
|
7974
|
+
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
7931
7975
|
}
|
|
7932
|
-
const dir =
|
|
7976
|
+
const dir = path19.dirname(path19.resolve(workspacePath));
|
|
7933
7977
|
const ws = await loadWorkspace(workspacePath);
|
|
7934
7978
|
const diagnostics = validateWorkspace(ws, dir);
|
|
7935
7979
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
7936
7980
|
if (errors.length) {
|
|
7937
|
-
throw new
|
|
7981
|
+
throw new ProjectError(
|
|
7938
7982
|
`Workspace catalog at ${workspacePath} is not safe to derive a claim set from:
|
|
7939
7983
|
` + errors.map((d) => ` ${d.id}: ${d.message}`).join("\n")
|
|
7940
7984
|
);
|
|
@@ -7952,9 +7996,9 @@ async function openAccountProject(args) {
|
|
|
7952
7996
|
return project;
|
|
7953
7997
|
}
|
|
7954
7998
|
async function open(args) {
|
|
7955
|
-
if (!
|
|
7956
|
-
throw new
|
|
7957
|
-
`No manifest at ${
|
|
7999
|
+
if (!existsSync14(path19.resolve(args.manifest))) {
|
|
8000
|
+
throw new ProjectError(
|
|
8001
|
+
`No manifest at ${path19.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
7958
8002
|
);
|
|
7959
8003
|
}
|
|
7960
8004
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -7962,7 +8006,7 @@ async function open(args) {
|
|
|
7962
8006
|
let specs = project.specs;
|
|
7963
8007
|
const accountProvider = ACCOUNT_COMMANDS.has(args.command) ? accountProviderId(loaded.manifest, args.provider, args.command) : void 0;
|
|
7964
8008
|
if (args.provider && !accountProvider) {
|
|
7965
|
-
throw new
|
|
8009
|
+
throw new UsageError("--provider is only used by balance, adopt, salvage, purge, or workspace add.");
|
|
7966
8010
|
}
|
|
7967
8011
|
if (accountProvider) specs = specs.filter((spec) => spec.provider === accountProvider);
|
|
7968
8012
|
const accountManifest = accountProvider ? {
|
|
@@ -7975,7 +8019,7 @@ async function open(args) {
|
|
|
7975
8019
|
)
|
|
7976
8020
|
} : loaded.manifest;
|
|
7977
8021
|
if (accountProvider && args.styles.some((styleId) => !accountManifest.styles[styleId])) {
|
|
7978
|
-
throw new
|
|
8022
|
+
throw new UsageError(`Selected style is not assigned to provider "${accountProvider}".`);
|
|
7979
8023
|
}
|
|
7980
8024
|
return { loaded, specs, lock, accountProvider, accountManifest };
|
|
7981
8025
|
}
|
|
@@ -7996,7 +8040,7 @@ function providerCache(mode2) {
|
|
|
7996
8040
|
|
|
7997
8041
|
// src/cli/commands/account.ts
|
|
7998
8042
|
async function runBalance(args) {
|
|
7999
|
-
loadEnvFiles(
|
|
8043
|
+
loadEnvFiles(path20.dirname(path20.resolve(args.manifest)));
|
|
8000
8044
|
loadEnvFiles(process.cwd());
|
|
8001
8045
|
const loaded = await loadManifest(args.manifest);
|
|
8002
8046
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -8043,12 +8087,12 @@ async function runAdopt(args) {
|
|
|
8043
8087
|
tagged ${n} object(s) upstream`);
|
|
8044
8088
|
}
|
|
8045
8089
|
if (args.writePrompts) {
|
|
8046
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
8090
|
+
const { filled, stillEmpty } = await writePromptsBack(path20.resolve(args.manifest), lock, {
|
|
8047
8091
|
onProgress: log,
|
|
8048
8092
|
provider: provider.id,
|
|
8049
8093
|
assetIds: specs.map((spec) => spec.assetId)
|
|
8050
8094
|
});
|
|
8051
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
8095
|
+
log(` recovered ${filled} prompt(s) into ${path20.relative(process.cwd(), args.manifest)}`);
|
|
8052
8096
|
const reloaded = await loadManifest(args.manifest);
|
|
8053
8097
|
const rebased = (await resolveSpecs(reloaded, {
|
|
8054
8098
|
styles: args.styles,
|
|
@@ -8078,12 +8122,12 @@ async function runSalvage2(args) {
|
|
|
8078
8122
|
const provider = providerFor(accountProvider);
|
|
8079
8123
|
const jsonMode = args.dryRun && args.json;
|
|
8080
8124
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
8081
|
-
const ownLock =
|
|
8125
|
+
const ownLock = path20.resolve(args.lock);
|
|
8082
8126
|
let workspaceProjects = [];
|
|
8083
8127
|
let workspaceDir = "";
|
|
8084
8128
|
if (args.workspace) {
|
|
8085
|
-
const workspacePath =
|
|
8086
|
-
workspaceDir =
|
|
8129
|
+
const workspacePath = path20.resolve(args.workspace);
|
|
8130
|
+
workspaceDir = path20.dirname(workspacePath);
|
|
8087
8131
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
8088
8132
|
workspaceProjects = complete.ws.projects;
|
|
8089
8133
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -8092,12 +8136,12 @@ async function runSalvage2(args) {
|
|
|
8092
8136
|
const lockPaths = [
|
|
8093
8137
|
.../* @__PURE__ */ new Set([
|
|
8094
8138
|
...workspaceLockPaths,
|
|
8095
|
-
...
|
|
8096
|
-
...args.claims.map((c) =>
|
|
8139
|
+
...existsSync15(ownLock) ? [ownLock] : [],
|
|
8140
|
+
...args.claims.map((c) => path20.resolve(c))
|
|
8097
8141
|
])
|
|
8098
8142
|
];
|
|
8099
8143
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
8100
|
-
for (const p of lockPaths) diag(` ${
|
|
8144
|
+
for (const p of lockPaths) diag(` ${path20.relative(process.cwd(), p)}`);
|
|
8101
8145
|
if (!args.claims.length && !args.workspace) {
|
|
8102
8146
|
diag(
|
|
8103
8147
|
`
|
|
@@ -8105,7 +8149,7 @@ async function runSalvage2(args) {
|
|
|
8105
8149
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
8106
8150
|
register every project in a workspace catalog and pass --workspace.`
|
|
8107
8151
|
);
|
|
8108
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
8152
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path20.resolve(args.manifest))) {
|
|
8109
8153
|
diag(
|
|
8110
8154
|
`
|
|
8111
8155
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -8189,7 +8233,7 @@ async function runSalvage2(args) {
|
|
|
8189
8233
|
manifestPath: loaded.path,
|
|
8190
8234
|
manifest: accountManifest,
|
|
8191
8235
|
styleId,
|
|
8192
|
-
importDir:
|
|
8236
|
+
importDir: path20.resolve(loaded.root, style.outDir),
|
|
8193
8237
|
lock,
|
|
8194
8238
|
lockPath: args.lock
|
|
8195
8239
|
},
|
|
@@ -8290,17 +8334,17 @@ async function runTag(args) {
|
|
|
8290
8334
|
}
|
|
8291
8335
|
|
|
8292
8336
|
// src/cli/commands/edit.ts
|
|
8293
|
-
import
|
|
8337
|
+
import path23 from "path";
|
|
8294
8338
|
|
|
8295
8339
|
// src/pipeline/hand-edit.ts
|
|
8296
|
-
import { existsSync as
|
|
8297
|
-
import { copyFile, mkdir as mkdir7, readFile as
|
|
8298
|
-
import
|
|
8340
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8341
|
+
import { copyFile, mkdir as mkdir7, readFile as readFile14, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
|
|
8342
|
+
import path22 from "path";
|
|
8299
8343
|
import { z as z6 } from "zod";
|
|
8300
8344
|
|
|
8301
8345
|
// src/manifest-edit.ts
|
|
8302
|
-
import { readFile as
|
|
8303
|
-
import
|
|
8346
|
+
import { readFile as readFile13, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
8347
|
+
import path21 from "path";
|
|
8304
8348
|
import { z as z5 } from "zod";
|
|
8305
8349
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
8306
8350
|
var AssetPatchSchema = z5.object({
|
|
@@ -8374,6 +8418,8 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
|
|
|
8374
8418
|
promptSuffix: z5.string().optional(),
|
|
8375
8419
|
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
8376
8420
|
palette: z5.array(z5.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
8421
|
+
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
8422
|
+
enforcePalette: z5.boolean().optional(),
|
|
8377
8423
|
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
8378
8424
|
view: z5.string().max(64).nullable().optional(),
|
|
8379
8425
|
/** pixflux only; null clears the style's own value. */
|
|
@@ -8465,6 +8511,7 @@ function applyEdit(raw, edit) {
|
|
|
8465
8511
|
const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
8466
8512
|
setOrDelete(style, "palette", colors.length ? colors : null);
|
|
8467
8513
|
}
|
|
8514
|
+
if (patch2.enforcePalette !== void 0) setOrDelete(style, "enforcePalette", patch2.enforcePalette || null);
|
|
8468
8515
|
if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
|
|
8469
8516
|
if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
|
|
8470
8517
|
return;
|
|
@@ -8512,8 +8559,8 @@ function applyEdit(raw, edit) {
|
|
|
8512
8559
|
}
|
|
8513
8560
|
}
|
|
8514
8561
|
async function applyManifestEdit(manifestPath, edit) {
|
|
8515
|
-
const absolute =
|
|
8516
|
-
const original = await
|
|
8562
|
+
const absolute = path21.resolve(manifestPath);
|
|
8563
|
+
const original = await readFile13(absolute, "utf8");
|
|
8517
8564
|
const before = sha256(original);
|
|
8518
8565
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
8519
8566
|
let raw;
|
|
@@ -8527,9 +8574,9 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8527
8574
|
applyEdit(raw, edit);
|
|
8528
8575
|
const next = serializeLike(original, raw);
|
|
8529
8576
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
8530
|
-
const tmp =
|
|
8531
|
-
|
|
8532
|
-
`.${
|
|
8577
|
+
const tmp = path21.join(
|
|
8578
|
+
path21.dirname(absolute),
|
|
8579
|
+
`.${path21.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
8533
8580
|
);
|
|
8534
8581
|
try {
|
|
8535
8582
|
await writeFile8(tmp, next);
|
|
@@ -8551,11 +8598,11 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8551
8598
|
var HAND_EDIT_DIR = "edits";
|
|
8552
8599
|
function handEditPath(loaded, spec) {
|
|
8553
8600
|
const style = loaded.manifest.styles[spec.styleId];
|
|
8554
|
-
const outDir =
|
|
8555
|
-
const relative =
|
|
8556
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
8557
|
-
const file = inside ? relative :
|
|
8558
|
-
return
|
|
8601
|
+
const outDir = path22.resolve(loaded.root, style?.outDir ?? ".");
|
|
8602
|
+
const relative = path22.relative(outDir, spec.outFile);
|
|
8603
|
+
const inside = relative && !relative.startsWith("..") && !path22.isAbsolute(relative);
|
|
8604
|
+
const file = inside ? relative : path22.basename(spec.outFile);
|
|
8605
|
+
return path22.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8559
8606
|
}
|
|
8560
8607
|
function handEditBases(spec, lock) {
|
|
8561
8608
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8568,10 +8615,10 @@ function handEditBases(spec, lock) {
|
|
|
8568
8615
|
}));
|
|
8569
8616
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
8570
8617
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
8571
|
-
if (!members.every((member) =>
|
|
8618
|
+
if (!members.every((member) => existsSync16(member.path))) return null;
|
|
8572
8619
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
8573
8620
|
}
|
|
8574
|
-
return
|
|
8621
|
+
return existsSync16(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
8575
8622
|
}
|
|
8576
8623
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
8577
8624
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8597,18 +8644,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
8597
8644
|
`${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`
|
|
8598
8645
|
);
|
|
8599
8646
|
}
|
|
8600
|
-
const existing = spec.source ?
|
|
8647
|
+
const existing = spec.source ? path22.resolve(loaded.root, spec.source) : null;
|
|
8601
8648
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8602
8649
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
8603
8650
|
let created = false;
|
|
8604
8651
|
for (const member of members) {
|
|
8605
|
-
if (
|
|
8652
|
+
if (existsSync16(member.path)) continue;
|
|
8606
8653
|
if (!member.base) {
|
|
8607
8654
|
throw new ManifestEditError(
|
|
8608
8655
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8609
8656
|
);
|
|
8610
8657
|
}
|
|
8611
|
-
await mkdir7(
|
|
8658
|
+
await mkdir7(path22.dirname(member.path), { recursive: true });
|
|
8612
8659
|
await copyFile(member.base, member.path);
|
|
8613
8660
|
created = true;
|
|
8614
8661
|
}
|
|
@@ -8687,7 +8734,7 @@ function handEditProjectPath(editPath) {
|
|
|
8687
8734
|
async function readHandEditCompanion(editPath) {
|
|
8688
8735
|
let text;
|
|
8689
8736
|
try {
|
|
8690
|
-
text = await
|
|
8737
|
+
text = await readFile14(handEditCompanionPath(editPath), "utf8");
|
|
8691
8738
|
} catch {
|
|
8692
8739
|
return null;
|
|
8693
8740
|
}
|
|
@@ -8730,7 +8777,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8730
8777
|
}
|
|
8731
8778
|
});
|
|
8732
8779
|
const bases = handEditBases(spec, lock);
|
|
8733
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
8780
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile14(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
8734
8781
|
for (const [index, image] of decoded.entries()) {
|
|
8735
8782
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
8736
8783
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -8746,7 +8793,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8746
8793
|
editor: input.editor,
|
|
8747
8794
|
protocol: input.protocol,
|
|
8748
8795
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8749
|
-
project: projectPath ?
|
|
8796
|
+
project: projectPath ? path22.basename(projectPath) : null,
|
|
8750
8797
|
outputs: incoming.map((frame, index) => ({
|
|
8751
8798
|
role: frame.role,
|
|
8752
8799
|
basedOn: basedOnHash(entry, index),
|
|
@@ -8806,7 +8853,7 @@ async function runEdit(args) {
|
|
|
8806
8853
|
const set = started.members.length > 1;
|
|
8807
8854
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
8808
8855
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
8809
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
8856
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path23.relative(process.cwd(), member.path)}`);
|
|
8810
8857
|
if (args.noOpen) {
|
|
8811
8858
|
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`);
|
|
8812
8859
|
} else {
|
|
@@ -8816,17 +8863,17 @@ async function runEdit(args) {
|
|
|
8816
8863
|
}
|
|
8817
8864
|
|
|
8818
8865
|
// src/cli/commands/gallery.ts
|
|
8819
|
-
import
|
|
8820
|
-
import { existsSync as
|
|
8866
|
+
import path30 from "path";
|
|
8867
|
+
import { existsSync as existsSync22 } from "fs";
|
|
8821
8868
|
|
|
8822
8869
|
// src/gallery/snapshot.ts
|
|
8823
8870
|
import { createHash as createHash3 } from "crypto";
|
|
8824
|
-
import { existsSync as
|
|
8825
|
-
import { readFile as
|
|
8826
|
-
import
|
|
8871
|
+
import { existsSync as existsSync18 } from "fs";
|
|
8872
|
+
import { readFile as readFile15, stat as stat2 } from "fs/promises";
|
|
8873
|
+
import path26 from "path";
|
|
8827
8874
|
|
|
8828
8875
|
// src/gallery/edit.ts
|
|
8829
|
-
import
|
|
8876
|
+
import path24 from "path";
|
|
8830
8877
|
import { z as z7 } from "zod";
|
|
8831
8878
|
var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
8832
8879
|
var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
@@ -8916,7 +8963,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8916
8963
|
}
|
|
8917
8964
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8918
8965
|
if (result.changed) {
|
|
8919
|
-
const relative =
|
|
8966
|
+
const relative = path24.relative(process.cwd(), result.manifestPath);
|
|
8920
8967
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8921
8968
|
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;
|
|
8922
8969
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -8926,8 +8973,8 @@ function createGalleryEditHandler(opts) {
|
|
|
8926
8973
|
}
|
|
8927
8974
|
|
|
8928
8975
|
// src/pipeline/history.ts
|
|
8929
|
-
import { existsSync as
|
|
8930
|
-
import
|
|
8976
|
+
import { existsSync as existsSync17 } from "fs";
|
|
8977
|
+
import path25 from "path";
|
|
8931
8978
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
8932
8979
|
function historyLimit(manifest, env = process.env) {
|
|
8933
8980
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -8959,6 +9006,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
8959
9006
|
cost: entry.cost,
|
|
8960
9007
|
costUnit: entry.costUnit,
|
|
8961
9008
|
provider: entry.provider,
|
|
9009
|
+
postprocess: entry.postprocess,
|
|
8962
9010
|
retiredAt
|
|
8963
9011
|
};
|
|
8964
9012
|
}
|
|
@@ -8971,9 +9019,13 @@ function historyAfterReplacing(previous, limit, retiredAt) {
|
|
|
8971
9019
|
}
|
|
8972
9020
|
function referencedHashes(lock) {
|
|
8973
9021
|
const hashes = /* @__PURE__ */ new Set();
|
|
9022
|
+
const add = (output) => {
|
|
9023
|
+
hashes.add(output.sha256);
|
|
9024
|
+
if (output.raw) hashes.add(output.raw);
|
|
9025
|
+
};
|
|
8974
9026
|
for (const entry of Object.values(lock.entries)) {
|
|
8975
|
-
for (const output of entry.outputs)
|
|
8976
|
-
for (const generation of entry.history ?? []) for (const output of generation.outputs)
|
|
9027
|
+
for (const output of entry.outputs) add(output);
|
|
9028
|
+
for (const generation of entry.history ?? []) for (const output of generation.outputs) add(output);
|
|
8977
9029
|
}
|
|
8978
9030
|
return hashes;
|
|
8979
9031
|
}
|
|
@@ -9008,10 +9060,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
9008
9060
|
}
|
|
9009
9061
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
9010
9062
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
9011
|
-
if (!
|
|
9063
|
+
if (!existsSync17(file)) continue;
|
|
9012
9064
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
9013
|
-
throw new
|
|
9014
|
-
`${
|
|
9065
|
+
throw new OverwriteRefusedError(
|
|
9066
|
+
`${path25.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
9067
|
+
[file]
|
|
9015
9068
|
);
|
|
9016
9069
|
}
|
|
9017
9070
|
}
|
|
@@ -9043,6 +9096,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
9043
9096
|
cost: generation.cost,
|
|
9044
9097
|
costUnit: generation.costUnit,
|
|
9045
9098
|
provider: generation.provider,
|
|
9099
|
+
postprocess: generation.postprocess,
|
|
9046
9100
|
history
|
|
9047
9101
|
});
|
|
9048
9102
|
await saveLock(lockPath, lock);
|
|
@@ -9061,7 +9115,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
9061
9115
|
// src/gallery/snapshot.ts
|
|
9062
9116
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
9063
9117
|
function galleryMediaId(absolutePath) {
|
|
9064
|
-
return createHash3("sha256").update(
|
|
9118
|
+
return createHash3("sha256").update(path26.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
9065
9119
|
}
|
|
9066
9120
|
function galleryMediaRoute(id) {
|
|
9067
9121
|
return `/media/${id}`;
|
|
@@ -9080,9 +9134,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
9080
9134
|
return (entry.history ?? []).map((generation, i) => {
|
|
9081
9135
|
const outputs = generation.outputs.map((output) => {
|
|
9082
9136
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
9083
|
-
const file =
|
|
9137
|
+
const file = path26.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
9084
9138
|
let url = null;
|
|
9085
|
-
if (
|
|
9139
|
+
if (existsSync18(file)) {
|
|
9086
9140
|
const id = galleryMediaId(file);
|
|
9087
9141
|
media.set(id, { path: file, contentType: mediaType });
|
|
9088
9142
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -9111,7 +9165,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
9111
9165
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
9112
9166
|
async function samePixels(a, b) {
|
|
9113
9167
|
try {
|
|
9114
|
-
const [left, right] = await Promise.all([
|
|
9168
|
+
const [left, right] = await Promise.all([readFile15(a), readFile15(b)]);
|
|
9115
9169
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
9116
9170
|
const x = decodePng(left);
|
|
9117
9171
|
const y = decodePng(right);
|
|
@@ -9164,10 +9218,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
9164
9218
|
async function describeQuality(media, root, inspection) {
|
|
9165
9219
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
9166
9220
|
const outputs = await Promise.all(
|
|
9167
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
9221
|
+
outputPaths.map((file) => describeOutput(media, root, path26.resolve(root, file)))
|
|
9168
9222
|
);
|
|
9169
|
-
const recordPath =
|
|
9170
|
-
const recordExists =
|
|
9223
|
+
const recordPath = path26.resolve(root, inspection.record);
|
|
9224
|
+
const recordExists = existsSync18(recordPath);
|
|
9171
9225
|
let options = null;
|
|
9172
9226
|
let check = null;
|
|
9173
9227
|
if (recordExists) {
|
|
@@ -9186,7 +9240,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
9186
9240
|
return {
|
|
9187
9241
|
state: inspection.state,
|
|
9188
9242
|
reason: inspection.reason,
|
|
9189
|
-
output: portableOutputPath(
|
|
9243
|
+
output: portableOutputPath(path26.resolve(root, inspection.output), root),
|
|
9190
9244
|
record: portableOutputPath(recordPath, root),
|
|
9191
9245
|
recordExists,
|
|
9192
9246
|
outputs,
|
|
@@ -9223,9 +9277,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
9223
9277
|
const media = /* @__PURE__ */ new Map();
|
|
9224
9278
|
const plan = await buildPlan(specs, lock);
|
|
9225
9279
|
const items = [];
|
|
9226
|
-
const manifestText = await
|
|
9280
|
+
const manifestText = await readFile15(loaded.path, "utf8");
|
|
9227
9281
|
const manifestSha256 = sha256(manifestText);
|
|
9228
|
-
const cacheDir =
|
|
9282
|
+
const cacheDir = path26.resolve(path26.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
9229
9283
|
let rawStyles = {};
|
|
9230
9284
|
try {
|
|
9231
9285
|
const raw = JSON.parse(manifestText);
|
|
@@ -9249,12 +9303,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
9249
9303
|
)
|
|
9250
9304
|
);
|
|
9251
9305
|
if (spec.source) {
|
|
9252
|
-
const editPath =
|
|
9306
|
+
const editPath = path26.resolve(root, spec.source);
|
|
9253
9307
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
9254
9308
|
const editShas = await Promise.all(
|
|
9255
9309
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
9256
9310
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
9257
|
-
return { file, sha256:
|
|
9311
|
+
return { file, sha256: existsSync18(file) ? await sha256File(file) : null };
|
|
9258
9312
|
})
|
|
9259
9313
|
);
|
|
9260
9314
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -9262,7 +9316,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9262
9316
|
const companion = await readHandEditCompanion(editPath);
|
|
9263
9317
|
if (companion) {
|
|
9264
9318
|
const projectPath = handEditProjectPath(editPath);
|
|
9265
|
-
const project = companion.project &&
|
|
9319
|
+
const project = companion.project && existsSync18(projectPath) ? await fileInfo(projectPath) : null;
|
|
9266
9320
|
let projectUrl = null;
|
|
9267
9321
|
if (project) {
|
|
9268
9322
|
const id = galleryMediaId(projectPath);
|
|
@@ -9298,8 +9352,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9298
9352
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
9299
9353
|
}
|
|
9300
9354
|
} else if (spec.source) {
|
|
9301
|
-
outputs = [await describeOutput(media, root,
|
|
9302
|
-
} else if (!entry &&
|
|
9355
|
+
outputs = [await describeOutput(media, root, path26.resolve(root, spec.source))];
|
|
9356
|
+
} else if (!entry && existsSync18(spec.outFile)) {
|
|
9303
9357
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
9304
9358
|
} else {
|
|
9305
9359
|
outputs = [];
|
|
@@ -9341,6 +9395,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9341
9395
|
outputs,
|
|
9342
9396
|
quality,
|
|
9343
9397
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
9398
|
+
postprocess: entry?.postprocess ?? null,
|
|
9344
9399
|
asset,
|
|
9345
9400
|
source: spec.source ?? null,
|
|
9346
9401
|
edit,
|
|
@@ -9401,6 +9456,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9401
9456
|
outputs,
|
|
9402
9457
|
quality: null,
|
|
9403
9458
|
providerMetadata: entry.providerMetadata ?? {},
|
|
9459
|
+
postprocess: entry.postprocess ?? null,
|
|
9404
9460
|
asset: null,
|
|
9405
9461
|
source: null,
|
|
9406
9462
|
edit: null,
|
|
@@ -9449,6 +9505,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9449
9505
|
promptPrefix: style?.promptPrefix ?? "",
|
|
9450
9506
|
promptSuffix: style?.promptSuffix ?? "",
|
|
9451
9507
|
palette: style?.palette ?? [],
|
|
9508
|
+
enforcePalette: style?.enforcePalette ?? false,
|
|
9452
9509
|
view: style?.view ?? null,
|
|
9453
9510
|
noBackground: style?.noBackground ?? true,
|
|
9454
9511
|
quality: Boolean(style?.quality),
|
|
@@ -9478,7 +9535,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9478
9535
|
id: loaded.manifest.name,
|
|
9479
9536
|
name: loaded.manifest.name,
|
|
9480
9537
|
manifest: loaded.path,
|
|
9481
|
-
lock:
|
|
9538
|
+
lock: path26.resolve(opts.lockPath),
|
|
9482
9539
|
root,
|
|
9483
9540
|
provider: loaded.manifest.provider,
|
|
9484
9541
|
account: null,
|
|
@@ -9504,7 +9561,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9504
9561
|
return { snapshot, media };
|
|
9505
9562
|
}
|
|
9506
9563
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
9507
|
-
const dir =
|
|
9564
|
+
const dir = path26.dirname(path26.resolve(opts.workspacePath));
|
|
9508
9565
|
const media = /* @__PURE__ */ new Map();
|
|
9509
9566
|
const projects = [];
|
|
9510
9567
|
const items = [];
|
|
@@ -9547,11 +9604,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9547
9604
|
id: project.id,
|
|
9548
9605
|
name: loaded.manifest.name,
|
|
9549
9606
|
manifest: manifestPath,
|
|
9550
|
-
lock:
|
|
9607
|
+
lock: path26.resolve(lockPath),
|
|
9551
9608
|
root: loaded.root,
|
|
9552
9609
|
provider: loaded.manifest.provider,
|
|
9553
9610
|
account: project.account ?? null,
|
|
9554
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
9611
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile15(manifestPath, "utf8")),
|
|
9555
9612
|
historyLimit: historyLimit(loaded.manifest),
|
|
9556
9613
|
entries: Object.keys(lock.entries).length,
|
|
9557
9614
|
items: projectItems.length,
|
|
@@ -9563,8 +9620,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9563
9620
|
id: project.id,
|
|
9564
9621
|
name: project.id,
|
|
9565
9622
|
manifest: manifestPath,
|
|
9566
|
-
lock:
|
|
9567
|
-
root:
|
|
9623
|
+
lock: path26.resolve(lockPath),
|
|
9624
|
+
root: path26.dirname(manifestPath),
|
|
9568
9625
|
provider: project.provider,
|
|
9569
9626
|
account: project.account ?? null,
|
|
9570
9627
|
manifestSha256: null,
|
|
@@ -9591,7 +9648,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9591
9648
|
version: 1,
|
|
9592
9649
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
9593
9650
|
project: null,
|
|
9594
|
-
workspace: { path:
|
|
9651
|
+
workspace: { path: path26.resolve(opts.workspacePath), dir, projects },
|
|
9595
9652
|
filter,
|
|
9596
9653
|
totals: {
|
|
9597
9654
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -9610,7 +9667,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9610
9667
|
// src/gallery/server.ts
|
|
9611
9668
|
import { randomBytes } from "crypto";
|
|
9612
9669
|
import { createServer as createServer2 } from "http";
|
|
9613
|
-
import { readFile as
|
|
9670
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
9614
9671
|
|
|
9615
9672
|
// src/gallery/page.ts
|
|
9616
9673
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -9851,7 +9908,7 @@ async function serveGallery(opts) {
|
|
|
9851
9908
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
9852
9909
|
if (!asset) return fail(404, "no such review asset");
|
|
9853
9910
|
try {
|
|
9854
|
-
const bytes = await
|
|
9911
|
+
const bytes = await readFile16(asset.path);
|
|
9855
9912
|
res.writeHead(200, {
|
|
9856
9913
|
"Content-Type": asset.contentType,
|
|
9857
9914
|
"Cache-Control": "no-store",
|
|
@@ -9927,7 +9984,7 @@ async function serveGallery(opts) {
|
|
|
9927
9984
|
if (!asset) return fail(404, "no such gallery media");
|
|
9928
9985
|
let bytes;
|
|
9929
9986
|
try {
|
|
9930
|
-
bytes = await
|
|
9987
|
+
bytes = await readFile16(asset.path);
|
|
9931
9988
|
} catch {
|
|
9932
9989
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
9933
9990
|
}
|
|
@@ -10080,7 +10137,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
10080
10137
|
if (opts.budget != null) {
|
|
10081
10138
|
const cost = [...estimates.values()].reduce((sum, estimate) => sum + estimate.amount, 0);
|
|
10082
10139
|
if (cost > opts.budget) {
|
|
10083
|
-
throw new
|
|
10140
|
+
throw new BudgetError(
|
|
10084
10141
|
`This run would spend ${formatCost(unit, cost)} but the budget is ${formatCost(unit, opts.budget)}. Narrow it with --only/--style, or raise --budget.`
|
|
10085
10142
|
);
|
|
10086
10143
|
}
|
|
@@ -10240,13 +10297,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
10240
10297
|
}
|
|
10241
10298
|
|
|
10242
10299
|
// src/pick/server.ts
|
|
10243
|
-
import { existsSync as
|
|
10244
|
-
import { readFile as
|
|
10300
|
+
import { existsSync as existsSync20 } from "fs";
|
|
10301
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
10245
10302
|
|
|
10246
10303
|
// src/pipeline/init.ts
|
|
10247
|
-
import { readdir, readFile as
|
|
10248
|
-
import { existsSync as
|
|
10249
|
-
import
|
|
10304
|
+
import { readdir, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
|
|
10305
|
+
import { existsSync as existsSync19 } from "fs";
|
|
10306
|
+
import path27 from "path";
|
|
10250
10307
|
function pngSize(buf) {
|
|
10251
10308
|
if (buf.length < 24) return null;
|
|
10252
10309
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -10260,7 +10317,7 @@ function slugify(value) {
|
|
|
10260
10317
|
async function walk(dir, exclude) {
|
|
10261
10318
|
const out = [];
|
|
10262
10319
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
10263
|
-
const full =
|
|
10320
|
+
const full = path27.join(dir, entry.name);
|
|
10264
10321
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
10265
10322
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
10266
10323
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -10274,14 +10331,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
10274
10331
|
const skipped = [];
|
|
10275
10332
|
const seen = /* @__PURE__ */ new Map();
|
|
10276
10333
|
for (const file of files.sort()) {
|
|
10277
|
-
const size = pngSize(await
|
|
10334
|
+
const size = pngSize(await readFile17(file));
|
|
10278
10335
|
if (!size) {
|
|
10279
10336
|
skipped.push(`${file} (not a readable PNG)`);
|
|
10280
10337
|
continue;
|
|
10281
10338
|
}
|
|
10282
|
-
const rel =
|
|
10283
|
-
const category =
|
|
10284
|
-
let id = slugify(
|
|
10339
|
+
const rel = path27.relative(root, file);
|
|
10340
|
+
const category = path27.dirname(rel) === "." ? "" : path27.dirname(rel);
|
|
10341
|
+
let id = slugify(path27.basename(rel));
|
|
10285
10342
|
if (seen.has(id)) {
|
|
10286
10343
|
const n = seen.get(id) + 1;
|
|
10287
10344
|
seen.set(id, n);
|
|
@@ -10324,6 +10381,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
10324
10381
|
promptSuffix: "",
|
|
10325
10382
|
styleImages: [],
|
|
10326
10383
|
palette: [],
|
|
10384
|
+
enforcePalette: false,
|
|
10327
10385
|
outDir,
|
|
10328
10386
|
tags: [name],
|
|
10329
10387
|
providerOptions: {}
|
|
@@ -10338,7 +10396,7 @@ function mode(values) {
|
|
|
10338
10396
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
10339
10397
|
}
|
|
10340
10398
|
async function writeManifestFile(target, manifest) {
|
|
10341
|
-
if (
|
|
10399
|
+
if (existsSync19(target)) {
|
|
10342
10400
|
throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
|
|
10343
10401
|
}
|
|
10344
10402
|
await writeFile10(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
@@ -10741,13 +10799,13 @@ async function currentArt(entry, spec) {
|
|
|
10741
10799
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
10742
10800
|
if (!previous) return null;
|
|
10743
10801
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
10744
|
-
if (!
|
|
10802
|
+
if (!existsSync20(file)) return null;
|
|
10745
10803
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
10746
10804
|
if (!contentType) return null;
|
|
10747
10805
|
let width = entry.width;
|
|
10748
10806
|
let height = entry.height;
|
|
10749
10807
|
if (contentType === "image/png") {
|
|
10750
|
-
const size = pngSize(await
|
|
10808
|
+
const size = pngSize(await readFile18(file));
|
|
10751
10809
|
if (size) ({ width, height } = size);
|
|
10752
10810
|
}
|
|
10753
10811
|
return { path: file, contentType, width, height };
|
|
@@ -11240,15 +11298,15 @@ function createGenerateHandlers(opts) {
|
|
|
11240
11298
|
}
|
|
11241
11299
|
|
|
11242
11300
|
// src/gallery/editor.ts
|
|
11243
|
-
import { readFile as
|
|
11244
|
-
import
|
|
11301
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
11302
|
+
import path29 from "path";
|
|
11245
11303
|
|
|
11246
11304
|
// src/editor/install.ts
|
|
11247
11305
|
import { createHash as createHash4 } from "crypto";
|
|
11248
|
-
import { existsSync as
|
|
11249
|
-
import { mkdir as mkdir8, readFile as
|
|
11306
|
+
import { existsSync as existsSync21 } from "fs";
|
|
11307
|
+
import { mkdir as mkdir8, readFile as readFile19, rename as rename8, rm as rm9, stat as stat3, writeFile as writeFile11 } from "fs/promises";
|
|
11250
11308
|
import os2 from "os";
|
|
11251
|
-
import
|
|
11309
|
+
import path28 from "path";
|
|
11252
11310
|
|
|
11253
11311
|
// tools/pixelorama-bridge/pin.json
|
|
11254
11312
|
var pin_default = {
|
|
@@ -11312,15 +11370,15 @@ var EditorInstallError = class extends Error {
|
|
|
11312
11370
|
};
|
|
11313
11371
|
function editorToolsRoot() {
|
|
11314
11372
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
11315
|
-
if (override) return
|
|
11316
|
-
if (process.platform === "darwin") return
|
|
11373
|
+
if (override) return path28.resolve(override);
|
|
11374
|
+
if (process.platform === "darwin") return path28.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
11317
11375
|
if (process.platform === "win32") {
|
|
11318
|
-
return
|
|
11376
|
+
return path28.join(process.env.LOCALAPPDATA ?? path28.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
11319
11377
|
}
|
|
11320
|
-
return
|
|
11378
|
+
return path28.join(process.env.XDG_CACHE_HOME ?? path28.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
11321
11379
|
}
|
|
11322
11380
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
11323
|
-
return
|
|
11381
|
+
return path28.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
11324
11382
|
}
|
|
11325
11383
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
11326
11384
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -11330,7 +11388,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
11330
11388
|
}
|
|
11331
11389
|
async function sha256Of(file) {
|
|
11332
11390
|
try {
|
|
11333
|
-
return createHash4("sha256").update(await
|
|
11391
|
+
return createHash4("sha256").update(await readFile19(file)).digest("hex");
|
|
11334
11392
|
} catch {
|
|
11335
11393
|
return null;
|
|
11336
11394
|
}
|
|
@@ -11343,8 +11401,8 @@ async function editorStatus(opts = {}) {
|
|
|
11343
11401
|
let totalBytes = 0;
|
|
11344
11402
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
11345
11403
|
totalBytes += expected.bytes;
|
|
11346
|
-
const file =
|
|
11347
|
-
const info =
|
|
11404
|
+
const file = path28.join(dir, name);
|
|
11405
|
+
const info = existsSync21(file) ? await stat3(file).catch(() => null) : null;
|
|
11348
11406
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
11349
11407
|
installedBytes += expected.bytes;
|
|
11350
11408
|
} else {
|
|
@@ -11401,7 +11459,7 @@ async function installEditor(opts = {}) {
|
|
|
11401
11459
|
if (digest2 !== expected.sha256) {
|
|
11402
11460
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
11403
11461
|
}
|
|
11404
|
-
const final =
|
|
11462
|
+
const final = path28.join(dir, name);
|
|
11405
11463
|
const tmp = `${final}.${process.pid}.part`;
|
|
11406
11464
|
try {
|
|
11407
11465
|
await writeFile11(tmp, bytes);
|
|
@@ -11447,7 +11505,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
11447
11505
|
".json": "application/json; charset=utf-8"
|
|
11448
11506
|
};
|
|
11449
11507
|
function editorContentType(name) {
|
|
11450
|
-
return EDITOR_CONTENT_TYPES[
|
|
11508
|
+
return EDITOR_CONTENT_TYPES[path28.extname(name).toLowerCase()] ?? null;
|
|
11451
11509
|
}
|
|
11452
11510
|
|
|
11453
11511
|
// src/gallery/editor.ts
|
|
@@ -11523,7 +11581,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
11523
11581
|
if (!status.installed) return null;
|
|
11524
11582
|
let bytes;
|
|
11525
11583
|
try {
|
|
11526
|
-
bytes = await
|
|
11584
|
+
bytes = await readFile20(path29.join(dir, name));
|
|
11527
11585
|
} catch {
|
|
11528
11586
|
verified = null;
|
|
11529
11587
|
return null;
|
|
@@ -11563,13 +11621,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
11563
11621
|
};
|
|
11564
11622
|
const loadProject = async (project) => {
|
|
11565
11623
|
const ctx = await access2.loadProject(project);
|
|
11566
|
-
const dir =
|
|
11624
|
+
const dir = path30.dirname(ctx.loaded.path);
|
|
11567
11625
|
const declared = readEnvFiles(dir);
|
|
11568
11626
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
11569
11627
|
for (const [name, value] of Object.entries(declared)) {
|
|
11570
11628
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
11571
11629
|
throw new Error(
|
|
11572
|
-
`${
|
|
11630
|
+
`${path30.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.`
|
|
11573
11631
|
);
|
|
11574
11632
|
}
|
|
11575
11633
|
}
|
|
@@ -11614,8 +11672,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
11614
11672
|
}
|
|
11615
11673
|
async function runGallery(args) {
|
|
11616
11674
|
if (args.workspace) {
|
|
11617
|
-
const workspacePath =
|
|
11618
|
-
if (!
|
|
11675
|
+
const workspacePath = path30.resolve(args.workspace);
|
|
11676
|
+
if (!existsSync22(workspacePath)) {
|
|
11619
11677
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
11620
11678
|
}
|
|
11621
11679
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -11635,7 +11693,7 @@ async function runGallery(args) {
|
|
|
11635
11693
|
const registered = async (projectId) => {
|
|
11636
11694
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
11637
11695
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
11638
|
-
return resolveProject(
|
|
11696
|
+
return resolveProject(path30.dirname(workspacePath), project);
|
|
11639
11697
|
};
|
|
11640
11698
|
await serveUntilStopped(initial, build2, args, {
|
|
11641
11699
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -11660,7 +11718,7 @@ async function runGallery(args) {
|
|
|
11660
11718
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
11661
11719
|
};
|
|
11662
11720
|
await serveUntilStopped(await build(), reload, args, {
|
|
11663
|
-
manifestFor: () =>
|
|
11721
|
+
manifestFor: () => path30.resolve(args.manifest),
|
|
11664
11722
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
11665
11723
|
});
|
|
11666
11724
|
}
|
|
@@ -11690,12 +11748,12 @@ async function runGenerate(args) {
|
|
|
11690
11748
|
balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`
|
|
11691
11749
|
);
|
|
11692
11750
|
if (balance.unit !== group.costUnit) {
|
|
11693
|
-
throw new
|
|
11751
|
+
throw new BudgetError(
|
|
11694
11752
|
`Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
|
|
11695
11753
|
);
|
|
11696
11754
|
}
|
|
11697
11755
|
if (balance.unit !== "free" && group.cost > balance.remaining) {
|
|
11698
|
-
throw new
|
|
11756
|
+
throw new BudgetError(
|
|
11699
11757
|
`${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
11700
11758
|
);
|
|
11701
11759
|
}
|
|
@@ -11707,7 +11765,7 @@ async function runGenerate(args) {
|
|
|
11707
11765
|
}
|
|
11708
11766
|
const ceiling = budgets.get(group.provider);
|
|
11709
11767
|
if (ceiling !== void 0 && group.cost > ceiling) {
|
|
11710
|
-
throw new
|
|
11768
|
+
throw new BudgetError(
|
|
11711
11769
|
`${group.provider} would spend ${formatCost(group.costUnit, group.cost)} but its budget is ${formatCost(group.costUnit, ceiling)}.`
|
|
11712
11770
|
);
|
|
11713
11771
|
}
|
|
@@ -11912,18 +11970,18 @@ async function runRestoreGeneration(args) {
|
|
|
11912
11970
|
}
|
|
11913
11971
|
|
|
11914
11972
|
// src/cli/commands/housekeeping.ts
|
|
11915
|
-
import { existsSync as
|
|
11973
|
+
import { existsSync as existsSync24 } from "fs";
|
|
11916
11974
|
|
|
11917
11975
|
// src/pipeline/cache-health.ts
|
|
11918
|
-
import { existsSync as
|
|
11919
|
-
import { readFile as
|
|
11920
|
-
import
|
|
11976
|
+
import { existsSync as existsSync23 } from "fs";
|
|
11977
|
+
import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
11978
|
+
import path31 from "path";
|
|
11921
11979
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11922
|
-
if (options.prune && !
|
|
11923
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
11980
|
+
if (options.prune && !existsSync23(lockPath)) {
|
|
11981
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path31.resolve(lockPath)}`);
|
|
11924
11982
|
}
|
|
11925
|
-
const contentDir =
|
|
11926
|
-
const remotePath =
|
|
11983
|
+
const contentDir = path31.resolve(path31.dirname(lockPath), ".pixelkiln", "cache");
|
|
11984
|
+
const remotePath = path31.resolve(cachePathFor(lockPath));
|
|
11927
11985
|
const referenced = referencedHashes(lock);
|
|
11928
11986
|
let content = await inspectContentCache(contentDir, referenced);
|
|
11929
11987
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -11935,7 +11993,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11935
11993
|
]);
|
|
11936
11994
|
for (const name of names) {
|
|
11937
11995
|
try {
|
|
11938
|
-
await rm10(
|
|
11996
|
+
await rm10(path31.join(contentDir, name), { force: true });
|
|
11939
11997
|
removed.contentFiles++;
|
|
11940
11998
|
} catch {
|
|
11941
11999
|
}
|
|
@@ -11944,7 +12002,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11944
12002
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
11945
12003
|
removed.resetRemoteHashCache = true;
|
|
11946
12004
|
} else if (remoteHashes.invalidIds.length) {
|
|
11947
|
-
const cache = parseCache(JSON.parse(await
|
|
12005
|
+
const cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
11948
12006
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
11949
12007
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
11950
12008
|
await saveCache(remotePath, cache);
|
|
@@ -11962,7 +12020,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11962
12020
|
async function inspectContentCache(contentDir, referenced) {
|
|
11963
12021
|
const report = {
|
|
11964
12022
|
path: contentDir,
|
|
11965
|
-
exists:
|
|
12023
|
+
exists: existsSync23(contentDir),
|
|
11966
12024
|
files: 0,
|
|
11967
12025
|
bytes: 0,
|
|
11968
12026
|
valid: 0,
|
|
@@ -11983,12 +12041,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11983
12041
|
continue;
|
|
11984
12042
|
}
|
|
11985
12043
|
report.files++;
|
|
11986
|
-
const file =
|
|
12044
|
+
const file = path31.join(contentDir, entry.name);
|
|
11987
12045
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11988
12046
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11989
12047
|
let bytes;
|
|
11990
12048
|
try {
|
|
11991
|
-
bytes = await
|
|
12049
|
+
bytes = await readFile21(file);
|
|
11992
12050
|
report.bytes += bytes.length;
|
|
11993
12051
|
} catch (err) {
|
|
11994
12052
|
report.invalid.push({
|
|
@@ -12026,7 +12084,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12026
12084
|
async function inspectRemoteHashCache(remotePath) {
|
|
12027
12085
|
const report = {
|
|
12028
12086
|
path: remotePath,
|
|
12029
|
-
exists:
|
|
12087
|
+
exists: existsSync23(remotePath),
|
|
12030
12088
|
entries: 0,
|
|
12031
12089
|
valid: 0,
|
|
12032
12090
|
invalidIds: [],
|
|
@@ -12035,7 +12093,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
12035
12093
|
if (!report.exists) return report;
|
|
12036
12094
|
let cache;
|
|
12037
12095
|
try {
|
|
12038
|
-
cache = parseCache(JSON.parse(await
|
|
12096
|
+
cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
12039
12097
|
} catch (err) {
|
|
12040
12098
|
report.error = err instanceof Error ? err.message : String(err);
|
|
12041
12099
|
return report;
|
|
@@ -12099,7 +12157,7 @@ async function runAccept(args) {
|
|
|
12099
12157
|
let intact = true;
|
|
12100
12158
|
for (const output of entry.outputs) {
|
|
12101
12159
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
12102
|
-
if (!
|
|
12160
|
+
if (!existsSync24(file) || await sha256File(file) !== output.sha256) {
|
|
12103
12161
|
intact = false;
|
|
12104
12162
|
break;
|
|
12105
12163
|
}
|
|
@@ -12154,42 +12212,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
12154
12212
|
}
|
|
12155
12213
|
|
|
12156
12214
|
// src/cli/commands/init.ts
|
|
12157
|
-
import
|
|
12158
|
-
import { existsSync as
|
|
12215
|
+
import path32 from "path";
|
|
12216
|
+
import { existsSync as existsSync25 } from "fs";
|
|
12159
12217
|
async function runInit(args) {
|
|
12160
12218
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
12161
|
-
const root =
|
|
12162
|
-
if (!
|
|
12219
|
+
const root = path32.resolve(args.from);
|
|
12220
|
+
if (!existsSync25(root)) throw new Error(`No directory at ${root}`);
|
|
12163
12221
|
const generator = args.generator ?? "map";
|
|
12164
12222
|
if (generator !== "1dir" && generator !== "map") {
|
|
12165
12223
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
12166
12224
|
}
|
|
12167
|
-
const target =
|
|
12225
|
+
const target = path32.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
12168
12226
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
12169
12227
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
12170
12228
|
const manifest = buildManifest(
|
|
12171
|
-
args.name ??
|
|
12229
|
+
args.name ?? path32.basename(path32.dirname(target)),
|
|
12172
12230
|
args.styles[0] ?? "base",
|
|
12173
12231
|
generator,
|
|
12174
|
-
|
|
12232
|
+
path32.relative(path32.dirname(target), root) || ".",
|
|
12175
12233
|
assets
|
|
12176
12234
|
);
|
|
12177
12235
|
await writeManifestFile(target, manifest);
|
|
12178
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
12236
|
+
log(` scanned ${assets.length} PNG(s) under ${path32.relative(process.cwd(), root)}`);
|
|
12179
12237
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
12180
|
-
log(` wrote ${
|
|
12238
|
+
log(` wrote ${path32.relative(process.cwd(), target)}`);
|
|
12181
12239
|
log(`
|
|
12182
12240
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
12183
12241
|
log(` PixelLab account instead of inventing them:`);
|
|
12184
12242
|
log(`
|
|
12185
|
-
pixelkiln adopt --manifest ${
|
|
12243
|
+
pixelkiln adopt --manifest ${path32.relative(process.cwd(), target)} --write-prompts
|
|
12186
12244
|
`);
|
|
12187
12245
|
}
|
|
12188
12246
|
|
|
12189
12247
|
// src/pipeline/doctor.ts
|
|
12190
|
-
import { constants, existsSync as
|
|
12248
|
+
import { constants, existsSync as existsSync26 } from "fs";
|
|
12191
12249
|
import { access, stat as stat4 } from "fs/promises";
|
|
12192
|
-
import
|
|
12250
|
+
import path33 from "path";
|
|
12193
12251
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
12194
12252
|
const checks = [];
|
|
12195
12253
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -12200,8 +12258,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12200
12258
|
);
|
|
12201
12259
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
12202
12260
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
12203
|
-
|
|
12204
|
-
...spec.quality ? [
|
|
12261
|
+
path33.dirname(spec.outFile),
|
|
12262
|
+
...spec.quality ? [path33.dirname(spec.quality.outFile)] : []
|
|
12205
12263
|
]))];
|
|
12206
12264
|
const unwritable = [];
|
|
12207
12265
|
for (const dir of dirs) {
|
|
@@ -12224,7 +12282,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12224
12282
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
12225
12283
|
const output = entry.outputs[index];
|
|
12226
12284
|
const spec = specByKey.get(key);
|
|
12227
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
12285
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path33.dirname(lockPath));
|
|
12228
12286
|
const owner = outputOwners.get(absolute);
|
|
12229
12287
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
12230
12288
|
outputOwners.set(absolute, key);
|
|
@@ -12239,7 +12297,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12239
12297
|
} else {
|
|
12240
12298
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
12241
12299
|
}
|
|
12242
|
-
const cacheDir =
|
|
12300
|
+
const cacheDir = path33.join(path33.dirname(lockPath), ".pixelkiln", "cache");
|
|
12243
12301
|
const stranded = [];
|
|
12244
12302
|
const invalidState = [];
|
|
12245
12303
|
const staleMapUrls = [];
|
|
@@ -12255,11 +12313,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12255
12313
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
12256
12314
|
let hasCache = false;
|
|
12257
12315
|
for (const output of entry.outputs) {
|
|
12258
|
-
const cached =
|
|
12316
|
+
const cached = path33.join(
|
|
12259
12317
|
cacheDir,
|
|
12260
12318
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
12261
12319
|
);
|
|
12262
|
-
if (
|
|
12320
|
+
if (existsSync26(cached) && await sha256File(cached) === output.sha256) {
|
|
12263
12321
|
hasCache = true;
|
|
12264
12322
|
break;
|
|
12265
12323
|
}
|
|
@@ -12355,12 +12413,158 @@ async function nearestExistingDirectory(start) {
|
|
|
12355
12413
|
if ((await stat4(current)).isDirectory()) return current;
|
|
12356
12414
|
} catch {
|
|
12357
12415
|
}
|
|
12358
|
-
const parent =
|
|
12416
|
+
const parent = path33.dirname(current);
|
|
12359
12417
|
if (parent === current) return current;
|
|
12360
12418
|
current = parent;
|
|
12361
12419
|
}
|
|
12362
12420
|
}
|
|
12363
12421
|
|
|
12422
|
+
// src/pipeline/audit.ts
|
|
12423
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
12424
|
+
import { existsSync as existsSync27 } from "fs";
|
|
12425
|
+
import path34 from "path";
|
|
12426
|
+
function paletteDistance(asset, reference) {
|
|
12427
|
+
if (!asset.length || !reference.length) return 0;
|
|
12428
|
+
let total = 0;
|
|
12429
|
+
let weight = 0;
|
|
12430
|
+
for (const color of asset) {
|
|
12431
|
+
let nearest = Infinity;
|
|
12432
|
+
for (const ref of reference) nearest = Math.min(nearest, colorDistance(color, ref));
|
|
12433
|
+
total += nearest * color.weight;
|
|
12434
|
+
weight += color.weight;
|
|
12435
|
+
}
|
|
12436
|
+
return weight === 0 ? 0 : total / weight;
|
|
12437
|
+
}
|
|
12438
|
+
function mergePalettes(palettes, topN = 24) {
|
|
12439
|
+
const totals = /* @__PURE__ */ new Map();
|
|
12440
|
+
for (const palette of palettes) {
|
|
12441
|
+
for (const color of palette) {
|
|
12442
|
+
const key = `${color.r},${color.g},${color.b}`;
|
|
12443
|
+
const existing = totals.get(key);
|
|
12444
|
+
if (existing) existing.weight += color.weight;
|
|
12445
|
+
else totals.set(key, { ...color });
|
|
12446
|
+
}
|
|
12447
|
+
}
|
|
12448
|
+
const merged = [...totals.values()].sort((a, b) => b.weight - a.weight).slice(0, topN);
|
|
12449
|
+
const sum = merged.reduce((s, c) => s + c.weight, 0) || 1;
|
|
12450
|
+
return merged.map((c) => ({ ...c, weight: c.weight / sum }));
|
|
12451
|
+
}
|
|
12452
|
+
async function auditStyle(loaded, specs, styleId, lock) {
|
|
12453
|
+
const style = loaded.manifest.styles[styleId];
|
|
12454
|
+
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
12455
|
+
const mine = specs.filter((s) => s.styleId === styleId);
|
|
12456
|
+
const assets = [];
|
|
12457
|
+
const missing = [];
|
|
12458
|
+
const unreadable = [];
|
|
12459
|
+
for (const spec of mine) {
|
|
12460
|
+
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
12461
|
+
if (!existsSync27(output.absolutePath)) {
|
|
12462
|
+
missing.push(output.id);
|
|
12463
|
+
continue;
|
|
12464
|
+
}
|
|
12465
|
+
try {
|
|
12466
|
+
const png = decodePng(await readFile22(output.absolutePath));
|
|
12467
|
+
const palette = extractPalette(png, 12);
|
|
12468
|
+
assets.push({
|
|
12469
|
+
assetId: spec.assetId,
|
|
12470
|
+
...output.role ? { outputRole: output.role } : {},
|
|
12471
|
+
id: output.id,
|
|
12472
|
+
file: output.absolutePath,
|
|
12473
|
+
width: png.width,
|
|
12474
|
+
height: png.height,
|
|
12475
|
+
paletteDistance: 0,
|
|
12476
|
+
// filled once the reference is known
|
|
12477
|
+
transparency: transparencyRatio(png),
|
|
12478
|
+
colorCount: countColors(png),
|
|
12479
|
+
palette
|
|
12480
|
+
});
|
|
12481
|
+
} catch (err) {
|
|
12482
|
+
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
}
|
|
12486
|
+
let reference = [];
|
|
12487
|
+
let referenceFromStyleImages = false;
|
|
12488
|
+
const refPalettes = [];
|
|
12489
|
+
for (const img of style.styleImages) {
|
|
12490
|
+
const abs = path34.resolve(loaded.root, img.path);
|
|
12491
|
+
if (!existsSync27(abs)) continue;
|
|
12492
|
+
try {
|
|
12493
|
+
refPalettes.push(extractPalette(decodePng(await readFile22(abs)), 12));
|
|
12494
|
+
} catch {
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
if (refPalettes.length) {
|
|
12498
|
+
reference = mergePalettes(refPalettes);
|
|
12499
|
+
referenceFromStyleImages = true;
|
|
12500
|
+
for (const asset of assets) {
|
|
12501
|
+
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
12502
|
+
}
|
|
12503
|
+
} else {
|
|
12504
|
+
reference = mergePalettes(assets.map((a) => a.palette));
|
|
12505
|
+
for (const asset of assets) {
|
|
12506
|
+
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
12507
|
+
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
12508
|
+
}
|
|
12509
|
+
}
|
|
12510
|
+
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
12511
|
+
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
12512
|
+
}
|
|
12513
|
+
function countColors(png) {
|
|
12514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12515
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
12516
|
+
if (png.pixels[i + 3] < 128) continue;
|
|
12517
|
+
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
12518
|
+
}
|
|
12519
|
+
return seen.size;
|
|
12520
|
+
}
|
|
12521
|
+
function outliers(audit, sigma = 1.5) {
|
|
12522
|
+
const n = audit.assets.length;
|
|
12523
|
+
if (n < 3) return [];
|
|
12524
|
+
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
12525
|
+
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
12526
|
+
const sd = Math.sqrt(variance);
|
|
12527
|
+
if (sd === 0) return [];
|
|
12528
|
+
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
12529
|
+
}
|
|
12530
|
+
function evaluateAudit(audit, thresholds = {}) {
|
|
12531
|
+
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
12532
|
+
const relative = outliers(audit, resolved.sigma);
|
|
12533
|
+
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
12534
|
+
const violations = [];
|
|
12535
|
+
for (const asset of audit.assets) {
|
|
12536
|
+
const reasons = [];
|
|
12537
|
+
if (relativeIds.has(asset.id)) {
|
|
12538
|
+
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
12539
|
+
}
|
|
12540
|
+
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
12541
|
+
reasons.push(
|
|
12542
|
+
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
12543
|
+
);
|
|
12544
|
+
}
|
|
12545
|
+
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
12546
|
+
reasons.push(
|
|
12547
|
+
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
12548
|
+
);
|
|
12549
|
+
}
|
|
12550
|
+
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
12551
|
+
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
12552
|
+
}
|
|
12553
|
+
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
12554
|
+
}
|
|
12555
|
+
return {
|
|
12556
|
+
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
12557
|
+
thresholds: resolved,
|
|
12558
|
+
outliers: [...relativeIds],
|
|
12559
|
+
violations,
|
|
12560
|
+
missing: [...audit.missing],
|
|
12561
|
+
unreadable: [...audit.unreadable]
|
|
12562
|
+
};
|
|
12563
|
+
}
|
|
12564
|
+
function hex(c) {
|
|
12565
|
+
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
12566
|
+
}
|
|
12567
|
+
|
|
12364
12568
|
// src/cli/commands/inspect.ts
|
|
12365
12569
|
async function runStatus(args) {
|
|
12366
12570
|
const { lock } = await openProject2(args);
|
|
@@ -13522,7 +13726,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
13522
13726
|
if (existsSync28(absolute) && !options.force) {
|
|
13523
13727
|
const current = await readFile24(absolute);
|
|
13524
13728
|
if (!current.equals(data)) {
|
|
13525
|
-
throw new
|
|
13729
|
+
throw new OverwriteRefusedError(
|
|
13730
|
+
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
13731
|
+
[absolute]
|
|
13732
|
+
);
|
|
13526
13733
|
}
|
|
13527
13734
|
}
|
|
13528
13735
|
const result = await writeArtifactBundle([{ path: absolute, data }]);
|
|
@@ -13913,7 +14120,7 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
13913
14120
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
13914
14121
|
);
|
|
13915
14122
|
}
|
|
13916
|
-
return { recipe: parsed.data, path: absolute, dir: path39.dirname(absolute), bundled };
|
|
14123
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path39.dirname(absolute), bundled };
|
|
13917
14124
|
}
|
|
13918
14125
|
async function walkRecipeFiles(root) {
|
|
13919
14126
|
if (!existsSync29(root)) return [];
|
|
@@ -13983,7 +14190,7 @@ async function verifyFile(root, file) {
|
|
|
13983
14190
|
}
|
|
13984
14191
|
async function verifyRecipe(target, options = {}) {
|
|
13985
14192
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
13986
|
-
const actualIntegrity = recipeDigest(loaded.
|
|
14193
|
+
const actualIntegrity = recipeDigest(loaded.authored);
|
|
13987
14194
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
13988
14195
|
const modelRoot = options.modelRoot ? path39.resolve(options.modelRoot) : null;
|
|
13989
14196
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
@@ -14051,8 +14258,9 @@ async function installRecipe(target, options = {}) {
|
|
|
14051
14258
|
if (!existsSync29(file.path)) continue;
|
|
14052
14259
|
const current = await readFile26(file.path);
|
|
14053
14260
|
if (!current.equals(file.data)) {
|
|
14054
|
-
throw new
|
|
14055
|
-
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files
|
|
14261
|
+
throw new OverwriteRefusedError(
|
|
14262
|
+
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
14263
|
+
[file.path]
|
|
14056
14264
|
);
|
|
14057
14265
|
}
|
|
14058
14266
|
}
|
|
@@ -14519,9 +14727,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
14519
14727
|
|
|
14520
14728
|
// src/cli.ts
|
|
14521
14729
|
main().catch((err) => {
|
|
14730
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
14731
|
+
const hint = err instanceof PixelKilnError && err.hint ? ` ${err.hint}
|
|
14732
|
+
` : "";
|
|
14522
14733
|
console.error(`
|
|
14523
|
-
error: ${
|
|
14524
|
-
`);
|
|
14525
|
-
process.exit(
|
|
14734
|
+
error: ${message6}
|
|
14735
|
+
${hint}`);
|
|
14736
|
+
process.exit(exitCodeFor(err));
|
|
14526
14737
|
});
|
|
14527
14738
|
//# sourceMappingURL=cli.js.map
|