pixelkiln 0.6.0 → 0.8.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 +228 -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 +68 -10
- 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 +54 -0
- package/skills/pixelkiln/references/mixed-providers.md +15 -7
package/dist/index.js
CHANGED
|
@@ -40,8 +40,8 @@ var StyleSchema = z.object({
|
|
|
40
40
|
// `map` is the default because it is 20-40x cheaper and correct for any
|
|
41
41
|
// asset that is not going to be rotated or animated.
|
|
42
42
|
generator: GeneratorSchema.default("map"),
|
|
43
|
-
/** Square edge length
|
|
44
|
-
size: z.number().int().min(
|
|
43
|
+
/** Square edge length. The active provider owns its exact limit. */
|
|
44
|
+
size: z.number().int().min(16).max(8192).optional(),
|
|
45
45
|
view: z.string().optional(),
|
|
46
46
|
/** Appended to every asset prompt in this style. Where the look is defined. */
|
|
47
47
|
promptSuffix: z.string().default(""),
|
|
@@ -157,8 +157,8 @@ var AssetSchema = z.object({
|
|
|
157
157
|
/** Subdirectory under the style's outDir. Optional. */
|
|
158
158
|
category: z.string().optional(),
|
|
159
159
|
/** Overrides the style default. `map` generator only. */
|
|
160
|
-
width: z.number().int().min(16).max(
|
|
161
|
-
height: z.number().int().min(16).max(
|
|
160
|
+
width: z.number().int().min(16).max(8192).optional(),
|
|
161
|
+
height: z.number().int().min(16).max(8192).optional(),
|
|
162
162
|
/** Overrides the style default. `1dir` generator only. */
|
|
163
163
|
size: z.number().int().min(32).max(256).optional(),
|
|
164
164
|
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
@@ -416,11 +416,11 @@ var PixelLabClient = class {
|
|
|
416
416
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
417
417
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
418
418
|
*/
|
|
419
|
-
async request(
|
|
419
|
+
async request(path18, init, attempt = 0) {
|
|
420
420
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
421
421
|
let res;
|
|
422
422
|
try {
|
|
423
|
-
res = await fetch(`${BASE}${
|
|
423
|
+
res = await fetch(`${BASE}${path18}`, {
|
|
424
424
|
...init,
|
|
425
425
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
426
426
|
headers: {
|
|
@@ -432,24 +432,24 @@ var PixelLabClient = class {
|
|
|
432
432
|
} catch (err) {
|
|
433
433
|
if (attempt < MAX_RETRIES) {
|
|
434
434
|
await sleep(backoffMs(attempt));
|
|
435
|
-
return this.request(
|
|
435
|
+
return this.request(path18, init, attempt + 1);
|
|
436
436
|
}
|
|
437
437
|
throw err;
|
|
438
438
|
}
|
|
439
439
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
440
440
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
441
441
|
await sleep(waitMs);
|
|
442
|
-
return this.request(
|
|
442
|
+
return this.request(path18, init, attempt + 1);
|
|
443
443
|
}
|
|
444
444
|
const text = await res.text();
|
|
445
445
|
if (!res.ok) {
|
|
446
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
446
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path18} \u2192 ${res.status}`, res.status, text);
|
|
447
447
|
}
|
|
448
448
|
if (!text) return {};
|
|
449
449
|
try {
|
|
450
450
|
return JSON.parse(text);
|
|
451
451
|
} catch {
|
|
452
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
452
|
+
throw new Error(`${init?.method ?? "GET"} ${path18} returned invalid JSON`);
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
async balance() {
|
|
@@ -1213,6 +1213,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1213
1213
|
};
|
|
1214
1214
|
}
|
|
1215
1215
|
validate(spec, styleImages) {
|
|
1216
|
+
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
1217
|
+
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
1218
|
+
}
|
|
1219
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
|
|
1220
|
+
throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
|
|
1221
|
+
}
|
|
1216
1222
|
if (spec.generator === "map") {
|
|
1217
1223
|
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1218
1224
|
requirePixelLabOption("outline", spec.outline, [
|
|
@@ -1489,9 +1495,9 @@ var RetroDiffusionClient = class {
|
|
|
1489
1495
|
}
|
|
1490
1496
|
return balance;
|
|
1491
1497
|
}
|
|
1492
|
-
async call(
|
|
1498
|
+
async call(path18, init = {}) {
|
|
1493
1499
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1494
|
-
const response = await this.request(`${this.baseUrl}${
|
|
1500
|
+
const response = await this.request(`${this.baseUrl}${path18}`, {
|
|
1495
1501
|
...init,
|
|
1496
1502
|
headers: {
|
|
1497
1503
|
"Content-Type": "application/json",
|
|
@@ -1761,8 +1767,470 @@ function retroError(value) {
|
|
|
1761
1767
|
return JSON.stringify(value);
|
|
1762
1768
|
}
|
|
1763
1769
|
|
|
1764
|
-
// src/providers/
|
|
1770
|
+
// src/providers/comfyui.ts
|
|
1771
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1772
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1773
|
+
import path2 from "path";
|
|
1774
|
+
|
|
1775
|
+
// src/hash.ts
|
|
1765
1776
|
import { createHash } from "crypto";
|
|
1777
|
+
import { readFile } from "fs/promises";
|
|
1778
|
+
function sha256(data) {
|
|
1779
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1780
|
+
}
|
|
1781
|
+
async function sha256File(path18) {
|
|
1782
|
+
return sha256(await readFile(path18));
|
|
1783
|
+
}
|
|
1784
|
+
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
|
|
1785
|
+
return sha256(
|
|
1786
|
+
JSON.stringify({
|
|
1787
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
1788
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1789
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1790
|
+
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
1791
|
+
generator: spec.generator,
|
|
1792
|
+
prompt: spec.prompt,
|
|
1793
|
+
width: spec.width,
|
|
1794
|
+
height: spec.height,
|
|
1795
|
+
view: spec.view,
|
|
1796
|
+
outline: spec.outline ?? null,
|
|
1797
|
+
shading: spec.shading ?? null,
|
|
1798
|
+
detail: spec.detail ?? null,
|
|
1799
|
+
seed: spec.seed ?? null,
|
|
1800
|
+
palette: spec.palette,
|
|
1801
|
+
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1802
|
+
// undefined for every other generator. `tileSize` is intentionally
|
|
1803
|
+
// absent — width/height are derived from it, so it is already covered.
|
|
1804
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1805
|
+
tileType: spec.tileType,
|
|
1806
|
+
tileView: spec.tileView,
|
|
1807
|
+
tileFeature: spec.tileFeature,
|
|
1808
|
+
outlineMode: spec.outlineMode,
|
|
1809
|
+
styleImages: styleImageHashes
|
|
1810
|
+
})
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// src/providers/comfyui.ts
|
|
1815
|
+
var DEFAULT_BASE_URL2 = "http://127.0.0.1:8188";
|
|
1816
|
+
var ComfyUIClient = class {
|
|
1817
|
+
constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL2, request = fetch) {
|
|
1818
|
+
this.request = request;
|
|
1819
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
1820
|
+
}
|
|
1821
|
+
request;
|
|
1822
|
+
baseUrl;
|
|
1823
|
+
async checkConnection() {
|
|
1824
|
+
const value = await this.json("system_stats");
|
|
1825
|
+
if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
|
|
1826
|
+
}
|
|
1827
|
+
async submit(workflow) {
|
|
1828
|
+
const value = await this.json("prompt", {
|
|
1829
|
+
method: "POST",
|
|
1830
|
+
body: JSON.stringify({ prompt: workflow, client_id: randomUUID2() })
|
|
1831
|
+
});
|
|
1832
|
+
if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
|
|
1833
|
+
const promptId = value.prompt_id;
|
|
1834
|
+
if (typeof promptId !== "string" || !promptId) {
|
|
1835
|
+
const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
"ComfyUI did not return a prompt id" + (nodes.length ? `; invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}` : "")
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return promptId;
|
|
1841
|
+
}
|
|
1842
|
+
async history(promptId) {
|
|
1843
|
+
return this.json(`history/${encodeURIComponent(promptId)}`);
|
|
1844
|
+
}
|
|
1845
|
+
viewUrl(image) {
|
|
1846
|
+
const url = this.endpoint("view");
|
|
1847
|
+
url.searchParams.set("filename", image.filename);
|
|
1848
|
+
url.searchParams.set("subfolder", image.subfolder);
|
|
1849
|
+
url.searchParams.set("type", image.type);
|
|
1850
|
+
return url.toString();
|
|
1851
|
+
}
|
|
1852
|
+
async download(source) {
|
|
1853
|
+
let target = source;
|
|
1854
|
+
if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
|
|
1855
|
+
const response = await this.request(target);
|
|
1856
|
+
if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
|
|
1857
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1858
|
+
}
|
|
1859
|
+
endpoint(route) {
|
|
1860
|
+
return new URL(route.replace(/^\/+/, ""), `${this.baseUrl}/`);
|
|
1861
|
+
}
|
|
1862
|
+
async json(route, init = {}) {
|
|
1863
|
+
const url = this.endpoint(route);
|
|
1864
|
+
const response = await this.request(url, {
|
|
1865
|
+
...init,
|
|
1866
|
+
headers: {
|
|
1867
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
1868
|
+
...init.headers
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
const text = await response.text();
|
|
1872
|
+
let value = null;
|
|
1873
|
+
try {
|
|
1874
|
+
value = text ? JSON.parse(text) : null;
|
|
1875
|
+
} catch {
|
|
1876
|
+
if (response.ok) throw new Error(`ComfyUI returned invalid JSON from ${url.pathname}`);
|
|
1877
|
+
value = null;
|
|
1878
|
+
}
|
|
1879
|
+
if (!response.ok) {
|
|
1880
|
+
const detail = apiError(value);
|
|
1881
|
+
throw new Error(
|
|
1882
|
+
`ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
return value;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
var ComfyUIProvider = class _ComfyUIProvider {
|
|
1889
|
+
constructor(client = new ComfyUIClient()) {
|
|
1890
|
+
this.client = client;
|
|
1891
|
+
}
|
|
1892
|
+
client;
|
|
1893
|
+
id = "comfyui";
|
|
1894
|
+
static fromEnv() {
|
|
1895
|
+
return new _ComfyUIProvider();
|
|
1896
|
+
}
|
|
1897
|
+
static forOffline() {
|
|
1898
|
+
return new _ComfyUIProvider(new ComfyUIClient(DEFAULT_BASE_URL2));
|
|
1899
|
+
}
|
|
1900
|
+
static forDownloads() {
|
|
1901
|
+
return new _ComfyUIProvider();
|
|
1902
|
+
}
|
|
1903
|
+
async resolveOptions(value, context) {
|
|
1904
|
+
const options = parseOptions(value);
|
|
1905
|
+
const workflowPath = path2.resolve(context.root, options.workflowFile);
|
|
1906
|
+
let raw;
|
|
1907
|
+
try {
|
|
1908
|
+
raw = await readFile2(workflowPath, "utf8");
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
throw new Error(
|
|
1911
|
+
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
let parsed;
|
|
1915
|
+
try {
|
|
1916
|
+
parsed = JSON.parse(raw);
|
|
1917
|
+
} catch {
|
|
1918
|
+
throw new Error(`ComfyUI workflow for style "${context.styleId}" is not valid JSON: ${workflowPath}`);
|
|
1919
|
+
}
|
|
1920
|
+
const workflow = parseWorkflow(parsed, workflowPath);
|
|
1921
|
+
validateWorkflowBindings(workflow, options);
|
|
1922
|
+
const workflowSha256 = sha256(JSON.stringify(canonical(workflow)));
|
|
1923
|
+
const resolved = { ...options, workflow, workflowSha256 };
|
|
1924
|
+
return {
|
|
1925
|
+
options: resolved,
|
|
1926
|
+
identity: {
|
|
1927
|
+
outputNodeId: options.outputNodeId,
|
|
1928
|
+
numImages: options.numImages,
|
|
1929
|
+
bindings: options.bindings,
|
|
1930
|
+
workflowSha256
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
supports(generator) {
|
|
1935
|
+
return generator === "map";
|
|
1936
|
+
}
|
|
1937
|
+
estimate(spec) {
|
|
1938
|
+
return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
|
|
1939
|
+
}
|
|
1940
|
+
validate(spec, styleImages) {
|
|
1941
|
+
const options = resolvedOptions(spec);
|
|
1942
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
1943
|
+
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
1944
|
+
}
|
|
1945
|
+
if (styleImages.length) {
|
|
1946
|
+
throw new Error("ComfyUI styleImages are not supported yet; keep references inside the workflow");
|
|
1947
|
+
}
|
|
1948
|
+
if (spec.palette.length) {
|
|
1949
|
+
throw new Error("ComfyUI does not map PixelKiln palette values yet; encode palette control in the workflow");
|
|
1950
|
+
}
|
|
1951
|
+
if (spec.seed != null && !options.bindings.seed) {
|
|
1952
|
+
throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
|
|
1953
|
+
}
|
|
1954
|
+
validateWorkflowBindings(options.workflow, options);
|
|
1955
|
+
}
|
|
1956
|
+
rateLimit() {
|
|
1957
|
+
return { spacingMs: 0, maxInFlight: 1 };
|
|
1958
|
+
}
|
|
1959
|
+
async submit(spec, styleImages) {
|
|
1960
|
+
this.validate(spec, styleImages);
|
|
1961
|
+
const options = resolvedOptions(spec);
|
|
1962
|
+
const workflow = structuredClone(options.workflow);
|
|
1963
|
+
setBinding(workflow, options.bindings.prompt, spec.prompt);
|
|
1964
|
+
setBinding(workflow, options.bindings.width, spec.width);
|
|
1965
|
+
setBinding(workflow, options.bindings.height, spec.height);
|
|
1966
|
+
setBinding(workflow, options.bindings.batchSize, options.numImages);
|
|
1967
|
+
if (spec.seed != null && options.bindings.seed) {
|
|
1968
|
+
setBinding(workflow, options.bindings.seed, spec.seed);
|
|
1969
|
+
}
|
|
1970
|
+
const promptId = await this.client.submit(workflow);
|
|
1971
|
+
return { jobId: encodeJob(promptId, options.outputNodeId) };
|
|
1972
|
+
}
|
|
1973
|
+
async poll(jobId, _generator, context) {
|
|
1974
|
+
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
1975
|
+
const history = await this.client.history(promptId);
|
|
1976
|
+
const entry = historyEntry(history, promptId);
|
|
1977
|
+
if (!entry) return { status: "processing" };
|
|
1978
|
+
const status = isObject(entry.status) ? entry.status : {};
|
|
1979
|
+
const statusText = typeof status.status_str === "string" ? status.status_str : "";
|
|
1980
|
+
if (statusText === "error") {
|
|
1981
|
+
return { status: "failed", error: historyError(status) };
|
|
1982
|
+
}
|
|
1983
|
+
if (status.completed === false) return { status: "processing" };
|
|
1984
|
+
const images = outputImages(entry, outputNodeId);
|
|
1985
|
+
if (images instanceof Error) return { status: "failed", error: images.message };
|
|
1986
|
+
const expected = context?.spec ? resolvedOptions(context.spec).numImages : null;
|
|
1987
|
+
if (expected != null && images.length !== expected) {
|
|
1988
|
+
return {
|
|
1989
|
+
status: "failed",
|
|
1990
|
+
error: `ComfyUI output node "${outputNodeId}" returned ${images.length} image(s), expected ${expected}`
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
const metadata = context?.spec ? comfyMetadata(context.spec, promptId, outputNodeId, images) : {
|
|
1994
|
+
promptId,
|
|
1995
|
+
outputNodeId,
|
|
1996
|
+
outputCount: images.length
|
|
1997
|
+
};
|
|
1998
|
+
if (images.length > 1) {
|
|
1999
|
+
return {
|
|
2000
|
+
status: "review",
|
|
2001
|
+
candidateUrls: images.map((image) => this.client.viewUrl(image)),
|
|
2002
|
+
metadata
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
const sourceUrl = sourceRef(images[0]);
|
|
2006
|
+
return {
|
|
2007
|
+
status: "ready",
|
|
2008
|
+
objectId: `${promptId}#${outputNodeId}#0`,
|
|
2009
|
+
sourceUrl,
|
|
2010
|
+
sources: [{ url: sourceUrl, mediaType: MediaType.PNG }],
|
|
2011
|
+
metadata
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
async selectCandidate(jobId, index) {
|
|
2015
|
+
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
2016
|
+
const history = await this.client.history(promptId);
|
|
2017
|
+
const entry = historyEntry(history, promptId);
|
|
2018
|
+
if (!entry) throw new Error(`ComfyUI prompt ${promptId} is not complete`);
|
|
2019
|
+
const images = outputImages(entry, outputNodeId);
|
|
2020
|
+
if (images instanceof Error) throw images;
|
|
2021
|
+
const image = images[index];
|
|
2022
|
+
if (!image) throw new Error(`ComfyUI prompt ${promptId} has no candidate at index ${index}`);
|
|
2023
|
+
return {
|
|
2024
|
+
objectId: `${promptId}#${outputNodeId}#${index}`,
|
|
2025
|
+
sourceUrl: sourceRef(image)
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
async download(url) {
|
|
2029
|
+
return this.client.download(url);
|
|
2030
|
+
}
|
|
2031
|
+
async checkConnection() {
|
|
2032
|
+
await this.client.checkConnection();
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
function parseOptions(value) {
|
|
2036
|
+
const allowed = /* @__PURE__ */ new Set(["workflowFile", "outputNodeId", "numImages", "bindings", "workflow", "workflowSha256"]);
|
|
2037
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
2038
|
+
if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
|
|
2039
|
+
const workflowFile = requiredString(value.workflowFile, "workflowFile");
|
|
2040
|
+
const outputNodeId = requiredString(value.outputNodeId, "outputNodeId");
|
|
2041
|
+
const numImages = value.numImages ?? 1;
|
|
2042
|
+
if (!Number.isInteger(numImages) || Number(numImages) < 1 || Number(numImages) > 16) {
|
|
2043
|
+
throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
|
|
2044
|
+
}
|
|
2045
|
+
if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
|
|
2046
|
+
const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
|
|
2047
|
+
const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
|
|
2048
|
+
if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
|
|
2049
|
+
const bindings = {
|
|
2050
|
+
prompt: parseBinding(value.bindings.prompt, "prompt"),
|
|
2051
|
+
width: parseBinding(value.bindings.width, "width"),
|
|
2052
|
+
height: parseBinding(value.bindings.height, "height"),
|
|
2053
|
+
batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
|
|
2054
|
+
...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
|
|
2055
|
+
};
|
|
2056
|
+
const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
|
|
2057
|
+
const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
|
|
2058
|
+
return {
|
|
2059
|
+
workflowFile,
|
|
2060
|
+
outputNodeId,
|
|
2061
|
+
numImages: Number(numImages),
|
|
2062
|
+
bindings,
|
|
2063
|
+
...workflow ? { workflow } : {},
|
|
2064
|
+
...workflowSha256 ? { workflowSha256 } : {}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function resolvedOptions(spec) {
|
|
2068
|
+
const options = parseOptions(spec.providerOptions);
|
|
2069
|
+
if (!options.workflow || !options.workflowSha256) {
|
|
2070
|
+
throw new Error("ComfyUI workflow options were not resolved from workflowFile");
|
|
2071
|
+
}
|
|
2072
|
+
return options;
|
|
2073
|
+
}
|
|
2074
|
+
function parseBinding(value, name) {
|
|
2075
|
+
if (!isObject(value)) throw new Error(`ComfyUI bindings.${name} must be an object`);
|
|
2076
|
+
const extra = Object.keys(value).filter((key) => key !== "nodeId" && key !== "input");
|
|
2077
|
+
if (extra.length) throw new Error(`Unknown ComfyUI bindings.${name} field(s): ${extra.join(", ")}`);
|
|
2078
|
+
return {
|
|
2079
|
+
nodeId: requiredString(value.nodeId, `bindings.${name}.nodeId`),
|
|
2080
|
+
input: requiredString(value.input, `bindings.${name}.input`)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function parseWorkflow(value, label) {
|
|
2084
|
+
if (!isObject(value) || !Object.keys(value).length) {
|
|
2085
|
+
throw new Error(`ComfyUI workflow is not a non-empty API-format object: ${label}`);
|
|
2086
|
+
}
|
|
2087
|
+
for (const [nodeId, node] of Object.entries(value)) {
|
|
2088
|
+
if (!isObject(node) || typeof node.class_type !== "string" || !isObject(node.inputs)) {
|
|
2089
|
+
throw new Error(`ComfyUI workflow node "${nodeId}" must contain class_type and inputs`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return value;
|
|
2093
|
+
}
|
|
2094
|
+
function validateWorkflowBindings(workflow, options) {
|
|
2095
|
+
for (const [name, binding] of Object.entries(options.bindings)) {
|
|
2096
|
+
if (!binding) continue;
|
|
2097
|
+
const node = workflow[binding.nodeId];
|
|
2098
|
+
if (!node) throw new Error(`ComfyUI ${name} binding refers to missing node "${binding.nodeId}"`);
|
|
2099
|
+
if (!Object.hasOwn(node.inputs, binding.input)) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (!workflow[options.outputNodeId]) {
|
|
2106
|
+
throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
function setBinding(workflow, binding, value) {
|
|
2110
|
+
const node = workflow[binding.nodeId];
|
|
2111
|
+
if (!node || !Object.hasOwn(node.inputs, binding.input)) {
|
|
2112
|
+
throw new Error(`ComfyUI binding ${binding.nodeId}.${binding.input} is unavailable`);
|
|
2113
|
+
}
|
|
2114
|
+
node.inputs[binding.input] = value;
|
|
2115
|
+
}
|
|
2116
|
+
function historyEntry(history, promptId) {
|
|
2117
|
+
if (!isObject(history)) throw new Error("ComfyUI returned invalid history JSON");
|
|
2118
|
+
const entry = history[promptId];
|
|
2119
|
+
if (entry == null) return null;
|
|
2120
|
+
if (!isObject(entry)) throw new Error(`ComfyUI returned invalid history for prompt ${promptId}`);
|
|
2121
|
+
return entry;
|
|
2122
|
+
}
|
|
2123
|
+
function outputImages(entry, outputNodeId) {
|
|
2124
|
+
if (!isObject(entry.outputs)) return new Error("ComfyUI completed without workflow outputs");
|
|
2125
|
+
const output = entry.outputs[outputNodeId];
|
|
2126
|
+
if (!isObject(output) || !Array.isArray(output.images)) {
|
|
2127
|
+
return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
|
|
2128
|
+
}
|
|
2129
|
+
const images = [];
|
|
2130
|
+
for (const value of output.images) {
|
|
2131
|
+
if (!isObject(value) || typeof value.filename !== "string" || typeof value.subfolder !== "string" || value.type !== "output") {
|
|
2132
|
+
return new Error(`ComfyUI output node "${outputNodeId}" returned an invalid image record`);
|
|
2133
|
+
}
|
|
2134
|
+
images.push({ filename: value.filename, subfolder: value.subfolder, type: "output" });
|
|
2135
|
+
}
|
|
2136
|
+
if (!images.length) return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
|
|
2137
|
+
return images;
|
|
2138
|
+
}
|
|
2139
|
+
function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
2140
|
+
const options = resolvedOptions(spec);
|
|
2141
|
+
return {
|
|
2142
|
+
promptId,
|
|
2143
|
+
outputNodeId,
|
|
2144
|
+
outputCount: images.length,
|
|
2145
|
+
workflowFile: options.workflowFile,
|
|
2146
|
+
workflowSha256: options.workflowSha256,
|
|
2147
|
+
files: images.map((image) => ({ ...image }))
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function encodeJob(promptId, outputNodeId) {
|
|
2151
|
+
return `${promptId}#${encodeURIComponent(outputNodeId)}`;
|
|
2152
|
+
}
|
|
2153
|
+
function decodeJob(jobId) {
|
|
2154
|
+
const split = jobId.lastIndexOf("#");
|
|
2155
|
+
if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
|
|
2156
|
+
return {
|
|
2157
|
+
promptId: jobId.slice(0, split),
|
|
2158
|
+
outputNodeId: decodeURIComponent(jobId.slice(split + 1))
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function sourceRef(image) {
|
|
2162
|
+
const url = new URL("comfyui://output");
|
|
2163
|
+
url.searchParams.set("filename", image.filename);
|
|
2164
|
+
url.searchParams.set("subfolder", image.subfolder);
|
|
2165
|
+
url.searchParams.set("type", image.type);
|
|
2166
|
+
return url.toString();
|
|
2167
|
+
}
|
|
2168
|
+
function parseSource(source) {
|
|
2169
|
+
let url;
|
|
2170
|
+
try {
|
|
2171
|
+
url = new URL(source);
|
|
2172
|
+
} catch {
|
|
2173
|
+
throw new Error("Invalid ComfyUI output reference");
|
|
2174
|
+
}
|
|
2175
|
+
const filename = url.searchParams.get("filename");
|
|
2176
|
+
const subfolder = url.searchParams.get("subfolder");
|
|
2177
|
+
const type = url.searchParams.get("type");
|
|
2178
|
+
if (url.protocol !== "comfyui:" || url.hostname !== "output" || !filename || subfolder == null || type !== "output") {
|
|
2179
|
+
throw new Error("Invalid ComfyUI output reference");
|
|
2180
|
+
}
|
|
2181
|
+
return { filename, subfolder, type };
|
|
2182
|
+
}
|
|
2183
|
+
function normalizeBaseUrl(value) {
|
|
2184
|
+
let url;
|
|
2185
|
+
try {
|
|
2186
|
+
url = new URL(value);
|
|
2187
|
+
} catch {
|
|
2188
|
+
throw new Error("COMFYUI_BASE_URL must be an absolute HTTP(S) URL");
|
|
2189
|
+
}
|
|
2190
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2191
|
+
throw new Error("COMFYUI_BASE_URL must use HTTP or HTTPS");
|
|
2192
|
+
}
|
|
2193
|
+
url.hash = "";
|
|
2194
|
+
url.search = "";
|
|
2195
|
+
return url.toString().replace(/\/$/, "");
|
|
2196
|
+
}
|
|
2197
|
+
function historyError(status) {
|
|
2198
|
+
const messages = Array.isArray(status.messages) ? status.messages : [];
|
|
2199
|
+
for (const message2 of messages) {
|
|
2200
|
+
if (!Array.isArray(message2) || message2[0] !== "execution_error" || !isObject(message2[1])) continue;
|
|
2201
|
+
const detail = message2[1].exception_message;
|
|
2202
|
+
if (typeof detail === "string" && detail.trim()) {
|
|
2203
|
+
return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return "ComfyUI execution failed";
|
|
2207
|
+
}
|
|
2208
|
+
function apiError(value) {
|
|
2209
|
+
if (!isObject(value)) return null;
|
|
2210
|
+
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;
|
|
2211
|
+
const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
|
|
2212
|
+
if (nodes.length) {
|
|
2213
|
+
return `${raw ? `${raw}; ` : ""}invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}`;
|
|
2214
|
+
}
|
|
2215
|
+
return raw?.trim().slice(0, 300) || null;
|
|
2216
|
+
}
|
|
2217
|
+
function requiredString(value, name) {
|
|
2218
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`ComfyUI ${name} must be a non-empty string`);
|
|
2219
|
+
return value.trim();
|
|
2220
|
+
}
|
|
2221
|
+
function isObject(value) {
|
|
2222
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2223
|
+
}
|
|
2224
|
+
function canonical(value) {
|
|
2225
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
2226
|
+
if (!isObject(value)) return value;
|
|
2227
|
+
return Object.fromEntries(
|
|
2228
|
+
Object.keys(value).sort().map((key) => [key, canonical(value[key])])
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/providers/fake.ts
|
|
2233
|
+
import { createHash as createHash2 } from "crypto";
|
|
1766
2234
|
var FAKE_PNG = Buffer.from(
|
|
1767
2235
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
1768
2236
|
"base64"
|
|
@@ -1846,7 +2314,7 @@ var FakeProvider = class {
|
|
|
1846
2314
|
}
|
|
1847
2315
|
async download(url) {
|
|
1848
2316
|
if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
|
|
1849
|
-
const salt =
|
|
2317
|
+
const salt = createHash2("sha256").update(url).digest();
|
|
1850
2318
|
return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
|
|
1851
2319
|
}
|
|
1852
2320
|
async balance() {
|
|
@@ -1945,56 +2413,23 @@ registerProvider({
|
|
|
1945
2413
|
return RetroDiffusionProvider.forOffline();
|
|
1946
2414
|
}
|
|
1947
2415
|
});
|
|
2416
|
+
registerProvider({
|
|
2417
|
+
id: "comfyui",
|
|
2418
|
+
create(mode2) {
|
|
2419
|
+
if (mode2 === "online") return ComfyUIProvider.fromEnv();
|
|
2420
|
+
if (mode2 === "downloads") return ComfyUIProvider.forDownloads();
|
|
2421
|
+
return ComfyUIProvider.forOffline();
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
1948
2424
|
|
|
1949
2425
|
// src/manifest.ts
|
|
1950
|
-
import { readFile as
|
|
2426
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1951
2427
|
import { existsSync as existsSync2 } from "fs";
|
|
1952
|
-
import
|
|
1953
|
-
|
|
1954
|
-
// src/hash.ts
|
|
1955
|
-
import { createHash as createHash2 } from "crypto";
|
|
1956
|
-
import { readFile } from "fs/promises";
|
|
1957
|
-
function sha256(data) {
|
|
1958
|
-
return createHash2("sha256").update(data).digest("hex");
|
|
1959
|
-
}
|
|
1960
|
-
async function sha256File(path17) {
|
|
1961
|
-
return sha256(await readFile(path17));
|
|
1962
|
-
}
|
|
1963
|
-
function specHash(spec, styleImageHashes) {
|
|
1964
|
-
return sha256(
|
|
1965
|
-
JSON.stringify({
|
|
1966
|
-
// Preserve every existing PixelLab hash while making a provider switch
|
|
1967
|
-
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1968
|
-
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1969
|
-
providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
|
|
1970
|
-
generator: spec.generator,
|
|
1971
|
-
prompt: spec.prompt,
|
|
1972
|
-
width: spec.width,
|
|
1973
|
-
height: spec.height,
|
|
1974
|
-
view: spec.view,
|
|
1975
|
-
outline: spec.outline ?? null,
|
|
1976
|
-
shading: spec.shading ?? null,
|
|
1977
|
-
detail: spec.detail ?? null,
|
|
1978
|
-
seed: spec.seed ?? null,
|
|
1979
|
-
palette: spec.palette,
|
|
1980
|
-
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1981
|
-
// undefined for every other generator. `tileSize` is intentionally
|
|
1982
|
-
// absent — width/height are derived from it, so it is already covered.
|
|
1983
|
-
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1984
|
-
tileType: spec.tileType,
|
|
1985
|
-
tileView: spec.tileView,
|
|
1986
|
-
tileFeature: spec.tileFeature,
|
|
1987
|
-
outlineMode: spec.outlineMode,
|
|
1988
|
-
styleImages: styleImageHashes
|
|
1989
|
-
})
|
|
1990
|
-
);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// src/manifest.ts
|
|
2428
|
+
import path3 from "path";
|
|
1994
2429
|
async function loadManifest(manifestPath) {
|
|
1995
|
-
const abs =
|
|
2430
|
+
const abs = path3.resolve(manifestPath);
|
|
1996
2431
|
if (!existsSync2(abs)) throw new Error(`No manifest at ${abs}`);
|
|
1997
|
-
const parsed = ManifestSchema.safeParse(JSON.parse(await
|
|
2432
|
+
const parsed = ManifestSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
|
|
1998
2433
|
if (!parsed.success) {
|
|
1999
2434
|
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
2000
2435
|
throw new Error(`Manifest at ${abs} is invalid:
|
|
@@ -2016,7 +2451,7 @@ ${issues}`);
|
|
|
2016
2451
|
throw new Error(`Manifest at ${abs} is invalid:
|
|
2017
2452
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
2018
2453
|
}
|
|
2019
|
-
return { manifest: parsed.data, root:
|
|
2454
|
+
return { manifest: parsed.data, root: path3.dirname(abs), path: abs };
|
|
2020
2455
|
}
|
|
2021
2456
|
async function resolveSpecs(loaded, filter) {
|
|
2022
2457
|
const { manifest, root } = loaded;
|
|
@@ -2039,11 +2474,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2039
2474
|
}
|
|
2040
2475
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
2041
2476
|
async function loadStyleImage(rel) {
|
|
2042
|
-
const abs =
|
|
2477
|
+
const abs = path3.resolve(root, rel);
|
|
2043
2478
|
let hit = styleImageCache.get(abs);
|
|
2044
2479
|
if (!hit) {
|
|
2045
2480
|
if (!existsSync2(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
2046
|
-
const buf = await
|
|
2481
|
+
const buf = await readFile3(abs);
|
|
2047
2482
|
const metadata = imageMetadata(buf);
|
|
2048
2483
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
2049
2484
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2056,6 +2491,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2056
2491
|
}
|
|
2057
2492
|
for (const styleId of styleIds) {
|
|
2058
2493
|
const style = manifest.styles[styleId];
|
|
2494
|
+
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
2495
|
+
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
2496
|
+
const providerOptions = optionResolution.options;
|
|
2497
|
+
const providerOptionIdentity = optionResolution.identity ?? providerOptions;
|
|
2059
2498
|
const styleImageHashes = [];
|
|
2060
2499
|
const styleImageDimensions = [];
|
|
2061
2500
|
for (const img of style.styleImages) {
|
|
@@ -2088,15 +2527,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2088
2527
|
}
|
|
2089
2528
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
2090
2529
|
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
2091
|
-
const relFile = asset.file ??
|
|
2092
|
-
const outFile =
|
|
2530
|
+
const relFile = asset.file ?? path3.join(asset.category ?? "", `${assetId}.png`);
|
|
2531
|
+
const outFile = path3.resolve(root, style.outDir, relFile);
|
|
2093
2532
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
2094
2533
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
2095
2534
|
const base = {
|
|
2096
2535
|
styleId,
|
|
2097
2536
|
assetId,
|
|
2098
2537
|
provider: activeProvider.id,
|
|
2099
|
-
providerOptions
|
|
2538
|
+
providerOptions,
|
|
2100
2539
|
generator,
|
|
2101
2540
|
prompt,
|
|
2102
2541
|
width,
|
|
@@ -2134,10 +2573,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2134
2573
|
outFile,
|
|
2135
2574
|
tags,
|
|
2136
2575
|
source: asset.source,
|
|
2137
|
-
specHash: specHash(base, styleImageHashes)
|
|
2576
|
+
specHash: specHash(base, styleImageHashes, providerOptionIdentity)
|
|
2138
2577
|
};
|
|
2139
2578
|
const resolvedImages = style.styleImages.map((image) => {
|
|
2140
|
-
const hit = styleImageCache.get(
|
|
2579
|
+
const hit = styleImageCache.get(path3.resolve(root, image.path));
|
|
2141
2580
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2142
2581
|
});
|
|
2143
2582
|
activeProvider.validate?.(resolved, resolvedImages);
|
|
@@ -2164,8 +2603,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
2164
2603
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
2165
2604
|
const out = [];
|
|
2166
2605
|
for (const img of style.styleImages) {
|
|
2167
|
-
const file =
|
|
2168
|
-
const buf = await
|
|
2606
|
+
const file = path3.resolve(loaded.root, img.path);
|
|
2607
|
+
const buf = await readFile3(file);
|
|
2169
2608
|
const metadata = imageMetadata(buf);
|
|
2170
2609
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
2171
2610
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2208,13 +2647,13 @@ function imageMetadata(buf) {
|
|
|
2208
2647
|
}
|
|
2209
2648
|
|
|
2210
2649
|
// src/lock.ts
|
|
2211
|
-
import { readFile as
|
|
2650
|
+
import { readFile as readFile4, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
2212
2651
|
import { existsSync as existsSync3 } from "fs";
|
|
2213
|
-
import
|
|
2652
|
+
import path4 from "path";
|
|
2214
2653
|
async function loadLock(lockPath) {
|
|
2215
2654
|
if (!existsSync3(lockPath)) return { version: 2, entries: {} };
|
|
2216
2655
|
try {
|
|
2217
|
-
return parseLock(JSON.parse(await
|
|
2656
|
+
return parseLock(JSON.parse(await readFile4(lockPath, "utf8")));
|
|
2218
2657
|
} catch (err) {
|
|
2219
2658
|
throw new Error(
|
|
2220
2659
|
`Lockfile at ${lockPath} is malformed:
|
|
@@ -2226,7 +2665,7 @@ var saveQueues = /* @__PURE__ */ new Map();
|
|
|
2226
2665
|
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
2227
2666
|
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
2228
2667
|
function saveLock(lockPath, lock) {
|
|
2229
|
-
const queueKey =
|
|
2668
|
+
const queueKey = path4.resolve(lockPath);
|
|
2230
2669
|
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
2231
2670
|
const next = previous.catch(() => {
|
|
2232
2671
|
}).then(() => writeLockNow(lockPath, lock));
|
|
@@ -2238,7 +2677,7 @@ function saveLock(lockPath, lock) {
|
|
|
2238
2677
|
return next;
|
|
2239
2678
|
}
|
|
2240
2679
|
async function writeLockNow(lockPath, lock) {
|
|
2241
|
-
await mkdir(
|
|
2680
|
+
await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
|
|
2242
2681
|
const release = await acquireFileLock(lockPath);
|
|
2243
2682
|
try {
|
|
2244
2683
|
await writeLockWhileHeld(lockPath, lock);
|
|
@@ -2250,7 +2689,7 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
2250
2689
|
let disk = { version: 2, entries: {} };
|
|
2251
2690
|
if (existsSync3(file)) {
|
|
2252
2691
|
try {
|
|
2253
|
-
disk = parseLock(JSON.parse(await
|
|
2692
|
+
disk = parseLock(JSON.parse(await readFile4(file, "utf8")));
|
|
2254
2693
|
} catch (err) {
|
|
2255
2694
|
throw new Error(
|
|
2256
2695
|
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2381,21 +2820,21 @@ function totalSpend(lock, unit = "generations") {
|
|
|
2381
2820
|
}
|
|
2382
2821
|
|
|
2383
2822
|
// src/outputs.ts
|
|
2384
|
-
import
|
|
2823
|
+
import path5 from "path";
|
|
2385
2824
|
function portableOutputPath(file, manifestDir) {
|
|
2386
|
-
const root =
|
|
2387
|
-
const absolute =
|
|
2388
|
-
const relative =
|
|
2389
|
-
const value =
|
|
2390
|
-
return value.split(
|
|
2825
|
+
const root = path5.resolve(manifestDir);
|
|
2826
|
+
const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
|
|
2827
|
+
const relative = path5.relative(root, absolute);
|
|
2828
|
+
const value = path5.isAbsolute(relative) ? absolute : relative;
|
|
2829
|
+
return value.split(path5.sep).join("/");
|
|
2391
2830
|
}
|
|
2392
2831
|
function resolveOutputPath(recordedPath, manifestDir) {
|
|
2393
|
-
if (
|
|
2394
|
-
if (
|
|
2395
|
-
return
|
|
2832
|
+
if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
|
|
2833
|
+
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
2834
|
+
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
2396
2835
|
}
|
|
2397
2836
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
2398
|
-
const originalExt =
|
|
2837
|
+
const originalExt = path5.extname(spec.outFile);
|
|
2399
2838
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
2400
2839
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2401
2840
|
if (total === 1) return `${stem}${ext}`;
|
|
@@ -2484,15 +2923,15 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
2484
2923
|
id: spec.assetId,
|
|
2485
2924
|
index: 0,
|
|
2486
2925
|
path: spec.outFile,
|
|
2487
|
-
absolutePath:
|
|
2926
|
+
absolutePath: path5.resolve(spec.outFile),
|
|
2488
2927
|
sha256: ""
|
|
2489
2928
|
}];
|
|
2490
2929
|
}
|
|
2491
2930
|
|
|
2492
2931
|
// src/artifacts.ts
|
|
2493
|
-
import
|
|
2494
|
-
import { createHash as createHash3, randomUUID as
|
|
2495
|
-
import { mkdir as mkdir2, readFile as
|
|
2932
|
+
import path6 from "path";
|
|
2933
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
2934
|
+
import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
2496
2935
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
2497
2936
|
function message(error) {
|
|
2498
2937
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2501,27 +2940,27 @@ function digest(data) {
|
|
|
2501
2940
|
return createHash3("sha256").update(data).digest("hex");
|
|
2502
2941
|
}
|
|
2503
2942
|
function portableRelative(from, to) {
|
|
2504
|
-
return
|
|
2943
|
+
return path6.relative(from, path6.resolve(to)).split(path6.sep).join("/") || ".";
|
|
2505
2944
|
}
|
|
2506
|
-
function
|
|
2945
|
+
function canonical2(value) {
|
|
2507
2946
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2508
2947
|
if (typeof value === "number") {
|
|
2509
2948
|
if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
|
|
2510
2949
|
return value;
|
|
2511
2950
|
}
|
|
2512
|
-
if (Array.isArray(value)) return value.map(
|
|
2951
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
2513
2952
|
if (typeof value === "object") {
|
|
2514
2953
|
const sorted = {};
|
|
2515
2954
|
for (const key of Object.keys(value).sort()) {
|
|
2516
2955
|
const item = value[key];
|
|
2517
|
-
if (item !== void 0) sorted[key] =
|
|
2956
|
+
if (item !== void 0) sorted[key] = canonical2(item);
|
|
2518
2957
|
}
|
|
2519
2958
|
return sorted;
|
|
2520
2959
|
}
|
|
2521
2960
|
throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
|
|
2522
2961
|
}
|
|
2523
2962
|
function fingerprint(provenance) {
|
|
2524
|
-
return digest(JSON.stringify(
|
|
2963
|
+
return digest(JSON.stringify(canonical2({
|
|
2525
2964
|
kind: provenance.kind,
|
|
2526
2965
|
sources: provenance.sources,
|
|
2527
2966
|
options: provenance.options,
|
|
@@ -2552,7 +2991,7 @@ function parseArtifactManifest(absolute, data) {
|
|
|
2552
2991
|
}
|
|
2553
2992
|
async function readOptional(file) {
|
|
2554
2993
|
try {
|
|
2555
|
-
return await
|
|
2994
|
+
return await readFile5(file);
|
|
2556
2995
|
} catch (error) {
|
|
2557
2996
|
if (isCode(error, "ENOENT")) return null;
|
|
2558
2997
|
throw error;
|
|
@@ -2571,8 +3010,8 @@ function processIsAlive(pid) {
|
|
|
2571
3010
|
}
|
|
2572
3011
|
}
|
|
2573
3012
|
function validTemporaryPath(candidate, destination, type) {
|
|
2574
|
-
return
|
|
2575
|
-
`.${
|
|
3013
|
+
return path6.dirname(candidate) === path6.dirname(destination) && path6.basename(candidate).startsWith(
|
|
3014
|
+
`.${path6.basename(destination)}.pixelkiln-${type}-`
|
|
2576
3015
|
);
|
|
2577
3016
|
}
|
|
2578
3017
|
function parseTransaction(journal, data) {
|
|
@@ -2592,7 +3031,7 @@ async function removeTransactionFiles(journal) {
|
|
|
2592
3031
|
await rm2(transactionMarker(journal), { force: true });
|
|
2593
3032
|
}
|
|
2594
3033
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
2595
|
-
const journal =
|
|
3034
|
+
const journal = path6.resolve(recoveryFile);
|
|
2596
3035
|
const bytes = await readOptional(journal);
|
|
2597
3036
|
if (!bytes) {
|
|
2598
3037
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -2608,7 +3047,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2608
3047
|
);
|
|
2609
3048
|
}
|
|
2610
3049
|
for (const entry of transaction.entries) {
|
|
2611
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
3050
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path6.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
2612
3051
|
throw new Error(
|
|
2613
3052
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
2614
3053
|
);
|
|
@@ -2654,7 +3093,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
2654
3093
|
await removeTransactionFiles(journal);
|
|
2655
3094
|
}
|
|
2656
3095
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
2657
|
-
const root =
|
|
3096
|
+
const root = path6.dirname(path6.resolve(manifestPath));
|
|
2658
3097
|
const sources = provenance.sources.map((source) => ({
|
|
2659
3098
|
...source,
|
|
2660
3099
|
path: portableRelative(root, source.path)
|
|
@@ -2662,7 +3101,7 @@ function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
|
2662
3101
|
const normalized = {
|
|
2663
3102
|
kind: provenance.kind,
|
|
2664
3103
|
sources,
|
|
2665
|
-
options:
|
|
3104
|
+
options: canonical2(provenance.options)
|
|
2666
3105
|
};
|
|
2667
3106
|
const manifestOutputs = outputs.map((output) => ({
|
|
2668
3107
|
path: portableRelative(root, output.path),
|
|
@@ -2684,11 +3123,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
2684
3123
|
];
|
|
2685
3124
|
}
|
|
2686
3125
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
2687
|
-
const absoluteManifest =
|
|
3126
|
+
const absoluteManifest = path6.resolve(manifestPath);
|
|
2688
3127
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
2689
3128
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
2690
3129
|
absoluteManifest,
|
|
2691
|
-
...outputs.map((output) =>
|
|
3130
|
+
...outputs.map((output) => path6.resolve(output.path))
|
|
2692
3131
|
]);
|
|
2693
3132
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
2694
3133
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -2706,13 +3145,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
2706
3145
|
}
|
|
2707
3146
|
}
|
|
2708
3147
|
if (!options.force) {
|
|
2709
|
-
const root =
|
|
3148
|
+
const root = path6.dirname(absoluteManifest);
|
|
2710
3149
|
const recorded = new Map(
|
|
2711
|
-
previous?.outputs.map((output) => [
|
|
3150
|
+
previous?.outputs.map((output) => [path6.resolve(root, output.path), output.sha256]) ?? []
|
|
2712
3151
|
);
|
|
2713
3152
|
const conflicts = [];
|
|
2714
3153
|
for (const output of outputs) {
|
|
2715
|
-
const destination =
|
|
3154
|
+
const destination = path6.resolve(output.path);
|
|
2716
3155
|
const current = await readOptional(destination);
|
|
2717
3156
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
2718
3157
|
const expected = recorded.get(destination);
|
|
@@ -2733,15 +3172,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
2733
3172
|
);
|
|
2734
3173
|
}
|
|
2735
3174
|
async function verifyArtifactBundle(manifestPath) {
|
|
2736
|
-
const absolute =
|
|
2737
|
-
const raw = parseArtifactManifest(absolute, await
|
|
2738
|
-
const root =
|
|
3175
|
+
const absolute = path6.resolve(manifestPath);
|
|
3176
|
+
const raw = parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
|
|
3177
|
+
const root = path6.dirname(absolute);
|
|
2739
3178
|
const changedSources = [];
|
|
2740
3179
|
const changedOutputs = [];
|
|
2741
3180
|
for (const source of raw.sources) {
|
|
2742
3181
|
let actual = null;
|
|
2743
3182
|
try {
|
|
2744
|
-
actual = digest(await
|
|
3183
|
+
actual = digest(await readFile5(path6.resolve(root, source.path)));
|
|
2745
3184
|
} catch (error) {
|
|
2746
3185
|
if (!isCode(error, "ENOENT")) throw error;
|
|
2747
3186
|
}
|
|
@@ -2749,7 +3188,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
2749
3188
|
}
|
|
2750
3189
|
for (const output of raw.outputs) {
|
|
2751
3190
|
try {
|
|
2752
|
-
if (digest(await
|
|
3191
|
+
if (digest(await readFile5(path6.resolve(root, output.path))) !== output.sha256) {
|
|
2753
3192
|
changedOutputs.push(output.path);
|
|
2754
3193
|
}
|
|
2755
3194
|
} catch (error) {
|
|
@@ -2805,7 +3244,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2805
3244
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
2806
3245
|
const normalized = files.map((file) => {
|
|
2807
3246
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
2808
|
-
return { destination:
|
|
3247
|
+
return { destination: path6.resolve(file.path), data: Buffer.from(file.data) };
|
|
2809
3248
|
});
|
|
2810
3249
|
const destinations = /* @__PURE__ */ new Set();
|
|
2811
3250
|
for (const artifact of normalized) {
|
|
@@ -2821,7 +3260,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2821
3260
|
const unchanged = [];
|
|
2822
3261
|
for (const artifact of normalized) {
|
|
2823
3262
|
try {
|
|
2824
|
-
const current = await
|
|
3263
|
+
const current = await readFile5(artifact.destination);
|
|
2825
3264
|
if (current.equals(artifact.data)) {
|
|
2826
3265
|
unchanged.push(artifact.destination);
|
|
2827
3266
|
} else {
|
|
@@ -2833,23 +3272,23 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2833
3272
|
}
|
|
2834
3273
|
}
|
|
2835
3274
|
if (!changed.length) return { changed: [], unchanged };
|
|
2836
|
-
const token =
|
|
3275
|
+
const token = randomUUID3();
|
|
2837
3276
|
for (const [index, artifact] of changed.entries()) {
|
|
2838
|
-
const basename =
|
|
2839
|
-
artifact.stage =
|
|
2840
|
-
|
|
3277
|
+
const basename = path6.basename(artifact.destination);
|
|
3278
|
+
artifact.stage = path6.join(
|
|
3279
|
+
path6.dirname(artifact.destination),
|
|
2841
3280
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
2842
3281
|
);
|
|
2843
3282
|
if (artifact.existed) {
|
|
2844
|
-
artifact.backup =
|
|
2845
|
-
|
|
3283
|
+
artifact.backup = path6.join(
|
|
3284
|
+
path6.dirname(artifact.destination),
|
|
2846
3285
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
2847
3286
|
);
|
|
2848
3287
|
}
|
|
2849
3288
|
}
|
|
2850
|
-
const journal = options.recoveryFile ?
|
|
3289
|
+
const journal = options.recoveryFile ? path6.resolve(options.recoveryFile) : null;
|
|
2851
3290
|
if (journal) {
|
|
2852
|
-
await mkdir2(
|
|
3291
|
+
await mkdir2(path6.dirname(journal), { recursive: true });
|
|
2853
3292
|
const transaction = {
|
|
2854
3293
|
format: "pixelkiln-artifact-transaction",
|
|
2855
3294
|
version: 1,
|
|
@@ -2874,7 +3313,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2874
3313
|
}
|
|
2875
3314
|
try {
|
|
2876
3315
|
for (const [index, artifact] of changed.entries()) {
|
|
2877
|
-
await mkdir2(
|
|
3316
|
+
await mkdir2(path6.dirname(artifact.destination), { recursive: true });
|
|
2878
3317
|
await options.beforeStage?.(artifact.destination, index);
|
|
2879
3318
|
await writeFile2(artifact.stage, artifact.data, { flag: "wx" });
|
|
2880
3319
|
}
|
|
@@ -2943,7 +3382,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
2943
3382
|
|
|
2944
3383
|
// src/pipeline/plan.ts
|
|
2945
3384
|
import { existsSync as existsSync4 } from "fs";
|
|
2946
|
-
import
|
|
3385
|
+
import path7 from "path";
|
|
2947
3386
|
async function anyOutputModified(entry, spec) {
|
|
2948
3387
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
2949
3388
|
const output = entry.outputs[index];
|
|
@@ -2962,7 +3401,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
2962
3401
|
state = "missing";
|
|
2963
3402
|
reason = "--force";
|
|
2964
3403
|
} else if (!entry && spec.source) {
|
|
2965
|
-
if (existsSync4(
|
|
3404
|
+
if (existsSync4(path7.resolve(spec.root, spec.source))) {
|
|
2966
3405
|
state = "ok";
|
|
2967
3406
|
reason = `placed from ${spec.source}; not generated`;
|
|
2968
3407
|
} else {
|
|
@@ -3028,9 +3467,9 @@ function summarize(plan) {
|
|
|
3028
3467
|
}
|
|
3029
3468
|
|
|
3030
3469
|
// src/pipeline/audit.ts
|
|
3031
|
-
import { readFile as
|
|
3470
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
3032
3471
|
import { existsSync as existsSync5 } from "fs";
|
|
3033
|
-
import
|
|
3472
|
+
import path8 from "path";
|
|
3034
3473
|
function colorDistance(a, b) {
|
|
3035
3474
|
const rmean = (a.r + b.r) / 2;
|
|
3036
3475
|
const dr = a.r - b.r;
|
|
@@ -3080,7 +3519,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3080
3519
|
continue;
|
|
3081
3520
|
}
|
|
3082
3521
|
try {
|
|
3083
|
-
const png = decodePng(await
|
|
3522
|
+
const png = decodePng(await readFile6(output.absolutePath));
|
|
3084
3523
|
const palette = extractPalette(png, 12);
|
|
3085
3524
|
assets.push({
|
|
3086
3525
|
assetId: spec.assetId,
|
|
@@ -3104,10 +3543,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3104
3543
|
let referenceFromStyleImages = false;
|
|
3105
3544
|
const refPalettes = [];
|
|
3106
3545
|
for (const img of style.styleImages) {
|
|
3107
|
-
const abs =
|
|
3546
|
+
const abs = path8.resolve(loaded.root, img.path);
|
|
3108
3547
|
if (!existsSync5(abs)) continue;
|
|
3109
3548
|
try {
|
|
3110
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
3549
|
+
refPalettes.push(extractPalette(decodePng(await readFile6(abs)), 12));
|
|
3111
3550
|
} catch {
|
|
3112
3551
|
}
|
|
3113
3552
|
}
|
|
@@ -3184,11 +3623,11 @@ function hex(c) {
|
|
|
3184
3623
|
|
|
3185
3624
|
// src/pipeline/cache-health.ts
|
|
3186
3625
|
import { existsSync as existsSync7 } from "fs";
|
|
3187
|
-
import { readFile as
|
|
3188
|
-
import
|
|
3626
|
+
import { readFile as readFile8, readdir, rm as rm4 } from "fs/promises";
|
|
3627
|
+
import path9 from "path";
|
|
3189
3628
|
|
|
3190
3629
|
// src/cache.ts
|
|
3191
|
-
import { mkdir as mkdir3, readFile as
|
|
3630
|
+
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
3192
3631
|
import { existsSync as existsSync6 } from "fs";
|
|
3193
3632
|
import pathModule from "path";
|
|
3194
3633
|
import { z as z3 } from "zod";
|
|
@@ -3204,10 +3643,10 @@ function isSha256Hash(value) {
|
|
|
3204
3643
|
function parseCache(value) {
|
|
3205
3644
|
return HashCacheSchema.parse(value);
|
|
3206
3645
|
}
|
|
3207
|
-
async function loadCache(
|
|
3208
|
-
if (!existsSync6(
|
|
3646
|
+
async function loadCache(path18) {
|
|
3647
|
+
if (!existsSync6(path18)) return { version: 1, hashes: {} };
|
|
3209
3648
|
try {
|
|
3210
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
3649
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path18, "utf8")));
|
|
3211
3650
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
3212
3651
|
return {
|
|
3213
3652
|
version: 1,
|
|
@@ -3219,18 +3658,18 @@ async function loadCache(path17) {
|
|
|
3219
3658
|
return { version: 1, hashes: {} };
|
|
3220
3659
|
}
|
|
3221
3660
|
}
|
|
3222
|
-
async function saveCache(
|
|
3661
|
+
async function saveCache(path18, cache) {
|
|
3223
3662
|
const sorted = {};
|
|
3224
3663
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
3225
3664
|
const hash = cache.hashes[key];
|
|
3226
3665
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
3227
3666
|
sorted[key] = hash;
|
|
3228
3667
|
}
|
|
3229
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
3230
|
-
const tmp = `${
|
|
3668
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path18)), { recursive: true });
|
|
3669
|
+
const tmp = `${path18}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3231
3670
|
try {
|
|
3232
3671
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
3233
|
-
await rename3(tmp,
|
|
3672
|
+
await rename3(tmp, path18);
|
|
3234
3673
|
} finally {
|
|
3235
3674
|
await rm3(tmp, { force: true });
|
|
3236
3675
|
}
|
|
@@ -3253,10 +3692,10 @@ function cachePathFor(lockPath) {
|
|
|
3253
3692
|
// src/pipeline/cache-health.ts
|
|
3254
3693
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
3255
3694
|
if (options.prune && !existsSync7(lockPath)) {
|
|
3256
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
3695
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path9.resolve(lockPath)}`);
|
|
3257
3696
|
}
|
|
3258
|
-
const contentDir =
|
|
3259
|
-
const remotePath =
|
|
3697
|
+
const contentDir = path9.resolve(path9.dirname(lockPath), ".pixelkiln", "cache");
|
|
3698
|
+
const remotePath = path9.resolve(cachePathFor(lockPath));
|
|
3260
3699
|
const referenced = new Set(
|
|
3261
3700
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
3262
3701
|
);
|
|
@@ -3270,7 +3709,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3270
3709
|
]);
|
|
3271
3710
|
for (const name of names) {
|
|
3272
3711
|
try {
|
|
3273
|
-
await rm4(
|
|
3712
|
+
await rm4(path9.join(contentDir, name), { force: true });
|
|
3274
3713
|
removed.contentFiles++;
|
|
3275
3714
|
} catch {
|
|
3276
3715
|
}
|
|
@@ -3279,7 +3718,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3279
3718
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
3280
3719
|
removed.resetRemoteHashCache = true;
|
|
3281
3720
|
} else if (remoteHashes.invalidIds.length) {
|
|
3282
|
-
const cache = parseCache(JSON.parse(await
|
|
3721
|
+
const cache = parseCache(JSON.parse(await readFile8(remotePath, "utf8")));
|
|
3283
3722
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
3284
3723
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
3285
3724
|
await saveCache(remotePath, cache);
|
|
@@ -3318,12 +3757,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3318
3757
|
continue;
|
|
3319
3758
|
}
|
|
3320
3759
|
report.files++;
|
|
3321
|
-
const file =
|
|
3760
|
+
const file = path9.join(contentDir, entry.name);
|
|
3322
3761
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
3323
3762
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
3324
3763
|
let bytes;
|
|
3325
3764
|
try {
|
|
3326
|
-
bytes = await
|
|
3765
|
+
bytes = await readFile8(file);
|
|
3327
3766
|
report.bytes += bytes.length;
|
|
3328
3767
|
} catch (err) {
|
|
3329
3768
|
report.invalid.push({
|
|
@@ -3370,7 +3809,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3370
3809
|
if (!report.exists) return report;
|
|
3371
3810
|
let cache;
|
|
3372
3811
|
try {
|
|
3373
|
-
cache = parseCache(JSON.parse(await
|
|
3812
|
+
cache = parseCache(JSON.parse(await readFile8(remotePath, "utf8")));
|
|
3374
3813
|
} catch (err) {
|
|
3375
3814
|
report.error = err instanceof Error ? err.message : String(err);
|
|
3376
3815
|
return report;
|
|
@@ -3386,18 +3825,18 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3386
3825
|
|
|
3387
3826
|
// src/pipeline/pack.ts
|
|
3388
3827
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3389
|
-
import
|
|
3828
|
+
import path10 from "path";
|
|
3390
3829
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
3391
3830
|
if (!Array.isArray(raw) || !raw.length) {
|
|
3392
3831
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
3393
3832
|
}
|
|
3394
|
-
const baseDir =
|
|
3833
|
+
const baseDir = path10.dirname(path10.resolve(inputsFilePath));
|
|
3395
3834
|
return raw.map((entry, i) => {
|
|
3396
3835
|
const e = entry;
|
|
3397
3836
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
3398
3837
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
3399
3838
|
}
|
|
3400
|
-
return { id: e.id, path:
|
|
3839
|
+
return { id: e.id, path: path10.resolve(baseDir, e.path) };
|
|
3401
3840
|
});
|
|
3402
3841
|
}
|
|
3403
3842
|
function packSprites(inputs, options = {}) {
|
|
@@ -3594,7 +4033,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3594
4033
|
const cell = cells[id];
|
|
3595
4034
|
const source = sources[id];
|
|
3596
4035
|
if (source) {
|
|
3597
|
-
placements.push({ id, path:
|
|
4036
|
+
placements.push({ id, path: path10.resolve(manifestDir, source), cell });
|
|
3598
4037
|
continue;
|
|
3599
4038
|
}
|
|
3600
4039
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
@@ -3618,7 +4057,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3618
4057
|
`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}`
|
|
3619
4058
|
);
|
|
3620
4059
|
}
|
|
3621
|
-
const basePng = mount.base ? readFileSync2(
|
|
4060
|
+
const basePng = mount.base ? readFileSync2(path10.resolve(manifestDir, mount.base)) : void 0;
|
|
3622
4061
|
const mounted = mountSprites(placements, {
|
|
3623
4062
|
cellWidth: mount.cellWidth,
|
|
3624
4063
|
cellHeight: mount.cellHeight,
|
|
@@ -3628,7 +4067,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
3628
4067
|
...mounted,
|
|
3629
4068
|
sources: mount.base ? [{
|
|
3630
4069
|
id: "$base",
|
|
3631
|
-
path:
|
|
4070
|
+
path: path10.resolve(manifestDir, mount.base),
|
|
3632
4071
|
sha256: sha256(basePng),
|
|
3633
4072
|
included: true
|
|
3634
4073
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -4040,7 +4479,11 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
4040
4479
|
spec: currentSpec
|
|
4041
4480
|
});
|
|
4042
4481
|
if (state.status === "review") {
|
|
4043
|
-
upsert(lock, key, {
|
|
4482
|
+
upsert(lock, key, {
|
|
4483
|
+
status: "review",
|
|
4484
|
+
reviewObjectId: entry.jobId,
|
|
4485
|
+
providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
|
|
4486
|
+
});
|
|
4044
4487
|
result.review++;
|
|
4045
4488
|
log(` review ${key} (${state.candidateUrls.length} candidates)`);
|
|
4046
4489
|
} else if (state.status === "ready") {
|
|
@@ -4073,15 +4516,15 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
4073
4516
|
|
|
4074
4517
|
// src/pipeline/fetch.ts
|
|
4075
4518
|
import { existsSync as existsSync8 } from "fs";
|
|
4076
|
-
import { mkdir as mkdir4, readFile as
|
|
4077
|
-
import
|
|
4519
|
+
import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
4520
|
+
import path11 from "path";
|
|
4078
4521
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
4079
4522
|
const log = opts.onProgress ?? (() => {
|
|
4080
4523
|
});
|
|
4081
4524
|
const result = { downloaded: 0, skipped: 0, failed: 0 };
|
|
4082
4525
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
4083
4526
|
normalizeLockOutputPaths(lock, specs);
|
|
4084
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
4527
|
+
const cacheDir = opts.cacheDir === false ? null : path11.resolve(opts.cacheDir ?? path11.join(path11.dirname(lockPath), ".pixelkiln", "cache"));
|
|
4085
4528
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
4086
4529
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
4087
4530
|
if (!opts.repair || e.status !== "downloaded") return false;
|
|
@@ -4133,7 +4576,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4133
4576
|
if (cacheDir) {
|
|
4134
4577
|
await cacheMedia(
|
|
4135
4578
|
cacheDir,
|
|
4136
|
-
await
|
|
4579
|
+
await readFile9(target),
|
|
4137
4580
|
recorded.mediaType ?? MediaType.PNG,
|
|
4138
4581
|
recorded.sha256
|
|
4139
4582
|
);
|
|
@@ -4144,7 +4587,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4144
4587
|
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
4145
4588
|
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
4146
4589
|
if (buf) {
|
|
4147
|
-
log(` cached ${
|
|
4590
|
+
log(` cached ${path11.relative(process.cwd(), target)}`);
|
|
4148
4591
|
} else {
|
|
4149
4592
|
if (!source.url) {
|
|
4150
4593
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
@@ -4162,7 +4605,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4162
4605
|
);
|
|
4163
4606
|
}
|
|
4164
4607
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
4165
|
-
await mkdir4(
|
|
4608
|
+
await mkdir4(path11.dirname(target), { recursive: true });
|
|
4166
4609
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
4167
4610
|
await writeFile4(tmp, buf);
|
|
4168
4611
|
outputs.push({
|
|
@@ -4174,7 +4617,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4174
4617
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
4175
4618
|
await saveLock(lockPath, lock);
|
|
4176
4619
|
await rename4(tmp, target);
|
|
4177
|
-
log(` wrote ${
|
|
4620
|
+
log(` wrote ${path11.relative(process.cwd(), target)}`);
|
|
4178
4621
|
}
|
|
4179
4622
|
upsert(lock, key, {
|
|
4180
4623
|
status: "downloaded",
|
|
@@ -4207,10 +4650,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4207
4650
|
return result;
|
|
4208
4651
|
}
|
|
4209
4652
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
4210
|
-
const file =
|
|
4653
|
+
const file = path11.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4211
4654
|
if (!existsSync8(file)) return null;
|
|
4212
4655
|
try {
|
|
4213
|
-
const buf = await
|
|
4656
|
+
const buf = await readFile9(file);
|
|
4214
4657
|
if (sha256(buf) !== hash) return null;
|
|
4215
4658
|
validateMedia(buf, mediaType);
|
|
4216
4659
|
return buf;
|
|
@@ -4221,7 +4664,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
4221
4664
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
4222
4665
|
const hash = knownHash ?? sha256(buf);
|
|
4223
4666
|
validateMedia(buf, mediaType);
|
|
4224
|
-
const file =
|
|
4667
|
+
const file = path11.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4225
4668
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
4226
4669
|
await mkdir4(cacheDir, { recursive: true });
|
|
4227
4670
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -4264,7 +4707,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
4264
4707
|
// src/pipeline/doctor.ts
|
|
4265
4708
|
import { constants, existsSync as existsSync9 } from "fs";
|
|
4266
4709
|
import { access, stat as stat2 } from "fs/promises";
|
|
4267
|
-
import
|
|
4710
|
+
import path12 from "path";
|
|
4268
4711
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
4269
4712
|
const checks = [];
|
|
4270
4713
|
const add = (id, level, message2) => checks.push({ id, level, message: message2 });
|
|
@@ -4274,7 +4717,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4274
4717
|
`${Object.keys(loaded.manifest.styles).length} style(s), ${Object.keys(loaded.manifest.assets).length} asset(s), ${specs.length} resolved spec(s)`
|
|
4275
4718
|
);
|
|
4276
4719
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
4277
|
-
const dirs = [...new Set(specs.map((s) =>
|
|
4720
|
+
const dirs = [...new Set(specs.map((s) => path12.dirname(s.outFile)))];
|
|
4278
4721
|
const unwritable = [];
|
|
4279
4722
|
for (const dir of dirs) {
|
|
4280
4723
|
const ancestor = await nearestExistingDirectory(dir);
|
|
@@ -4296,7 +4739,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4296
4739
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
4297
4740
|
const output = entry.outputs[index];
|
|
4298
4741
|
const spec = specByKey.get(key);
|
|
4299
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
4742
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path12.dirname(lockPath));
|
|
4300
4743
|
const owner = outputOwners.get(absolute);
|
|
4301
4744
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
4302
4745
|
outputOwners.set(absolute, key);
|
|
@@ -4311,7 +4754,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4311
4754
|
} else {
|
|
4312
4755
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
4313
4756
|
}
|
|
4314
|
-
const cacheDir =
|
|
4757
|
+
const cacheDir = path12.join(path12.dirname(lockPath), ".pixelkiln", "cache");
|
|
4315
4758
|
const stranded = [];
|
|
4316
4759
|
const invalidState = [];
|
|
4317
4760
|
const staleMapUrls = [];
|
|
@@ -4327,7 +4770,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4327
4770
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
4328
4771
|
let hasCache = false;
|
|
4329
4772
|
for (const output of entry.outputs) {
|
|
4330
|
-
const cached =
|
|
4773
|
+
const cached = path12.join(
|
|
4331
4774
|
cacheDir,
|
|
4332
4775
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
4333
4776
|
);
|
|
@@ -4362,8 +4805,15 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
4362
4805
|
"error",
|
|
4363
4806
|
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
4364
4807
|
);
|
|
4808
|
+
} else if (!opts.provider.balance && !opts.provider.checkConnection) {
|
|
4809
|
+
add("provider", "ok", `${opts.provider.id} configured; connectivity check unavailable`);
|
|
4365
4810
|
} else if (!opts.provider.balance) {
|
|
4366
|
-
|
|
4811
|
+
try {
|
|
4812
|
+
await opts.provider.checkConnection();
|
|
4813
|
+
add("provider", "ok", `${opts.provider.id} reachable; balance reporting unavailable`);
|
|
4814
|
+
} catch (err) {
|
|
4815
|
+
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4816
|
+
}
|
|
4367
4817
|
} else {
|
|
4368
4818
|
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
4369
4819
|
try {
|
|
@@ -4382,7 +4832,7 @@ async function nearestExistingDirectory(start) {
|
|
|
4382
4832
|
if ((await stat2(current)).isDirectory()) return current;
|
|
4383
4833
|
} catch {
|
|
4384
4834
|
}
|
|
4385
|
-
const parent =
|
|
4835
|
+
const parent = path12.dirname(current);
|
|
4386
4836
|
if (parent === current) return current;
|
|
4387
4837
|
current = parent;
|
|
4388
4838
|
}
|
|
@@ -4390,8 +4840,8 @@ async function nearestExistingDirectory(start) {
|
|
|
4390
4840
|
|
|
4391
4841
|
// src/pipeline/adopt.ts
|
|
4392
4842
|
import { existsSync as existsSync10 } from "fs";
|
|
4393
|
-
import { readFile as
|
|
4394
|
-
import
|
|
4843
|
+
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
4844
|
+
import path13 from "path";
|
|
4395
4845
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
4396
4846
|
const log = opts.onProgress ?? (() => {
|
|
4397
4847
|
});
|
|
@@ -4406,7 +4856,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
4406
4856
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
4407
4857
|
continue;
|
|
4408
4858
|
}
|
|
4409
|
-
const bytes = await
|
|
4859
|
+
const bytes = await readFile10(spec.outFile);
|
|
4410
4860
|
try {
|
|
4411
4861
|
decodePng(bytes);
|
|
4412
4862
|
} catch (err) {
|
|
@@ -4882,9 +5332,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4882
5332
|
}
|
|
4883
5333
|
|
|
4884
5334
|
// src/pipeline/init.ts
|
|
4885
|
-
import { readdir as readdir2, readFile as
|
|
5335
|
+
import { readdir as readdir2, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
4886
5336
|
import { existsSync as existsSync11 } from "fs";
|
|
4887
|
-
import
|
|
5337
|
+
import path14 from "path";
|
|
4888
5338
|
function pngSize(buf) {
|
|
4889
5339
|
if (buf.length < 24) return null;
|
|
4890
5340
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -4898,7 +5348,7 @@ function slugify(value) {
|
|
|
4898
5348
|
async function walk(dir, exclude) {
|
|
4899
5349
|
const out = [];
|
|
4900
5350
|
for (const entry of await readdir2(dir, { withFileTypes: true })) {
|
|
4901
|
-
const full =
|
|
5351
|
+
const full = path14.join(dir, entry.name);
|
|
4902
5352
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
4903
5353
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
4904
5354
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -4912,14 +5362,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
4912
5362
|
const skipped = [];
|
|
4913
5363
|
const seen = /* @__PURE__ */ new Map();
|
|
4914
5364
|
for (const file of files.sort()) {
|
|
4915
|
-
const size = pngSize(await
|
|
5365
|
+
const size = pngSize(await readFile11(file));
|
|
4916
5366
|
if (!size) {
|
|
4917
5367
|
skipped.push(`${file} (not a readable PNG)`);
|
|
4918
5368
|
continue;
|
|
4919
5369
|
}
|
|
4920
|
-
const rel =
|
|
4921
|
-
const category =
|
|
4922
|
-
let id = slugify(
|
|
5370
|
+
const rel = path14.relative(root, file);
|
|
5371
|
+
const category = path14.dirname(rel) === "." ? "" : path14.dirname(rel);
|
|
5372
|
+
let id = slugify(path14.basename(rel));
|
|
4923
5373
|
if (seen.has(id)) {
|
|
4924
5374
|
const n = seen.get(id) + 1;
|
|
4925
5375
|
seen.set(id, n);
|
|
@@ -4975,16 +5425,16 @@ function mode(values) {
|
|
|
4975
5425
|
}
|
|
4976
5426
|
|
|
4977
5427
|
// src/pipeline/salvage.ts
|
|
4978
|
-
import { readFile as
|
|
5428
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4979
5429
|
import { existsSync as existsSync12 } from "fs";
|
|
4980
|
-
import
|
|
5430
|
+
import path15 from "path";
|
|
4981
5431
|
async function loadClaims(lockPaths) {
|
|
4982
5432
|
const claimed = /* @__PURE__ */ new Set();
|
|
4983
5433
|
for (const p of lockPaths) {
|
|
4984
5434
|
if (!existsSync12(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
4985
5435
|
let parsed;
|
|
4986
5436
|
try {
|
|
4987
|
-
parsed = parseLock(JSON.parse(await
|
|
5437
|
+
parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
|
|
4988
5438
|
} catch {
|
|
4989
5439
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
4990
5440
|
}
|
|
@@ -5031,20 +5481,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
5031
5481
|
return matchStyleByPattern(prompt, manifest);
|
|
5032
5482
|
}
|
|
5033
5483
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
5034
|
-
const own =
|
|
5484
|
+
const own = path15.resolve(ownManifestPath);
|
|
5035
5485
|
const siblingManifestPaths = [
|
|
5036
5486
|
.../* @__PURE__ */ new Set([
|
|
5037
5487
|
...workspaceManifestPaths,
|
|
5038
|
-
...claimPaths.map((c) =>
|
|
5488
|
+
...claimPaths.map((c) => path15.join(path15.dirname(path15.resolve(c)), "pixelkiln.manifest.json"))
|
|
5039
5489
|
])
|
|
5040
5490
|
];
|
|
5041
5491
|
const siblings = [];
|
|
5042
5492
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
5043
|
-
if (
|
|
5493
|
+
if (path15.resolve(siblingManifestPath) === own) continue;
|
|
5044
5494
|
if (!existsSync12(siblingManifestPath)) continue;
|
|
5045
5495
|
try {
|
|
5046
5496
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
5047
|
-
siblings.push({ label:
|
|
5497
|
+
siblings.push({ label: path15.basename(path15.dirname(siblingManifestPath)), manifest });
|
|
5048
5498
|
} catch {
|
|
5049
5499
|
}
|
|
5050
5500
|
}
|
|
@@ -5160,8 +5610,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
5160
5610
|
}
|
|
5161
5611
|
|
|
5162
5612
|
// src/pick/salvage-server.ts
|
|
5163
|
-
import { mkdir as mkdir5, writeFile as writeFile7, readFile as
|
|
5164
|
-
import
|
|
5613
|
+
import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
|
|
5614
|
+
import path16 from "path";
|
|
5165
5615
|
|
|
5166
5616
|
// src/pick/salvage-sheet.ts
|
|
5167
5617
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -5326,7 +5776,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5326
5776
|
});
|
|
5327
5777
|
const html = renderSalvageSheet(orphans, {
|
|
5328
5778
|
styleId: ctx.styleId,
|
|
5329
|
-
importDir:
|
|
5779
|
+
importDir: path16.relative(process.cwd(), ctx.importDir) || "."
|
|
5330
5780
|
});
|
|
5331
5781
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
5332
5782
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -5355,9 +5805,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5355
5805
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
5356
5806
|
decodePng(buf);
|
|
5357
5807
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
5358
|
-
const rel =
|
|
5359
|
-
const outFile =
|
|
5360
|
-
await mkdir5(
|
|
5808
|
+
const rel = path16.join("_salvaged", `${assetId}.png`);
|
|
5809
|
+
const outFile = path16.resolve(ctx.importDir, rel);
|
|
5810
|
+
await mkdir5(path16.dirname(outFile), { recursive: true });
|
|
5361
5811
|
await writeFile7(outFile, buf);
|
|
5362
5812
|
ctx.manifest.assets[assetId] = {
|
|
5363
5813
|
prompt: orphan.prompt,
|
|
@@ -5384,7 +5834,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5384
5834
|
error: null,
|
|
5385
5835
|
sourceUrl: orphan.previewUrl,
|
|
5386
5836
|
outputs: [{
|
|
5387
|
-
path: portableOutputPath(outFile,
|
|
5837
|
+
path: portableOutputPath(outFile, path16.dirname(ctx.manifestPath)),
|
|
5388
5838
|
sha256: sha256(buf)
|
|
5389
5839
|
}],
|
|
5390
5840
|
submittedAt: orphan.createdAt,
|
|
@@ -5408,7 +5858,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5408
5858
|
}
|
|
5409
5859
|
}
|
|
5410
5860
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
5411
|
-
const raw = JSON.parse(await
|
|
5861
|
+
const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
|
|
5412
5862
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
5413
5863
|
await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
5414
5864
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
@@ -5418,9 +5868,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5418
5868
|
}
|
|
5419
5869
|
|
|
5420
5870
|
// src/workspace.ts
|
|
5421
|
-
import { mkdir as mkdir6, readFile as
|
|
5871
|
+
import { mkdir as mkdir6, readFile as readFile14, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
5422
5872
|
import { existsSync as existsSync13 } from "fs";
|
|
5423
|
-
import
|
|
5873
|
+
import path17 from "path";
|
|
5424
5874
|
import { z as z4 } from "zod";
|
|
5425
5875
|
var WorkspaceProjectSchema = z4.object({
|
|
5426
5876
|
id: z4.string().min(1),
|
|
@@ -5448,7 +5898,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
5448
5898
|
if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
|
|
5449
5899
|
let raw;
|
|
5450
5900
|
try {
|
|
5451
|
-
raw = JSON.parse(await
|
|
5901
|
+
raw = JSON.parse(await readFile14(workspacePath, "utf8"));
|
|
5452
5902
|
} catch (err) {
|
|
5453
5903
|
throw new Error(
|
|
5454
5904
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -5462,7 +5912,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
5462
5912
|
version: 1,
|
|
5463
5913
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
5464
5914
|
};
|
|
5465
|
-
await mkdir6(
|
|
5915
|
+
await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
|
|
5466
5916
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
5467
5917
|
try {
|
|
5468
5918
|
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -5472,12 +5922,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
5472
5922
|
}
|
|
5473
5923
|
}
|
|
5474
5924
|
function toPortablePath(dir, absolute) {
|
|
5475
|
-
return
|
|
5925
|
+
return path17.relative(dir, absolute).split(path17.sep).join("/");
|
|
5476
5926
|
}
|
|
5477
5927
|
function resolveProject(dir, project) {
|
|
5478
5928
|
return {
|
|
5479
|
-
manifestPath:
|
|
5480
|
-
lockPath:
|
|
5929
|
+
manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
|
|
5930
|
+
lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
|
|
5481
5931
|
};
|
|
5482
5932
|
}
|
|
5483
5933
|
function validateWorkspace(ws, dir) {
|
|
@@ -5490,7 +5940,7 @@ function validateWorkspace(ws, dir) {
|
|
|
5490
5940
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5491
5941
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
5492
5942
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
5493
|
-
if (
|
|
5943
|
+
if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
|
|
5494
5944
|
diagnostics.push({
|
|
5495
5945
|
id: "absolute-path",
|
|
5496
5946
|
level: "warning",
|
|
@@ -5645,6 +6095,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
5645
6095
|
}
|
|
5646
6096
|
export {
|
|
5647
6097
|
AssetSchema,
|
|
6098
|
+
ComfyUIProvider,
|
|
5648
6099
|
DEFAULT_RATE_LIMIT,
|
|
5649
6100
|
FAKE_PNG,
|
|
5650
6101
|
FakeProvider,
|