pixelkiln 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROVIDERS.md +80 -50
- package/README.md +24 -13
- package/dist/cli.js +1347 -897
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +694 -242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +655 -204
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +7 -5
- package/docs/ARCHITECTURE.md +15 -6
- package/docs/CLI.md +6 -4
- package/docs/COMFYUI.md +191 -0
- package/docs/GETTING_STARTED.md +14 -7
- package/docs/LIBRARY.md +9 -7
- package/docs/MANIFEST.md +54 -8
- package/docs/PROVIDER_BENCHMARK.md +46 -8
- package/docs/README.md +3 -2
- package/docs/RETRO_DIFFUSION.md +1 -1
- package/examples/comfyui/README.md +24 -0
- package/examples/comfyui/pixelkiln.manifest.json +35 -0
- package/examples/comfyui/workflow-api.json +59 -0
- package/package.json +2 -1
- package/schema/manifest.schema.json +4 -4
- package/skills/pixelkiln/SKILL.md +5 -3
- package/skills/pixelkiln/references/comfyui.md +46 -0
- package/skills/pixelkiln/references/mixed-providers.md +15 -7
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AssetSchema: () => AssetSchema,
|
|
34
|
+
ComfyUIProvider: () => ComfyUIProvider,
|
|
34
35
|
DEFAULT_RATE_LIMIT: () => DEFAULT_RATE_LIMIT,
|
|
35
36
|
FAKE_PNG: () => FAKE_PNG,
|
|
36
37
|
FakeProvider: () => FakeProvider,
|
|
@@ -202,8 +203,8 @@ var StyleSchema = import_zod.z.object({
|
|
|
202
203
|
// `map` is the default because it is 20-40x cheaper and correct for any
|
|
203
204
|
// asset that is not going to be rotated or animated.
|
|
204
205
|
generator: GeneratorSchema.default("map"),
|
|
205
|
-
/** Square edge length
|
|
206
|
-
size: import_zod.z.number().int().min(
|
|
206
|
+
/** Square edge length. The active provider owns its exact limit. */
|
|
207
|
+
size: import_zod.z.number().int().min(16).max(8192).optional(),
|
|
207
208
|
view: import_zod.z.string().optional(),
|
|
208
209
|
/** Appended to every asset prompt in this style. Where the look is defined. */
|
|
209
210
|
promptSuffix: import_zod.z.string().default(""),
|
|
@@ -319,8 +320,8 @@ var AssetSchema = import_zod.z.object({
|
|
|
319
320
|
/** Subdirectory under the style's outDir. Optional. */
|
|
320
321
|
category: import_zod.z.string().optional(),
|
|
321
322
|
/** Overrides the style default. `map` generator only. */
|
|
322
|
-
width: import_zod.z.number().int().min(16).max(
|
|
323
|
-
height: import_zod.z.number().int().min(16).max(
|
|
323
|
+
width: import_zod.z.number().int().min(16).max(8192).optional(),
|
|
324
|
+
height: import_zod.z.number().int().min(16).max(8192).optional(),
|
|
324
325
|
/** Overrides the style default. `1dir` generator only. */
|
|
325
326
|
size: import_zod.z.number().int().min(32).max(256).optional(),
|
|
326
327
|
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
@@ -578,11 +579,11 @@ var PixelLabClient = class {
|
|
|
578
579
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
579
580
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
580
581
|
*/
|
|
581
|
-
async request(
|
|
582
|
+
async request(path18, init, attempt = 0) {
|
|
582
583
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
583
584
|
let res;
|
|
584
585
|
try {
|
|
585
|
-
res = await fetch(`${BASE}${
|
|
586
|
+
res = await fetch(`${BASE}${path18}`, {
|
|
586
587
|
...init,
|
|
587
588
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
588
589
|
headers: {
|
|
@@ -594,24 +595,24 @@ var PixelLabClient = class {
|
|
|
594
595
|
} catch (err) {
|
|
595
596
|
if (attempt < MAX_RETRIES) {
|
|
596
597
|
await sleep(backoffMs(attempt));
|
|
597
|
-
return this.request(
|
|
598
|
+
return this.request(path18, init, attempt + 1);
|
|
598
599
|
}
|
|
599
600
|
throw err;
|
|
600
601
|
}
|
|
601
602
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
602
603
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
603
604
|
await sleep(waitMs);
|
|
604
|
-
return this.request(
|
|
605
|
+
return this.request(path18, init, attempt + 1);
|
|
605
606
|
}
|
|
606
607
|
const text = await res.text();
|
|
607
608
|
if (!res.ok) {
|
|
608
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
609
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path18} \u2192 ${res.status}`, res.status, text);
|
|
609
610
|
}
|
|
610
611
|
if (!text) return {};
|
|
611
612
|
try {
|
|
612
613
|
return JSON.parse(text);
|
|
613
614
|
} catch {
|
|
614
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
615
|
+
throw new Error(`${init?.method ?? "GET"} ${path18} returned invalid JSON`);
|
|
615
616
|
}
|
|
616
617
|
}
|
|
617
618
|
async balance() {
|
|
@@ -1375,6 +1376,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1375
1376
|
};
|
|
1376
1377
|
}
|
|
1377
1378
|
validate(spec, styleImages) {
|
|
1379
|
+
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
1380
|
+
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
1381
|
+
}
|
|
1382
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
|
|
1383
|
+
throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
|
|
1384
|
+
}
|
|
1378
1385
|
if (spec.generator === "map") {
|
|
1379
1386
|
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1380
1387
|
requirePixelLabOption("outline", spec.outline, [
|
|
@@ -1651,9 +1658,9 @@ var RetroDiffusionClient = class {
|
|
|
1651
1658
|
}
|
|
1652
1659
|
return balance;
|
|
1653
1660
|
}
|
|
1654
|
-
async call(
|
|
1661
|
+
async call(path18, init = {}) {
|
|
1655
1662
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1656
|
-
const response = await this.request(`${this.baseUrl}${
|
|
1663
|
+
const response = await this.request(`${this.baseUrl}${path18}`, {
|
|
1657
1664
|
...init,
|
|
1658
1665
|
headers: {
|
|
1659
1666
|
"Content-Type": "application/json",
|
|
@@ -1923,8 +1930,470 @@ function retroError(value) {
|
|
|
1923
1930
|
return JSON.stringify(value);
|
|
1924
1931
|
}
|
|
1925
1932
|
|
|
1926
|
-
// src/providers/
|
|
1933
|
+
// src/providers/comfyui.ts
|
|
1934
|
+
var import_node_crypto3 = require("crypto");
|
|
1935
|
+
var import_promises2 = require("fs/promises");
|
|
1936
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
1937
|
+
|
|
1938
|
+
// src/hash.ts
|
|
1927
1939
|
var import_node_crypto2 = require("crypto");
|
|
1940
|
+
var import_promises = require("fs/promises");
|
|
1941
|
+
function sha256(data) {
|
|
1942
|
+
return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
|
|
1943
|
+
}
|
|
1944
|
+
async function sha256File(path18) {
|
|
1945
|
+
return sha256(await (0, import_promises.readFile)(path18));
|
|
1946
|
+
}
|
|
1947
|
+
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
|
|
1948
|
+
return sha256(
|
|
1949
|
+
JSON.stringify({
|
|
1950
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
1951
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1952
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1953
|
+
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
1954
|
+
generator: spec.generator,
|
|
1955
|
+
prompt: spec.prompt,
|
|
1956
|
+
width: spec.width,
|
|
1957
|
+
height: spec.height,
|
|
1958
|
+
view: spec.view,
|
|
1959
|
+
outline: spec.outline ?? null,
|
|
1960
|
+
shading: spec.shading ?? null,
|
|
1961
|
+
detail: spec.detail ?? null,
|
|
1962
|
+
seed: spec.seed ?? null,
|
|
1963
|
+
palette: spec.palette,
|
|
1964
|
+
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1965
|
+
// undefined for every other generator. `tileSize` is intentionally
|
|
1966
|
+
// absent — width/height are derived from it, so it is already covered.
|
|
1967
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1968
|
+
tileType: spec.tileType,
|
|
1969
|
+
tileView: spec.tileView,
|
|
1970
|
+
tileFeature: spec.tileFeature,
|
|
1971
|
+
outlineMode: spec.outlineMode,
|
|
1972
|
+
styleImages: styleImageHashes
|
|
1973
|
+
})
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/providers/comfyui.ts
|
|
1978
|
+
var DEFAULT_BASE_URL2 = "http://127.0.0.1:8188";
|
|
1979
|
+
var ComfyUIClient = class {
|
|
1980
|
+
constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL2, request = fetch) {
|
|
1981
|
+
this.request = request;
|
|
1982
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
1983
|
+
}
|
|
1984
|
+
request;
|
|
1985
|
+
baseUrl;
|
|
1986
|
+
async checkConnection() {
|
|
1987
|
+
const value = await this.json("system_stats");
|
|
1988
|
+
if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
|
|
1989
|
+
}
|
|
1990
|
+
async submit(workflow) {
|
|
1991
|
+
const value = await this.json("prompt", {
|
|
1992
|
+
method: "POST",
|
|
1993
|
+
body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto3.randomUUID)() })
|
|
1994
|
+
});
|
|
1995
|
+
if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
|
|
1996
|
+
const promptId = value.prompt_id;
|
|
1997
|
+
if (typeof promptId !== "string" || !promptId) {
|
|
1998
|
+
const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
|
|
1999
|
+
throw new Error(
|
|
2000
|
+
"ComfyUI did not return a prompt id" + (nodes.length ? `; invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}` : "")
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
return promptId;
|
|
2004
|
+
}
|
|
2005
|
+
async history(promptId) {
|
|
2006
|
+
return this.json(`history/${encodeURIComponent(promptId)}`);
|
|
2007
|
+
}
|
|
2008
|
+
viewUrl(image) {
|
|
2009
|
+
const url = this.endpoint("view");
|
|
2010
|
+
url.searchParams.set("filename", image.filename);
|
|
2011
|
+
url.searchParams.set("subfolder", image.subfolder);
|
|
2012
|
+
url.searchParams.set("type", image.type);
|
|
2013
|
+
return url.toString();
|
|
2014
|
+
}
|
|
2015
|
+
async download(source) {
|
|
2016
|
+
let target = source;
|
|
2017
|
+
if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
|
|
2018
|
+
const response = await this.request(target);
|
|
2019
|
+
if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
|
|
2020
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2021
|
+
}
|
|
2022
|
+
endpoint(route) {
|
|
2023
|
+
return new URL(route.replace(/^\/+/, ""), `${this.baseUrl}/`);
|
|
2024
|
+
}
|
|
2025
|
+
async json(route, init = {}) {
|
|
2026
|
+
const url = this.endpoint(route);
|
|
2027
|
+
const response = await this.request(url, {
|
|
2028
|
+
...init,
|
|
2029
|
+
headers: {
|
|
2030
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
2031
|
+
...init.headers
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
const text = await response.text();
|
|
2035
|
+
let value = null;
|
|
2036
|
+
try {
|
|
2037
|
+
value = text ? JSON.parse(text) : null;
|
|
2038
|
+
} catch {
|
|
2039
|
+
if (response.ok) throw new Error(`ComfyUI returned invalid JSON from ${url.pathname}`);
|
|
2040
|
+
value = null;
|
|
2041
|
+
}
|
|
2042
|
+
if (!response.ok) {
|
|
2043
|
+
const detail = apiError(value);
|
|
2044
|
+
throw new Error(
|
|
2045
|
+
`ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
return value;
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
var ComfyUIProvider = class _ComfyUIProvider {
|
|
2052
|
+
constructor(client = new ComfyUIClient()) {
|
|
2053
|
+
this.client = client;
|
|
2054
|
+
}
|
|
2055
|
+
client;
|
|
2056
|
+
id = "comfyui";
|
|
2057
|
+
static fromEnv() {
|
|
2058
|
+
return new _ComfyUIProvider();
|
|
2059
|
+
}
|
|
2060
|
+
static forOffline() {
|
|
2061
|
+
return new _ComfyUIProvider(new ComfyUIClient(DEFAULT_BASE_URL2));
|
|
2062
|
+
}
|
|
2063
|
+
static forDownloads() {
|
|
2064
|
+
return new _ComfyUIProvider();
|
|
2065
|
+
}
|
|
2066
|
+
async resolveOptions(value, context) {
|
|
2067
|
+
const options = parseOptions(value);
|
|
2068
|
+
const workflowPath = import_node_path2.default.resolve(context.root, options.workflowFile);
|
|
2069
|
+
let raw;
|
|
2070
|
+
try {
|
|
2071
|
+
raw = await (0, import_promises2.readFile)(workflowPath, "utf8");
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
let parsed;
|
|
2078
|
+
try {
|
|
2079
|
+
parsed = JSON.parse(raw);
|
|
2080
|
+
} catch {
|
|
2081
|
+
throw new Error(`ComfyUI workflow for style "${context.styleId}" is not valid JSON: ${workflowPath}`);
|
|
2082
|
+
}
|
|
2083
|
+
const workflow = parseWorkflow(parsed, workflowPath);
|
|
2084
|
+
validateWorkflowBindings(workflow, options);
|
|
2085
|
+
const workflowSha256 = sha256(JSON.stringify(canonical(workflow)));
|
|
2086
|
+
const resolved = { ...options, workflow, workflowSha256 };
|
|
2087
|
+
return {
|
|
2088
|
+
options: resolved,
|
|
2089
|
+
identity: {
|
|
2090
|
+
outputNodeId: options.outputNodeId,
|
|
2091
|
+
numImages: options.numImages,
|
|
2092
|
+
bindings: options.bindings,
|
|
2093
|
+
workflowSha256
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
supports(generator) {
|
|
2098
|
+
return generator === "map";
|
|
2099
|
+
}
|
|
2100
|
+
estimate(spec) {
|
|
2101
|
+
return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
|
|
2102
|
+
}
|
|
2103
|
+
validate(spec, styleImages) {
|
|
2104
|
+
const options = resolvedOptions(spec);
|
|
2105
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
2106
|
+
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
2107
|
+
}
|
|
2108
|
+
if (styleImages.length) {
|
|
2109
|
+
throw new Error("ComfyUI styleImages are not supported yet; keep references inside the workflow");
|
|
2110
|
+
}
|
|
2111
|
+
if (spec.palette.length) {
|
|
2112
|
+
throw new Error("ComfyUI does not map PixelKiln palette values yet; encode palette control in the workflow");
|
|
2113
|
+
}
|
|
2114
|
+
if (spec.seed != null && !options.bindings.seed) {
|
|
2115
|
+
throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
|
|
2116
|
+
}
|
|
2117
|
+
validateWorkflowBindings(options.workflow, options);
|
|
2118
|
+
}
|
|
2119
|
+
rateLimit() {
|
|
2120
|
+
return { spacingMs: 0, maxInFlight: 1 };
|
|
2121
|
+
}
|
|
2122
|
+
async submit(spec, styleImages) {
|
|
2123
|
+
this.validate(spec, styleImages);
|
|
2124
|
+
const options = resolvedOptions(spec);
|
|
2125
|
+
const workflow = structuredClone(options.workflow);
|
|
2126
|
+
setBinding(workflow, options.bindings.prompt, spec.prompt);
|
|
2127
|
+
setBinding(workflow, options.bindings.width, spec.width);
|
|
2128
|
+
setBinding(workflow, options.bindings.height, spec.height);
|
|
2129
|
+
setBinding(workflow, options.bindings.batchSize, options.numImages);
|
|
2130
|
+
if (spec.seed != null && options.bindings.seed) {
|
|
2131
|
+
setBinding(workflow, options.bindings.seed, spec.seed);
|
|
2132
|
+
}
|
|
2133
|
+
const promptId = await this.client.submit(workflow);
|
|
2134
|
+
return { jobId: encodeJob(promptId, options.outputNodeId) };
|
|
2135
|
+
}
|
|
2136
|
+
async poll(jobId, _generator, context) {
|
|
2137
|
+
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
2138
|
+
const history = await this.client.history(promptId);
|
|
2139
|
+
const entry = historyEntry(history, promptId);
|
|
2140
|
+
if (!entry) return { status: "processing" };
|
|
2141
|
+
const status = isObject(entry.status) ? entry.status : {};
|
|
2142
|
+
const statusText = typeof status.status_str === "string" ? status.status_str : "";
|
|
2143
|
+
if (statusText === "error") {
|
|
2144
|
+
return { status: "failed", error: historyError(status) };
|
|
2145
|
+
}
|
|
2146
|
+
if (status.completed === false) return { status: "processing" };
|
|
2147
|
+
const images = outputImages(entry, outputNodeId);
|
|
2148
|
+
if (images instanceof Error) return { status: "failed", error: images.message };
|
|
2149
|
+
const expected = context?.spec ? resolvedOptions(context.spec).numImages : null;
|
|
2150
|
+
if (expected != null && images.length !== expected) {
|
|
2151
|
+
return {
|
|
2152
|
+
status: "failed",
|
|
2153
|
+
error: `ComfyUI output node "${outputNodeId}" returned ${images.length} image(s), expected ${expected}`
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
const metadata = context?.spec ? comfyMetadata(context.spec, promptId, outputNodeId, images) : {
|
|
2157
|
+
promptId,
|
|
2158
|
+
outputNodeId,
|
|
2159
|
+
outputCount: images.length
|
|
2160
|
+
};
|
|
2161
|
+
if (images.length > 1) {
|
|
2162
|
+
return {
|
|
2163
|
+
status: "review",
|
|
2164
|
+
candidateUrls: images.map((image) => this.client.viewUrl(image)),
|
|
2165
|
+
metadata
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
const sourceUrl = sourceRef(images[0]);
|
|
2169
|
+
return {
|
|
2170
|
+
status: "ready",
|
|
2171
|
+
objectId: `${promptId}#${outputNodeId}#0`,
|
|
2172
|
+
sourceUrl,
|
|
2173
|
+
sources: [{ url: sourceUrl, mediaType: MediaType.PNG }],
|
|
2174
|
+
metadata
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
async selectCandidate(jobId, index) {
|
|
2178
|
+
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
2179
|
+
const history = await this.client.history(promptId);
|
|
2180
|
+
const entry = historyEntry(history, promptId);
|
|
2181
|
+
if (!entry) throw new Error(`ComfyUI prompt ${promptId} is not complete`);
|
|
2182
|
+
const images = outputImages(entry, outputNodeId);
|
|
2183
|
+
if (images instanceof Error) throw images;
|
|
2184
|
+
const image = images[index];
|
|
2185
|
+
if (!image) throw new Error(`ComfyUI prompt ${promptId} has no candidate at index ${index}`);
|
|
2186
|
+
return {
|
|
2187
|
+
objectId: `${promptId}#${outputNodeId}#${index}`,
|
|
2188
|
+
sourceUrl: sourceRef(image)
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
async download(url) {
|
|
2192
|
+
return this.client.download(url);
|
|
2193
|
+
}
|
|
2194
|
+
async checkConnection() {
|
|
2195
|
+
await this.client.checkConnection();
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
function parseOptions(value) {
|
|
2199
|
+
const allowed = /* @__PURE__ */ new Set(["workflowFile", "outputNodeId", "numImages", "bindings", "workflow", "workflowSha256"]);
|
|
2200
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
2201
|
+
if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
|
|
2202
|
+
const workflowFile = requiredString(value.workflowFile, "workflowFile");
|
|
2203
|
+
const outputNodeId = requiredString(value.outputNodeId, "outputNodeId");
|
|
2204
|
+
const numImages = value.numImages ?? 1;
|
|
2205
|
+
if (!Number.isInteger(numImages) || Number(numImages) < 1 || Number(numImages) > 16) {
|
|
2206
|
+
throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
|
|
2207
|
+
}
|
|
2208
|
+
if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
|
|
2209
|
+
const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
|
|
2210
|
+
const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
|
|
2211
|
+
if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
|
|
2212
|
+
const bindings = {
|
|
2213
|
+
prompt: parseBinding(value.bindings.prompt, "prompt"),
|
|
2214
|
+
width: parseBinding(value.bindings.width, "width"),
|
|
2215
|
+
height: parseBinding(value.bindings.height, "height"),
|
|
2216
|
+
batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
|
|
2217
|
+
...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
|
|
2218
|
+
};
|
|
2219
|
+
const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
|
|
2220
|
+
const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
|
|
2221
|
+
return {
|
|
2222
|
+
workflowFile,
|
|
2223
|
+
outputNodeId,
|
|
2224
|
+
numImages: Number(numImages),
|
|
2225
|
+
bindings,
|
|
2226
|
+
...workflow ? { workflow } : {},
|
|
2227
|
+
...workflowSha256 ? { workflowSha256 } : {}
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
function resolvedOptions(spec) {
|
|
2231
|
+
const options = parseOptions(spec.providerOptions);
|
|
2232
|
+
if (!options.workflow || !options.workflowSha256) {
|
|
2233
|
+
throw new Error("ComfyUI workflow options were not resolved from workflowFile");
|
|
2234
|
+
}
|
|
2235
|
+
return options;
|
|
2236
|
+
}
|
|
2237
|
+
function parseBinding(value, name) {
|
|
2238
|
+
if (!isObject(value)) throw new Error(`ComfyUI bindings.${name} must be an object`);
|
|
2239
|
+
const extra = Object.keys(value).filter((key) => key !== "nodeId" && key !== "input");
|
|
2240
|
+
if (extra.length) throw new Error(`Unknown ComfyUI bindings.${name} field(s): ${extra.join(", ")}`);
|
|
2241
|
+
return {
|
|
2242
|
+
nodeId: requiredString(value.nodeId, `bindings.${name}.nodeId`),
|
|
2243
|
+
input: requiredString(value.input, `bindings.${name}.input`)
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function parseWorkflow(value, label) {
|
|
2247
|
+
if (!isObject(value) || !Object.keys(value).length) {
|
|
2248
|
+
throw new Error(`ComfyUI workflow is not a non-empty API-format object: ${label}`);
|
|
2249
|
+
}
|
|
2250
|
+
for (const [nodeId, node] of Object.entries(value)) {
|
|
2251
|
+
if (!isObject(node) || typeof node.class_type !== "string" || !isObject(node.inputs)) {
|
|
2252
|
+
throw new Error(`ComfyUI workflow node "${nodeId}" must contain class_type and inputs`);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return value;
|
|
2256
|
+
}
|
|
2257
|
+
function validateWorkflowBindings(workflow, options) {
|
|
2258
|
+
for (const [name, binding] of Object.entries(options.bindings)) {
|
|
2259
|
+
if (!binding) continue;
|
|
2260
|
+
const node = workflow[binding.nodeId];
|
|
2261
|
+
if (!node) throw new Error(`ComfyUI ${name} binding refers to missing node "${binding.nodeId}"`);
|
|
2262
|
+
if (!Object.hasOwn(node.inputs, binding.input)) {
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
`ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (!workflow[options.outputNodeId]) {
|
|
2269
|
+
throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function setBinding(workflow, binding, value) {
|
|
2273
|
+
const node = workflow[binding.nodeId];
|
|
2274
|
+
if (!node || !Object.hasOwn(node.inputs, binding.input)) {
|
|
2275
|
+
throw new Error(`ComfyUI binding ${binding.nodeId}.${binding.input} is unavailable`);
|
|
2276
|
+
}
|
|
2277
|
+
node.inputs[binding.input] = value;
|
|
2278
|
+
}
|
|
2279
|
+
function historyEntry(history, promptId) {
|
|
2280
|
+
if (!isObject(history)) throw new Error("ComfyUI returned invalid history JSON");
|
|
2281
|
+
const entry = history[promptId];
|
|
2282
|
+
if (entry == null) return null;
|
|
2283
|
+
if (!isObject(entry)) throw new Error(`ComfyUI returned invalid history for prompt ${promptId}`);
|
|
2284
|
+
return entry;
|
|
2285
|
+
}
|
|
2286
|
+
function outputImages(entry, outputNodeId) {
|
|
2287
|
+
if (!isObject(entry.outputs)) return new Error("ComfyUI completed without workflow outputs");
|
|
2288
|
+
const output = entry.outputs[outputNodeId];
|
|
2289
|
+
if (!isObject(output) || !Array.isArray(output.images)) {
|
|
2290
|
+
return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
|
|
2291
|
+
}
|
|
2292
|
+
const images = [];
|
|
2293
|
+
for (const value of output.images) {
|
|
2294
|
+
if (!isObject(value) || typeof value.filename !== "string" || typeof value.subfolder !== "string" || value.type !== "output") {
|
|
2295
|
+
return new Error(`ComfyUI output node "${outputNodeId}" returned an invalid image record`);
|
|
2296
|
+
}
|
|
2297
|
+
images.push({ filename: value.filename, subfolder: value.subfolder, type: "output" });
|
|
2298
|
+
}
|
|
2299
|
+
if (!images.length) return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
|
|
2300
|
+
return images;
|
|
2301
|
+
}
|
|
2302
|
+
function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
2303
|
+
const options = resolvedOptions(spec);
|
|
2304
|
+
return {
|
|
2305
|
+
promptId,
|
|
2306
|
+
outputNodeId,
|
|
2307
|
+
outputCount: images.length,
|
|
2308
|
+
workflowFile: options.workflowFile,
|
|
2309
|
+
workflowSha256: options.workflowSha256,
|
|
2310
|
+
files: images.map((image) => ({ ...image }))
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function encodeJob(promptId, outputNodeId) {
|
|
2314
|
+
return `${promptId}#${encodeURIComponent(outputNodeId)}`;
|
|
2315
|
+
}
|
|
2316
|
+
function decodeJob(jobId) {
|
|
2317
|
+
const split = jobId.lastIndexOf("#");
|
|
2318
|
+
if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
|
|
2319
|
+
return {
|
|
2320
|
+
promptId: jobId.slice(0, split),
|
|
2321
|
+
outputNodeId: decodeURIComponent(jobId.slice(split + 1))
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
function sourceRef(image) {
|
|
2325
|
+
const url = new URL("comfyui://output");
|
|
2326
|
+
url.searchParams.set("filename", image.filename);
|
|
2327
|
+
url.searchParams.set("subfolder", image.subfolder);
|
|
2328
|
+
url.searchParams.set("type", image.type);
|
|
2329
|
+
return url.toString();
|
|
2330
|
+
}
|
|
2331
|
+
function parseSource(source) {
|
|
2332
|
+
let url;
|
|
2333
|
+
try {
|
|
2334
|
+
url = new URL(source);
|
|
2335
|
+
} catch {
|
|
2336
|
+
throw new Error("Invalid ComfyUI output reference");
|
|
2337
|
+
}
|
|
2338
|
+
const filename = url.searchParams.get("filename");
|
|
2339
|
+
const subfolder = url.searchParams.get("subfolder");
|
|
2340
|
+
const type = url.searchParams.get("type");
|
|
2341
|
+
if (url.protocol !== "comfyui:" || url.hostname !== "output" || !filename || subfolder == null || type !== "output") {
|
|
2342
|
+
throw new Error("Invalid ComfyUI output reference");
|
|
2343
|
+
}
|
|
2344
|
+
return { filename, subfolder, type };
|
|
2345
|
+
}
|
|
2346
|
+
function normalizeBaseUrl(value) {
|
|
2347
|
+
let url;
|
|
2348
|
+
try {
|
|
2349
|
+
url = new URL(value);
|
|
2350
|
+
} catch {
|
|
2351
|
+
throw new Error("COMFYUI_BASE_URL must be an absolute HTTP(S) URL");
|
|
2352
|
+
}
|
|
2353
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2354
|
+
throw new Error("COMFYUI_BASE_URL must use HTTP or HTTPS");
|
|
2355
|
+
}
|
|
2356
|
+
url.hash = "";
|
|
2357
|
+
url.search = "";
|
|
2358
|
+
return url.toString().replace(/\/$/, "");
|
|
2359
|
+
}
|
|
2360
|
+
function historyError(status) {
|
|
2361
|
+
const messages = Array.isArray(status.messages) ? status.messages : [];
|
|
2362
|
+
for (const message2 of messages) {
|
|
2363
|
+
if (!Array.isArray(message2) || message2[0] !== "execution_error" || !isObject(message2[1])) continue;
|
|
2364
|
+
const detail = message2[1].exception_message;
|
|
2365
|
+
if (typeof detail === "string" && detail.trim()) {
|
|
2366
|
+
return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return "ComfyUI execution failed";
|
|
2370
|
+
}
|
|
2371
|
+
function apiError(value) {
|
|
2372
|
+
if (!isObject(value)) return null;
|
|
2373
|
+
const raw = typeof value.error === "string" ? value.error : isObject(value.error) && typeof value.error.message === "string" ? value.error.message : typeof value.message === "string" ? value.message : null;
|
|
2374
|
+
const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
|
|
2375
|
+
if (nodes.length) {
|
|
2376
|
+
return `${raw ? `${raw}; ` : ""}invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}`;
|
|
2377
|
+
}
|
|
2378
|
+
return raw?.trim().slice(0, 300) || null;
|
|
2379
|
+
}
|
|
2380
|
+
function requiredString(value, name) {
|
|
2381
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`ComfyUI ${name} must be a non-empty string`);
|
|
2382
|
+
return value.trim();
|
|
2383
|
+
}
|
|
2384
|
+
function isObject(value) {
|
|
2385
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2386
|
+
}
|
|
2387
|
+
function canonical(value) {
|
|
2388
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
2389
|
+
if (!isObject(value)) return value;
|
|
2390
|
+
return Object.fromEntries(
|
|
2391
|
+
Object.keys(value).sort().map((key) => [key, canonical(value[key])])
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/providers/fake.ts
|
|
2396
|
+
var import_node_crypto4 = require("crypto");
|
|
1928
2397
|
var FAKE_PNG = Buffer.from(
|
|
1929
2398
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
1930
2399
|
"base64"
|
|
@@ -2008,7 +2477,7 @@ var FakeProvider = class {
|
|
|
2008
2477
|
}
|
|
2009
2478
|
async download(url) {
|
|
2010
2479
|
if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
|
|
2011
|
-
const salt = (0,
|
|
2480
|
+
const salt = (0, import_node_crypto4.createHash)("sha256").update(url).digest();
|
|
2012
2481
|
return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
|
|
2013
2482
|
}
|
|
2014
2483
|
async balance() {
|
|
@@ -2107,56 +2576,23 @@ registerProvider({
|
|
|
2107
2576
|
return RetroDiffusionProvider.forOffline();
|
|
2108
2577
|
}
|
|
2109
2578
|
});
|
|
2579
|
+
registerProvider({
|
|
2580
|
+
id: "comfyui",
|
|
2581
|
+
create(mode2) {
|
|
2582
|
+
if (mode2 === "online") return ComfyUIProvider.fromEnv();
|
|
2583
|
+
if (mode2 === "downloads") return ComfyUIProvider.forDownloads();
|
|
2584
|
+
return ComfyUIProvider.forOffline();
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2110
2587
|
|
|
2111
2588
|
// src/manifest.ts
|
|
2112
|
-
var
|
|
2589
|
+
var import_promises3 = require("fs/promises");
|
|
2113
2590
|
var import_node_fs2 = require("fs");
|
|
2114
|
-
var
|
|
2115
|
-
|
|
2116
|
-
// src/hash.ts
|
|
2117
|
-
var import_node_crypto3 = require("crypto");
|
|
2118
|
-
var import_promises = require("fs/promises");
|
|
2119
|
-
function sha256(data) {
|
|
2120
|
-
return (0, import_node_crypto3.createHash)("sha256").update(data).digest("hex");
|
|
2121
|
-
}
|
|
2122
|
-
async function sha256File(path17) {
|
|
2123
|
-
return sha256(await (0, import_promises.readFile)(path17));
|
|
2124
|
-
}
|
|
2125
|
-
function specHash(spec, styleImageHashes) {
|
|
2126
|
-
return sha256(
|
|
2127
|
-
JSON.stringify({
|
|
2128
|
-
// Preserve every existing PixelLab hash while making a provider switch
|
|
2129
|
-
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
2130
|
-
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
2131
|
-
providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
|
|
2132
|
-
generator: spec.generator,
|
|
2133
|
-
prompt: spec.prompt,
|
|
2134
|
-
width: spec.width,
|
|
2135
|
-
height: spec.height,
|
|
2136
|
-
view: spec.view,
|
|
2137
|
-
outline: spec.outline ?? null,
|
|
2138
|
-
shading: spec.shading ?? null,
|
|
2139
|
-
detail: spec.detail ?? null,
|
|
2140
|
-
seed: spec.seed ?? null,
|
|
2141
|
-
palette: spec.palette,
|
|
2142
|
-
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
2143
|
-
// undefined for every other generator. `tileSize` is intentionally
|
|
2144
|
-
// absent — width/height are derived from it, so it is already covered.
|
|
2145
|
-
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
2146
|
-
tileType: spec.tileType,
|
|
2147
|
-
tileView: spec.tileView,
|
|
2148
|
-
tileFeature: spec.tileFeature,
|
|
2149
|
-
outlineMode: spec.outlineMode,
|
|
2150
|
-
styleImages: styleImageHashes
|
|
2151
|
-
})
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// src/manifest.ts
|
|
2591
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
2156
2592
|
async function loadManifest(manifestPath) {
|
|
2157
|
-
const abs =
|
|
2593
|
+
const abs = import_node_path3.default.resolve(manifestPath);
|
|
2158
2594
|
if (!(0, import_node_fs2.existsSync)(abs)) throw new Error(`No manifest at ${abs}`);
|
|
2159
|
-
const parsed = ManifestSchema.safeParse(JSON.parse(await (0,
|
|
2595
|
+
const parsed = ManifestSchema.safeParse(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
|
|
2160
2596
|
if (!parsed.success) {
|
|
2161
2597
|
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
2162
2598
|
throw new Error(`Manifest at ${abs} is invalid:
|
|
@@ -2178,7 +2614,7 @@ ${issues}`);
|
|
|
2178
2614
|
throw new Error(`Manifest at ${abs} is invalid:
|
|
2179
2615
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
2180
2616
|
}
|
|
2181
|
-
return { manifest: parsed.data, root:
|
|
2617
|
+
return { manifest: parsed.data, root: import_node_path3.default.dirname(abs), path: abs };
|
|
2182
2618
|
}
|
|
2183
2619
|
async function resolveSpecs(loaded, filter) {
|
|
2184
2620
|
const { manifest, root } = loaded;
|
|
@@ -2201,11 +2637,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2201
2637
|
}
|
|
2202
2638
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
2203
2639
|
async function loadStyleImage(rel) {
|
|
2204
|
-
const abs =
|
|
2640
|
+
const abs = import_node_path3.default.resolve(root, rel);
|
|
2205
2641
|
let hit = styleImageCache.get(abs);
|
|
2206
2642
|
if (!hit) {
|
|
2207
2643
|
if (!(0, import_node_fs2.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
2208
|
-
const buf = await (0,
|
|
2644
|
+
const buf = await (0, import_promises3.readFile)(abs);
|
|
2209
2645
|
const metadata = imageMetadata(buf);
|
|
2210
2646
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
2211
2647
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2218,6 +2654,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2218
2654
|
}
|
|
2219
2655
|
for (const styleId of styleIds) {
|
|
2220
2656
|
const style = manifest.styles[styleId];
|
|
2657
|
+
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
2658
|
+
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
2659
|
+
const providerOptions = optionResolution.options;
|
|
2660
|
+
const providerOptionIdentity = optionResolution.identity ?? providerOptions;
|
|
2221
2661
|
const styleImageHashes = [];
|
|
2222
2662
|
const styleImageDimensions = [];
|
|
2223
2663
|
for (const img of style.styleImages) {
|
|
@@ -2250,15 +2690,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2250
2690
|
}
|
|
2251
2691
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
2252
2692
|
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
2253
|
-
const relFile = asset.file ??
|
|
2254
|
-
const outFile =
|
|
2693
|
+
const relFile = asset.file ?? import_node_path3.default.join(asset.category ?? "", `${assetId}.png`);
|
|
2694
|
+
const outFile = import_node_path3.default.resolve(root, style.outDir, relFile);
|
|
2255
2695
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
2256
2696
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
2257
2697
|
const base = {
|
|
2258
2698
|
styleId,
|
|
2259
2699
|
assetId,
|
|
2260
2700
|
provider: activeProvider.id,
|
|
2261
|
-
providerOptions
|
|
2701
|
+
providerOptions,
|
|
2262
2702
|
generator,
|
|
2263
2703
|
prompt,
|
|
2264
2704
|
width,
|
|
@@ -2296,10 +2736,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2296
2736
|
outFile,
|
|
2297
2737
|
tags,
|
|
2298
2738
|
source: asset.source,
|
|
2299
|
-
specHash: specHash(base, styleImageHashes)
|
|
2739
|
+
specHash: specHash(base, styleImageHashes, providerOptionIdentity)
|
|
2300
2740
|
};
|
|
2301
2741
|
const resolvedImages = style.styleImages.map((image) => {
|
|
2302
|
-
const hit = styleImageCache.get(
|
|
2742
|
+
const hit = styleImageCache.get(import_node_path3.default.resolve(root, image.path));
|
|
2303
2743
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2304
2744
|
});
|
|
2305
2745
|
activeProvider.validate?.(resolved, resolvedImages);
|
|
@@ -2326,8 +2766,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
2326
2766
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
2327
2767
|
const out = [];
|
|
2328
2768
|
for (const img of style.styleImages) {
|
|
2329
|
-
const file =
|
|
2330
|
-
const buf = await (0,
|
|
2769
|
+
const file = import_node_path3.default.resolve(loaded.root, img.path);
|
|
2770
|
+
const buf = await (0, import_promises3.readFile)(file);
|
|
2331
2771
|
const metadata = imageMetadata(buf);
|
|
2332
2772
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
2333
2773
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2370,13 +2810,13 @@ function imageMetadata(buf) {
|
|
|
2370
2810
|
}
|
|
2371
2811
|
|
|
2372
2812
|
// src/lock.ts
|
|
2373
|
-
var
|
|
2813
|
+
var import_promises4 = require("fs/promises");
|
|
2374
2814
|
var import_node_fs3 = require("fs");
|
|
2375
|
-
var
|
|
2815
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
2376
2816
|
async function loadLock(lockPath) {
|
|
2377
2817
|
if (!(0, import_node_fs3.existsSync)(lockPath)) return { version: 2, entries: {} };
|
|
2378
2818
|
try {
|
|
2379
|
-
return parseLock(JSON.parse(await (0,
|
|
2819
|
+
return parseLock(JSON.parse(await (0, import_promises4.readFile)(lockPath, "utf8")));
|
|
2380
2820
|
} catch (err) {
|
|
2381
2821
|
throw new Error(
|
|
2382
2822
|
`Lockfile at ${lockPath} is malformed:
|
|
@@ -2388,7 +2828,7 @@ var saveQueues = /* @__PURE__ */ new Map();
|
|
|
2388
2828
|
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
2389
2829
|
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
2390
2830
|
function saveLock(lockPath, lock) {
|
|
2391
|
-
const queueKey =
|
|
2831
|
+
const queueKey = import_node_path4.default.resolve(lockPath);
|
|
2392
2832
|
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
2393
2833
|
const next = previous.catch(() => {
|
|
2394
2834
|
}).then(() => writeLockNow(lockPath, lock));
|
|
@@ -2400,7 +2840,7 @@ function saveLock(lockPath, lock) {
|
|
|
2400
2840
|
return next;
|
|
2401
2841
|
}
|
|
2402
2842
|
async function writeLockNow(lockPath, lock) {
|
|
2403
|
-
await (0,
|
|
2843
|
+
await (0, import_promises4.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
|
|
2404
2844
|
const release = await acquireFileLock(lockPath);
|
|
2405
2845
|
try {
|
|
2406
2846
|
await writeLockWhileHeld(lockPath, lock);
|
|
@@ -2412,7 +2852,7 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
2412
2852
|
let disk = { version: 2, entries: {} };
|
|
2413
2853
|
if ((0, import_node_fs3.existsSync)(file)) {
|
|
2414
2854
|
try {
|
|
2415
|
-
disk = parseLock(JSON.parse(await (0,
|
|
2855
|
+
disk = parseLock(JSON.parse(await (0, import_promises4.readFile)(file, "utf8")));
|
|
2416
2856
|
} catch (err) {
|
|
2417
2857
|
throw new Error(
|
|
2418
2858
|
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2440,10 +2880,10 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
2440
2880
|
}
|
|
2441
2881
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2442
2882
|
try {
|
|
2443
|
-
await (0,
|
|
2444
|
-
await (0,
|
|
2883
|
+
await (0, import_promises4.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
2884
|
+
await (0, import_promises4.rename)(tmp, file);
|
|
2445
2885
|
} finally {
|
|
2446
|
-
await (0,
|
|
2886
|
+
await (0, import_promises4.rm)(tmp, { force: true });
|
|
2447
2887
|
}
|
|
2448
2888
|
const currentDirty = dirtyPatches.get(lock);
|
|
2449
2889
|
if (currentDirty) {
|
|
@@ -2506,18 +2946,18 @@ async function acquireFileLock(file) {
|
|
|
2506
2946
|
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
2507
2947
|
for (; ; ) {
|
|
2508
2948
|
try {
|
|
2509
|
-
await (0,
|
|
2949
|
+
await (0, import_promises4.mkdir)(lockDir);
|
|
2510
2950
|
return async () => {
|
|
2511
|
-
await (0,
|
|
2951
|
+
await (0, import_promises4.rmdir)(lockDir).catch(() => {
|
|
2512
2952
|
});
|
|
2513
2953
|
};
|
|
2514
2954
|
} catch (err) {
|
|
2515
2955
|
const code = err.code;
|
|
2516
2956
|
if (code !== "EEXIST") throw err;
|
|
2517
2957
|
try {
|
|
2518
|
-
const age = Date.now() - (await (0,
|
|
2958
|
+
const age = Date.now() - (await (0, import_promises4.stat)(lockDir)).mtimeMs;
|
|
2519
2959
|
if (age > STALE_FILE_LOCK_MS) {
|
|
2520
|
-
await (0,
|
|
2960
|
+
await (0, import_promises4.rmdir)(lockDir);
|
|
2521
2961
|
continue;
|
|
2522
2962
|
}
|
|
2523
2963
|
} catch (lockErr) {
|
|
@@ -2543,21 +2983,21 @@ function totalSpend(lock, unit = "generations") {
|
|
|
2543
2983
|
}
|
|
2544
2984
|
|
|
2545
2985
|
// src/outputs.ts
|
|
2546
|
-
var
|
|
2986
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
2547
2987
|
function portableOutputPath(file, manifestDir) {
|
|
2548
|
-
const root =
|
|
2549
|
-
const absolute =
|
|
2550
|
-
const relative =
|
|
2551
|
-
const value =
|
|
2552
|
-
return value.split(
|
|
2988
|
+
const root = import_node_path5.default.resolve(manifestDir);
|
|
2989
|
+
const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
|
|
2990
|
+
const relative = import_node_path5.default.relative(root, absolute);
|
|
2991
|
+
const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
|
|
2992
|
+
return value.split(import_node_path5.default.sep).join("/");
|
|
2553
2993
|
}
|
|
2554
2994
|
function resolveOutputPath(recordedPath, manifestDir) {
|
|
2555
|
-
if (
|
|
2556
|
-
if (
|
|
2557
|
-
return
|
|
2995
|
+
if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
|
|
2996
|
+
if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
2997
|
+
return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
|
|
2558
2998
|
}
|
|
2559
2999
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
2560
|
-
const originalExt =
|
|
3000
|
+
const originalExt = import_node_path5.default.extname(spec.outFile);
|
|
2561
3001
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
2562
3002
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2563
3003
|
if (total === 1) return `${stem}${ext}`;
|
|
@@ -2646,44 +3086,44 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
2646
3086
|
id: spec.assetId,
|
|
2647
3087
|
index: 0,
|
|
2648
3088
|
path: spec.outFile,
|
|
2649
|
-
absolutePath:
|
|
3089
|
+
absolutePath: import_node_path5.default.resolve(spec.outFile),
|
|
2650
3090
|
sha256: ""
|
|
2651
3091
|
}];
|
|
2652
3092
|
}
|
|
2653
3093
|
|
|
2654
3094
|
// src/artifacts.ts
|
|
2655
|
-
var
|
|
2656
|
-
var
|
|
2657
|
-
var
|
|
3095
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
3096
|
+
var import_node_crypto5 = require("crypto");
|
|
3097
|
+
var import_promises5 = require("fs/promises");
|
|
2658
3098
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
2659
3099
|
function message(error) {
|
|
2660
3100
|
return error instanceof Error ? error.message : String(error);
|
|
2661
3101
|
}
|
|
2662
3102
|
function digest(data) {
|
|
2663
|
-
return (0,
|
|
3103
|
+
return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
|
|
2664
3104
|
}
|
|
2665
3105
|
function portableRelative(from, to) {
|
|
2666
|
-
return
|
|
3106
|
+
return import_node_path6.default.relative(from, import_node_path6.default.resolve(to)).split(import_node_path6.default.sep).join("/") || ".";
|
|
2667
3107
|
}
|
|
2668
|
-
function
|
|
3108
|
+
function canonical2(value) {
|
|
2669
3109
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2670
3110
|
if (typeof value === "number") {
|
|
2671
3111
|
if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
|
|
2672
3112
|
return value;
|
|
2673
3113
|
}
|
|
2674
|
-
if (Array.isArray(value)) return value.map(
|
|
3114
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
2675
3115
|
if (typeof value === "object") {
|
|
2676
3116
|
const sorted = {};
|
|
2677
3117
|
for (const key of Object.keys(value).sort()) {
|
|
2678
3118
|
const item = value[key];
|
|
2679
|
-
if (item !== void 0) sorted[key] =
|
|
3119
|
+
if (item !== void 0) sorted[key] = canonical2(item);
|
|
2680
3120
|
}
|
|
2681
3121
|
return sorted;
|
|
2682
3122
|
}
|
|
2683
3123
|
throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
|
|
2684
3124
|
}
|
|
2685
3125
|
function fingerprint(provenance) {
|
|
2686
|
-
return digest(JSON.stringify(
|
|
3126
|
+
return digest(JSON.stringify(canonical2({
|
|
2687
3127
|
kind: provenance.kind,
|
|
2688
3128
|
sources: provenance.sources,
|
|
2689
3129
|
options: provenance.options,
|
|
@@ -2714,7 +3154,7 @@ function parseArtifactManifest(absolute, data) {
|
|
|
2714
3154
|
}
|
|
2715
3155
|
async function readOptional(file) {
|
|
2716
3156
|
try {
|
|
2717
|
-
return await (0,
|
|
3157
|
+
return await (0, import_promises5.readFile)(file);
|
|
2718
3158
|
} catch (error) {
|
|
2719
3159
|
if (isCode(error, "ENOENT")) return null;
|
|
2720
3160
|
throw error;
|
|
@@ -2733,8 +3173,8 @@ function processIsAlive(pid) {
|
|
|
2733
3173
|
}
|
|
2734
3174
|
}
|
|
2735
3175
|
function validTemporaryPath(candidate, destination, type) {
|
|
2736
|
-
return
|
|
2737
|
-
`.${
|
|
3176
|
+
return import_node_path6.default.dirname(candidate) === import_node_path6.default.dirname(destination) && import_node_path6.default.basename(candidate).startsWith(
|
|
3177
|
+
`.${import_node_path6.default.basename(destination)}.pixelkiln-${type}-`
|
|
2738
3178
|
);
|
|
2739
3179
|
}
|
|
2740
3180
|
function parseTransaction(journal, data) {
|
|
@@ -2750,14 +3190,14 @@ function parseTransaction(journal, data) {
|
|
|
2750
3190
|
return raw;
|
|
2751
3191
|
}
|
|
2752
3192
|
async function removeTransactionFiles(journal) {
|
|
2753
|
-
await (0,
|
|
2754
|
-
await (0,
|
|
3193
|
+
await (0, import_promises5.rm)(journal, { force: true });
|
|
3194
|
+
await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
|
|
2755
3195
|
}
|
|
2756
3196
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
2757
|
-
const journal =
|
|
3197
|
+
const journal = import_node_path6.default.resolve(recoveryFile);
|
|
2758
3198
|
const bytes = await readOptional(journal);
|
|
2759
3199
|
if (!bytes) {
|
|
2760
|
-
await (0,
|
|
3200
|
+
await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
|
|
2761
3201
|
return;
|
|
2762
3202
|
}
|
|
2763
3203
|
if (activeTransactions.has(journal)) {
|
|
@@ -2770,7 +3210,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2770
3210
|
);
|
|
2771
3211
|
}
|
|
2772
3212
|
for (const entry of transaction.entries) {
|
|
2773
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
3213
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path6.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
2774
3214
|
throw new Error(
|
|
2775
3215
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
2776
3216
|
);
|
|
@@ -2796,7 +3236,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2796
3236
|
}
|
|
2797
3237
|
await remove2(entry.destination, errors);
|
|
2798
3238
|
try {
|
|
2799
|
-
await (0,
|
|
3239
|
+
await (0, import_promises5.rename)(entry.backup, entry.destination);
|
|
2800
3240
|
} catch (error) {
|
|
2801
3241
|
errors.push(
|
|
2802
3242
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -2816,7 +3256,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2816
3256
|
await removeTransactionFiles(journal);
|
|
2817
3257
|
}
|
|
2818
3258
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
2819
|
-
const root =
|
|
3259
|
+
const root = import_node_path6.default.dirname(import_node_path6.default.resolve(manifestPath));
|
|
2820
3260
|
const sources = provenance.sources.map((source) => ({
|
|
2821
3261
|
...source,
|
|
2822
3262
|
path: portableRelative(root, source.path)
|
|
@@ -2824,7 +3264,7 @@ function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
|
2824
3264
|
const normalized = {
|
|
2825
3265
|
kind: provenance.kind,
|
|
2826
3266
|
sources,
|
|
2827
|
-
options:
|
|
3267
|
+
options: canonical2(provenance.options)
|
|
2828
3268
|
};
|
|
2829
3269
|
const manifestOutputs = outputs.map((output) => ({
|
|
2830
3270
|
path: portableRelative(root, output.path),
|
|
@@ -2846,11 +3286,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
2846
3286
|
];
|
|
2847
3287
|
}
|
|
2848
3288
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
2849
|
-
const absoluteManifest =
|
|
3289
|
+
const absoluteManifest = import_node_path6.default.resolve(manifestPath);
|
|
2850
3290
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
2851
3291
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
2852
3292
|
absoluteManifest,
|
|
2853
|
-
...outputs.map((output) =>
|
|
3293
|
+
...outputs.map((output) => import_node_path6.default.resolve(output.path))
|
|
2854
3294
|
]);
|
|
2855
3295
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
2856
3296
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -2868,13 +3308,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
2868
3308
|
}
|
|
2869
3309
|
}
|
|
2870
3310
|
if (!options.force) {
|
|
2871
|
-
const root =
|
|
3311
|
+
const root = import_node_path6.default.dirname(absoluteManifest);
|
|
2872
3312
|
const recorded = new Map(
|
|
2873
|
-
previous?.outputs.map((output) => [
|
|
3313
|
+
previous?.outputs.map((output) => [import_node_path6.default.resolve(root, output.path), output.sha256]) ?? []
|
|
2874
3314
|
);
|
|
2875
3315
|
const conflicts = [];
|
|
2876
3316
|
for (const output of outputs) {
|
|
2877
|
-
const destination =
|
|
3317
|
+
const destination = import_node_path6.default.resolve(output.path);
|
|
2878
3318
|
const current = await readOptional(destination);
|
|
2879
3319
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
2880
3320
|
const expected = recorded.get(destination);
|
|
@@ -2895,15 +3335,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
2895
3335
|
);
|
|
2896
3336
|
}
|
|
2897
3337
|
async function verifyArtifactBundle(manifestPath) {
|
|
2898
|
-
const absolute =
|
|
2899
|
-
const raw = parseArtifactManifest(absolute, await (0,
|
|
2900
|
-
const root =
|
|
3338
|
+
const absolute = import_node_path6.default.resolve(manifestPath);
|
|
3339
|
+
const raw = parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
|
|
3340
|
+
const root = import_node_path6.default.dirname(absolute);
|
|
2901
3341
|
const changedSources = [];
|
|
2902
3342
|
const changedOutputs = [];
|
|
2903
3343
|
for (const source of raw.sources) {
|
|
2904
3344
|
let actual = null;
|
|
2905
3345
|
try {
|
|
2906
|
-
actual = digest(await (0,
|
|
3346
|
+
actual = digest(await (0, import_promises5.readFile)(import_node_path6.default.resolve(root, source.path)));
|
|
2907
3347
|
} catch (error) {
|
|
2908
3348
|
if (!isCode(error, "ENOENT")) throw error;
|
|
2909
3349
|
}
|
|
@@ -2911,7 +3351,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
2911
3351
|
}
|
|
2912
3352
|
for (const output of raw.outputs) {
|
|
2913
3353
|
try {
|
|
2914
|
-
if (digest(await (0,
|
|
3354
|
+
if (digest(await (0, import_promises5.readFile)(import_node_path6.default.resolve(root, output.path))) !== output.sha256) {
|
|
2915
3355
|
changedOutputs.push(output.path);
|
|
2916
3356
|
}
|
|
2917
3357
|
} catch (error) {
|
|
@@ -2937,7 +3377,7 @@ function isCode(error, code) {
|
|
|
2937
3377
|
}
|
|
2938
3378
|
async function remove2(file, errors) {
|
|
2939
3379
|
try {
|
|
2940
|
-
await (0,
|
|
3380
|
+
await (0, import_promises5.rm)(file, { force: true });
|
|
2941
3381
|
} catch (error) {
|
|
2942
3382
|
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
2943
3383
|
}
|
|
@@ -2950,7 +3390,7 @@ async function rollback(prepared) {
|
|
|
2950
3390
|
for (const artifact of [...prepared].reverse()) {
|
|
2951
3391
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
2952
3392
|
try {
|
|
2953
|
-
await (0,
|
|
3393
|
+
await (0, import_promises5.rename)(artifact.backup, artifact.destination);
|
|
2954
3394
|
artifact.backedUp = false;
|
|
2955
3395
|
} catch (error) {
|
|
2956
3396
|
errors.push(
|
|
@@ -2967,7 +3407,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2967
3407
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
2968
3408
|
const normalized = files.map((file) => {
|
|
2969
3409
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
2970
|
-
return { destination:
|
|
3410
|
+
return { destination: import_node_path6.default.resolve(file.path), data: Buffer.from(file.data) };
|
|
2971
3411
|
});
|
|
2972
3412
|
const destinations = /* @__PURE__ */ new Set();
|
|
2973
3413
|
for (const artifact of normalized) {
|
|
@@ -2983,7 +3423,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2983
3423
|
const unchanged = [];
|
|
2984
3424
|
for (const artifact of normalized) {
|
|
2985
3425
|
try {
|
|
2986
|
-
const current = await (0,
|
|
3426
|
+
const current = await (0, import_promises5.readFile)(artifact.destination);
|
|
2987
3427
|
if (current.equals(artifact.data)) {
|
|
2988
3428
|
unchanged.push(artifact.destination);
|
|
2989
3429
|
} else {
|
|
@@ -2995,23 +3435,23 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2995
3435
|
}
|
|
2996
3436
|
}
|
|
2997
3437
|
if (!changed.length) return { changed: [], unchanged };
|
|
2998
|
-
const token = (0,
|
|
3438
|
+
const token = (0, import_node_crypto5.randomUUID)();
|
|
2999
3439
|
for (const [index, artifact] of changed.entries()) {
|
|
3000
|
-
const basename =
|
|
3001
|
-
artifact.stage =
|
|
3002
|
-
|
|
3440
|
+
const basename = import_node_path6.default.basename(artifact.destination);
|
|
3441
|
+
artifact.stage = import_node_path6.default.join(
|
|
3442
|
+
import_node_path6.default.dirname(artifact.destination),
|
|
3003
3443
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
3004
3444
|
);
|
|
3005
3445
|
if (artifact.existed) {
|
|
3006
|
-
artifact.backup =
|
|
3007
|
-
|
|
3446
|
+
artifact.backup = import_node_path6.default.join(
|
|
3447
|
+
import_node_path6.default.dirname(artifact.destination),
|
|
3008
3448
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
3009
3449
|
);
|
|
3010
3450
|
}
|
|
3011
3451
|
}
|
|
3012
|
-
const journal = options.recoveryFile ?
|
|
3452
|
+
const journal = options.recoveryFile ? import_node_path6.default.resolve(options.recoveryFile) : null;
|
|
3013
3453
|
if (journal) {
|
|
3014
|
-
await (0,
|
|
3454
|
+
await (0, import_promises5.mkdir)(import_node_path6.default.dirname(journal), { recursive: true });
|
|
3015
3455
|
const transaction = {
|
|
3016
3456
|
format: "pixelkiln-artifact-transaction",
|
|
3017
3457
|
version: 1,
|
|
@@ -3025,7 +3465,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
3025
3465
|
}))
|
|
3026
3466
|
};
|
|
3027
3467
|
try {
|
|
3028
|
-
await (0,
|
|
3468
|
+
await (0, import_promises5.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
3029
3469
|
activeTransactions.add(journal);
|
|
3030
3470
|
} catch (error) {
|
|
3031
3471
|
throw new Error(
|
|
@@ -3036,9 +3476,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
3036
3476
|
}
|
|
3037
3477
|
try {
|
|
3038
3478
|
for (const [index, artifact] of changed.entries()) {
|
|
3039
|
-
await (0,
|
|
3479
|
+
await (0, import_promises5.mkdir)(import_node_path6.default.dirname(artifact.destination), { recursive: true });
|
|
3040
3480
|
await options.beforeStage?.(artifact.destination, index);
|
|
3041
|
-
await (0,
|
|
3481
|
+
await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
|
|
3042
3482
|
}
|
|
3043
3483
|
} catch (error) {
|
|
3044
3484
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -3051,17 +3491,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
3051
3491
|
try {
|
|
3052
3492
|
for (const artifact of changed) {
|
|
3053
3493
|
if (!artifact.existed) continue;
|
|
3054
|
-
await (0,
|
|
3494
|
+
await (0, import_promises5.rename)(artifact.destination, artifact.backup);
|
|
3055
3495
|
artifact.backedUp = true;
|
|
3056
3496
|
}
|
|
3057
3497
|
for (const [index, artifact] of changed.entries()) {
|
|
3058
3498
|
await options.beforePromote?.(artifact.destination, index);
|
|
3059
|
-
await (0,
|
|
3499
|
+
await (0, import_promises5.rename)(artifact.stage, artifact.destination);
|
|
3060
3500
|
artifact.stage = void 0;
|
|
3061
3501
|
artifact.promoted = true;
|
|
3062
3502
|
}
|
|
3063
3503
|
if (journal) {
|
|
3064
|
-
await (0,
|
|
3504
|
+
await (0, import_promises5.writeFile)(transactionMarker(journal), `${token}
|
|
3065
3505
|
`, { flag: "wx" });
|
|
3066
3506
|
durableCommit = true;
|
|
3067
3507
|
await options.afterCommit?.();
|
|
@@ -3105,7 +3545,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
3105
3545
|
|
|
3106
3546
|
// src/pipeline/plan.ts
|
|
3107
3547
|
var import_node_fs4 = require("fs");
|
|
3108
|
-
var
|
|
3548
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
3109
3549
|
async function anyOutputModified(entry, spec) {
|
|
3110
3550
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
3111
3551
|
const output = entry.outputs[index];
|
|
@@ -3124,7 +3564,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
3124
3564
|
state = "missing";
|
|
3125
3565
|
reason = "--force";
|
|
3126
3566
|
} else if (!entry && spec.source) {
|
|
3127
|
-
if ((0, import_node_fs4.existsSync)(
|
|
3567
|
+
if ((0, import_node_fs4.existsSync)(import_node_path7.default.resolve(spec.root, spec.source))) {
|
|
3128
3568
|
state = "ok";
|
|
3129
3569
|
reason = `placed from ${spec.source}; not generated`;
|
|
3130
3570
|
} else {
|
|
@@ -3190,9 +3630,9 @@ function summarize(plan) {
|
|
|
3190
3630
|
}
|
|
3191
3631
|
|
|
3192
3632
|
// src/pipeline/audit.ts
|
|
3193
|
-
var
|
|
3633
|
+
var import_promises6 = require("fs/promises");
|
|
3194
3634
|
var import_node_fs5 = require("fs");
|
|
3195
|
-
var
|
|
3635
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
3196
3636
|
function colorDistance(a, b) {
|
|
3197
3637
|
const rmean = (a.r + b.r) / 2;
|
|
3198
3638
|
const dr = a.r - b.r;
|
|
@@ -3242,7 +3682,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3242
3682
|
continue;
|
|
3243
3683
|
}
|
|
3244
3684
|
try {
|
|
3245
|
-
const png = decodePng(await (0,
|
|
3685
|
+
const png = decodePng(await (0, import_promises6.readFile)(output.absolutePath));
|
|
3246
3686
|
const palette = extractPalette(png, 12);
|
|
3247
3687
|
assets.push({
|
|
3248
3688
|
assetId: spec.assetId,
|
|
@@ -3266,10 +3706,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3266
3706
|
let referenceFromStyleImages = false;
|
|
3267
3707
|
const refPalettes = [];
|
|
3268
3708
|
for (const img of style.styleImages) {
|
|
3269
|
-
const abs =
|
|
3709
|
+
const abs = import_node_path8.default.resolve(loaded.root, img.path);
|
|
3270
3710
|
if (!(0, import_node_fs5.existsSync)(abs)) continue;
|
|
3271
3711
|
try {
|
|
3272
|
-
refPalettes.push(extractPalette(decodePng(await (0,
|
|
3712
|
+
refPalettes.push(extractPalette(decodePng(await (0, import_promises6.readFile)(abs)), 12));
|
|
3273
3713
|
} catch {
|
|
3274
3714
|
}
|
|
3275
3715
|
}
|
|
@@ -3346,13 +3786,13 @@ function hex(c) {
|
|
|
3346
3786
|
|
|
3347
3787
|
// src/pipeline/cache-health.ts
|
|
3348
3788
|
var import_node_fs7 = require("fs");
|
|
3349
|
-
var
|
|
3350
|
-
var
|
|
3789
|
+
var import_promises8 = require("fs/promises");
|
|
3790
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
3351
3791
|
|
|
3352
3792
|
// src/cache.ts
|
|
3353
|
-
var
|
|
3793
|
+
var import_promises7 = require("fs/promises");
|
|
3354
3794
|
var import_node_fs6 = require("fs");
|
|
3355
|
-
var
|
|
3795
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
3356
3796
|
var import_zod3 = require("zod");
|
|
3357
3797
|
var HashCacheSchema = import_zod3.z.object({
|
|
3358
3798
|
version: import_zod3.z.literal(1),
|
|
@@ -3366,10 +3806,10 @@ function isSha256Hash(value) {
|
|
|
3366
3806
|
function parseCache(value) {
|
|
3367
3807
|
return HashCacheSchema.parse(value);
|
|
3368
3808
|
}
|
|
3369
|
-
async function loadCache(
|
|
3370
|
-
if (!(0, import_node_fs6.existsSync)(
|
|
3809
|
+
async function loadCache(path18) {
|
|
3810
|
+
if (!(0, import_node_fs6.existsSync)(path18)) return { version: 1, hashes: {} };
|
|
3371
3811
|
try {
|
|
3372
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0,
|
|
3812
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises7.readFile)(path18, "utf8")));
|
|
3373
3813
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
3374
3814
|
return {
|
|
3375
3815
|
version: 1,
|
|
@@ -3381,20 +3821,20 @@ async function loadCache(path17) {
|
|
|
3381
3821
|
return { version: 1, hashes: {} };
|
|
3382
3822
|
}
|
|
3383
3823
|
}
|
|
3384
|
-
async function saveCache(
|
|
3824
|
+
async function saveCache(path18, cache) {
|
|
3385
3825
|
const sorted = {};
|
|
3386
3826
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
3387
3827
|
const hash = cache.hashes[key];
|
|
3388
3828
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
3389
3829
|
sorted[key] = hash;
|
|
3390
3830
|
}
|
|
3391
|
-
await (0,
|
|
3392
|
-
const tmp = `${
|
|
3831
|
+
await (0, import_promises7.mkdir)(import_node_path9.default.dirname(import_node_path9.default.resolve(path18)), { recursive: true });
|
|
3832
|
+
const tmp = `${path18}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3393
3833
|
try {
|
|
3394
|
-
await (0,
|
|
3395
|
-
await (0,
|
|
3834
|
+
await (0, import_promises7.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
3835
|
+
await (0, import_promises7.rename)(tmp, path18);
|
|
3396
3836
|
} finally {
|
|
3397
|
-
await (0,
|
|
3837
|
+
await (0, import_promises7.rm)(tmp, { force: true });
|
|
3398
3838
|
}
|
|
3399
3839
|
}
|
|
3400
3840
|
function pruneCache(cache, liveIds) {
|
|
@@ -3415,10 +3855,10 @@ function cachePathFor(lockPath) {
|
|
|
3415
3855
|
// src/pipeline/cache-health.ts
|
|
3416
3856
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
3417
3857
|
if (options.prune && !(0, import_node_fs7.existsSync)(lockPath)) {
|
|
3418
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
3858
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path10.default.resolve(lockPath)}`);
|
|
3419
3859
|
}
|
|
3420
|
-
const contentDir =
|
|
3421
|
-
const remotePath =
|
|
3860
|
+
const contentDir = import_node_path10.default.resolve(import_node_path10.default.dirname(lockPath), ".pixelkiln", "cache");
|
|
3861
|
+
const remotePath = import_node_path10.default.resolve(cachePathFor(lockPath));
|
|
3422
3862
|
const referenced = new Set(
|
|
3423
3863
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
3424
3864
|
);
|
|
@@ -3432,7 +3872,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3432
3872
|
]);
|
|
3433
3873
|
for (const name of names) {
|
|
3434
3874
|
try {
|
|
3435
|
-
await (0,
|
|
3875
|
+
await (0, import_promises8.rm)(import_node_path10.default.join(contentDir, name), { force: true });
|
|
3436
3876
|
removed.contentFiles++;
|
|
3437
3877
|
} catch {
|
|
3438
3878
|
}
|
|
@@ -3441,7 +3881,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3441
3881
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
3442
3882
|
removed.resetRemoteHashCache = true;
|
|
3443
3883
|
} else if (remoteHashes.invalidIds.length) {
|
|
3444
|
-
const cache = parseCache(JSON.parse(await (0,
|
|
3884
|
+
const cache = parseCache(JSON.parse(await (0, import_promises8.readFile)(remotePath, "utf8")));
|
|
3445
3885
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
3446
3886
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
3447
3887
|
await saveCache(remotePath, cache);
|
|
@@ -3473,19 +3913,19 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3473
3913
|
return report;
|
|
3474
3914
|
}
|
|
3475
3915
|
const present = /* @__PURE__ */ new Set();
|
|
3476
|
-
const entries = await (0,
|
|
3916
|
+
const entries = await (0, import_promises8.readdir)(contentDir, { withFileTypes: true });
|
|
3477
3917
|
for (const entry of entries) {
|
|
3478
3918
|
if (!entry.isFile()) {
|
|
3479
3919
|
report.invalid.push({ name: entry.name, reason: "not a regular file" });
|
|
3480
3920
|
continue;
|
|
3481
3921
|
}
|
|
3482
3922
|
report.files++;
|
|
3483
|
-
const file =
|
|
3923
|
+
const file = import_node_path10.default.join(contentDir, entry.name);
|
|
3484
3924
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
3485
3925
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
3486
3926
|
let bytes;
|
|
3487
3927
|
try {
|
|
3488
|
-
bytes = await (0,
|
|
3928
|
+
bytes = await (0, import_promises8.readFile)(file);
|
|
3489
3929
|
report.bytes += bytes.length;
|
|
3490
3930
|
} catch (err) {
|
|
3491
3931
|
report.invalid.push({
|
|
@@ -3532,7 +3972,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3532
3972
|
if (!report.exists) return report;
|
|
3533
3973
|
let cache;
|
|
3534
3974
|
try {
|
|
3535
|
-
cache = parseCache(JSON.parse(await (0,
|
|
3975
|
+
cache = parseCache(JSON.parse(await (0, import_promises8.readFile)(remotePath, "utf8")));
|
|
3536
3976
|
} catch (err) {
|
|
3537
3977
|
report.error = err instanceof Error ? err.message : String(err);
|
|
3538
3978
|
return report;
|
|
@@ -3548,18 +3988,18 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3548
3988
|
|
|
3549
3989
|
// src/pipeline/pack.ts
|
|
3550
3990
|
var import_node_fs8 = require("fs");
|
|
3551
|
-
var
|
|
3991
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
3552
3992
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
3553
3993
|
if (!Array.isArray(raw) || !raw.length) {
|
|
3554
3994
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
3555
3995
|
}
|
|
3556
|
-
const baseDir =
|
|
3996
|
+
const baseDir = import_node_path11.default.dirname(import_node_path11.default.resolve(inputsFilePath));
|
|
3557
3997
|
return raw.map((entry, i) => {
|
|
3558
3998
|
const e = entry;
|
|
3559
3999
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
3560
4000
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
3561
4001
|
}
|
|
3562
|
-
return { id: e.id, path:
|
|
4002
|
+
return { id: e.id, path: import_node_path11.default.resolve(baseDir, e.path) };
|
|
3563
4003
|
});
|
|
3564
4004
|
}
|
|
3565
4005
|
function packSprites(inputs, options = {}) {
|
|
@@ -3756,7 +4196,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3756
4196
|
const cell = cells[id];
|
|
3757
4197
|
const source = sources[id];
|
|
3758
4198
|
if (source) {
|
|
3759
|
-
placements.push({ id, path:
|
|
4199
|
+
placements.push({ id, path: import_node_path11.default.resolve(manifestDir, source), cell });
|
|
3760
4200
|
continue;
|
|
3761
4201
|
}
|
|
3762
4202
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
@@ -3780,7 +4220,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3780
4220
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
3781
4221
|
);
|
|
3782
4222
|
}
|
|
3783
|
-
const basePng = mount.base ? (0, import_node_fs8.readFileSync)(
|
|
4223
|
+
const basePng = mount.base ? (0, import_node_fs8.readFileSync)(import_node_path11.default.resolve(manifestDir, mount.base)) : void 0;
|
|
3784
4224
|
const mounted = mountSprites(placements, {
|
|
3785
4225
|
cellWidth: mount.cellWidth,
|
|
3786
4226
|
cellHeight: mount.cellHeight,
|
|
@@ -3790,7 +4230,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3790
4230
|
...mounted,
|
|
3791
4231
|
sources: mount.base ? [{
|
|
3792
4232
|
id: "$base",
|
|
3793
|
-
path:
|
|
4233
|
+
path: import_node_path11.default.resolve(manifestDir, mount.base),
|
|
3794
4234
|
sha256: sha256(basePng),
|
|
3795
4235
|
included: true
|
|
3796
4236
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -4202,7 +4642,11 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
4202
4642
|
spec: currentSpec
|
|
4203
4643
|
});
|
|
4204
4644
|
if (state.status === "review") {
|
|
4205
|
-
upsert(lock, key, {
|
|
4645
|
+
upsert(lock, key, {
|
|
4646
|
+
status: "review",
|
|
4647
|
+
reviewObjectId: entry.jobId,
|
|
4648
|
+
providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
|
|
4649
|
+
});
|
|
4206
4650
|
result.review++;
|
|
4207
4651
|
log(` review ${key} (${state.candidateUrls.length} candidates)`);
|
|
4208
4652
|
} else if (state.status === "ready") {
|
|
@@ -4235,15 +4679,15 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
4235
4679
|
|
|
4236
4680
|
// src/pipeline/fetch.ts
|
|
4237
4681
|
var import_node_fs9 = require("fs");
|
|
4238
|
-
var
|
|
4239
|
-
var
|
|
4682
|
+
var import_promises9 = require("fs/promises");
|
|
4683
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
4240
4684
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
4241
4685
|
const log = opts.onProgress ?? (() => {
|
|
4242
4686
|
});
|
|
4243
4687
|
const result = { downloaded: 0, skipped: 0, failed: 0 };
|
|
4244
4688
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
4245
4689
|
normalizeLockOutputPaths(lock, specs);
|
|
4246
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
4690
|
+
const cacheDir = opts.cacheDir === false ? null : import_node_path12.default.resolve(opts.cacheDir ?? import_node_path12.default.join(import_node_path12.default.dirname(lockPath), ".pixelkiln", "cache"));
|
|
4247
4691
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
4248
4692
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
4249
4693
|
if (!opts.repair || e.status !== "downloaded") return false;
|
|
@@ -4295,7 +4739,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4295
4739
|
if (cacheDir) {
|
|
4296
4740
|
await cacheMedia(
|
|
4297
4741
|
cacheDir,
|
|
4298
|
-
await (0,
|
|
4742
|
+
await (0, import_promises9.readFile)(target),
|
|
4299
4743
|
recorded.mediaType ?? MediaType.PNG,
|
|
4300
4744
|
recorded.sha256
|
|
4301
4745
|
);
|
|
@@ -4306,7 +4750,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4306
4750
|
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
4307
4751
|
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
4308
4752
|
if (buf) {
|
|
4309
|
-
log(` cached ${
|
|
4753
|
+
log(` cached ${import_node_path12.default.relative(process.cwd(), target)}`);
|
|
4310
4754
|
} else {
|
|
4311
4755
|
if (!source.url) {
|
|
4312
4756
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
@@ -4324,9 +4768,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4324
4768
|
);
|
|
4325
4769
|
}
|
|
4326
4770
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
4327
|
-
await (0,
|
|
4771
|
+
await (0, import_promises9.mkdir)(import_node_path12.default.dirname(target), { recursive: true });
|
|
4328
4772
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
4329
|
-
await (0,
|
|
4773
|
+
await (0, import_promises9.writeFile)(tmp, buf);
|
|
4330
4774
|
outputs.push({
|
|
4331
4775
|
path: portableOutputPath(target, spec.root),
|
|
4332
4776
|
sha256: sha256(buf),
|
|
@@ -4335,8 +4779,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4335
4779
|
});
|
|
4336
4780
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
4337
4781
|
await saveLock(lockPath, lock);
|
|
4338
|
-
await (0,
|
|
4339
|
-
log(` wrote ${
|
|
4782
|
+
await (0, import_promises9.rename)(tmp, target);
|
|
4783
|
+
log(` wrote ${import_node_path12.default.relative(process.cwd(), target)}`);
|
|
4340
4784
|
}
|
|
4341
4785
|
upsert(lock, key, {
|
|
4342
4786
|
status: "downloaded",
|
|
@@ -4369,10 +4813,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4369
4813
|
return result;
|
|
4370
4814
|
}
|
|
4371
4815
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
4372
|
-
const file =
|
|
4816
|
+
const file = import_node_path12.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4373
4817
|
if (!(0, import_node_fs9.existsSync)(file)) return null;
|
|
4374
4818
|
try {
|
|
4375
|
-
const buf = await (0,
|
|
4819
|
+
const buf = await (0, import_promises9.readFile)(file);
|
|
4376
4820
|
if (sha256(buf) !== hash) return null;
|
|
4377
4821
|
validateMedia(buf, mediaType);
|
|
4378
4822
|
return buf;
|
|
@@ -4383,17 +4827,17 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
4383
4827
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
4384
4828
|
const hash = knownHash ?? sha256(buf);
|
|
4385
4829
|
validateMedia(buf, mediaType);
|
|
4386
|
-
const file =
|
|
4830
|
+
const file = import_node_path12.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4387
4831
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
4388
|
-
await (0,
|
|
4832
|
+
await (0, import_promises9.mkdir)(cacheDir, { recursive: true });
|
|
4389
4833
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4390
|
-
await (0,
|
|
4834
|
+
await (0, import_promises9.writeFile)(tmp, buf);
|
|
4391
4835
|
try {
|
|
4392
|
-
await (0,
|
|
4836
|
+
await (0, import_promises9.rename)(tmp, file);
|
|
4393
4837
|
} catch (err) {
|
|
4394
4838
|
if (!(0, import_node_fs9.existsSync)(file)) throw err;
|
|
4395
4839
|
} finally {
|
|
4396
|
-
await (0,
|
|
4840
|
+
await (0, import_promises9.rm)(tmp, { force: true });
|
|
4397
4841
|
}
|
|
4398
4842
|
}
|
|
4399
4843
|
function mergeOutputs(previous, current) {
|
|
@@ -4425,8 +4869,8 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
4425
4869
|
|
|
4426
4870
|
// src/pipeline/doctor.ts
|
|
4427
4871
|
var import_node_fs10 = require("fs");
|
|
4428
|
-
var
|
|
4429
|
-
var
|
|
4872
|
+
var import_promises10 = require("fs/promises");
|
|
4873
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
4430
4874
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
4431
4875
|
const checks = [];
|
|
4432
4876
|
const add = (id, level, message2) => checks.push({ id, level, message: message2 });
|
|
@@ -4436,12 +4880,12 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4436
4880
|
`${Object.keys(loaded.manifest.styles).length} style(s), ${Object.keys(loaded.manifest.assets).length} asset(s), ${specs.length} resolved spec(s)`
|
|
4437
4881
|
);
|
|
4438
4882
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
4439
|
-
const dirs = [...new Set(specs.map((s) =>
|
|
4883
|
+
const dirs = [...new Set(specs.map((s) => import_node_path13.default.dirname(s.outFile)))];
|
|
4440
4884
|
const unwritable = [];
|
|
4441
4885
|
for (const dir of dirs) {
|
|
4442
4886
|
const ancestor = await nearestExistingDirectory(dir);
|
|
4443
4887
|
try {
|
|
4444
|
-
await (0,
|
|
4888
|
+
await (0, import_promises10.access)(ancestor, import_node_fs10.constants.W_OK);
|
|
4445
4889
|
} catch {
|
|
4446
4890
|
unwritable.push(dir);
|
|
4447
4891
|
}
|
|
@@ -4458,7 +4902,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4458
4902
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
4459
4903
|
const output = entry.outputs[index];
|
|
4460
4904
|
const spec = specByKey.get(key);
|
|
4461
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
4905
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path13.default.dirname(lockPath));
|
|
4462
4906
|
const owner = outputOwners.get(absolute);
|
|
4463
4907
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
4464
4908
|
outputOwners.set(absolute, key);
|
|
@@ -4473,7 +4917,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4473
4917
|
} else {
|
|
4474
4918
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
4475
4919
|
}
|
|
4476
|
-
const cacheDir =
|
|
4920
|
+
const cacheDir = import_node_path13.default.join(import_node_path13.default.dirname(lockPath), ".pixelkiln", "cache");
|
|
4477
4921
|
const stranded = [];
|
|
4478
4922
|
const invalidState = [];
|
|
4479
4923
|
const staleMapUrls = [];
|
|
@@ -4489,7 +4933,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4489
4933
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
4490
4934
|
let hasCache = false;
|
|
4491
4935
|
for (const output of entry.outputs) {
|
|
4492
|
-
const cached =
|
|
4936
|
+
const cached = import_node_path13.default.join(
|
|
4493
4937
|
cacheDir,
|
|
4494
4938
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
4495
4939
|
);
|
|
@@ -4524,8 +4968,15 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4524
4968
|
"error",
|
|
4525
4969
|
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
4526
4970
|
);
|
|
4971
|
+
} else if (!opts.provider.balance && !opts.provider.checkConnection) {
|
|
4972
|
+
add("provider", "ok", `${opts.provider.id} configured; connectivity check unavailable`);
|
|
4527
4973
|
} else if (!opts.provider.balance) {
|
|
4528
|
-
|
|
4974
|
+
try {
|
|
4975
|
+
await opts.provider.checkConnection();
|
|
4976
|
+
add("provider", "ok", `${opts.provider.id} reachable; balance reporting unavailable`);
|
|
4977
|
+
} catch (err) {
|
|
4978
|
+
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4979
|
+
}
|
|
4529
4980
|
} else {
|
|
4530
4981
|
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
4531
4982
|
try {
|
|
@@ -4541,10 +4992,10 @@ async function nearestExistingDirectory(start) {
|
|
|
4541
4992
|
let current = start;
|
|
4542
4993
|
for (; ; ) {
|
|
4543
4994
|
try {
|
|
4544
|
-
if ((await (0,
|
|
4995
|
+
if ((await (0, import_promises10.stat)(current)).isDirectory()) return current;
|
|
4545
4996
|
} catch {
|
|
4546
4997
|
}
|
|
4547
|
-
const parent =
|
|
4998
|
+
const parent = import_node_path13.default.dirname(current);
|
|
4548
4999
|
if (parent === current) return current;
|
|
4549
5000
|
current = parent;
|
|
4550
5001
|
}
|
|
@@ -4552,8 +5003,8 @@ async function nearestExistingDirectory(start) {
|
|
|
4552
5003
|
|
|
4553
5004
|
// src/pipeline/adopt.ts
|
|
4554
5005
|
var import_node_fs11 = require("fs");
|
|
4555
|
-
var
|
|
4556
|
-
var
|
|
5006
|
+
var import_promises11 = require("fs/promises");
|
|
5007
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
4557
5008
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
4558
5009
|
const log = opts.onProgress ?? (() => {
|
|
4559
5010
|
});
|
|
@@ -4568,7 +5019,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4568
5019
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
4569
5020
|
continue;
|
|
4570
5021
|
}
|
|
4571
|
-
const bytes = await (0,
|
|
5022
|
+
const bytes = await (0, import_promises11.readFile)(spec.outFile);
|
|
4572
5023
|
try {
|
|
4573
5024
|
decodePng(bytes);
|
|
4574
5025
|
} catch (err) {
|
|
@@ -5044,9 +5495,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
5044
5495
|
}
|
|
5045
5496
|
|
|
5046
5497
|
// src/pipeline/init.ts
|
|
5047
|
-
var
|
|
5498
|
+
var import_promises12 = require("fs/promises");
|
|
5048
5499
|
var import_node_fs12 = require("fs");
|
|
5049
|
-
var
|
|
5500
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
5050
5501
|
function pngSize(buf) {
|
|
5051
5502
|
if (buf.length < 24) return null;
|
|
5052
5503
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -5059,8 +5510,8 @@ function slugify(value) {
|
|
|
5059
5510
|
}
|
|
5060
5511
|
async function walk(dir, exclude) {
|
|
5061
5512
|
const out = [];
|
|
5062
|
-
for (const entry of await (0,
|
|
5063
|
-
const full =
|
|
5513
|
+
for (const entry of await (0, import_promises12.readdir)(dir, { withFileTypes: true })) {
|
|
5514
|
+
const full = import_node_path15.default.join(dir, entry.name);
|
|
5064
5515
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
5065
5516
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
5066
5517
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -5074,14 +5525,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
5074
5525
|
const skipped = [];
|
|
5075
5526
|
const seen = /* @__PURE__ */ new Map();
|
|
5076
5527
|
for (const file of files.sort()) {
|
|
5077
|
-
const size = pngSize(await (0,
|
|
5528
|
+
const size = pngSize(await (0, import_promises12.readFile)(file));
|
|
5078
5529
|
if (!size) {
|
|
5079
5530
|
skipped.push(`${file} (not a readable PNG)`);
|
|
5080
5531
|
continue;
|
|
5081
5532
|
}
|
|
5082
|
-
const rel =
|
|
5083
|
-
const category =
|
|
5084
|
-
let id = slugify(
|
|
5533
|
+
const rel = import_node_path15.default.relative(root, file);
|
|
5534
|
+
const category = import_node_path15.default.dirname(rel) === "." ? "" : import_node_path15.default.dirname(rel);
|
|
5535
|
+
let id = slugify(import_node_path15.default.basename(rel));
|
|
5085
5536
|
if (seen.has(id)) {
|
|
5086
5537
|
const n = seen.get(id) + 1;
|
|
5087
5538
|
seen.set(id, n);
|
|
@@ -5137,16 +5588,16 @@ function mode(values) {
|
|
|
5137
5588
|
}
|
|
5138
5589
|
|
|
5139
5590
|
// src/pipeline/salvage.ts
|
|
5140
|
-
var
|
|
5591
|
+
var import_promises13 = require("fs/promises");
|
|
5141
5592
|
var import_node_fs13 = require("fs");
|
|
5142
|
-
var
|
|
5593
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
5143
5594
|
async function loadClaims(lockPaths) {
|
|
5144
5595
|
const claimed = /* @__PURE__ */ new Set();
|
|
5145
5596
|
for (const p of lockPaths) {
|
|
5146
5597
|
if (!(0, import_node_fs13.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
5147
5598
|
let parsed;
|
|
5148
5599
|
try {
|
|
5149
|
-
parsed = parseLock(JSON.parse(await (0,
|
|
5600
|
+
parsed = parseLock(JSON.parse(await (0, import_promises13.readFile)(p, "utf8")));
|
|
5150
5601
|
} catch {
|
|
5151
5602
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
5152
5603
|
}
|
|
@@ -5193,20 +5644,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
5193
5644
|
return matchStyleByPattern(prompt, manifest);
|
|
5194
5645
|
}
|
|
5195
5646
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
5196
|
-
const own =
|
|
5647
|
+
const own = import_node_path16.default.resolve(ownManifestPath);
|
|
5197
5648
|
const siblingManifestPaths = [
|
|
5198
5649
|
.../* @__PURE__ */ new Set([
|
|
5199
5650
|
...workspaceManifestPaths,
|
|
5200
|
-
...claimPaths.map((c) =>
|
|
5651
|
+
...claimPaths.map((c) => import_node_path16.default.join(import_node_path16.default.dirname(import_node_path16.default.resolve(c)), "pixelkiln.manifest.json"))
|
|
5201
5652
|
])
|
|
5202
5653
|
];
|
|
5203
5654
|
const siblings = [];
|
|
5204
5655
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
5205
|
-
if (
|
|
5656
|
+
if (import_node_path16.default.resolve(siblingManifestPath) === own) continue;
|
|
5206
5657
|
if (!(0, import_node_fs13.existsSync)(siblingManifestPath)) continue;
|
|
5207
5658
|
try {
|
|
5208
5659
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
5209
|
-
siblings.push({ label:
|
|
5660
|
+
siblings.push({ label: import_node_path16.default.basename(import_node_path16.default.dirname(siblingManifestPath)), manifest });
|
|
5210
5661
|
} catch {
|
|
5211
5662
|
}
|
|
5212
5663
|
}
|
|
@@ -5322,8 +5773,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
5322
5773
|
}
|
|
5323
5774
|
|
|
5324
5775
|
// src/pick/salvage-server.ts
|
|
5325
|
-
var
|
|
5326
|
-
var
|
|
5776
|
+
var import_promises14 = require("fs/promises");
|
|
5777
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
5327
5778
|
|
|
5328
5779
|
// src/pick/salvage-sheet.ts
|
|
5329
5780
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -5488,7 +5939,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5488
5939
|
});
|
|
5489
5940
|
const html = renderSalvageSheet(orphans, {
|
|
5490
5941
|
styleId: ctx.styleId,
|
|
5491
|
-
importDir:
|
|
5942
|
+
importDir: import_node_path17.default.relative(process.cwd(), ctx.importDir) || "."
|
|
5492
5943
|
});
|
|
5493
5944
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
5494
5945
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -5517,10 +5968,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5517
5968
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
5518
5969
|
decodePng(buf);
|
|
5519
5970
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
5520
|
-
const rel =
|
|
5521
|
-
const outFile =
|
|
5522
|
-
await (0,
|
|
5523
|
-
await (0,
|
|
5971
|
+
const rel = import_node_path17.default.join("_salvaged", `${assetId}.png`);
|
|
5972
|
+
const outFile = import_node_path17.default.resolve(ctx.importDir, rel);
|
|
5973
|
+
await (0, import_promises14.mkdir)(import_node_path17.default.dirname(outFile), { recursive: true });
|
|
5974
|
+
await (0, import_promises14.writeFile)(outFile, buf);
|
|
5524
5975
|
ctx.manifest.assets[assetId] = {
|
|
5525
5976
|
prompt: orphan.prompt,
|
|
5526
5977
|
promptByStyle: {},
|
|
@@ -5546,7 +5997,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5546
5997
|
error: null,
|
|
5547
5998
|
sourceUrl: orphan.previewUrl,
|
|
5548
5999
|
outputs: [{
|
|
5549
|
-
path: portableOutputPath(outFile,
|
|
6000
|
+
path: portableOutputPath(outFile, import_node_path17.default.dirname(ctx.manifestPath)),
|
|
5550
6001
|
sha256: sha256(buf)
|
|
5551
6002
|
}],
|
|
5552
6003
|
submittedAt: orphan.createdAt,
|
|
@@ -5570,9 +6021,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5570
6021
|
}
|
|
5571
6022
|
}
|
|
5572
6023
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
5573
|
-
const raw = JSON.parse(await (0,
|
|
6024
|
+
const raw = JSON.parse(await (0, import_promises14.readFile)(ctx.manifestPath, "utf8"));
|
|
5574
6025
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
5575
|
-
await (0,
|
|
6026
|
+
await (0, import_promises14.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
5576
6027
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
5577
6028
|
return result;
|
|
5578
6029
|
}
|
|
@@ -5580,9 +6031,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5580
6031
|
}
|
|
5581
6032
|
|
|
5582
6033
|
// src/workspace.ts
|
|
5583
|
-
var
|
|
6034
|
+
var import_promises15 = require("fs/promises");
|
|
5584
6035
|
var import_node_fs14 = require("fs");
|
|
5585
|
-
var
|
|
6036
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
5586
6037
|
var import_zod4 = require("zod");
|
|
5587
6038
|
var WorkspaceProjectSchema = import_zod4.z.object({
|
|
5588
6039
|
id: import_zod4.z.string().min(1),
|
|
@@ -5610,7 +6061,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
5610
6061
|
if (!(0, import_node_fs14.existsSync)(workspacePath)) return { version: 1, projects: [] };
|
|
5611
6062
|
let raw;
|
|
5612
6063
|
try {
|
|
5613
|
-
raw = JSON.parse(await (0,
|
|
6064
|
+
raw = JSON.parse(await (0, import_promises15.readFile)(workspacePath, "utf8"));
|
|
5614
6065
|
} catch (err) {
|
|
5615
6066
|
throw new Error(
|
|
5616
6067
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -5624,22 +6075,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
5624
6075
|
version: 1,
|
|
5625
6076
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
5626
6077
|
};
|
|
5627
|
-
await (0,
|
|
6078
|
+
await (0, import_promises15.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(workspacePath)), { recursive: true });
|
|
5628
6079
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
5629
6080
|
try {
|
|
5630
|
-
await (0,
|
|
5631
|
-
await (0,
|
|
6081
|
+
await (0, import_promises15.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
6082
|
+
await (0, import_promises15.rename)(tmp, workspacePath);
|
|
5632
6083
|
} finally {
|
|
5633
|
-
await (0,
|
|
6084
|
+
await (0, import_promises15.rm)(tmp, { force: true });
|
|
5634
6085
|
}
|
|
5635
6086
|
}
|
|
5636
6087
|
function toPortablePath(dir, absolute) {
|
|
5637
|
-
return
|
|
6088
|
+
return import_node_path18.default.relative(dir, absolute).split(import_node_path18.default.sep).join("/");
|
|
5638
6089
|
}
|
|
5639
6090
|
function resolveProject(dir, project) {
|
|
5640
6091
|
return {
|
|
5641
|
-
manifestPath:
|
|
5642
|
-
lockPath:
|
|
6092
|
+
manifestPath: import_node_path18.default.resolve(dir, project.manifest.split("/").join(import_node_path18.default.sep)),
|
|
6093
|
+
lockPath: import_node_path18.default.resolve(dir, project.lock.split("/").join(import_node_path18.default.sep))
|
|
5643
6094
|
};
|
|
5644
6095
|
}
|
|
5645
6096
|
function validateWorkspace(ws, dir) {
|
|
@@ -5652,7 +6103,7 @@ function validateWorkspace(ws, dir) {
|
|
|
5652
6103
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5653
6104
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
5654
6105
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
5655
|
-
if (
|
|
6106
|
+
if (import_node_path18.default.isAbsolute(project.manifest) || import_node_path18.default.isAbsolute(project.lock)) {
|
|
5656
6107
|
diagnostics.push({
|
|
5657
6108
|
id: "absolute-path",
|
|
5658
6109
|
level: "warning",
|
|
@@ -5808,6 +6259,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
5808
6259
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5809
6260
|
0 && (module.exports = {
|
|
5810
6261
|
AssetSchema,
|
|
6262
|
+
ComfyUIProvider,
|
|
5811
6263
|
DEFAULT_RATE_LIMIT,
|
|
5812
6264
|
FAKE_PNG,
|
|
5813
6265
|
FakeProvider,
|