pixelkiln 0.23.0 → 0.24.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/dist/cli.d.ts +1 -1
- package/dist/cli.js +637 -287
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +382 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -105
- package/dist/index.d.ts +218 -105
- package/dist/index.js +370 -67
- package/dist/index.js.map +1 -1
- package/docs/ARTIFACTS.md +10 -5
- package/docs/CLI.md +36 -1
- package/docs/MANIFEST.md +5 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +8 -0
- package/skills/pixelkiln/SKILL.md +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import { existsSync as
|
|
4
|
+
import path28 from "path";
|
|
5
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6
6
|
import { readFile as readFile22 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
@@ -134,10 +134,10 @@ import { createReadStream } from "fs";
|
|
|
134
134
|
function sha256(data) {
|
|
135
135
|
return createHash("sha256").update(data).digest("hex");
|
|
136
136
|
}
|
|
137
|
-
async function sha256File(
|
|
137
|
+
async function sha256File(path29) {
|
|
138
138
|
return new Promise((resolve, reject) => {
|
|
139
139
|
const hash = createHash("sha256");
|
|
140
|
-
const stream = createReadStream(
|
|
140
|
+
const stream = createReadStream(path29);
|
|
141
141
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
142
142
|
stream.on("error", reject);
|
|
143
143
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -1617,11 +1617,11 @@ var PixelLabClient = class {
|
|
|
1617
1617
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1618
1618
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1619
1619
|
*/
|
|
1620
|
-
async request(
|
|
1620
|
+
async request(path29, init, attempt = 0) {
|
|
1621
1621
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1622
1622
|
let res;
|
|
1623
1623
|
try {
|
|
1624
|
-
res = await fetch(`${BASE}${
|
|
1624
|
+
res = await fetch(`${BASE}${path29}`, {
|
|
1625
1625
|
...init,
|
|
1626
1626
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1627
1627
|
headers: {
|
|
@@ -1633,24 +1633,24 @@ var PixelLabClient = class {
|
|
|
1633
1633
|
} catch (err) {
|
|
1634
1634
|
if (attempt < MAX_RETRIES) {
|
|
1635
1635
|
await sleep(backoffMs(attempt));
|
|
1636
|
-
return this.request(
|
|
1636
|
+
return this.request(path29, init, attempt + 1);
|
|
1637
1637
|
}
|
|
1638
1638
|
throw err;
|
|
1639
1639
|
}
|
|
1640
1640
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1641
1641
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1642
1642
|
await sleep(waitMs);
|
|
1643
|
-
return this.request(
|
|
1643
|
+
return this.request(path29, init, attempt + 1);
|
|
1644
1644
|
}
|
|
1645
1645
|
const text = await res.text();
|
|
1646
1646
|
if (!res.ok) {
|
|
1647
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1647
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path29} \u2192 ${res.status}`, res.status, text);
|
|
1648
1648
|
}
|
|
1649
1649
|
if (!text) return {};
|
|
1650
1650
|
try {
|
|
1651
1651
|
return JSON.parse(text);
|
|
1652
1652
|
} catch {
|
|
1653
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1653
|
+
throw new Error(`${init?.method ?? "GET"} ${path29} returned invalid JSON`);
|
|
1654
1654
|
}
|
|
1655
1655
|
}
|
|
1656
1656
|
async balance() {
|
|
@@ -2112,6 +2112,15 @@ var AssetSchema = z2.object({
|
|
|
2112
2112
|
* that wasn't. `prompt` still records what was asked for.
|
|
2113
2113
|
*/
|
|
2114
2114
|
source: z2.string().optional(),
|
|
2115
|
+
/**
|
|
2116
|
+
* Per-style replacement for `source`, keyed by style id.
|
|
2117
|
+
*
|
|
2118
|
+
* A hand edit belongs to one generation: the same asset generated in two
|
|
2119
|
+
* styles is two different pictures, so the touch-up of one must not be
|
|
2120
|
+
* placed for the other. `pixelkiln edit` writes here when an asset is in
|
|
2121
|
+
* more than one style, and to `source` when it is in exactly one.
|
|
2122
|
+
*/
|
|
2123
|
+
sourceByStyle: z2.record(z2.string().min(1)).default({}),
|
|
2115
2124
|
/** Controlled generation derived from another asset in the same style. */
|
|
2116
2125
|
revision: RevisionSchema.optional(),
|
|
2117
2126
|
/**
|
|
@@ -2146,7 +2155,7 @@ var AssetSchema = z2.object({
|
|
|
2146
2155
|
z2.array(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).min(2).max(64)
|
|
2147
2156
|
])).default({})
|
|
2148
2157
|
}).strict().superRefine((asset, context) => {
|
|
2149
|
-
if (asset.source && asset.revision) {
|
|
2158
|
+
if ((asset.source || Object.keys(asset.sourceByStyle).length) && asset.revision) {
|
|
2150
2159
|
context.addIssue({
|
|
2151
2160
|
code: z2.ZodIssueCode.custom,
|
|
2152
2161
|
message: "source and revision are mutually exclusive",
|
|
@@ -2613,9 +2622,9 @@ var RetroDiffusionClient = class {
|
|
|
2613
2622
|
}
|
|
2614
2623
|
return balance;
|
|
2615
2624
|
}
|
|
2616
|
-
async call(
|
|
2625
|
+
async call(path29, init = {}) {
|
|
2617
2626
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2618
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2627
|
+
const response = await this.request(`${this.baseUrl}${path29}`, {
|
|
2619
2628
|
...init,
|
|
2620
2629
|
headers: {
|
|
2621
2630
|
"Content-Type": "application/json",
|
|
@@ -3784,6 +3793,11 @@ ${issues}`);
|
|
|
3784
3793
|
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3785
3794
|
}
|
|
3786
3795
|
}
|
|
3796
|
+
for (const styleId of Object.keys(asset.sourceByStyle)) {
|
|
3797
|
+
if (!styleIds.has(styleId)) {
|
|
3798
|
+
unknownReferences.push(`assets.${assetId}.sourceByStyle: unknown style "${styleId}"`);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3787
3801
|
if (asset.revision) {
|
|
3788
3802
|
if (!parsed.data.assets[asset.revision.from]) {
|
|
3789
3803
|
unknownReferences.push(
|
|
@@ -4048,7 +4062,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4048
4062
|
}
|
|
4049
4063
|
} : {},
|
|
4050
4064
|
tags,
|
|
4051
|
-
source: asset.source,
|
|
4065
|
+
source: asset.sourceByStyle[styleId] ?? asset.source,
|
|
4052
4066
|
// Revision identity is attached after every dependency in this style
|
|
4053
4067
|
// has a concrete output target. Finalization below computes the hash.
|
|
4054
4068
|
specHash: ""
|
|
@@ -4306,16 +4320,24 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
4306
4320
|
}
|
|
4307
4321
|
const prefix = `${styleId}/`;
|
|
4308
4322
|
const entries = Object.entries(lock.entries).filter(([key]) => key.startsWith(prefix));
|
|
4309
|
-
|
|
4323
|
+
const sources = options.sources ?? {};
|
|
4324
|
+
if (!entries.length && !Object.keys(sources).length) {
|
|
4310
4325
|
throw new Error(
|
|
4311
4326
|
`No locked assets for style "${styleId}". Run \`pixelkiln gen --style ${styleId}\` first.`
|
|
4312
4327
|
);
|
|
4313
4328
|
}
|
|
4314
4329
|
const inputs = [];
|
|
4315
4330
|
const noOutput = [];
|
|
4331
|
+
const locked = /* @__PURE__ */ new Set();
|
|
4316
4332
|
for (const [key, entry] of entries) {
|
|
4317
4333
|
const id = key.slice(prefix.length);
|
|
4334
|
+
locked.add(id);
|
|
4318
4335
|
const outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
4336
|
+
const source = sources[id];
|
|
4337
|
+
if (source && outputs.length <= 1) {
|
|
4338
|
+
inputs.push({ id, path: path7.resolve(manifestDir, source) });
|
|
4339
|
+
continue;
|
|
4340
|
+
}
|
|
4319
4341
|
let selected = outputs;
|
|
4320
4342
|
if (options.outputRoles?.length) {
|
|
4321
4343
|
selected = outputs.filter(
|
|
@@ -4332,6 +4354,9 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
4332
4354
|
}
|
|
4333
4355
|
for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
|
|
4334
4356
|
}
|
|
4357
|
+
for (const [id, source] of Object.entries(sources)) {
|
|
4358
|
+
if (!locked.has(id)) inputs.push({ id, path: path7.resolve(manifestDir, source) });
|
|
4359
|
+
}
|
|
4335
4360
|
if (!inputs.length) {
|
|
4336
4361
|
throw new Error(`No readable sprites for style "${styleId}" \u2014 ${noOutput.length} skipped.`);
|
|
4337
4362
|
}
|
|
@@ -6821,10 +6846,10 @@ function isSha256Hash(value) {
|
|
|
6821
6846
|
function parseCache(value) {
|
|
6822
6847
|
return HashCacheSchema.parse(value);
|
|
6823
6848
|
}
|
|
6824
|
-
async function loadCache(
|
|
6825
|
-
if (!existsSync11(
|
|
6849
|
+
async function loadCache(path29) {
|
|
6850
|
+
if (!existsSync11(path29)) return { version: 1, hashes: {} };
|
|
6826
6851
|
try {
|
|
6827
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
6852
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path29, "utf8")));
|
|
6828
6853
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6829
6854
|
return {
|
|
6830
6855
|
version: 1,
|
|
@@ -6836,18 +6861,18 @@ async function loadCache(path27) {
|
|
|
6836
6861
|
return { version: 1, hashes: {} };
|
|
6837
6862
|
}
|
|
6838
6863
|
}
|
|
6839
|
-
async function saveCache(
|
|
6864
|
+
async function saveCache(path29, cache) {
|
|
6840
6865
|
const sorted = {};
|
|
6841
6866
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6842
6867
|
const hash = cache.hashes[key];
|
|
6843
6868
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6844
6869
|
sorted[key] = hash;
|
|
6845
6870
|
}
|
|
6846
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6847
|
-
const tmp = `${
|
|
6871
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path29)), { recursive: true });
|
|
6872
|
+
const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6848
6873
|
try {
|
|
6849
6874
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6850
|
-
await rename4(tmp,
|
|
6875
|
+
await rename4(tmp, path29);
|
|
6851
6876
|
} finally {
|
|
6852
6877
|
await rm5(tmp, { force: true });
|
|
6853
6878
|
}
|
|
@@ -7096,6 +7121,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
7096
7121
|
assets[asset.id] = {
|
|
7097
7122
|
prompt: "",
|
|
7098
7123
|
promptByStyle: {},
|
|
7124
|
+
sourceByStyle: {},
|
|
7099
7125
|
category: asset.category || void 0,
|
|
7100
7126
|
file: asset.file,
|
|
7101
7127
|
tags: [],
|
|
@@ -7789,9 +7815,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
7789
7815
|
|
|
7790
7816
|
// src/gallery/snapshot.ts
|
|
7791
7817
|
import { createHash as createHash3 } from "crypto";
|
|
7792
|
-
import { existsSync as
|
|
7818
|
+
import { existsSync as existsSync17 } from "fs";
|
|
7793
7819
|
import { readFile as readFile15, stat as stat3 } from "fs/promises";
|
|
7794
|
-
import
|
|
7820
|
+
import path22 from "path";
|
|
7795
7821
|
|
|
7796
7822
|
// src/workspace.ts
|
|
7797
7823
|
import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -7927,6 +7953,10 @@ function validateWorkspace(ws, dir) {
|
|
|
7927
7953
|
}
|
|
7928
7954
|
|
|
7929
7955
|
// src/gallery/edit.ts
|
|
7956
|
+
import path21 from "path";
|
|
7957
|
+
import { z as z6 } from "zod";
|
|
7958
|
+
|
|
7959
|
+
// src/manifest-edit.ts
|
|
7930
7960
|
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
7931
7961
|
import path19 from "path";
|
|
7932
7962
|
import { z as z5 } from "zod";
|
|
@@ -7973,6 +8003,22 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
|
|
|
7973
8003
|
expectedSha256: HexSha,
|
|
7974
8004
|
asset: NewAssetSchema
|
|
7975
8005
|
}).strict(),
|
|
8006
|
+
z5.object({
|
|
8007
|
+
action: z5.literal("set-source"),
|
|
8008
|
+
project: z5.string().min(1).optional(),
|
|
8009
|
+
assetId: z5.string().min(1),
|
|
8010
|
+
styleId: z5.string().min(1),
|
|
8011
|
+
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
8012
|
+
source: z5.string().min(1),
|
|
8013
|
+
expectedSha256: HexSha
|
|
8014
|
+
}).strict(),
|
|
8015
|
+
z5.object({
|
|
8016
|
+
action: z5.literal("clear-source"),
|
|
8017
|
+
project: z5.string().min(1).optional(),
|
|
8018
|
+
assetId: z5.string().min(1),
|
|
8019
|
+
styleId: z5.string().min(1),
|
|
8020
|
+
expectedSha256: HexSha
|
|
8021
|
+
}).strict(),
|
|
7976
8022
|
z5.object({
|
|
7977
8023
|
action: z5.literal("patch-style"),
|
|
7978
8024
|
project: z5.string().min(1).optional(),
|
|
@@ -8023,8 +8069,38 @@ function setOrDelete(target, key, value) {
|
|
|
8023
8069
|
if (value === null || value === void 0) delete target[key];
|
|
8024
8070
|
else target[key] = value;
|
|
8025
8071
|
}
|
|
8072
|
+
function assetStyles(raw, asset) {
|
|
8073
|
+
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
8074
|
+
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
8075
|
+
}
|
|
8026
8076
|
function applyEdit(raw, edit) {
|
|
8027
8077
|
raw.assets ??= {};
|
|
8078
|
+
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
8079
|
+
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
8080
|
+
if (!asset2) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
|
|
8081
|
+
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
8082
|
+
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
8083
|
+
}
|
|
8084
|
+
if (!assetStyles(raw, asset2).includes(edit.styleId)) {
|
|
8085
|
+
throw new ManifestEditError(`asset "${edit.assetId}" is not in style "${edit.styleId}"`);
|
|
8086
|
+
}
|
|
8087
|
+
const single = assetStyles(raw, asset2).length === 1;
|
|
8088
|
+
const byStyle = { ...asset2.sourceByStyle ?? {} };
|
|
8089
|
+
if (edit.action === "set-source") {
|
|
8090
|
+
if (single) {
|
|
8091
|
+
asset2.source = edit.source;
|
|
8092
|
+
delete byStyle[edit.styleId];
|
|
8093
|
+
} else {
|
|
8094
|
+
byStyle[edit.styleId] = edit.source;
|
|
8095
|
+
}
|
|
8096
|
+
} else {
|
|
8097
|
+
delete byStyle[edit.styleId];
|
|
8098
|
+
if (single) delete asset2.source;
|
|
8099
|
+
}
|
|
8100
|
+
if (Object.keys(byStyle).length) asset2.sourceByStyle = byStyle;
|
|
8101
|
+
else delete asset2.sourceByStyle;
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
8028
8104
|
if (edit.action === "patch-style") {
|
|
8029
8105
|
const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
|
|
8030
8106
|
if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
|
|
@@ -8128,10 +8204,134 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8128
8204
|
}
|
|
8129
8205
|
return { manifestPath: absolute, sha256: sha256(next), changed: true };
|
|
8130
8206
|
}
|
|
8207
|
+
|
|
8208
|
+
// src/pipeline/hand-edit.ts
|
|
8209
|
+
import { spawn as spawn2 } from "child_process";
|
|
8210
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8211
|
+
import { copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
8212
|
+
import path20 from "path";
|
|
8213
|
+
var HAND_EDIT_DIR = "edits";
|
|
8214
|
+
function handEditPath(loaded, spec) {
|
|
8215
|
+
const style = loaded.manifest.styles[spec.styleId];
|
|
8216
|
+
const outDir = path20.resolve(loaded.root, style?.outDir ?? ".");
|
|
8217
|
+
const relative = path20.relative(outDir, spec.outFile);
|
|
8218
|
+
const inside = relative && !relative.startsWith("..") && !path20.isAbsolute(relative);
|
|
8219
|
+
const file = inside ? relative : path20.basename(spec.outFile);
|
|
8220
|
+
return path20.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8221
|
+
}
|
|
8222
|
+
function handEditBase(spec, lock) {
|
|
8223
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8224
|
+
if (entry) {
|
|
8225
|
+
if (entry.outputs.length !== 1) return null;
|
|
8226
|
+
const primary = primaryOutput(entry);
|
|
8227
|
+
if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
|
|
8228
|
+
const file = currentEntryOutputPath(entry, spec, 0);
|
|
8229
|
+
return existsSync16(file) ? file : null;
|
|
8230
|
+
}
|
|
8231
|
+
return existsSync16(spec.outFile) ? spec.outFile : null;
|
|
8232
|
+
}
|
|
8233
|
+
async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
8234
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8235
|
+
if (entry && entry.outputs.length > 1) {
|
|
8236
|
+
throw new ManifestEditError(
|
|
8237
|
+
`${spec.styleId}/${spec.assetId} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
|
|
8238
|
+
);
|
|
8239
|
+
}
|
|
8240
|
+
const existing = spec.source ? path20.resolve(loaded.root, spec.source) : null;
|
|
8241
|
+
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8242
|
+
let created = false;
|
|
8243
|
+
if (!existsSync16(editPath)) {
|
|
8244
|
+
const base = handEditBase(spec, lock);
|
|
8245
|
+
if (!base) {
|
|
8246
|
+
throw new ManifestEditError(
|
|
8247
|
+
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8248
|
+
);
|
|
8249
|
+
}
|
|
8250
|
+
await mkdir6(path20.dirname(editPath), { recursive: true });
|
|
8251
|
+
await copyFile(base, editPath);
|
|
8252
|
+
created = true;
|
|
8253
|
+
}
|
|
8254
|
+
const source = portableOutputPath(editPath, loaded.root);
|
|
8255
|
+
let declared = false;
|
|
8256
|
+
let manifestSha256 = opts.expectedSha256 ?? await sha256File(loaded.path);
|
|
8257
|
+
if (spec.source !== source) {
|
|
8258
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
8259
|
+
action: "set-source",
|
|
8260
|
+
assetId: spec.assetId,
|
|
8261
|
+
styleId: spec.styleId,
|
|
8262
|
+
source,
|
|
8263
|
+
expectedSha256: manifestSha256
|
|
8264
|
+
});
|
|
8265
|
+
declared = result.changed;
|
|
8266
|
+
manifestSha256 = result.sha256;
|
|
8267
|
+
}
|
|
8268
|
+
return { editPath, source, created, declared, manifestSha256 };
|
|
8269
|
+
}
|
|
8270
|
+
async function detachHandEdit(loaded, spec, opts = {}) {
|
|
8271
|
+
if (!spec.source) {
|
|
8272
|
+
return { manifestSha256: opts.expectedSha256 ?? await sha256File(loaded.path), changed: false };
|
|
8273
|
+
}
|
|
8274
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
8275
|
+
action: "clear-source",
|
|
8276
|
+
assetId: spec.assetId,
|
|
8277
|
+
styleId: spec.styleId,
|
|
8278
|
+
expectedSha256: opts.expectedSha256 ?? await sha256File(loaded.path)
|
|
8279
|
+
});
|
|
8280
|
+
return { manifestSha256: result.sha256, changed: result.changed };
|
|
8281
|
+
}
|
|
8282
|
+
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
8283
|
+
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
8284
|
+
const [program, ...args] = command;
|
|
8285
|
+
const child = spawn2(program, [...args, file], { stdio: "ignore", detached: true });
|
|
8286
|
+
child.on("error", () => {
|
|
8287
|
+
});
|
|
8288
|
+
child.unref();
|
|
8289
|
+
return command.join(" ");
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
// src/gallery/edit.ts
|
|
8293
|
+
var HandEditRequestSchema = z6.discriminatedUnion("action", [
|
|
8294
|
+
z6.object({
|
|
8295
|
+
action: z6.literal("start-edit"),
|
|
8296
|
+
project: z6.string().min(1).optional(),
|
|
8297
|
+
assetId: z6.string().min(1),
|
|
8298
|
+
styleId: z6.string().min(1),
|
|
8299
|
+
expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
8300
|
+
/** Also launch the author's editor on the file. */
|
|
8301
|
+
open: z6.boolean().optional()
|
|
8302
|
+
}).strict(),
|
|
8303
|
+
z6.object({
|
|
8304
|
+
action: z6.literal("detach-edit"),
|
|
8305
|
+
project: z6.string().min(1).optional(),
|
|
8306
|
+
assetId: z6.string().min(1),
|
|
8307
|
+
styleId: z6.string().min(1),
|
|
8308
|
+
expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/)
|
|
8309
|
+
}).strict()
|
|
8310
|
+
]);
|
|
8131
8311
|
function createGalleryEditHandler(opts) {
|
|
8132
8312
|
const log2 = opts.onProgress ?? (() => {
|
|
8133
8313
|
});
|
|
8134
8314
|
return async (body) => {
|
|
8315
|
+
const handEdit = HandEditRequestSchema.safeParse(body);
|
|
8316
|
+
if (handEdit.success) {
|
|
8317
|
+
if (!opts.loadProject) throw new ManifestEditError("hand edits are not available in this gallery");
|
|
8318
|
+
const request = handEdit.data;
|
|
8319
|
+
const ctx = await opts.loadProject(request.project);
|
|
8320
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === lockKey(request.styleId, request.assetId));
|
|
8321
|
+
if (!spec) throw new ManifestEditError(`"${request.styleId}/${request.assetId}" is not declared by the manifest`);
|
|
8322
|
+
if (request.action === "start-edit") {
|
|
8323
|
+
const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
|
|
8324
|
+
log2(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
|
|
8325
|
+
if (request.open) {
|
|
8326
|
+
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
8327
|
+
log2(` opened with: ${command}`);
|
|
8328
|
+
}
|
|
8329
|
+
} else {
|
|
8330
|
+
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
8331
|
+
if (result2.changed) log2(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
8332
|
+
}
|
|
8333
|
+
return opts.reload();
|
|
8334
|
+
}
|
|
8135
8335
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
8136
8336
|
if (!parsed.success) {
|
|
8137
8337
|
throw new ManifestEditError(
|
|
@@ -8146,9 +8346,9 @@ function createGalleryEditHandler(opts) {
|
|
|
8146
8346
|
}
|
|
8147
8347
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8148
8348
|
if (result.changed) {
|
|
8149
|
-
const relative =
|
|
8349
|
+
const relative = path21.relative(process.cwd(), result.manifestPath);
|
|
8150
8350
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8151
|
-
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.assetId;
|
|
8351
|
+
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
|
|
8152
8352
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
8153
8353
|
}
|
|
8154
8354
|
return opts.reload();
|
|
@@ -8157,7 +8357,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8157
8357
|
|
|
8158
8358
|
// src/gallery/snapshot.ts
|
|
8159
8359
|
function galleryMediaId(absolutePath) {
|
|
8160
|
-
return createHash3("sha256").update(
|
|
8360
|
+
return createHash3("sha256").update(path22.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8161
8361
|
}
|
|
8162
8362
|
function galleryMediaRoute(id) {
|
|
8163
8363
|
return `/media/${id}`;
|
|
@@ -8182,23 +8382,24 @@ function metadataFps(entry) {
|
|
|
8182
8382
|
}
|
|
8183
8383
|
return null;
|
|
8184
8384
|
}
|
|
8185
|
-
async function
|
|
8385
|
+
async function fileInfo(absolutePath) {
|
|
8186
8386
|
try {
|
|
8187
8387
|
const info = await stat3(absolutePath);
|
|
8188
|
-
return info.isFile() ? info.size : null;
|
|
8388
|
+
return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
|
|
8189
8389
|
} catch {
|
|
8190
8390
|
return null;
|
|
8191
8391
|
}
|
|
8192
8392
|
}
|
|
8193
8393
|
async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
8194
|
-
const
|
|
8195
|
-
const exists =
|
|
8394
|
+
const info = await fileInfo(absolutePath);
|
|
8395
|
+
const exists = info !== null;
|
|
8196
8396
|
const mediaType = recorded.mediaType ?? mediaTypeFromExtension(absolutePath);
|
|
8197
8397
|
let url = null;
|
|
8198
8398
|
if (exists && mediaType) {
|
|
8199
8399
|
const id = galleryMediaId(absolutePath);
|
|
8200
8400
|
media.set(id, { path: absolutePath, contentType: mediaType });
|
|
8201
|
-
|
|
8401
|
+
const version = recorded.sha256 ?? `${info.bytes}-${Date.parse(info.modifiedAt).toString(36)}`;
|
|
8402
|
+
url = `${galleryMediaRoute(id)}?v=${version.slice(0, 24)}`;
|
|
8202
8403
|
}
|
|
8203
8404
|
return {
|
|
8204
8405
|
path: portableOutputPath(absolutePath, root),
|
|
@@ -8207,17 +8408,18 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
8207
8408
|
role: recorded.role ?? null,
|
|
8208
8409
|
mediaType,
|
|
8209
8410
|
exists,
|
|
8210
|
-
bytes,
|
|
8411
|
+
bytes: info?.bytes ?? null,
|
|
8412
|
+
modifiedAt: info?.modifiedAt ?? null,
|
|
8211
8413
|
url
|
|
8212
8414
|
};
|
|
8213
8415
|
}
|
|
8214
8416
|
async function describeQuality(media, root, inspection) {
|
|
8215
8417
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
8216
8418
|
const outputs = await Promise.all(
|
|
8217
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
8419
|
+
outputPaths.map((file) => describeOutput(media, root, path22.resolve(root, file)))
|
|
8218
8420
|
);
|
|
8219
|
-
const recordPath =
|
|
8220
|
-
const recordExists =
|
|
8421
|
+
const recordPath = path22.resolve(root, inspection.record);
|
|
8422
|
+
const recordExists = existsSync17(recordPath);
|
|
8221
8423
|
let options = null;
|
|
8222
8424
|
let check = null;
|
|
8223
8425
|
if (recordExists) {
|
|
@@ -8236,7 +8438,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
8236
8438
|
return {
|
|
8237
8439
|
state: inspection.state,
|
|
8238
8440
|
reason: inspection.reason,
|
|
8239
|
-
output: portableOutputPath(
|
|
8441
|
+
output: portableOutputPath(path22.resolve(root, inspection.output), root),
|
|
8240
8442
|
record: portableOutputPath(recordPath, root),
|
|
8241
8443
|
recordExists,
|
|
8242
8444
|
outputs,
|
|
@@ -8286,15 +8488,24 @@ async function buildGallerySnapshot(opts) {
|
|
|
8286
8488
|
const entry = lock.entries[key];
|
|
8287
8489
|
const asset = loaded.manifest.assets[spec.assetId] ?? null;
|
|
8288
8490
|
let outputs;
|
|
8491
|
+
let edit = null;
|
|
8492
|
+
let editStatus = null;
|
|
8289
8493
|
if (entry && entry.outputs.length) {
|
|
8290
8494
|
outputs = await Promise.all(
|
|
8291
8495
|
entry.outputs.map(
|
|
8292
8496
|
(output, index) => describeOutput(media, root, currentEntryOutputPath(entry, spec, index), output)
|
|
8293
8497
|
)
|
|
8294
8498
|
);
|
|
8499
|
+
if (spec.source) {
|
|
8500
|
+
const editPath = path22.resolve(root, spec.source);
|
|
8501
|
+
const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
|
|
8502
|
+
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
8503
|
+
const generated = outputs[0];
|
|
8504
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists ? "regenerated-since" : "edited";
|
|
8505
|
+
}
|
|
8295
8506
|
} else if (spec.source) {
|
|
8296
|
-
outputs = [await describeOutput(media, root,
|
|
8297
|
-
} else if (!entry &&
|
|
8507
|
+
outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
|
|
8508
|
+
} else if (!entry && existsSync17(spec.outFile)) {
|
|
8298
8509
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
8299
8510
|
} else {
|
|
8300
8511
|
outputs = [];
|
|
@@ -8338,6 +8549,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8338
8549
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
8339
8550
|
asset,
|
|
8340
8551
|
source: spec.source ?? null,
|
|
8552
|
+
edit,
|
|
8553
|
+
editStatus,
|
|
8341
8554
|
tags: spec.tags,
|
|
8342
8555
|
category: asset?.category ?? null
|
|
8343
8556
|
});
|
|
@@ -8390,6 +8603,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8390
8603
|
providerMetadata: entry.providerMetadata ?? {},
|
|
8391
8604
|
asset: null,
|
|
8392
8605
|
source: null,
|
|
8606
|
+
edit: null,
|
|
8607
|
+
editStatus: null,
|
|
8393
8608
|
tags: [],
|
|
8394
8609
|
category: null
|
|
8395
8610
|
});
|
|
@@ -8457,7 +8672,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8457
8672
|
id: loaded.manifest.name,
|
|
8458
8673
|
name: loaded.manifest.name,
|
|
8459
8674
|
manifest: loaded.path,
|
|
8460
|
-
lock:
|
|
8675
|
+
lock: path22.resolve(opts.lockPath),
|
|
8461
8676
|
root,
|
|
8462
8677
|
provider: loaded.manifest.provider,
|
|
8463
8678
|
account: null,
|
|
@@ -8482,7 +8697,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8482
8697
|
return { snapshot, media };
|
|
8483
8698
|
}
|
|
8484
8699
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
8485
|
-
const dir =
|
|
8700
|
+
const dir = path22.dirname(path22.resolve(opts.workspacePath));
|
|
8486
8701
|
const media = /* @__PURE__ */ new Map();
|
|
8487
8702
|
const projects = [];
|
|
8488
8703
|
const items = [];
|
|
@@ -8525,7 +8740,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8525
8740
|
id: project.id,
|
|
8526
8741
|
name: loaded.manifest.name,
|
|
8527
8742
|
manifest: manifestPath,
|
|
8528
|
-
lock:
|
|
8743
|
+
lock: path22.resolve(lockPath),
|
|
8529
8744
|
root: loaded.root,
|
|
8530
8745
|
provider: loaded.manifest.provider,
|
|
8531
8746
|
account: project.account ?? null,
|
|
@@ -8540,8 +8755,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8540
8755
|
id: project.id,
|
|
8541
8756
|
name: project.id,
|
|
8542
8757
|
manifest: manifestPath,
|
|
8543
|
-
lock:
|
|
8544
|
-
root:
|
|
8758
|
+
lock: path22.resolve(lockPath),
|
|
8759
|
+
root: path22.dirname(manifestPath),
|
|
8545
8760
|
provider: project.provider,
|
|
8546
8761
|
account: project.account ?? null,
|
|
8547
8762
|
manifestSha256: null,
|
|
@@ -8567,7 +8782,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8567
8782
|
version: 1,
|
|
8568
8783
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
8569
8784
|
project: null,
|
|
8570
|
-
workspace: { path:
|
|
8785
|
+
workspace: { path: path22.resolve(opts.workspacePath), dir, projects },
|
|
8571
8786
|
filter,
|
|
8572
8787
|
totals: {
|
|
8573
8788
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -8584,7 +8799,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8584
8799
|
}
|
|
8585
8800
|
|
|
8586
8801
|
// src/gallery/server.ts
|
|
8587
|
-
import { spawn as
|
|
8802
|
+
import { spawn as spawn3 } from "child_process";
|
|
8588
8803
|
import { randomBytes } from "crypto";
|
|
8589
8804
|
import { createServer as createServer2 } from "http";
|
|
8590
8805
|
import { readFile as readFile16 } from "fs/promises";
|
|
@@ -8860,6 +9075,14 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
8860
9075
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
8861
9076
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
8862
9077
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9078
|
+
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9079
|
+
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
9080
|
+
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
9081
|
+
.pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
|
|
9082
|
+
.pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
|
|
9083
|
+
.card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
|
|
9084
|
+
font:700 11px/1 var(--mono); padding:3px 5px; }
|
|
9085
|
+
.hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
8863
9086
|
.drawer .gen button.primary { padding:7px 14px; }
|
|
8864
9087
|
@media (max-width: 720px) {
|
|
8865
9088
|
.bar, .totals, .chips, .style, footer { padding-inline:14px; }
|
|
@@ -9306,7 +9529,8 @@ function clearFilters() {
|
|
|
9306
9529
|
|
|
9307
9530
|
function thumb(item) {
|
|
9308
9531
|
const cell = el('div', 'cell');
|
|
9309
|
-
|
|
9532
|
+
// A hand edit is what ships, so it is what the card shows.
|
|
9533
|
+
const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
|
|
9310
9534
|
if (!shown.length) {
|
|
9311
9535
|
cell.classList.add('none');
|
|
9312
9536
|
cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
|
|
@@ -9357,6 +9581,9 @@ function card(item) {
|
|
|
9357
9581
|
body.append(meta);
|
|
9358
9582
|
const cell = thumb(item);
|
|
9359
9583
|
if (slot >= 0) cell.append(el('span', 'slot', String(slot + 1)));
|
|
9584
|
+
if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
|
|
9585
|
+
const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
|
|
9586
|
+
}
|
|
9360
9587
|
c.append(cell, body);
|
|
9361
9588
|
c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
|
|
9362
9589
|
return c;
|
|
@@ -9683,6 +9910,78 @@ function openCompare() {
|
|
|
9683
9910
|
close.focus({ preventScroll: true });
|
|
9684
9911
|
}
|
|
9685
9912
|
|
|
9913
|
+
// ---- hand edits -------------------------------------------------------------
|
|
9914
|
+
|
|
9915
|
+
const EDIT_STATUS_TEXT = {
|
|
9916
|
+
same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
|
|
9917
|
+
edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
|
|
9918
|
+
'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
|
|
9919
|
+
missing: 'The declared file is not on disk.',
|
|
9920
|
+
};
|
|
9921
|
+
async function postHandEdit(item, action, extra) {
|
|
9922
|
+
const pr = projectOf(item);
|
|
9923
|
+
const body = { action, assetId: item.assetId, styleId: item.styleId, expectedSha256: pr.manifestSha256, ...extra };
|
|
9924
|
+
if (item.project) body.project = item.project;
|
|
9925
|
+
snap = await postEdit(body);
|
|
9926
|
+
}
|
|
9927
|
+
function handEditSection(item) {
|
|
9928
|
+
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
|
|
9929
|
+
item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
|
|
9930
|
+
if (!item.edit && !canStart) return null;
|
|
9931
|
+
const s = el('section', 'meta');
|
|
9932
|
+
s.append(el('h3', null, 'Hand edit'));
|
|
9933
|
+
if (item.edit) {
|
|
9934
|
+
const pair = el('div', 'pair');
|
|
9935
|
+
const shared = displayScale(item.width, item.height, 200, 160);
|
|
9936
|
+
const fig = (out, label) => {
|
|
9937
|
+
const f = el('figure');
|
|
9938
|
+
if (out && out.url) {
|
|
9939
|
+
const img = el('img'); img.src = out.url; img.alt = label;
|
|
9940
|
+
if (shared >= 1) { img.width = item.width * shared; img.height = item.height * shared; }
|
|
9941
|
+
f.append(img);
|
|
9942
|
+
} else f.append(el('span', 'state-dim', 'not on disk'));
|
|
9943
|
+
f.append(el('figcaption', null, label));
|
|
9944
|
+
return f;
|
|
9945
|
+
};
|
|
9946
|
+
pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
|
|
9947
|
+
s.append(pair);
|
|
9948
|
+
const dl = el('dl');
|
|
9949
|
+
const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
|
|
9950
|
+
const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
|
|
9951
|
+
row(dl, 'status', st);
|
|
9952
|
+
row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
|
|
9953
|
+
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
9954
|
+
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
9955
|
+
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
9956
|
+
s.append(dl);
|
|
9957
|
+
} else {
|
|
9958
|
+
s.append(el('div', 'state-dim', 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
|
|
9959
|
+
}
|
|
9960
|
+
if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
|
|
9961
|
+
const acts = el('div', 'hand-actions');
|
|
9962
|
+
const msg = el('span', 'msg');
|
|
9963
|
+
const go = async (label, action, extra, done) => {
|
|
9964
|
+
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
9965
|
+
b.onclick = async () => {
|
|
9966
|
+
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
9967
|
+
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
9968
|
+
catch (err) { b.disabled = false; msg.className = 'msg bad'; msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : ''); }
|
|
9969
|
+
};
|
|
9970
|
+
acts.append(b);
|
|
9971
|
+
};
|
|
9972
|
+
if (item.edit) {
|
|
9973
|
+
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
9974
|
+
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
9975
|
+
} else if (canStart) {
|
|
9976
|
+
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
|
|
9977
|
+
go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
|
|
9978
|
+
}
|
|
9979
|
+
acts.append(msg);
|
|
9980
|
+
s.append(acts);
|
|
9981
|
+
}
|
|
9982
|
+
return s;
|
|
9983
|
+
}
|
|
9984
|
+
|
|
9686
9985
|
// ---- editing (only when the server minted a session) ---------------------
|
|
9687
9986
|
|
|
9688
9987
|
// Styles a parent edit reaches: every descendant (via extends) that does not
|
|
@@ -10174,6 +10473,9 @@ function renderDrawer() {
|
|
|
10174
10473
|
body.append(s);
|
|
10175
10474
|
}
|
|
10176
10475
|
|
|
10476
|
+
const hand = handEditSection(item);
|
|
10477
|
+
if (hand) body.append(hand);
|
|
10478
|
+
|
|
10177
10479
|
if (item.revision || item.revisionParentKey) {
|
|
10178
10480
|
const { s, dl } = section('Lineage');
|
|
10179
10481
|
row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
|
|
@@ -10641,7 +10943,7 @@ async function serveGallery(opts) {
|
|
|
10641
10943
|
});
|
|
10642
10944
|
opts.onReady?.(url);
|
|
10643
10945
|
if (opts.open !== false && process.platform === "darwin") {
|
|
10644
|
-
|
|
10946
|
+
spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
10645
10947
|
}
|
|
10646
10948
|
return {
|
|
10647
10949
|
url,
|
|
@@ -10655,14 +10957,14 @@ async function serveGallery(opts) {
|
|
|
10655
10957
|
|
|
10656
10958
|
// src/gallery/generate.ts
|
|
10657
10959
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
10658
|
-
import { z as
|
|
10659
|
-
var GenerateRequestSchema =
|
|
10660
|
-
project:
|
|
10661
|
-
keys:
|
|
10960
|
+
import { z as z7 } from "zod";
|
|
10961
|
+
var GenerateRequestSchema = z7.object({
|
|
10962
|
+
project: z7.string().min(1).optional(),
|
|
10963
|
+
keys: z7.array(z7.string().min(1)).min(1).max(500),
|
|
10662
10964
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
10663
|
-
force:
|
|
10965
|
+
force: z7.boolean().optional(),
|
|
10664
10966
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
10665
|
-
resume:
|
|
10967
|
+
resume: z7.boolean().optional()
|
|
10666
10968
|
}).strict();
|
|
10667
10969
|
var GenerateRequestError = class extends Error {
|
|
10668
10970
|
constructor(message6, status = 400) {
|
|
@@ -10900,15 +11202,15 @@ function createGenerateHandlers(opts) {
|
|
|
10900
11202
|
|
|
10901
11203
|
// src/pipeline/salvage.ts
|
|
10902
11204
|
import { readFile as readFile17 } from "fs/promises";
|
|
10903
|
-
import { existsSync as
|
|
10904
|
-
import
|
|
11205
|
+
import { existsSync as existsSync18 } from "fs";
|
|
11206
|
+
import path23 from "path";
|
|
10905
11207
|
function providerClaimId(provider, objectId) {
|
|
10906
11208
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
10907
11209
|
}
|
|
10908
11210
|
async function loadClaims(lockPaths, opts = {}) {
|
|
10909
11211
|
const claimed = /* @__PURE__ */ new Set();
|
|
10910
11212
|
for (const p of lockPaths) {
|
|
10911
|
-
if (!
|
|
11213
|
+
if (!existsSync18(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
10912
11214
|
let parsed;
|
|
10913
11215
|
try {
|
|
10914
11216
|
parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
|
|
@@ -10959,20 +11261,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
10959
11261
|
return null;
|
|
10960
11262
|
}
|
|
10961
11263
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
10962
|
-
const own =
|
|
11264
|
+
const own = path23.resolve(ownManifestPath);
|
|
10963
11265
|
const siblingManifestPaths = [
|
|
10964
11266
|
.../* @__PURE__ */ new Set([
|
|
10965
11267
|
...workspaceManifestPaths,
|
|
10966
|
-
...claimPaths.map((c) =>
|
|
11268
|
+
...claimPaths.map((c) => path23.join(path23.dirname(path23.resolve(c)), "pixelkiln.manifest.json"))
|
|
10967
11269
|
])
|
|
10968
11270
|
];
|
|
10969
11271
|
const siblings = [];
|
|
10970
11272
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
10971
|
-
if (
|
|
10972
|
-
if (!
|
|
11273
|
+
if (path23.resolve(siblingManifestPath) === own) continue;
|
|
11274
|
+
if (!existsSync18(siblingManifestPath)) continue;
|
|
10973
11275
|
try {
|
|
10974
11276
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
10975
|
-
siblings.push({ label:
|
|
11277
|
+
siblings.push({ label: path23.basename(path23.dirname(siblingManifestPath)), manifest });
|
|
10976
11278
|
} catch {
|
|
10977
11279
|
}
|
|
10978
11280
|
}
|
|
@@ -11191,15 +11493,15 @@ async function workspaceStatus(ws, dir) {
|
|
|
11191
11493
|
}
|
|
11192
11494
|
|
|
11193
11495
|
// src/pipeline/cache-health.ts
|
|
11194
|
-
import { existsSync as
|
|
11496
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11195
11497
|
import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
|
|
11196
|
-
import
|
|
11498
|
+
import path24 from "path";
|
|
11197
11499
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11198
|
-
if (options.prune && !
|
|
11199
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
11500
|
+
if (options.prune && !existsSync19(lockPath)) {
|
|
11501
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path24.resolve(lockPath)}`);
|
|
11200
11502
|
}
|
|
11201
|
-
const contentDir =
|
|
11202
|
-
const remotePath =
|
|
11503
|
+
const contentDir = path24.resolve(path24.dirname(lockPath), ".pixelkiln", "cache");
|
|
11504
|
+
const remotePath = path24.resolve(cachePathFor(lockPath));
|
|
11203
11505
|
const referenced = new Set(
|
|
11204
11506
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
11205
11507
|
);
|
|
@@ -11213,7 +11515,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11213
11515
|
]);
|
|
11214
11516
|
for (const name of names) {
|
|
11215
11517
|
try {
|
|
11216
|
-
await rm8(
|
|
11518
|
+
await rm8(path24.join(contentDir, name), { force: true });
|
|
11217
11519
|
removed.contentFiles++;
|
|
11218
11520
|
} catch {
|
|
11219
11521
|
}
|
|
@@ -11240,7 +11542,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11240
11542
|
async function inspectContentCache(contentDir, referenced) {
|
|
11241
11543
|
const report = {
|
|
11242
11544
|
path: contentDir,
|
|
11243
|
-
exists:
|
|
11545
|
+
exists: existsSync19(contentDir),
|
|
11244
11546
|
files: 0,
|
|
11245
11547
|
bytes: 0,
|
|
11246
11548
|
valid: 0,
|
|
@@ -11261,7 +11563,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11261
11563
|
continue;
|
|
11262
11564
|
}
|
|
11263
11565
|
report.files++;
|
|
11264
|
-
const file =
|
|
11566
|
+
const file = path24.join(contentDir, entry.name);
|
|
11265
11567
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11266
11568
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11267
11569
|
let bytes;
|
|
@@ -11304,7 +11606,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11304
11606
|
async function inspectRemoteHashCache(remotePath) {
|
|
11305
11607
|
const report = {
|
|
11306
11608
|
path: remotePath,
|
|
11307
|
-
exists:
|
|
11609
|
+
exists: existsSync19(remotePath),
|
|
11308
11610
|
entries: 0,
|
|
11309
11611
|
valid: 0,
|
|
11310
11612
|
invalidIds: [],
|
|
@@ -11584,8 +11886,8 @@ function isRecord2(value) {
|
|
|
11584
11886
|
}
|
|
11585
11887
|
|
|
11586
11888
|
// src/pick/salvage-server.ts
|
|
11587
|
-
import { mkdir as
|
|
11588
|
-
import
|
|
11889
|
+
import { mkdir as mkdir7, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
|
|
11890
|
+
import path25 from "path";
|
|
11589
11891
|
|
|
11590
11892
|
// src/pick/salvage-sheet.ts
|
|
11591
11893
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -11750,7 +12052,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11750
12052
|
});
|
|
11751
12053
|
const html = renderSalvageSheet(orphans, {
|
|
11752
12054
|
styleId: ctx.styleId,
|
|
11753
|
-
importDir:
|
|
12055
|
+
importDir: path25.relative(process.cwd(), ctx.importDir) || "."
|
|
11754
12056
|
});
|
|
11755
12057
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
11756
12058
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -11779,13 +12081,14 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11779
12081
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
11780
12082
|
decodePng(buf);
|
|
11781
12083
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
11782
|
-
const rel =
|
|
11783
|
-
const outFile =
|
|
11784
|
-
await
|
|
12084
|
+
const rel = path25.join("_salvaged", `${assetId}.png`);
|
|
12085
|
+
const outFile = path25.resolve(ctx.importDir, rel);
|
|
12086
|
+
await mkdir7(path25.dirname(outFile), { recursive: true });
|
|
11785
12087
|
await writeFile9(outFile, buf);
|
|
11786
12088
|
ctx.manifest.assets[assetId] = {
|
|
11787
12089
|
prompt: orphan.prompt,
|
|
11788
12090
|
promptByStyle: {},
|
|
12091
|
+
sourceByStyle: {},
|
|
11789
12092
|
category: "_salvaged",
|
|
11790
12093
|
file: rel,
|
|
11791
12094
|
tags: ["salvaged"],
|
|
@@ -11813,7 +12116,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11813
12116
|
sourceUrl: durableSource,
|
|
11814
12117
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
11815
12118
|
outputs: [{
|
|
11816
|
-
path: portableOutputPath(outFile,
|
|
12119
|
+
path: portableOutputPath(outFile, path25.dirname(ctx.manifestPath)),
|
|
11817
12120
|
sha256: sha256(buf)
|
|
11818
12121
|
}],
|
|
11819
12122
|
submittedAt: orphan.createdAt,
|
|
@@ -11847,51 +12150,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11847
12150
|
}
|
|
11848
12151
|
|
|
11849
12152
|
// src/recipes.ts
|
|
11850
|
-
import
|
|
11851
|
-
import { existsSync as
|
|
12153
|
+
import path26 from "path";
|
|
12154
|
+
import { existsSync as existsSync20 } from "fs";
|
|
11852
12155
|
import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
|
|
11853
12156
|
import { fileURLToPath } from "url";
|
|
11854
|
-
import { z as
|
|
12157
|
+
import { z as z8 } from "zod";
|
|
11855
12158
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
11856
12159
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
11857
12160
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
11858
12161
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
11859
12162
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
11860
|
-
var PortablePathSchema =
|
|
11861
|
-
if (value.includes("\\") ||
|
|
12163
|
+
var PortablePathSchema = z8.string().min(1).refine((value) => {
|
|
12164
|
+
if (value.includes("\\") || path26.posix.isAbsolute(value)) return false;
|
|
11862
12165
|
const parts = value.split("/");
|
|
11863
12166
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
11864
12167
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
11865
|
-
var RecipeBindingSchema =
|
|
11866
|
-
nodeId:
|
|
11867
|
-
input:
|
|
12168
|
+
var RecipeBindingSchema = z8.object({
|
|
12169
|
+
nodeId: z8.string().min(1),
|
|
12170
|
+
input: z8.string().min(1)
|
|
11868
12171
|
}).strict();
|
|
11869
|
-
var RecipeSchema =
|
|
11870
|
-
$schema:
|
|
11871
|
-
format:
|
|
11872
|
-
schemaVersion:
|
|
11873
|
-
id:
|
|
11874
|
-
version:
|
|
11875
|
-
provider:
|
|
11876
|
-
summary:
|
|
11877
|
-
files:
|
|
12172
|
+
var RecipeSchema = z8.object({
|
|
12173
|
+
$schema: z8.string().url().optional(),
|
|
12174
|
+
format: z8.literal("pixelkiln-recipe"),
|
|
12175
|
+
schemaVersion: z8.literal(1),
|
|
12176
|
+
id: z8.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
12177
|
+
version: z8.string().regex(VERSION_RE, "expected x.y.z"),
|
|
12178
|
+
provider: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
12179
|
+
summary: z8.string().min(1).max(240),
|
|
12180
|
+
files: z8.array(z8.object({
|
|
11878
12181
|
path: PortablePathSchema,
|
|
11879
|
-
role:
|
|
11880
|
-
sha256:
|
|
12182
|
+
role: z8.enum(["workflow", "reference"]),
|
|
12183
|
+
sha256: z8.string().regex(SHA256_RE)
|
|
11881
12184
|
}).strict()).default([]),
|
|
11882
|
-
models:
|
|
12185
|
+
models: z8.array(z8.object({
|
|
11883
12186
|
path: PortablePathSchema,
|
|
11884
|
-
sha256:
|
|
11885
|
-
source:
|
|
11886
|
-
license:
|
|
12187
|
+
sha256: z8.string().regex(SHA256_RE),
|
|
12188
|
+
source: z8.string().url(),
|
|
12189
|
+
license: z8.string().min(1)
|
|
11887
12190
|
}).strict()).default([]),
|
|
11888
|
-
styleId:
|
|
12191
|
+
styleId: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
11889
12192
|
style: StyleSchema,
|
|
11890
|
-
workflow:
|
|
12193
|
+
workflow: z8.object({
|
|
11891
12194
|
path: PortablePathSchema,
|
|
11892
|
-
outputNodeId:
|
|
11893
|
-
numImages:
|
|
11894
|
-
bindings:
|
|
12195
|
+
outputNodeId: z8.string().min(1),
|
|
12196
|
+
numImages: z8.number().int().min(1).max(16),
|
|
12197
|
+
bindings: z8.object({
|
|
11895
12198
|
prompt: RecipeBindingSchema,
|
|
11896
12199
|
width: RecipeBindingSchema.optional(),
|
|
11897
12200
|
height: RecipeBindingSchema.optional(),
|
|
@@ -11902,67 +12205,67 @@ var RecipeSchema = z7.object({
|
|
|
11902
12205
|
strength: RecipeBindingSchema.optional()
|
|
11903
12206
|
}).strict()
|
|
11904
12207
|
}).strict().optional(),
|
|
11905
|
-
quality:
|
|
11906
|
-
stage:
|
|
11907
|
-
workingCanvas:
|
|
11908
|
-
width:
|
|
11909
|
-
height:
|
|
12208
|
+
quality: z8.object({
|
|
12209
|
+
stage: z8.enum(["composition-source", "production-candidate"]),
|
|
12210
|
+
workingCanvas: z8.object({
|
|
12211
|
+
width: z8.number().int().min(16),
|
|
12212
|
+
height: z8.number().int().min(16)
|
|
11910
12213
|
}).strict(),
|
|
11911
|
-
recommendedNativeSize:
|
|
11912
|
-
min:
|
|
11913
|
-
max:
|
|
12214
|
+
recommendedNativeSize: z8.object({
|
|
12215
|
+
min: z8.number().int().min(1),
|
|
12216
|
+
max: z8.number().int().min(1)
|
|
11914
12217
|
}).strict(),
|
|
11915
|
-
paletteColors:
|
|
11916
|
-
min:
|
|
11917
|
-
max:
|
|
12218
|
+
paletteColors: z8.object({
|
|
12219
|
+
min: z8.number().int().min(1),
|
|
12220
|
+
max: z8.number().int().min(1)
|
|
11918
12221
|
}).strict(),
|
|
11919
|
-
background:
|
|
11920
|
-
checks:
|
|
12222
|
+
background: z8.enum(["opaque", "transparent", "full-bleed"]),
|
|
12223
|
+
checks: z8.array(z8.enum([
|
|
11921
12224
|
"prompt-coverage",
|
|
11922
12225
|
"native-grid",
|
|
11923
12226
|
"final-palette",
|
|
11924
12227
|
"alpha-edge",
|
|
11925
12228
|
"human-1x"
|
|
11926
12229
|
])).min(1),
|
|
11927
|
-
notes:
|
|
12230
|
+
notes: z8.array(z8.string().min(1)).default([])
|
|
11928
12231
|
}).strict(),
|
|
11929
|
-
integrity:
|
|
11930
|
-
algorithm:
|
|
11931
|
-
digest:
|
|
12232
|
+
integrity: z8.object({
|
|
12233
|
+
algorithm: z8.literal("sha256"),
|
|
12234
|
+
digest: z8.string().regex(SHA256_RE)
|
|
11932
12235
|
}).strict()
|
|
11933
12236
|
}).strict().superRefine((recipe, context) => {
|
|
11934
12237
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
11935
12238
|
context.addIssue({
|
|
11936
|
-
code:
|
|
12239
|
+
code: z8.ZodIssueCode.custom,
|
|
11937
12240
|
path: ["style", "provider"],
|
|
11938
12241
|
message: "must match the recipe provider"
|
|
11939
12242
|
});
|
|
11940
12243
|
}
|
|
11941
12244
|
const filePaths = recipe.files.map((file) => file.path);
|
|
11942
12245
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
11943
|
-
context.addIssue({ code:
|
|
12246
|
+
context.addIssue({ code: z8.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
11944
12247
|
}
|
|
11945
12248
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
11946
12249
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
11947
|
-
context.addIssue({ code:
|
|
12250
|
+
context.addIssue({ code: z8.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
11948
12251
|
}
|
|
11949
12252
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
11950
12253
|
context.addIssue({
|
|
11951
|
-
code:
|
|
12254
|
+
code: z8.ZodIssueCode.custom,
|
|
11952
12255
|
path: ["files"],
|
|
11953
12256
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
11954
12257
|
});
|
|
11955
12258
|
}
|
|
11956
12259
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
11957
12260
|
context.addIssue({
|
|
11958
|
-
code:
|
|
12261
|
+
code: z8.ZodIssueCode.custom,
|
|
11959
12262
|
path: ["quality", "recommendedNativeSize"],
|
|
11960
12263
|
message: "min cannot exceed max"
|
|
11961
12264
|
});
|
|
11962
12265
|
}
|
|
11963
12266
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
11964
12267
|
context.addIssue({
|
|
11965
|
-
code:
|
|
12268
|
+
code: z8.ZodIssueCode.custom,
|
|
11966
12269
|
path: ["quality", "paletteColors"],
|
|
11967
12270
|
message: "min cannot exceed max"
|
|
11968
12271
|
});
|
|
@@ -11971,7 +12274,7 @@ var RecipeSchema = z7.object({
|
|
|
11971
12274
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
11972
12275
|
)) {
|
|
11973
12276
|
context.addIssue({
|
|
11974
|
-
code:
|
|
12277
|
+
code: z8.ZodIssueCode.custom,
|
|
11975
12278
|
path: ["workflow", "path"],
|
|
11976
12279
|
message: "must name a file whose role is workflow"
|
|
11977
12280
|
});
|
|
@@ -11979,7 +12282,7 @@ var RecipeSchema = z7.object({
|
|
|
11979
12282
|
if (recipe.provider === "comfyui") {
|
|
11980
12283
|
if (!recipe.workflow) {
|
|
11981
12284
|
context.addIssue({
|
|
11982
|
-
code:
|
|
12285
|
+
code: z8.ZodIssueCode.custom,
|
|
11983
12286
|
path: ["workflow"],
|
|
11984
12287
|
message: "ComfyUI recipes require workflow metadata"
|
|
11985
12288
|
});
|
|
@@ -11988,7 +12291,7 @@ var RecipeSchema = z7.object({
|
|
|
11988
12291
|
const options = recipe.style.providerOptions.comfyui;
|
|
11989
12292
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
11990
12293
|
context.addIssue({
|
|
11991
|
-
code:
|
|
12294
|
+
code: z8.ZodIssueCode.custom,
|
|
11992
12295
|
path: ["style", "providerOptions", "comfyui"],
|
|
11993
12296
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
11994
12297
|
});
|
|
@@ -12002,7 +12305,7 @@ var RecipeSchema = z7.object({
|
|
|
12002
12305
|
};
|
|
12003
12306
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
12004
12307
|
context.addIssue({
|
|
12005
|
-
code:
|
|
12308
|
+
code: z8.ZodIssueCode.custom,
|
|
12006
12309
|
path: ["style", "providerOptions", "comfyui"],
|
|
12007
12310
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
12008
12311
|
});
|
|
@@ -12034,7 +12337,7 @@ function message4(error) {
|
|
|
12034
12337
|
return error instanceof Error ? error.message : String(error);
|
|
12035
12338
|
}
|
|
12036
12339
|
async function readRecipeFile(recipePath, bundled) {
|
|
12037
|
-
const absolute =
|
|
12340
|
+
const absolute = path26.resolve(recipePath);
|
|
12038
12341
|
let raw;
|
|
12039
12342
|
try {
|
|
12040
12343
|
raw = JSON.parse(await readFile20(absolute, "utf8"));
|
|
@@ -12048,13 +12351,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12048
12351
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12049
12352
|
);
|
|
12050
12353
|
}
|
|
12051
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
12354
|
+
return { recipe: parsed.data, path: absolute, dir: path26.dirname(absolute), bundled };
|
|
12052
12355
|
}
|
|
12053
12356
|
async function walkRecipeFiles(root) {
|
|
12054
|
-
if (!
|
|
12357
|
+
if (!existsSync20(root)) return [];
|
|
12055
12358
|
const out = [];
|
|
12056
12359
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
12057
|
-
const resolved =
|
|
12360
|
+
const resolved = path26.join(root, entry.name);
|
|
12058
12361
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12059
12362
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12060
12363
|
}
|
|
@@ -12065,7 +12368,7 @@ function bundledRecipeRoot() {
|
|
|
12065
12368
|
}
|
|
12066
12369
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12067
12370
|
const recipes = await Promise.all(
|
|
12068
|
-
(await walkRecipeFiles(
|
|
12371
|
+
(await walkRecipeFiles(path26.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12069
12372
|
);
|
|
12070
12373
|
return recipes.sort(
|
|
12071
12374
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12088,9 +12391,9 @@ function parseSelector(selector) {
|
|
|
12088
12391
|
return { id: match[1], version: match[2] };
|
|
12089
12392
|
}
|
|
12090
12393
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12091
|
-
const local =
|
|
12092
|
-
if (
|
|
12093
|
-
const recipePath =
|
|
12394
|
+
const local = path26.resolve(target);
|
|
12395
|
+
if (existsSync20(local)) {
|
|
12396
|
+
const recipePath = path26.basename(local) === RECIPE_FILE ? local : path26.join(local, RECIPE_FILE);
|
|
12094
12397
|
return readRecipeFile(recipePath, false);
|
|
12095
12398
|
}
|
|
12096
12399
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12104,8 +12407,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12104
12407
|
return matches[0];
|
|
12105
12408
|
}
|
|
12106
12409
|
async function verifyFile(root, file) {
|
|
12107
|
-
const absolute =
|
|
12108
|
-
if (!
|
|
12410
|
+
const absolute = path26.resolve(root, ...file.path.split("/"));
|
|
12411
|
+
if (!existsSync20(absolute)) {
|
|
12109
12412
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12110
12413
|
}
|
|
12111
12414
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12120,7 +12423,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12120
12423
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12121
12424
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
12122
12425
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12123
|
-
const modelRoot = options.modelRoot ?
|
|
12426
|
+
const modelRoot = options.modelRoot ? path26.resolve(options.modelRoot) : null;
|
|
12124
12427
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12125
12428
|
...await verifyFile(modelRoot, model),
|
|
12126
12429
|
source: model.source,
|
|
@@ -12154,7 +12457,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12154
12457
|
};
|
|
12155
12458
|
}
|
|
12156
12459
|
function renderStyle(style, destination, cwd) {
|
|
12157
|
-
const portableDir =
|
|
12460
|
+
const portableDir = path26.relative(cwd, destination).split(path26.sep).join("/") || ".";
|
|
12158
12461
|
const replace = (value) => {
|
|
12159
12462
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12160
12463
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12166,24 +12469,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
12166
12469
|
return replace(style);
|
|
12167
12470
|
}
|
|
12168
12471
|
async function installRecipe(target, options = {}) {
|
|
12169
|
-
const cwd =
|
|
12472
|
+
const cwd = path26.resolve(options.cwd ?? process.cwd());
|
|
12170
12473
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12171
12474
|
const verification = await verifyRecipe(loaded.path);
|
|
12172
12475
|
if (!verification.ok) {
|
|
12173
12476
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12174
12477
|
}
|
|
12175
|
-
const destination = options.out ?
|
|
12478
|
+
const destination = options.out ? path26.resolve(cwd, options.out) : path26.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12176
12479
|
const sources = [
|
|
12177
12480
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12178
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
12481
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path26.join(loaded.dir, ...file.path.split("/")) }))
|
|
12179
12482
|
];
|
|
12180
12483
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12181
|
-
path:
|
|
12484
|
+
path: path26.join(destination, ...file.path.split("/")),
|
|
12182
12485
|
data: await readFile20(file.source)
|
|
12183
12486
|
})));
|
|
12184
12487
|
if (!options.force) {
|
|
12185
12488
|
for (const file of files) {
|
|
12186
|
-
if (!
|
|
12489
|
+
if (!existsSync20(file.path)) continue;
|
|
12187
12490
|
const current = await readFile20(file.path);
|
|
12188
12491
|
if (!current.equals(file.data)) {
|
|
12189
12492
|
throw new Error(
|
|
@@ -12207,15 +12510,15 @@ async function installRecipe(target, options = {}) {
|
|
|
12207
12510
|
}
|
|
12208
12511
|
|
|
12209
12512
|
// src/pipeline/quality-regression.ts
|
|
12210
|
-
import
|
|
12211
|
-
import { existsSync as
|
|
12513
|
+
import path27 from "path";
|
|
12514
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12212
12515
|
import { readFile as readFile21 } from "fs/promises";
|
|
12213
|
-
import { z as
|
|
12516
|
+
import { z as z9 } from "zod";
|
|
12214
12517
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
12215
12518
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
12216
12519
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
12217
|
-
var QualityPathSchema =
|
|
12218
|
-
(value) => !
|
|
12520
|
+
var QualityPathSchema = z9.string().min(1).refine(
|
|
12521
|
+
(value) => !path27.posix.isAbsolute(value) && !value.includes("\\"),
|
|
12219
12522
|
"expected a portable relative path"
|
|
12220
12523
|
);
|
|
12221
12524
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -12228,53 +12531,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
12228
12531
|
maxEdgeContrastDrop: 0.03,
|
|
12229
12532
|
maxIsolatedPixelIncrease: 5e-3
|
|
12230
12533
|
};
|
|
12231
|
-
var QualityToleranceSchema =
|
|
12232
|
-
requireExactHash:
|
|
12233
|
-
maxColorCountIncrease:
|
|
12234
|
-
maxNewColors:
|
|
12235
|
-
maxTransparencyDelta:
|
|
12236
|
-
maxPartialAlphaIncrease:
|
|
12237
|
-
maxEdgeDensityDelta:
|
|
12238
|
-
maxEdgeContrastDrop:
|
|
12239
|
-
maxIsolatedPixelIncrease:
|
|
12534
|
+
var QualityToleranceSchema = z9.object({
|
|
12535
|
+
requireExactHash: z9.boolean(),
|
|
12536
|
+
maxColorCountIncrease: z9.number().int().min(0),
|
|
12537
|
+
maxNewColors: z9.number().int().min(0),
|
|
12538
|
+
maxTransparencyDelta: z9.number().min(0).max(1),
|
|
12539
|
+
maxPartialAlphaIncrease: z9.number().min(0).max(1),
|
|
12540
|
+
maxEdgeDensityDelta: z9.number().min(0).max(1),
|
|
12541
|
+
maxEdgeContrastDrop: z9.number().min(0).max(1),
|
|
12542
|
+
maxIsolatedPixelIncrease: z9.number().min(0).max(1)
|
|
12240
12543
|
}).strict();
|
|
12241
|
-
var ImageQualityMetricsSchema =
|
|
12242
|
-
sha256:
|
|
12243
|
-
width:
|
|
12244
|
-
height:
|
|
12245
|
-
colorCount:
|
|
12246
|
-
colors:
|
|
12247
|
-
transparency:
|
|
12248
|
-
partialAlpha:
|
|
12249
|
-
edgeDensity:
|
|
12250
|
-
meanEdgeContrast:
|
|
12251
|
-
isolatedPixelRatio:
|
|
12544
|
+
var ImageQualityMetricsSchema = z9.object({
|
|
12545
|
+
sha256: z9.string().regex(SHA256_RE2),
|
|
12546
|
+
width: z9.number().int().min(1),
|
|
12547
|
+
height: z9.number().int().min(1),
|
|
12548
|
+
colorCount: z9.number().int().min(0),
|
|
12549
|
+
colors: z9.array(z9.string().regex(HEX_RE)),
|
|
12550
|
+
transparency: z9.number().min(0).max(1),
|
|
12551
|
+
partialAlpha: z9.number().min(0).max(1),
|
|
12552
|
+
edgeDensity: z9.number().min(0).max(1),
|
|
12553
|
+
meanEdgeContrast: z9.number().min(0).max(1),
|
|
12554
|
+
isolatedPixelRatio: z9.number().min(0).max(1)
|
|
12252
12555
|
}).strict();
|
|
12253
|
-
var QualityBaselineCaseSchema =
|
|
12254
|
-
id:
|
|
12556
|
+
var QualityBaselineCaseSchema = z9.object({
|
|
12557
|
+
id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12255
12558
|
file: QualityPathSchema,
|
|
12256
|
-
record:
|
|
12559
|
+
record: z9.object({
|
|
12257
12560
|
path: QualityPathSchema,
|
|
12258
|
-
sha256:
|
|
12561
|
+
sha256: z9.string().regex(SHA256_RE2)
|
|
12259
12562
|
}).strict().optional(),
|
|
12260
12563
|
expected: ImageQualityMetricsSchema,
|
|
12261
12564
|
tolerances: QualityToleranceSchema
|
|
12262
12565
|
}).strict();
|
|
12263
|
-
var QualityBaselineSchema =
|
|
12264
|
-
$schema:
|
|
12265
|
-
format:
|
|
12266
|
-
schemaVersion:
|
|
12267
|
-
cases:
|
|
12566
|
+
var QualityBaselineSchema = z9.object({
|
|
12567
|
+
$schema: z9.string().url().optional(),
|
|
12568
|
+
format: z9.literal("pixelkiln-quality-baseline"),
|
|
12569
|
+
schemaVersion: z9.literal(1),
|
|
12570
|
+
cases: z9.array(QualityBaselineCaseSchema).min(1)
|
|
12268
12571
|
}).strict().superRefine((baseline, context) => {
|
|
12269
12572
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
12270
12573
|
if (new Set(ids).size !== ids.length) {
|
|
12271
|
-
context.addIssue({ code:
|
|
12574
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
12272
12575
|
}
|
|
12273
12576
|
});
|
|
12274
|
-
var QualityInputSchema =
|
|
12275
|
-
id:
|
|
12276
|
-
path:
|
|
12277
|
-
record:
|
|
12577
|
+
var QualityInputSchema = z9.object({
|
|
12578
|
+
id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12579
|
+
path: z9.string().min(1),
|
|
12580
|
+
record: z9.string().min(1).optional(),
|
|
12278
12581
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
12279
12582
|
}).strict();
|
|
12280
12583
|
function message5(error) {
|
|
@@ -12362,7 +12665,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
12362
12665
|
};
|
|
12363
12666
|
}
|
|
12364
12667
|
async function measureImageQuality(file) {
|
|
12365
|
-
const absolute =
|
|
12668
|
+
const absolute = path27.resolve(file);
|
|
12366
12669
|
let bytes;
|
|
12367
12670
|
try {
|
|
12368
12671
|
bytes = await readFile21(absolute);
|
|
@@ -12376,7 +12679,7 @@ async function measureImageQuality(file) {
|
|
|
12376
12679
|
}
|
|
12377
12680
|
}
|
|
12378
12681
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
12379
|
-
const parsed =
|
|
12682
|
+
const parsed = z9.array(QualityInputSchema).min(1).safeParse(raw);
|
|
12380
12683
|
if (!parsed.success) {
|
|
12381
12684
|
throw new Error(
|
|
12382
12685
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -12384,11 +12687,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12384
12687
|
}
|
|
12385
12688
|
const ids = parsed.data.map((entry) => entry.id);
|
|
12386
12689
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
12387
|
-
const root =
|
|
12690
|
+
const root = path27.dirname(path27.resolve(inputsFilePath));
|
|
12388
12691
|
return parsed.data.map((entry) => ({
|
|
12389
12692
|
id: entry.id,
|
|
12390
|
-
file:
|
|
12391
|
-
...entry.record ? { record:
|
|
12693
|
+
file: path27.resolve(root, entry.path),
|
|
12694
|
+
...entry.record ? { record: path27.resolve(root, entry.record) } : {},
|
|
12392
12695
|
tolerances: QualityToleranceSchema.parse({
|
|
12393
12696
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
12394
12697
|
...entry.tolerances
|
|
@@ -12396,7 +12699,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12396
12699
|
}));
|
|
12397
12700
|
}
|
|
12398
12701
|
function portableRelative2(from, to) {
|
|
12399
|
-
return
|
|
12702
|
+
return path27.relative(from, path27.resolve(to)).split(path27.sep).join("/") || ".";
|
|
12400
12703
|
}
|
|
12401
12704
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
12402
12705
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -12411,20 +12714,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
12411
12714
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
12412
12715
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
12413
12716
|
}
|
|
12414
|
-
const outputs = record.outputs.map((output) =>
|
|
12415
|
-
if (!outputs.includes(
|
|
12717
|
+
const outputs = record.outputs.map((output) => path27.resolve(path27.dirname(recordPath), output.path));
|
|
12718
|
+
if (!outputs.includes(path27.resolve(imagePath))) {
|
|
12416
12719
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
12417
12720
|
}
|
|
12418
12721
|
}
|
|
12419
12722
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
12420
12723
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
12421
|
-
const absolute =
|
|
12724
|
+
const absolute = path27.resolve(baselinePath);
|
|
12422
12725
|
for (const input of inputs) {
|
|
12423
|
-
if (
|
|
12726
|
+
if (path27.resolve(input.file) === absolute || input.record && path27.resolve(input.record) === absolute) {
|
|
12424
12727
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
12425
12728
|
}
|
|
12426
12729
|
}
|
|
12427
|
-
const root =
|
|
12730
|
+
const root = path27.dirname(absolute);
|
|
12428
12731
|
const cases = [];
|
|
12429
12732
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
12430
12733
|
const expected = await measureImageQuality(input.file);
|
|
@@ -12451,7 +12754,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12451
12754
|
cases
|
|
12452
12755
|
});
|
|
12453
12756
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
12454
|
-
if (
|
|
12757
|
+
if (existsSync21(absolute) && !options.force) {
|
|
12455
12758
|
const current = await readFile21(absolute);
|
|
12456
12759
|
if (!current.equals(data)) {
|
|
12457
12760
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
@@ -12461,7 +12764,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12461
12764
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
12462
12765
|
}
|
|
12463
12766
|
async function readQualityBaseline(baselinePath) {
|
|
12464
|
-
const absolute =
|
|
12767
|
+
const absolute = path27.resolve(baselinePath);
|
|
12465
12768
|
let raw;
|
|
12466
12769
|
try {
|
|
12467
12770
|
raw = JSON.parse(await readFile21(absolute, "utf8"));
|
|
@@ -12534,12 +12837,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
12534
12837
|
return { violations, warnings };
|
|
12535
12838
|
}
|
|
12536
12839
|
async function checkQualityBaseline(baselinePath) {
|
|
12537
|
-
const absolute =
|
|
12840
|
+
const absolute = path27.resolve(baselinePath);
|
|
12538
12841
|
const baseline = await readQualityBaseline(absolute);
|
|
12539
|
-
const root =
|
|
12842
|
+
const root = path27.dirname(absolute);
|
|
12540
12843
|
const cases = [];
|
|
12541
12844
|
for (const entry of baseline.cases) {
|
|
12542
|
-
const file =
|
|
12845
|
+
const file = path27.resolve(root, entry.file);
|
|
12543
12846
|
let actual = null;
|
|
12544
12847
|
const violations = [];
|
|
12545
12848
|
const warnings = [];
|
|
@@ -12552,7 +12855,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12552
12855
|
violations.push(message5(error));
|
|
12553
12856
|
}
|
|
12554
12857
|
if (entry.record) {
|
|
12555
|
-
const recordPath =
|
|
12858
|
+
const recordPath = path27.resolve(root, entry.record.path);
|
|
12556
12859
|
try {
|
|
12557
12860
|
const recordHash = await sha256File(recordPath);
|
|
12558
12861
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -12638,13 +12941,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12638
12941
|
};
|
|
12639
12942
|
const loadProject = async (project) => {
|
|
12640
12943
|
const ctx = await access2.loadProject(project);
|
|
12641
|
-
const dir =
|
|
12944
|
+
const dir = path28.dirname(ctx.loaded.path);
|
|
12642
12945
|
const declared = readEnvFiles(dir);
|
|
12643
12946
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
12644
12947
|
for (const [name, value] of Object.entries(declared)) {
|
|
12645
12948
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
12646
12949
|
throw new Error(
|
|
12647
|
-
`${
|
|
12950
|
+
`${path28.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
12648
12951
|
);
|
|
12649
12952
|
}
|
|
12650
12953
|
}
|
|
@@ -12670,7 +12973,7 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12670
12973
|
args.edit,
|
|
12671
12974
|
budget ? describeBudget(budget) : null
|
|
12672
12975
|
),
|
|
12673
|
-
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, reload, onProgress: log }) } : {},
|
|
12976
|
+
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
12674
12977
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
|
|
12675
12978
|
});
|
|
12676
12979
|
await new Promise((resolve) => {
|
|
@@ -12686,11 +12989,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12686
12989
|
log("\n gallery closed");
|
|
12687
12990
|
}
|
|
12688
12991
|
async function provenanceFile(id, file) {
|
|
12689
|
-
const absolute =
|
|
12992
|
+
const absolute = path28.resolve(file);
|
|
12690
12993
|
return {
|
|
12691
12994
|
id,
|
|
12692
12995
|
path: absolute,
|
|
12693
|
-
sha256:
|
|
12996
|
+
sha256: existsSync22(absolute) ? await sha256File(absolute) : null,
|
|
12694
12997
|
included: true
|
|
12695
12998
|
};
|
|
12696
12999
|
}
|
|
@@ -12765,6 +13068,7 @@ var COMMANDS = [
|
|
|
12765
13068
|
"balance",
|
|
12766
13069
|
"status",
|
|
12767
13070
|
"gallery",
|
|
13071
|
+
"edit",
|
|
12768
13072
|
"quality",
|
|
12769
13073
|
"refine",
|
|
12770
13074
|
"recipe",
|
|
@@ -12779,6 +13083,7 @@ var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
|
12779
13083
|
var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
12780
13084
|
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
12781
13085
|
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
13086
|
+
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
12782
13087
|
function parseArgs(argv) {
|
|
12783
13088
|
const [command = "help"] = argv;
|
|
12784
13089
|
if (!COMMANDS.includes(command)) {
|
|
@@ -12836,6 +13141,12 @@ function parseArgs(argv) {
|
|
|
12836
13141
|
}
|
|
12837
13142
|
rest = rest.slice(1);
|
|
12838
13143
|
}
|
|
13144
|
+
} else if (command === "edit") {
|
|
13145
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "start" : rest[0];
|
|
13146
|
+
if (!EDIT_SUBCOMMANDS.includes(subcommand)) {
|
|
13147
|
+
throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
|
|
13148
|
+
}
|
|
13149
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
12839
13150
|
} else if (command === "refine") {
|
|
12840
13151
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
12841
13152
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -12949,7 +13260,7 @@ function parseArgs(argv) {
|
|
|
12949
13260
|
return {
|
|
12950
13261
|
command,
|
|
12951
13262
|
manifest,
|
|
12952
|
-
lock: get("--lock") ??
|
|
13263
|
+
lock: get("--lock") ?? path28.join(path28.dirname(path28.resolve(manifest)), "pixelkiln.lock.json"),
|
|
12953
13264
|
explicitLock: get("--lock"),
|
|
12954
13265
|
styles: list("--style"),
|
|
12955
13266
|
assets: list("--only"),
|
|
@@ -13030,6 +13341,9 @@ Commands
|
|
|
13030
13341
|
tag Push manifest tags to the objects upstream (free).
|
|
13031
13342
|
balance Show the provider's remaining balance.
|
|
13032
13343
|
status Summarise the lockfile.
|
|
13344
|
+
edit Hand-edit an asset in your own editor: copies the generated PNG to
|
|
13345
|
+
<outDir>/edits/, declares it as the asset's source, and opens it.
|
|
13346
|
+
edit detach places the generated art again (the file is kept).
|
|
13033
13347
|
gallery Open a local read-only gallery of every generation and its
|
|
13034
13348
|
provenance: prompt, provider, cost, outputs, lineage, quality.
|
|
13035
13349
|
--workspace <catalog> shows every registered project at once;
|
|
@@ -13072,7 +13386,7 @@ Options
|
|
|
13072
13386
|
and the gallery snapshot (no server)
|
|
13073
13387
|
--check plan/audit/cache: exit nonzero when the selected state is unsafe
|
|
13074
13388
|
--yes, -y Skip the confirmation prompt
|
|
13075
|
-
--no-open Do not auto-open the browser
|
|
13389
|
+
--no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
|
|
13076
13390
|
--edit gallery: allow manifest edits from the page (never spends)
|
|
13077
13391
|
--tag Also push tags upstream after fetch
|
|
13078
13392
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
@@ -13089,6 +13403,8 @@ Examples
|
|
|
13089
13403
|
pixelkiln gallery --style heybud-premium
|
|
13090
13404
|
pixelkiln gallery --workspace pixelkiln.workspace.json
|
|
13091
13405
|
pixelkiln gallery --edit --budget 80
|
|
13406
|
+
pixelkiln edit --style base --only anvil
|
|
13407
|
+
PIXELKILN_EDITOR="open -a Aseprite" pixelkiln edit --only anvil --style base
|
|
13092
13408
|
pixelkiln adopt --tag
|
|
13093
13409
|
pixelkiln pack --style heybud-premium
|
|
13094
13410
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
@@ -13165,6 +13481,15 @@ function printResumeActions(specs, lock) {
|
|
|
13165
13481
|
}
|
|
13166
13482
|
return actions.length;
|
|
13167
13483
|
}
|
|
13484
|
+
function manifestSources(manifest, styleId) {
|
|
13485
|
+
const sources = {};
|
|
13486
|
+
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
13487
|
+
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
13488
|
+
const source = asset.sourceByStyle[styleId] ?? asset.source;
|
|
13489
|
+
if (source) sources[assetId] = source;
|
|
13490
|
+
}
|
|
13491
|
+
return sources;
|
|
13492
|
+
}
|
|
13168
13493
|
function manifestProviderIds(manifest) {
|
|
13169
13494
|
return [...new Set(
|
|
13170
13495
|
Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
|
|
@@ -13238,10 +13563,10 @@ async function confirm(question, auto) {
|
|
|
13238
13563
|
return answer === "y" || answer === "yes";
|
|
13239
13564
|
}
|
|
13240
13565
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13241
|
-
if (!
|
|
13566
|
+
if (!existsSync22(workspacePath)) {
|
|
13242
13567
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13243
13568
|
}
|
|
13244
|
-
const dir =
|
|
13569
|
+
const dir = path28.dirname(path28.resolve(workspacePath));
|
|
13245
13570
|
const ws = await loadWorkspace(workspacePath);
|
|
13246
13571
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13247
13572
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13273,15 +13598,15 @@ async function main() {
|
|
|
13273
13598
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
13274
13599
|
let raw;
|
|
13275
13600
|
try {
|
|
13276
|
-
raw = JSON.parse(await readFile22(
|
|
13601
|
+
raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
|
|
13277
13602
|
} catch (error) {
|
|
13278
13603
|
throw new Error(
|
|
13279
|
-
`Could not read quality inputs ${
|
|
13604
|
+
`Could not read quality inputs ${path28.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13280
13605
|
{ cause: error }
|
|
13281
13606
|
);
|
|
13282
13607
|
}
|
|
13283
13608
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
13284
|
-
if (
|
|
13609
|
+
if (path28.resolve(args.inputs) === path28.resolve(args.out)) {
|
|
13285
13610
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
13286
13611
|
}
|
|
13287
13612
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -13294,7 +13619,7 @@ async function main() {
|
|
|
13294
13619
|
}, null, 2));
|
|
13295
13620
|
} else {
|
|
13296
13621
|
log(
|
|
13297
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
13622
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path28.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
13298
13623
|
);
|
|
13299
13624
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
13300
13625
|
}
|
|
@@ -13389,14 +13714,14 @@ async function main() {
|
|
|
13389
13714
|
}, null, 2));
|
|
13390
13715
|
} else {
|
|
13391
13716
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
13392
|
-
log(` ${
|
|
13717
|
+
log(` ${path28.relative(process.cwd(), result.destination) || "."}`);
|
|
13393
13718
|
log(`
|
|
13394
13719
|
Add this entry under your manifest's styles object:`);
|
|
13395
13720
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
13396
13721
|
if (result.recipe.models.length) {
|
|
13397
13722
|
log(`
|
|
13398
13723
|
Models are not downloaded automatically. Verify them with:`);
|
|
13399
|
-
log(` pixelkiln recipe verify ${
|
|
13724
|
+
log(` pixelkiln recipe verify ${path28.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
13400
13725
|
}
|
|
13401
13726
|
}
|
|
13402
13727
|
return;
|
|
@@ -13430,12 +13755,12 @@ async function main() {
|
|
|
13430
13755
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
13431
13756
|
);
|
|
13432
13757
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
13433
|
-
log(` wrote ${
|
|
13434
|
-
log(` quality record: ${
|
|
13758
|
+
log(` wrote ${path28.relative(process.cwd(), result2.output)}`);
|
|
13759
|
+
log(` quality record: ${path28.relative(process.cwd(), result2.record)}`);
|
|
13435
13760
|
log(`
|
|
13436
13761
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
13437
13762
|
log(
|
|
13438
|
-
` pixelkiln refine approve --from ${
|
|
13763
|
+
` pixelkiln refine approve --from ${path28.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
13439
13764
|
);
|
|
13440
13765
|
}
|
|
13441
13766
|
return;
|
|
@@ -13463,7 +13788,7 @@ async function main() {
|
|
|
13463
13788
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
13464
13789
|
else {
|
|
13465
13790
|
log(
|
|
13466
|
-
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${
|
|
13791
|
+
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path28.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path28.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
|
|
13467
13792
|
);
|
|
13468
13793
|
}
|
|
13469
13794
|
return;
|
|
@@ -13472,14 +13797,14 @@ async function main() {
|
|
|
13472
13797
|
if (args.json) {
|
|
13473
13798
|
log(JSON.stringify(result, null, 2));
|
|
13474
13799
|
} else {
|
|
13475
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
13800
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path28.relative(process.cwd(), result.output || result.record)}`);
|
|
13476
13801
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
13477
13802
|
}
|
|
13478
13803
|
if (!result.safe) process.exitCode = 1;
|
|
13479
13804
|
return;
|
|
13480
13805
|
}
|
|
13481
13806
|
if (args.command === "balance") {
|
|
13482
|
-
loadEnvFiles(
|
|
13807
|
+
loadEnvFiles(path28.dirname(path28.resolve(args.manifest)));
|
|
13483
13808
|
loadEnvFiles(process.cwd());
|
|
13484
13809
|
const loaded2 = await loadManifest(args.manifest);
|
|
13485
13810
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -13492,31 +13817,31 @@ async function main() {
|
|
|
13492
13817
|
}
|
|
13493
13818
|
if (args.command === "init") {
|
|
13494
13819
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13495
|
-
const root =
|
|
13496
|
-
if (!
|
|
13820
|
+
const root = path28.resolve(args.from);
|
|
13821
|
+
if (!existsSync22(root)) throw new Error(`No directory at ${root}`);
|
|
13497
13822
|
const generator = args.generator ?? "map";
|
|
13498
13823
|
if (generator !== "1dir" && generator !== "map") {
|
|
13499
13824
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13500
13825
|
}
|
|
13501
|
-
const target =
|
|
13826
|
+
const target = path28.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13502
13827
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13503
13828
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13504
13829
|
const manifest = buildManifest(
|
|
13505
|
-
args.name ??
|
|
13830
|
+
args.name ?? path28.basename(path28.dirname(target)),
|
|
13506
13831
|
args.styles[0] ?? "base",
|
|
13507
13832
|
generator,
|
|
13508
|
-
|
|
13833
|
+
path28.relative(path28.dirname(target), root) || ".",
|
|
13509
13834
|
assets
|
|
13510
13835
|
);
|
|
13511
13836
|
await writeManifestFile(target, manifest);
|
|
13512
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
13837
|
+
log(` scanned ${assets.length} PNG(s) under ${path28.relative(process.cwd(), root)}`);
|
|
13513
13838
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13514
|
-
log(` wrote ${
|
|
13839
|
+
log(` wrote ${path28.relative(process.cwd(), target)}`);
|
|
13515
13840
|
log(`
|
|
13516
13841
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13517
13842
|
log(` PixelLab account instead of inventing them:`);
|
|
13518
13843
|
log(`
|
|
13519
|
-
pixelkiln adopt --manifest ${
|
|
13844
|
+
pixelkiln adopt --manifest ${path28.relative(process.cwd(), target)} --write-prompts
|
|
13520
13845
|
`);
|
|
13521
13846
|
return;
|
|
13522
13847
|
}
|
|
@@ -13564,10 +13889,10 @@ async function main() {
|
|
|
13564
13889
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
13565
13890
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
13566
13891
|
}
|
|
13567
|
-
const raw = JSON.parse(await readFile22(
|
|
13892
|
+
const raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
|
|
13568
13893
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13569
13894
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13570
|
-
const base =
|
|
13895
|
+
const base = path28.resolve(args.out.replace(/\.png$/, ""));
|
|
13571
13896
|
const outputs = [
|
|
13572
13897
|
{ path: `${base}.png`, data: png },
|
|
13573
13898
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -13580,17 +13905,17 @@ async function main() {
|
|
|
13580
13905
|
log(
|
|
13581
13906
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
13582
13907
|
);
|
|
13583
|
-
log(` ${
|
|
13908
|
+
log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
13584
13909
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13585
13910
|
return;
|
|
13586
13911
|
}
|
|
13587
13912
|
if (args.command === "workspace") {
|
|
13588
|
-
const workspacePath =
|
|
13589
|
-
const dir =
|
|
13913
|
+
const workspacePath = path28.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
13914
|
+
const dir = path28.dirname(workspacePath);
|
|
13590
13915
|
if (args.subcommand === "add") {
|
|
13591
|
-
const manifestPath =
|
|
13916
|
+
const manifestPath = path28.resolve(args.target);
|
|
13592
13917
|
const loadedTarget = await loadManifest(manifestPath);
|
|
13593
|
-
const lockPath = args.explicitLock ?
|
|
13918
|
+
const lockPath = args.explicitLock ? path28.resolve(args.explicitLock) : path28.join(path28.dirname(manifestPath), "pixelkiln.lock.json");
|
|
13594
13919
|
const ws = await loadWorkspace(workspacePath);
|
|
13595
13920
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
13596
13921
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -13610,10 +13935,10 @@ async function main() {
|
|
|
13610
13935
|
...args.account ? { account: args.account } : {}
|
|
13611
13936
|
};
|
|
13612
13937
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
13613
|
-
log(` registered "${id}" in ${
|
|
13938
|
+
log(` registered "${id}" in ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13614
13939
|
log(` manifest: ${project.manifest}`);
|
|
13615
13940
|
log(` lock: ${project.lock}`);
|
|
13616
|
-
if (!
|
|
13941
|
+
if (!existsSync22(lockPath)) {
|
|
13617
13942
|
log(
|
|
13618
13943
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
13619
13944
|
);
|
|
@@ -13622,7 +13947,7 @@ async function main() {
|
|
|
13622
13947
|
}
|
|
13623
13948
|
if (args.subcommand === "remove") {
|
|
13624
13949
|
const ws = await loadWorkspace(workspacePath);
|
|
13625
|
-
const resolvedTarget =
|
|
13950
|
+
const resolvedTarget = path28.resolve(args.target);
|
|
13626
13951
|
const match = ws.projects.find(
|
|
13627
13952
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
13628
13953
|
);
|
|
@@ -13633,11 +13958,11 @@ async function main() {
|
|
|
13633
13958
|
version: 1,
|
|
13634
13959
|
projects: ws.projects.filter((p) => p !== match)
|
|
13635
13960
|
});
|
|
13636
|
-
log(` removed "${match.id}" from ${
|
|
13961
|
+
log(` removed "${match.id}" from ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13637
13962
|
return;
|
|
13638
13963
|
}
|
|
13639
13964
|
if (args.subcommand === "list") {
|
|
13640
|
-
if (!
|
|
13965
|
+
if (!existsSync22(workspacePath)) {
|
|
13641
13966
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13642
13967
|
}
|
|
13643
13968
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -13645,9 +13970,9 @@ async function main() {
|
|
|
13645
13970
|
if (args.json) {
|
|
13646
13971
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
13647
13972
|
} else if (!ws.projects.length) {
|
|
13648
|
-
log(` no projects registered in ${
|
|
13973
|
+
log(` no projects registered in ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13649
13974
|
} else {
|
|
13650
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
13975
|
+
log(` ${ws.projects.length} project(s) in ${path28.relative(process.cwd(), workspacePath)}:`);
|
|
13651
13976
|
for (const p of ws.projects) {
|
|
13652
13977
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
13653
13978
|
}
|
|
@@ -13657,7 +13982,7 @@ async function main() {
|
|
|
13657
13982
|
return;
|
|
13658
13983
|
}
|
|
13659
13984
|
if (args.subcommand === "status") {
|
|
13660
|
-
if (!
|
|
13985
|
+
if (!existsSync22(workspacePath)) {
|
|
13661
13986
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13662
13987
|
}
|
|
13663
13988
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -13665,7 +13990,7 @@ async function main() {
|
|
|
13665
13990
|
if (args.json) {
|
|
13666
13991
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
13667
13992
|
} else {
|
|
13668
|
-
log(` workspace: ${
|
|
13993
|
+
log(` workspace: ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13669
13994
|
for (const p of report.projects) {
|
|
13670
13995
|
if (p.error) {
|
|
13671
13996
|
log(`
|
|
@@ -13712,8 +14037,8 @@ async function main() {
|
|
|
13712
14037
|
}
|
|
13713
14038
|
}
|
|
13714
14039
|
if (args.command === "gallery" && args.workspace) {
|
|
13715
|
-
const workspacePath =
|
|
13716
|
-
if (!
|
|
14040
|
+
const workspacePath = path28.resolve(args.workspace);
|
|
14041
|
+
if (!existsSync22(workspacePath)) {
|
|
13717
14042
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13718
14043
|
}
|
|
13719
14044
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -13733,7 +14058,7 @@ async function main() {
|
|
|
13733
14058
|
const registered = async (projectId) => {
|
|
13734
14059
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
13735
14060
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
13736
|
-
return resolveProject(
|
|
14061
|
+
return resolveProject(path28.dirname(workspacePath), project);
|
|
13737
14062
|
};
|
|
13738
14063
|
await runGallery(initial, build, args, {
|
|
13739
14064
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -13748,14 +14073,14 @@ async function main() {
|
|
|
13748
14073
|
});
|
|
13749
14074
|
return;
|
|
13750
14075
|
}
|
|
13751
|
-
if (!
|
|
14076
|
+
if (!existsSync22(path28.resolve(args.manifest))) {
|
|
13752
14077
|
throw new Error(
|
|
13753
|
-
`No manifest at ${
|
|
14078
|
+
`No manifest at ${path28.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
13754
14079
|
);
|
|
13755
14080
|
}
|
|
13756
|
-
const manifestDir =
|
|
14081
|
+
const manifestDir = path28.dirname(path28.resolve(args.manifest));
|
|
13757
14082
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
13758
|
-
if (
|
|
14083
|
+
if (path28.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
13759
14084
|
const loaded = await loadManifest(args.manifest);
|
|
13760
14085
|
let specs = await resolveSpecs(loaded, {
|
|
13761
14086
|
styles: args.styles,
|
|
@@ -13828,7 +14153,7 @@ async function main() {
|
|
|
13828
14153
|
});
|
|
13829
14154
|
};
|
|
13830
14155
|
await runGallery(await build(), reload, args, {
|
|
13831
|
-
manifestFor: () =>
|
|
14156
|
+
manifestFor: () => path28.resolve(args.manifest),
|
|
13832
14157
|
loadProject: async () => {
|
|
13833
14158
|
const freshLoaded = await loadManifest(args.manifest);
|
|
13834
14159
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -13839,6 +14164,29 @@ async function main() {
|
|
|
13839
14164
|
});
|
|
13840
14165
|
return;
|
|
13841
14166
|
}
|
|
14167
|
+
if (args.command === "edit") {
|
|
14168
|
+
if (!args.assets.length) throw new Error("edit needs --only <asset id> (and --style when the asset is in several styles).");
|
|
14169
|
+
if (specs.length !== 1) {
|
|
14170
|
+
throw new Error(
|
|
14171
|
+
specs.length ? `edit works on one asset in one style; --only ${args.assets.join(",")} resolves to ${specs.length}: ` + specs.map((spec2) => `${spec2.styleId}/${spec2.assetId}`).join(", ") + ". Add --style." : "edit matched no asset."
|
|
14172
|
+
);
|
|
14173
|
+
}
|
|
14174
|
+
const spec = specs[0];
|
|
14175
|
+
if (args.subcommand === "detach") {
|
|
14176
|
+
const result = await detachHandEdit(loaded, spec);
|
|
14177
|
+
log(result.changed ? ` detached the hand edit for ${spec.styleId}/${spec.assetId}; the generated art is placed again (the edit file is kept)` : ` ${spec.styleId}/${spec.assetId} has no hand edit`);
|
|
14178
|
+
return;
|
|
14179
|
+
}
|
|
14180
|
+
const started = await startHandEdit(loaded, lock, spec);
|
|
14181
|
+
log(` ${started.created ? "created" : "found"} ${started.source}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
14182
|
+
if (args.noOpen) {
|
|
14183
|
+
log(` edit it with your own tool, then run pixelkiln plan; mount and pack place it in place of the generated art`);
|
|
14184
|
+
} else {
|
|
14185
|
+
const command = openInEditor(started.editPath);
|
|
14186
|
+
log(` opened with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
|
|
14187
|
+
}
|
|
14188
|
+
return;
|
|
14189
|
+
}
|
|
13842
14190
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
13843
14191
|
if (args.command === "refine") {
|
|
13844
14192
|
const pathOwnedFlags = [
|
|
@@ -13880,7 +14228,7 @@ async function main() {
|
|
|
13880
14228
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
13881
14229
|
if (item.state === "needs-approval") {
|
|
13882
14230
|
log(
|
|
13883
|
-
` pixelkiln refine approve --from ${
|
|
14231
|
+
` pixelkiln refine approve --from ${path28.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
13884
14232
|
);
|
|
13885
14233
|
}
|
|
13886
14234
|
}
|
|
@@ -13971,7 +14319,7 @@ async function main() {
|
|
|
13971
14319
|
let intact = true;
|
|
13972
14320
|
for (const output of entry.outputs) {
|
|
13973
14321
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
13974
|
-
if (!
|
|
14322
|
+
if (!existsSync22(file) || await sha256File(file) !== output.sha256) {
|
|
13975
14323
|
intact = false;
|
|
13976
14324
|
break;
|
|
13977
14325
|
}
|
|
@@ -14029,7 +14377,7 @@ async function main() {
|
|
|
14029
14377
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14030
14378
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14031
14379
|
}
|
|
14032
|
-
const manifestDir2 =
|
|
14380
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14033
14381
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14034
14382
|
for (const styleId of styleIds) {
|
|
14035
14383
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14038,10 +14386,11 @@ async function main() {
|
|
|
14038
14386
|
columns: args.columns,
|
|
14039
14387
|
outputRoles: args.outputRoles,
|
|
14040
14388
|
primaryOnly: args.primaryOnly,
|
|
14041
|
-
sourceOverrides: qualitySources
|
|
14389
|
+
sourceOverrides: qualitySources,
|
|
14390
|
+
sources: manifestSources(loaded.manifest, styleId)
|
|
14042
14391
|
});
|
|
14043
14392
|
const style = loaded.manifest.styles[styleId];
|
|
14044
|
-
const base = args.out ?
|
|
14393
|
+
const base = args.out ? path28.resolve(args.out.replace(/\.png$/, "")) : path28.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
14045
14394
|
const outputs = [
|
|
14046
14395
|
{ path: `${base}.png`, data: png },
|
|
14047
14396
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -14071,13 +14420,13 @@ async function main() {
|
|
|
14071
14420
|
log(
|
|
14072
14421
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
14073
14422
|
);
|
|
14074
|
-
log(` ${
|
|
14423
|
+
log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14075
14424
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14076
14425
|
}
|
|
14077
14426
|
return;
|
|
14078
14427
|
}
|
|
14079
14428
|
if (args.command === "mount") {
|
|
14080
|
-
const manifestDir2 =
|
|
14429
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14081
14430
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14082
14431
|
for (const styleId of styleIds) {
|
|
14083
14432
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14092,10 +14441,11 @@ async function main() {
|
|
|
14092
14441
|
const cells = {};
|
|
14093
14442
|
const sources = {};
|
|
14094
14443
|
const outputRoles = {};
|
|
14444
|
+
const styleSources = manifestSources(loaded.manifest, styleId);
|
|
14095
14445
|
for (const [assetId, asset] of Object.entries(loaded.manifest.assets)) {
|
|
14096
14446
|
if (!asset.cell) continue;
|
|
14097
14447
|
cells[assetId] = asset.cell;
|
|
14098
|
-
if (
|
|
14448
|
+
if (styleSources[assetId]) sources[assetId] = styleSources[assetId];
|
|
14099
14449
|
if (asset.outputRole) outputRoles[assetId] = asset.outputRole;
|
|
14100
14450
|
}
|
|
14101
14451
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14115,7 +14465,7 @@ async function main() {
|
|
|
14115
14465
|
sources,
|
|
14116
14466
|
outputRoles
|
|
14117
14467
|
);
|
|
14118
|
-
const out =
|
|
14468
|
+
const out = path28.resolve(manifestDir2, style.mount.out);
|
|
14119
14469
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
14120
14470
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14121
14471
|
const outputs = [
|
|
@@ -14135,7 +14485,7 @@ async function main() {
|
|
|
14135
14485
|
await provenanceFile("$lock", args.lock),
|
|
14136
14486
|
...qualityRecords,
|
|
14137
14487
|
...artifactSources.filter(
|
|
14138
|
-
(source) => source.id !== "$base" ||
|
|
14488
|
+
(source) => source.id !== "$base" || path28.resolve(source.path) !== out
|
|
14139
14489
|
)
|
|
14140
14490
|
],
|
|
14141
14491
|
options: {
|
|
@@ -14148,7 +14498,7 @@ async function main() {
|
|
|
14148
14498
|
log(
|
|
14149
14499
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14150
14500
|
);
|
|
14151
|
-
log(` ${
|
|
14501
|
+
log(` ${path28.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14152
14502
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14153
14503
|
}
|
|
14154
14504
|
return;
|
|
@@ -14204,7 +14554,7 @@ async function main() {
|
|
|
14204
14554
|
}
|
|
14205
14555
|
if (args.command === "export") {
|
|
14206
14556
|
const format = args.format ?? "generic";
|
|
14207
|
-
const manifestDir2 =
|
|
14557
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14208
14558
|
const selected = specs.filter((spec) => {
|
|
14209
14559
|
if (spec.generator !== "tiles") return false;
|
|
14210
14560
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14220,12 +14570,12 @@ async function main() {
|
|
|
14220
14570
|
for (const spec of selected) {
|
|
14221
14571
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14222
14572
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14223
|
-
const defaultBase =
|
|
14224
|
-
const base = args.out ?
|
|
14573
|
+
const defaultBase = path28.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
14574
|
+
const base = args.out ? path28.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14225
14575
|
const result = exportTileset(entry, spec, {
|
|
14226
14576
|
format,
|
|
14227
14577
|
manifestDir: manifestDir2,
|
|
14228
|
-
imageName:
|
|
14578
|
+
imageName: path28.basename(`${base}.png`),
|
|
14229
14579
|
columns: args.columns
|
|
14230
14580
|
});
|
|
14231
14581
|
const outputs = [
|
|
@@ -14243,7 +14593,7 @@ async function main() {
|
|
|
14243
14593
|
asset: spec.assetId,
|
|
14244
14594
|
columns: args.columns ?? null,
|
|
14245
14595
|
format,
|
|
14246
|
-
image:
|
|
14596
|
+
image: path28.basename(`${base}.png`),
|
|
14247
14597
|
providerRules: result.generic.providerRules,
|
|
14248
14598
|
style: spec.styleId,
|
|
14249
14599
|
tileType: spec.tileType ?? null
|
|
@@ -14253,7 +14603,7 @@ async function main() {
|
|
|
14253
14603
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14254
14604
|
);
|
|
14255
14605
|
log(
|
|
14256
|
-
` ${
|
|
14606
|
+
` ${path28.relative(process.cwd(), base)}.png + ${path28.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14257
14607
|
);
|
|
14258
14608
|
}
|
|
14259
14609
|
return;
|
|
@@ -14303,12 +14653,12 @@ async function main() {
|
|
|
14303
14653
|
tagged ${n} object(s) upstream`);
|
|
14304
14654
|
}
|
|
14305
14655
|
if (args.writePrompts) {
|
|
14306
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
14656
|
+
const { filled, stillEmpty } = await writePromptsBack(path28.resolve(args.manifest), lock, {
|
|
14307
14657
|
onProgress: log,
|
|
14308
14658
|
provider: provider.id,
|
|
14309
14659
|
assetIds: specs.map((spec) => spec.assetId)
|
|
14310
14660
|
});
|
|
14311
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
14661
|
+
log(` recovered ${filled} prompt(s) into ${path28.relative(process.cwd(), args.manifest)}`);
|
|
14312
14662
|
const reloaded = await loadManifest(args.manifest);
|
|
14313
14663
|
const rebased = (await resolveSpecs(reloaded, {
|
|
14314
14664
|
styles: args.styles,
|
|
@@ -14337,12 +14687,12 @@ async function main() {
|
|
|
14337
14687
|
const provider = providerFor(selectedAccountProvider);
|
|
14338
14688
|
const jsonMode = args.dryRun && args.json;
|
|
14339
14689
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
14340
|
-
const ownLock =
|
|
14690
|
+
const ownLock = path28.resolve(args.lock);
|
|
14341
14691
|
let workspaceProjects = [];
|
|
14342
14692
|
let workspaceDir = "";
|
|
14343
14693
|
if (args.workspace) {
|
|
14344
|
-
const workspacePath =
|
|
14345
|
-
workspaceDir =
|
|
14694
|
+
const workspacePath = path28.resolve(args.workspace);
|
|
14695
|
+
workspaceDir = path28.dirname(workspacePath);
|
|
14346
14696
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
14347
14697
|
workspaceProjects = complete.ws.projects;
|
|
14348
14698
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -14351,12 +14701,12 @@ async function main() {
|
|
|
14351
14701
|
const lockPaths = [
|
|
14352
14702
|
.../* @__PURE__ */ new Set([
|
|
14353
14703
|
...workspaceLockPaths,
|
|
14354
|
-
...
|
|
14355
|
-
...args.claims.map((c) =>
|
|
14704
|
+
...existsSync22(ownLock) ? [ownLock] : [],
|
|
14705
|
+
...args.claims.map((c) => path28.resolve(c))
|
|
14356
14706
|
])
|
|
14357
14707
|
];
|
|
14358
14708
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
14359
|
-
for (const p of lockPaths) diag(` ${
|
|
14709
|
+
for (const p of lockPaths) diag(` ${path28.relative(process.cwd(), p)}`);
|
|
14360
14710
|
if (!args.claims.length && !args.workspace) {
|
|
14361
14711
|
diag(
|
|
14362
14712
|
`
|
|
@@ -14364,7 +14714,7 @@ async function main() {
|
|
|
14364
14714
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
14365
14715
|
register every project in a workspace catalog and pass --workspace.`
|
|
14366
14716
|
);
|
|
14367
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
14717
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path28.resolve(args.manifest))) {
|
|
14368
14718
|
diag(
|
|
14369
14719
|
`
|
|
14370
14720
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14448,7 +14798,7 @@ async function main() {
|
|
|
14448
14798
|
manifestPath: loaded.path,
|
|
14449
14799
|
manifest: accountManifest,
|
|
14450
14800
|
styleId,
|
|
14451
|
-
importDir:
|
|
14801
|
+
importDir: path28.resolve(loaded.root, style.outDir),
|
|
14452
14802
|
lock,
|
|
14453
14803
|
lockPath: args.lock
|
|
14454
14804
|
},
|