pixelkiln 0.22.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 +684 -292
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +429 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +238 -105
- package/dist/index.d.ts +238 -105
- package/dist/index.js +417 -72
- package/dist/index.js.map +1 -1
- package/docs/ARTIFACTS.md +10 -5
- package/docs/CLI.md +42 -5
- 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/index.js
CHANGED
|
@@ -258,6 +258,15 @@ var AssetSchema = z.object({
|
|
|
258
258
|
* that wasn't. `prompt` still records what was asked for.
|
|
259
259
|
*/
|
|
260
260
|
source: z.string().optional(),
|
|
261
|
+
/**
|
|
262
|
+
* Per-style replacement for `source`, keyed by style id.
|
|
263
|
+
*
|
|
264
|
+
* A hand edit belongs to one generation: the same asset generated in two
|
|
265
|
+
* styles is two different pictures, so the touch-up of one must not be
|
|
266
|
+
* placed for the other. `pixelkiln edit` writes here when an asset is in
|
|
267
|
+
* more than one style, and to `source` when it is in exactly one.
|
|
268
|
+
*/
|
|
269
|
+
sourceByStyle: z.record(z.string().min(1)).default({}),
|
|
261
270
|
/** Controlled generation derived from another asset in the same style. */
|
|
262
271
|
revision: RevisionSchema.optional(),
|
|
263
272
|
/**
|
|
@@ -292,7 +301,7 @@ var AssetSchema = z.object({
|
|
|
292
301
|
z.array(z.union([z.string(), z.number().finite(), z.boolean()])).min(2).max(64)
|
|
293
302
|
])).default({})
|
|
294
303
|
}).strict().superRefine((asset, context) => {
|
|
295
|
-
if (asset.source && asset.revision) {
|
|
304
|
+
if ((asset.source || Object.keys(asset.sourceByStyle).length) && asset.revision) {
|
|
296
305
|
context.addIssue({
|
|
297
306
|
code: z.ZodIssueCode.custom,
|
|
298
307
|
message: "source and revision are mutually exclusive",
|
|
@@ -535,11 +544,11 @@ var PixelLabClient = class {
|
|
|
535
544
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
536
545
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
537
546
|
*/
|
|
538
|
-
async request(
|
|
547
|
+
async request(path27, init, attempt = 0) {
|
|
539
548
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
540
549
|
let res;
|
|
541
550
|
try {
|
|
542
|
-
res = await fetch(`${BASE}${
|
|
551
|
+
res = await fetch(`${BASE}${path27}`, {
|
|
543
552
|
...init,
|
|
544
553
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
545
554
|
headers: {
|
|
@@ -551,24 +560,24 @@ var PixelLabClient = class {
|
|
|
551
560
|
} catch (err) {
|
|
552
561
|
if (attempt < MAX_RETRIES) {
|
|
553
562
|
await sleep(backoffMs(attempt));
|
|
554
|
-
return this.request(
|
|
563
|
+
return this.request(path27, init, attempt + 1);
|
|
555
564
|
}
|
|
556
565
|
throw err;
|
|
557
566
|
}
|
|
558
567
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
559
568
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
560
569
|
await sleep(waitMs);
|
|
561
|
-
return this.request(
|
|
570
|
+
return this.request(path27, init, attempt + 1);
|
|
562
571
|
}
|
|
563
572
|
const text = await res.text();
|
|
564
573
|
if (!res.ok) {
|
|
565
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
574
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path27} \u2192 ${res.status}`, res.status, text);
|
|
566
575
|
}
|
|
567
576
|
if (!text) return {};
|
|
568
577
|
try {
|
|
569
578
|
return JSON.parse(text);
|
|
570
579
|
} catch {
|
|
571
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
580
|
+
throw new Error(`${init?.method ?? "GET"} ${path27} returned invalid JSON`);
|
|
572
581
|
}
|
|
573
582
|
}
|
|
574
583
|
async balance() {
|
|
@@ -1615,9 +1624,9 @@ var RetroDiffusionClient = class {
|
|
|
1615
1624
|
}
|
|
1616
1625
|
return balance;
|
|
1617
1626
|
}
|
|
1618
|
-
async call(
|
|
1627
|
+
async call(path27, init = {}) {
|
|
1619
1628
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1620
|
-
const response = await this.request(`${this.baseUrl}${
|
|
1629
|
+
const response = await this.request(`${this.baseUrl}${path27}`, {
|
|
1621
1630
|
...init,
|
|
1622
1631
|
headers: {
|
|
1623
1632
|
"Content-Type": "application/json",
|
|
@@ -2420,10 +2429,10 @@ import { createReadStream } from "fs";
|
|
|
2420
2429
|
function sha256(data) {
|
|
2421
2430
|
return createHash("sha256").update(data).digest("hex");
|
|
2422
2431
|
}
|
|
2423
|
-
async function sha256File(
|
|
2432
|
+
async function sha256File(path27) {
|
|
2424
2433
|
return new Promise((resolve, reject) => {
|
|
2425
2434
|
const hash = createHash("sha256");
|
|
2426
|
-
const stream = createReadStream(
|
|
2435
|
+
const stream = createReadStream(path27);
|
|
2427
2436
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
2428
2437
|
stream.on("error", reject);
|
|
2429
2438
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -3878,6 +3887,11 @@ ${issues}`);
|
|
|
3878
3887
|
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3879
3888
|
}
|
|
3880
3889
|
}
|
|
3890
|
+
for (const styleId of Object.keys(asset.sourceByStyle)) {
|
|
3891
|
+
if (!styleIds.has(styleId)) {
|
|
3892
|
+
unknownReferences.push(`assets.${assetId}.sourceByStyle: unknown style "${styleId}"`);
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3881
3895
|
if (asset.revision) {
|
|
3882
3896
|
if (!parsed.data.assets[asset.revision.from]) {
|
|
3883
3897
|
unknownReferences.push(
|
|
@@ -4142,7 +4156,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4142
4156
|
}
|
|
4143
4157
|
} : {},
|
|
4144
4158
|
tags,
|
|
4145
|
-
source: asset.source,
|
|
4159
|
+
source: asset.sourceByStyle[styleId] ?? asset.source,
|
|
4146
4160
|
// Revision identity is attached after every dependency in this style
|
|
4147
4161
|
// has a concrete output target. Finalization below computes the hash.
|
|
4148
4162
|
specHash: ""
|
|
@@ -6746,10 +6760,10 @@ function isSha256Hash(value) {
|
|
|
6746
6760
|
function parseCache(value) {
|
|
6747
6761
|
return HashCacheSchema.parse(value);
|
|
6748
6762
|
}
|
|
6749
|
-
async function loadCache(
|
|
6750
|
-
if (!existsSync10(
|
|
6763
|
+
async function loadCache(path27) {
|
|
6764
|
+
if (!existsSync10(path27)) return { version: 1, hashes: {} };
|
|
6751
6765
|
try {
|
|
6752
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(
|
|
6766
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path27, "utf8")));
|
|
6753
6767
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6754
6768
|
return {
|
|
6755
6769
|
version: 1,
|
|
@@ -6761,18 +6775,18 @@ async function loadCache(path25) {
|
|
|
6761
6775
|
return { version: 1, hashes: {} };
|
|
6762
6776
|
}
|
|
6763
6777
|
}
|
|
6764
|
-
async function saveCache(
|
|
6778
|
+
async function saveCache(path27, cache) {
|
|
6765
6779
|
const sorted = {};
|
|
6766
6780
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6767
6781
|
const hash = cache.hashes[key];
|
|
6768
6782
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6769
6783
|
sorted[key] = hash;
|
|
6770
6784
|
}
|
|
6771
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
6772
|
-
const tmp = `${
|
|
6785
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path27)), { recursive: true });
|
|
6786
|
+
const tmp = `${path27}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6773
6787
|
try {
|
|
6774
6788
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6775
|
-
await rename3(tmp,
|
|
6789
|
+
await rename3(tmp, path27);
|
|
6776
6790
|
} finally {
|
|
6777
6791
|
await rm4(tmp, { force: true });
|
|
6778
6792
|
}
|
|
@@ -7019,16 +7033,24 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
7019
7033
|
}
|
|
7020
7034
|
const prefix = `${styleId}/`;
|
|
7021
7035
|
const entries = Object.entries(lock.entries).filter(([key]) => key.startsWith(prefix));
|
|
7022
|
-
|
|
7036
|
+
const sources = options.sources ?? {};
|
|
7037
|
+
if (!entries.length && !Object.keys(sources).length) {
|
|
7023
7038
|
throw new Error(
|
|
7024
7039
|
`No locked assets for style "${styleId}". Run \`pixelkiln gen --style ${styleId}\` first.`
|
|
7025
7040
|
);
|
|
7026
7041
|
}
|
|
7027
7042
|
const inputs = [];
|
|
7028
7043
|
const noOutput = [];
|
|
7044
|
+
const locked = /* @__PURE__ */ new Set();
|
|
7029
7045
|
for (const [key, entry] of entries) {
|
|
7030
7046
|
const id = key.slice(prefix.length);
|
|
7047
|
+
locked.add(id);
|
|
7031
7048
|
const outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
7049
|
+
const source = sources[id];
|
|
7050
|
+
if (source && outputs.length <= 1) {
|
|
7051
|
+
inputs.push({ id, path: path15.resolve(manifestDir, source) });
|
|
7052
|
+
continue;
|
|
7053
|
+
}
|
|
7032
7054
|
let selected = outputs;
|
|
7033
7055
|
if (options.outputRoles?.length) {
|
|
7034
7056
|
selected = outputs.filter(
|
|
@@ -7045,6 +7067,9 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
7045
7067
|
}
|
|
7046
7068
|
for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
|
|
7047
7069
|
}
|
|
7070
|
+
for (const [id, source] of Object.entries(sources)) {
|
|
7071
|
+
if (!locked.has(id)) inputs.push({ id, path: path15.resolve(manifestDir, source) });
|
|
7072
|
+
}
|
|
7048
7073
|
if (!inputs.length) {
|
|
7049
7074
|
throw new Error(`No readable sprites for style "${styleId}" \u2014 ${noOutput.length} skipped.`);
|
|
7050
7075
|
}
|
|
@@ -8290,6 +8315,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
8290
8315
|
assets[asset.id] = {
|
|
8291
8316
|
prompt: "",
|
|
8292
8317
|
promptByStyle: {},
|
|
8318
|
+
sourceByStyle: {},
|
|
8293
8319
|
category: asset.category || void 0,
|
|
8294
8320
|
file: asset.file,
|
|
8295
8321
|
tags: [],
|
|
@@ -8977,9 +9003,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
8977
9003
|
|
|
8978
9004
|
// src/gallery/snapshot.ts
|
|
8979
9005
|
import { createHash as createHash4 } from "crypto";
|
|
8980
|
-
import { existsSync as
|
|
9006
|
+
import { existsSync as existsSync19 } from "fs";
|
|
8981
9007
|
import { readFile as readFile18, stat as stat3 } from "fs/promises";
|
|
8982
|
-
import
|
|
9008
|
+
import path24 from "path";
|
|
8983
9009
|
|
|
8984
9010
|
// src/workspace.ts
|
|
8985
9011
|
import { mkdir as mkdir5, readFile as readFile16, rename as rename5, rm as rm7, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -9115,6 +9141,10 @@ function validateWorkspace(ws, dir) {
|
|
|
9115
9141
|
}
|
|
9116
9142
|
|
|
9117
9143
|
// src/gallery/edit.ts
|
|
9144
|
+
import path23 from "path";
|
|
9145
|
+
import { z as z8 } from "zod";
|
|
9146
|
+
|
|
9147
|
+
// src/manifest-edit.ts
|
|
9118
9148
|
import { readFile as readFile17, rename as rename6, rm as rm8, writeFile as writeFile8 } from "fs/promises";
|
|
9119
9149
|
import path21 from "path";
|
|
9120
9150
|
import { z as z7 } from "zod";
|
|
@@ -9161,6 +9191,22 @@ var ManifestEditSchema = z7.discriminatedUnion("action", [
|
|
|
9161
9191
|
expectedSha256: HexSha,
|
|
9162
9192
|
asset: NewAssetSchema
|
|
9163
9193
|
}).strict(),
|
|
9194
|
+
z7.object({
|
|
9195
|
+
action: z7.literal("set-source"),
|
|
9196
|
+
project: z7.string().min(1).optional(),
|
|
9197
|
+
assetId: z7.string().min(1),
|
|
9198
|
+
styleId: z7.string().min(1),
|
|
9199
|
+
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
9200
|
+
source: z7.string().min(1),
|
|
9201
|
+
expectedSha256: HexSha
|
|
9202
|
+
}).strict(),
|
|
9203
|
+
z7.object({
|
|
9204
|
+
action: z7.literal("clear-source"),
|
|
9205
|
+
project: z7.string().min(1).optional(),
|
|
9206
|
+
assetId: z7.string().min(1),
|
|
9207
|
+
styleId: z7.string().min(1),
|
|
9208
|
+
expectedSha256: HexSha
|
|
9209
|
+
}).strict(),
|
|
9164
9210
|
z7.object({
|
|
9165
9211
|
action: z7.literal("patch-style"),
|
|
9166
9212
|
project: z7.string().min(1).optional(),
|
|
@@ -9173,7 +9219,11 @@ var ManifestEditSchema = z7.discriminatedUnion("action", [
|
|
|
9173
9219
|
promptPrefix: z7.string().optional(),
|
|
9174
9220
|
promptSuffix: z7.string().optional(),
|
|
9175
9221
|
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
9176
|
-
palette: z7.array(z7.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional()
|
|
9222
|
+
palette: z7.array(z7.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
9223
|
+
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
9224
|
+
view: z7.string().max(64).nullable().optional(),
|
|
9225
|
+
/** pixflux only; null clears the style's own value. */
|
|
9226
|
+
noBackground: z7.boolean().nullable().optional()
|
|
9177
9227
|
}).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
|
|
9178
9228
|
}).strict()
|
|
9179
9229
|
]);
|
|
@@ -9207,8 +9257,38 @@ function setOrDelete(target, key, value) {
|
|
|
9207
9257
|
if (value === null || value === void 0) delete target[key];
|
|
9208
9258
|
else target[key] = value;
|
|
9209
9259
|
}
|
|
9260
|
+
function assetStyles(raw, asset) {
|
|
9261
|
+
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
9262
|
+
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
9263
|
+
}
|
|
9210
9264
|
function applyEdit(raw, edit) {
|
|
9211
9265
|
raw.assets ??= {};
|
|
9266
|
+
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
9267
|
+
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
9268
|
+
if (!asset2) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
|
|
9269
|
+
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
9270
|
+
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
9271
|
+
}
|
|
9272
|
+
if (!assetStyles(raw, asset2).includes(edit.styleId)) {
|
|
9273
|
+
throw new ManifestEditError(`asset "${edit.assetId}" is not in style "${edit.styleId}"`);
|
|
9274
|
+
}
|
|
9275
|
+
const single = assetStyles(raw, asset2).length === 1;
|
|
9276
|
+
const byStyle = { ...asset2.sourceByStyle ?? {} };
|
|
9277
|
+
if (edit.action === "set-source") {
|
|
9278
|
+
if (single) {
|
|
9279
|
+
asset2.source = edit.source;
|
|
9280
|
+
delete byStyle[edit.styleId];
|
|
9281
|
+
} else {
|
|
9282
|
+
byStyle[edit.styleId] = edit.source;
|
|
9283
|
+
}
|
|
9284
|
+
} else {
|
|
9285
|
+
delete byStyle[edit.styleId];
|
|
9286
|
+
if (single) delete asset2.source;
|
|
9287
|
+
}
|
|
9288
|
+
if (Object.keys(byStyle).length) asset2.sourceByStyle = byStyle;
|
|
9289
|
+
else delete asset2.sourceByStyle;
|
|
9290
|
+
return;
|
|
9291
|
+
}
|
|
9212
9292
|
if (edit.action === "patch-style") {
|
|
9213
9293
|
const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
|
|
9214
9294
|
if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
|
|
@@ -9231,6 +9311,8 @@ function applyEdit(raw, edit) {
|
|
|
9231
9311
|
const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
9232
9312
|
setOrDelete(style, "palette", colors.length ? colors : null);
|
|
9233
9313
|
}
|
|
9314
|
+
if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
|
|
9315
|
+
if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
|
|
9234
9316
|
return;
|
|
9235
9317
|
}
|
|
9236
9318
|
if (edit.action === "add-asset") {
|
|
@@ -9310,10 +9392,134 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9310
9392
|
}
|
|
9311
9393
|
return { manifestPath: absolute, sha256: sha256(next), changed: true };
|
|
9312
9394
|
}
|
|
9395
|
+
|
|
9396
|
+
// src/pipeline/hand-edit.ts
|
|
9397
|
+
import { spawn as spawn2 } from "child_process";
|
|
9398
|
+
import { existsSync as existsSync18 } from "fs";
|
|
9399
|
+
import { copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
9400
|
+
import path22 from "path";
|
|
9401
|
+
var HAND_EDIT_DIR = "edits";
|
|
9402
|
+
function handEditPath(loaded, spec) {
|
|
9403
|
+
const style = loaded.manifest.styles[spec.styleId];
|
|
9404
|
+
const outDir = path22.resolve(loaded.root, style?.outDir ?? ".");
|
|
9405
|
+
const relative = path22.relative(outDir, spec.outFile);
|
|
9406
|
+
const inside = relative && !relative.startsWith("..") && !path22.isAbsolute(relative);
|
|
9407
|
+
const file = inside ? relative : path22.basename(spec.outFile);
|
|
9408
|
+
return path22.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
9409
|
+
}
|
|
9410
|
+
function handEditBase(spec, lock) {
|
|
9411
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9412
|
+
if (entry) {
|
|
9413
|
+
if (entry.outputs.length !== 1) return null;
|
|
9414
|
+
const primary = primaryOutput(entry);
|
|
9415
|
+
if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
|
|
9416
|
+
const file = currentEntryOutputPath(entry, spec, 0);
|
|
9417
|
+
return existsSync18(file) ? file : null;
|
|
9418
|
+
}
|
|
9419
|
+
return existsSync18(spec.outFile) ? spec.outFile : null;
|
|
9420
|
+
}
|
|
9421
|
+
async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
9422
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9423
|
+
if (entry && entry.outputs.length > 1) {
|
|
9424
|
+
throw new ManifestEditError(
|
|
9425
|
+
`${spec.styleId}/${spec.assetId} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
|
|
9426
|
+
);
|
|
9427
|
+
}
|
|
9428
|
+
const existing = spec.source ? path22.resolve(loaded.root, spec.source) : null;
|
|
9429
|
+
const editPath = existing ?? handEditPath(loaded, spec);
|
|
9430
|
+
let created = false;
|
|
9431
|
+
if (!existsSync18(editPath)) {
|
|
9432
|
+
const base = handEditBase(spec, lock);
|
|
9433
|
+
if (!base) {
|
|
9434
|
+
throw new ManifestEditError(
|
|
9435
|
+
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
9436
|
+
);
|
|
9437
|
+
}
|
|
9438
|
+
await mkdir6(path22.dirname(editPath), { recursive: true });
|
|
9439
|
+
await copyFile(base, editPath);
|
|
9440
|
+
created = true;
|
|
9441
|
+
}
|
|
9442
|
+
const source = portableOutputPath(editPath, loaded.root);
|
|
9443
|
+
let declared = false;
|
|
9444
|
+
let manifestSha256 = opts.expectedSha256 ?? await sha256File(loaded.path);
|
|
9445
|
+
if (spec.source !== source) {
|
|
9446
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
9447
|
+
action: "set-source",
|
|
9448
|
+
assetId: spec.assetId,
|
|
9449
|
+
styleId: spec.styleId,
|
|
9450
|
+
source,
|
|
9451
|
+
expectedSha256: manifestSha256
|
|
9452
|
+
});
|
|
9453
|
+
declared = result.changed;
|
|
9454
|
+
manifestSha256 = result.sha256;
|
|
9455
|
+
}
|
|
9456
|
+
return { editPath, source, created, declared, manifestSha256 };
|
|
9457
|
+
}
|
|
9458
|
+
async function detachHandEdit(loaded, spec, opts = {}) {
|
|
9459
|
+
if (!spec.source) {
|
|
9460
|
+
return { manifestSha256: opts.expectedSha256 ?? await sha256File(loaded.path), changed: false };
|
|
9461
|
+
}
|
|
9462
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
9463
|
+
action: "clear-source",
|
|
9464
|
+
assetId: spec.assetId,
|
|
9465
|
+
styleId: spec.styleId,
|
|
9466
|
+
expectedSha256: opts.expectedSha256 ?? await sha256File(loaded.path)
|
|
9467
|
+
});
|
|
9468
|
+
return { manifestSha256: result.sha256, changed: result.changed };
|
|
9469
|
+
}
|
|
9470
|
+
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
9471
|
+
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
9472
|
+
const [program, ...args] = command;
|
|
9473
|
+
const child = spawn2(program, [...args, file], { stdio: "ignore", detached: true });
|
|
9474
|
+
child.on("error", () => {
|
|
9475
|
+
});
|
|
9476
|
+
child.unref();
|
|
9477
|
+
return command.join(" ");
|
|
9478
|
+
}
|
|
9479
|
+
|
|
9480
|
+
// src/gallery/edit.ts
|
|
9481
|
+
var HandEditRequestSchema = z8.discriminatedUnion("action", [
|
|
9482
|
+
z8.object({
|
|
9483
|
+
action: z8.literal("start-edit"),
|
|
9484
|
+
project: z8.string().min(1).optional(),
|
|
9485
|
+
assetId: z8.string().min(1),
|
|
9486
|
+
styleId: z8.string().min(1),
|
|
9487
|
+
expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
|
|
9488
|
+
/** Also launch the author's editor on the file. */
|
|
9489
|
+
open: z8.boolean().optional()
|
|
9490
|
+
}).strict(),
|
|
9491
|
+
z8.object({
|
|
9492
|
+
action: z8.literal("detach-edit"),
|
|
9493
|
+
project: z8.string().min(1).optional(),
|
|
9494
|
+
assetId: z8.string().min(1),
|
|
9495
|
+
styleId: z8.string().min(1),
|
|
9496
|
+
expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/)
|
|
9497
|
+
}).strict()
|
|
9498
|
+
]);
|
|
9313
9499
|
function createGalleryEditHandler(opts) {
|
|
9314
9500
|
const log = opts.onProgress ?? (() => {
|
|
9315
9501
|
});
|
|
9316
9502
|
return async (body) => {
|
|
9503
|
+
const handEdit = HandEditRequestSchema.safeParse(body);
|
|
9504
|
+
if (handEdit.success) {
|
|
9505
|
+
if (!opts.loadProject) throw new ManifestEditError("hand edits are not available in this gallery");
|
|
9506
|
+
const request = handEdit.data;
|
|
9507
|
+
const ctx = await opts.loadProject(request.project);
|
|
9508
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === lockKey(request.styleId, request.assetId));
|
|
9509
|
+
if (!spec) throw new ManifestEditError(`"${request.styleId}/${request.assetId}" is not declared by the manifest`);
|
|
9510
|
+
if (request.action === "start-edit") {
|
|
9511
|
+
const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
|
|
9512
|
+
log(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
|
|
9513
|
+
if (request.open) {
|
|
9514
|
+
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
9515
|
+
log(` opened with: ${command}`);
|
|
9516
|
+
}
|
|
9517
|
+
} else {
|
|
9518
|
+
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
9519
|
+
if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
9520
|
+
}
|
|
9521
|
+
return opts.reload();
|
|
9522
|
+
}
|
|
9317
9523
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
9318
9524
|
if (!parsed.success) {
|
|
9319
9525
|
throw new ManifestEditError(
|
|
@@ -9328,9 +9534,9 @@ function createGalleryEditHandler(opts) {
|
|
|
9328
9534
|
}
|
|
9329
9535
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
9330
9536
|
if (result.changed) {
|
|
9331
|
-
const relative =
|
|
9537
|
+
const relative = path23.relative(process.cwd(), result.manifestPath);
|
|
9332
9538
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
9333
|
-
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.assetId;
|
|
9539
|
+
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;
|
|
9334
9540
|
log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
9335
9541
|
}
|
|
9336
9542
|
return opts.reload();
|
|
@@ -9339,7 +9545,7 @@ function createGalleryEditHandler(opts) {
|
|
|
9339
9545
|
|
|
9340
9546
|
// src/gallery/snapshot.ts
|
|
9341
9547
|
function galleryMediaId(absolutePath) {
|
|
9342
|
-
return createHash4("sha256").update(
|
|
9548
|
+
return createHash4("sha256").update(path24.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
9343
9549
|
}
|
|
9344
9550
|
function galleryMediaRoute(id) {
|
|
9345
9551
|
return `/media/${id}`;
|
|
@@ -9364,23 +9570,24 @@ function metadataFps(entry) {
|
|
|
9364
9570
|
}
|
|
9365
9571
|
return null;
|
|
9366
9572
|
}
|
|
9367
|
-
async function
|
|
9573
|
+
async function fileInfo(absolutePath) {
|
|
9368
9574
|
try {
|
|
9369
9575
|
const info = await stat3(absolutePath);
|
|
9370
|
-
return info.isFile() ? info.size : null;
|
|
9576
|
+
return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
|
|
9371
9577
|
} catch {
|
|
9372
9578
|
return null;
|
|
9373
9579
|
}
|
|
9374
9580
|
}
|
|
9375
9581
|
async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
9376
|
-
const
|
|
9377
|
-
const exists =
|
|
9582
|
+
const info = await fileInfo(absolutePath);
|
|
9583
|
+
const exists = info !== null;
|
|
9378
9584
|
const mediaType = recorded.mediaType ?? mediaTypeFromExtension(absolutePath);
|
|
9379
9585
|
let url = null;
|
|
9380
9586
|
if (exists && mediaType) {
|
|
9381
9587
|
const id = galleryMediaId(absolutePath);
|
|
9382
9588
|
media.set(id, { path: absolutePath, contentType: mediaType });
|
|
9383
|
-
|
|
9589
|
+
const version = recorded.sha256 ?? `${info.bytes}-${Date.parse(info.modifiedAt).toString(36)}`;
|
|
9590
|
+
url = `${galleryMediaRoute(id)}?v=${version.slice(0, 24)}`;
|
|
9384
9591
|
}
|
|
9385
9592
|
return {
|
|
9386
9593
|
path: portableOutputPath(absolutePath, root),
|
|
@@ -9389,17 +9596,18 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
9389
9596
|
role: recorded.role ?? null,
|
|
9390
9597
|
mediaType,
|
|
9391
9598
|
exists,
|
|
9392
|
-
bytes,
|
|
9599
|
+
bytes: info?.bytes ?? null,
|
|
9600
|
+
modifiedAt: info?.modifiedAt ?? null,
|
|
9393
9601
|
url
|
|
9394
9602
|
};
|
|
9395
9603
|
}
|
|
9396
9604
|
async function describeQuality(media, root, inspection) {
|
|
9397
9605
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
9398
9606
|
const outputs = await Promise.all(
|
|
9399
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
9607
|
+
outputPaths.map((file) => describeOutput(media, root, path24.resolve(root, file)))
|
|
9400
9608
|
);
|
|
9401
|
-
const recordPath =
|
|
9402
|
-
const recordExists =
|
|
9609
|
+
const recordPath = path24.resolve(root, inspection.record);
|
|
9610
|
+
const recordExists = existsSync19(recordPath);
|
|
9403
9611
|
let options = null;
|
|
9404
9612
|
let check = null;
|
|
9405
9613
|
if (recordExists) {
|
|
@@ -9418,7 +9626,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
9418
9626
|
return {
|
|
9419
9627
|
state: inspection.state,
|
|
9420
9628
|
reason: inspection.reason,
|
|
9421
|
-
output: portableOutputPath(
|
|
9629
|
+
output: portableOutputPath(path24.resolve(root, inspection.output), root),
|
|
9422
9630
|
record: portableOutputPath(recordPath, root),
|
|
9423
9631
|
recordExists,
|
|
9424
9632
|
outputs,
|
|
@@ -9468,15 +9676,24 @@ async function buildGallerySnapshot(opts) {
|
|
|
9468
9676
|
const entry = lock.entries[key];
|
|
9469
9677
|
const asset = loaded.manifest.assets[spec.assetId] ?? null;
|
|
9470
9678
|
let outputs;
|
|
9679
|
+
let edit = null;
|
|
9680
|
+
let editStatus = null;
|
|
9471
9681
|
if (entry && entry.outputs.length) {
|
|
9472
9682
|
outputs = await Promise.all(
|
|
9473
9683
|
entry.outputs.map(
|
|
9474
9684
|
(output, index) => describeOutput(media, root, currentEntryOutputPath(entry, spec, index), output)
|
|
9475
9685
|
)
|
|
9476
9686
|
);
|
|
9687
|
+
if (spec.source) {
|
|
9688
|
+
const editPath = path24.resolve(root, spec.source);
|
|
9689
|
+
const editSha = existsSync19(editPath) ? await sha256File(editPath) : null;
|
|
9690
|
+
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
9691
|
+
const generated = outputs[0];
|
|
9692
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists ? "regenerated-since" : "edited";
|
|
9693
|
+
}
|
|
9477
9694
|
} else if (spec.source) {
|
|
9478
|
-
outputs = [await describeOutput(media, root,
|
|
9479
|
-
} else if (!entry &&
|
|
9695
|
+
outputs = [await describeOutput(media, root, path24.resolve(root, spec.source))];
|
|
9696
|
+
} else if (!entry && existsSync19(spec.outFile)) {
|
|
9480
9697
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
9481
9698
|
} else {
|
|
9482
9699
|
outputs = [];
|
|
@@ -9520,6 +9737,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9520
9737
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
9521
9738
|
asset,
|
|
9522
9739
|
source: spec.source ?? null,
|
|
9740
|
+
edit,
|
|
9741
|
+
editStatus,
|
|
9523
9742
|
tags: spec.tags,
|
|
9524
9743
|
category: asset?.category ?? null
|
|
9525
9744
|
});
|
|
@@ -9572,6 +9791,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9572
9791
|
providerMetadata: entry.providerMetadata ?? {},
|
|
9573
9792
|
asset: null,
|
|
9574
9793
|
source: null,
|
|
9794
|
+
edit: null,
|
|
9795
|
+
editStatus: null,
|
|
9575
9796
|
tags: [],
|
|
9576
9797
|
category: null
|
|
9577
9798
|
});
|
|
@@ -9610,6 +9831,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9610
9831
|
promptPrefix: style?.promptPrefix ?? "",
|
|
9611
9832
|
promptSuffix: style?.promptSuffix ?? "",
|
|
9612
9833
|
palette: style?.palette ?? [],
|
|
9834
|
+
view: style?.view ?? null,
|
|
9835
|
+
noBackground: style?.noBackground ?? true,
|
|
9613
9836
|
quality: Boolean(style?.quality),
|
|
9614
9837
|
tags: style?.tags ?? [],
|
|
9615
9838
|
extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
|
|
@@ -9637,7 +9860,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9637
9860
|
id: loaded.manifest.name,
|
|
9638
9861
|
name: loaded.manifest.name,
|
|
9639
9862
|
manifest: loaded.path,
|
|
9640
|
-
lock:
|
|
9863
|
+
lock: path24.resolve(opts.lockPath),
|
|
9641
9864
|
root,
|
|
9642
9865
|
provider: loaded.manifest.provider,
|
|
9643
9866
|
account: null,
|
|
@@ -9662,7 +9885,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9662
9885
|
return { snapshot, media };
|
|
9663
9886
|
}
|
|
9664
9887
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
9665
|
-
const dir =
|
|
9888
|
+
const dir = path24.dirname(path24.resolve(opts.workspacePath));
|
|
9666
9889
|
const media = /* @__PURE__ */ new Map();
|
|
9667
9890
|
const projects = [];
|
|
9668
9891
|
const items = [];
|
|
@@ -9705,7 +9928,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9705
9928
|
id: project.id,
|
|
9706
9929
|
name: loaded.manifest.name,
|
|
9707
9930
|
manifest: manifestPath,
|
|
9708
|
-
lock:
|
|
9931
|
+
lock: path24.resolve(lockPath),
|
|
9709
9932
|
root: loaded.root,
|
|
9710
9933
|
provider: loaded.manifest.provider,
|
|
9711
9934
|
account: project.account ?? null,
|
|
@@ -9720,8 +9943,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9720
9943
|
id: project.id,
|
|
9721
9944
|
name: project.id,
|
|
9722
9945
|
manifest: manifestPath,
|
|
9723
|
-
lock:
|
|
9724
|
-
root:
|
|
9946
|
+
lock: path24.resolve(lockPath),
|
|
9947
|
+
root: path24.dirname(manifestPath),
|
|
9725
9948
|
provider: project.provider,
|
|
9726
9949
|
account: project.account ?? null,
|
|
9727
9950
|
manifestSha256: null,
|
|
@@ -9747,7 +9970,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9747
9970
|
version: 1,
|
|
9748
9971
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
9749
9972
|
project: null,
|
|
9750
|
-
workspace: { path:
|
|
9973
|
+
workspace: { path: path24.resolve(opts.workspacePath), dir, projects },
|
|
9751
9974
|
filter,
|
|
9752
9975
|
totals: {
|
|
9753
9976
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -10034,6 +10257,14 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10034
10257
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10035
10258
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
10036
10259
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10260
|
+
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10261
|
+
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
10262
|
+
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
10263
|
+
.pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
|
|
10264
|
+
.pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
|
|
10265
|
+
.card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
|
|
10266
|
+
font:700 11px/1 var(--mono); padding:3px 5px; }
|
|
10267
|
+
.hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
10037
10268
|
.drawer .gen button.primary { padding:7px 14px; }
|
|
10038
10269
|
@media (max-width: 720px) {
|
|
10039
10270
|
.bar, .totals, .chips, .style, footer { padding-inline:14px; }
|
|
@@ -10084,6 +10315,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10084
10315
|
</footer>
|
|
10085
10316
|
<div id="drawer-host"></div>
|
|
10086
10317
|
<div id="dialog-host"></div>
|
|
10318
|
+
<datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
|
|
10087
10319
|
<script>
|
|
10088
10320
|
const INITIAL = ${data};
|
|
10089
10321
|
const SESSION = ${session};
|
|
@@ -10479,7 +10711,8 @@ function clearFilters() {
|
|
|
10479
10711
|
|
|
10480
10712
|
function thumb(item) {
|
|
10481
10713
|
const cell = el('div', 'cell');
|
|
10482
|
-
|
|
10714
|
+
// A hand edit is what ships, so it is what the card shows.
|
|
10715
|
+
const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
|
|
10483
10716
|
if (!shown.length) {
|
|
10484
10717
|
cell.classList.add('none');
|
|
10485
10718
|
cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
|
|
@@ -10530,6 +10763,9 @@ function card(item) {
|
|
|
10530
10763
|
body.append(meta);
|
|
10531
10764
|
const cell = thumb(item);
|
|
10532
10765
|
if (slot >= 0) cell.append(el('span', 'slot', String(slot + 1)));
|
|
10766
|
+
if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
|
|
10767
|
+
const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
|
|
10768
|
+
}
|
|
10533
10769
|
c.append(cell, body);
|
|
10534
10770
|
c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
|
|
10535
10771
|
return c;
|
|
@@ -10856,6 +11092,78 @@ function openCompare() {
|
|
|
10856
11092
|
close.focus({ preventScroll: true });
|
|
10857
11093
|
}
|
|
10858
11094
|
|
|
11095
|
+
// ---- hand edits -------------------------------------------------------------
|
|
11096
|
+
|
|
11097
|
+
const EDIT_STATUS_TEXT = {
|
|
11098
|
+
same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
|
|
11099
|
+
edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
|
|
11100
|
+
'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
|
|
11101
|
+
missing: 'The declared file is not on disk.',
|
|
11102
|
+
};
|
|
11103
|
+
async function postHandEdit(item, action, extra) {
|
|
11104
|
+
const pr = projectOf(item);
|
|
11105
|
+
const body = { action, assetId: item.assetId, styleId: item.styleId, expectedSha256: pr.manifestSha256, ...extra };
|
|
11106
|
+
if (item.project) body.project = item.project;
|
|
11107
|
+
snap = await postEdit(body);
|
|
11108
|
+
}
|
|
11109
|
+
function handEditSection(item) {
|
|
11110
|
+
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
|
|
11111
|
+
item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
|
|
11112
|
+
if (!item.edit && !canStart) return null;
|
|
11113
|
+
const s = el('section', 'meta');
|
|
11114
|
+
s.append(el('h3', null, 'Hand edit'));
|
|
11115
|
+
if (item.edit) {
|
|
11116
|
+
const pair = el('div', 'pair');
|
|
11117
|
+
const shared = displayScale(item.width, item.height, 200, 160);
|
|
11118
|
+
const fig = (out, label) => {
|
|
11119
|
+
const f = el('figure');
|
|
11120
|
+
if (out && out.url) {
|
|
11121
|
+
const img = el('img'); img.src = out.url; img.alt = label;
|
|
11122
|
+
if (shared >= 1) { img.width = item.width * shared; img.height = item.height * shared; }
|
|
11123
|
+
f.append(img);
|
|
11124
|
+
} else f.append(el('span', 'state-dim', 'not on disk'));
|
|
11125
|
+
f.append(el('figcaption', null, label));
|
|
11126
|
+
return f;
|
|
11127
|
+
};
|
|
11128
|
+
pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
|
|
11129
|
+
s.append(pair);
|
|
11130
|
+
const dl = el('dl');
|
|
11131
|
+
const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
|
|
11132
|
+
const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
|
|
11133
|
+
row(dl, 'status', st);
|
|
11134
|
+
row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
|
|
11135
|
+
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
11136
|
+
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
11137
|
+
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
11138
|
+
s.append(dl);
|
|
11139
|
+
} else {
|
|
11140
|
+
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.'));
|
|
11141
|
+
}
|
|
11142
|
+
if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
|
|
11143
|
+
const acts = el('div', 'hand-actions');
|
|
11144
|
+
const msg = el('span', 'msg');
|
|
11145
|
+
const go = async (label, action, extra, done) => {
|
|
11146
|
+
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
11147
|
+
b.onclick = async () => {
|
|
11148
|
+
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
11149
|
+
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
11150
|
+
catch (err) { b.disabled = false; msg.className = 'msg bad'; msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : ''); }
|
|
11151
|
+
};
|
|
11152
|
+
acts.append(b);
|
|
11153
|
+
};
|
|
11154
|
+
if (item.edit) {
|
|
11155
|
+
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11156
|
+
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
11157
|
+
} else if (canStart) {
|
|
11158
|
+
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.');
|
|
11159
|
+
go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
|
|
11160
|
+
}
|
|
11161
|
+
acts.append(msg);
|
|
11162
|
+
s.append(acts);
|
|
11163
|
+
}
|
|
11164
|
+
return s;
|
|
11165
|
+
}
|
|
11166
|
+
|
|
10859
11167
|
// ---- editing (only when the server minted a session) ---------------------
|
|
10860
11168
|
|
|
10861
11169
|
// Styles a parent edit reaches: every descendant (via extends) that does not
|
|
@@ -10875,9 +11183,18 @@ function inheritorsOf(style, field) {
|
|
|
10875
11183
|
}
|
|
10876
11184
|
return out;
|
|
10877
11185
|
}
|
|
11186
|
+
// noBackground reaches the request only for pixflux and non-PixelLab
|
|
11187
|
+
// providers; elsewhere a change is recorded but alters nothing.
|
|
11188
|
+
const fieldReaches = (field, style) =>
|
|
11189
|
+
field !== 'noBackground' || style.generator === 'pixflux' || style.provider !== 'pixellab';
|
|
10878
11190
|
function blastRadius(style, fields) {
|
|
10879
|
-
const
|
|
10880
|
-
|
|
11191
|
+
const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
|
|
11192
|
+
const styleIds = new Set();
|
|
11193
|
+
for (const field of fields) {
|
|
11194
|
+
for (const id of [style.id, ...inheritorsOf(style, field)]) {
|
|
11195
|
+
if (fieldReaches(field, byId.get(id) || style)) styleIds.add(id);
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
10881
11198
|
const affected = snap.items.filter((i) => i.project === style.project && i.declared && styleIds.has(i.styleId));
|
|
10882
11199
|
const units = new Map();
|
|
10883
11200
|
for (const i of affected) if (i.estimatedCost !== null) units.set(i.costUnit, (units.get(i.costUnit) || 0) + i.estimatedCost);
|
|
@@ -10902,6 +11219,11 @@ function styleForm(style) {
|
|
|
10902
11219
|
const prefix = el('input'); prefix.type = 'text'; prefix.value = style.promptPrefix; prefix.placeholder = 'none';
|
|
10903
11220
|
const suffix = el('input'); suffix.type = 'text'; suffix.value = style.promptSuffix; suffix.placeholder = 'none';
|
|
10904
11221
|
const palette = el('input'); palette.type = 'text'; palette.value = style.palette.join(', '); palette.placeholder = '#rrggbb, #rrggbb \u2014 leave empty for no forced palette';
|
|
11222
|
+
const view = el('input'); view.type = 'text'; view.value = style.view || ''; view.placeholder = 'provider default';
|
|
11223
|
+
view.setAttribute('list', 'view-options');
|
|
11224
|
+
const noBg = el('select');
|
|
11225
|
+
noBg.append(new Option(style.extends ? 'inherit from ' + style.extends : 'default (on)', 'default'), new Option('on \u2014 strip the generated background', 'on'), new Option('off \u2014 keep the background (scenes, banners)', 'off'));
|
|
11226
|
+
noBg.value = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
|
|
10905
11227
|
const swatches = el('div', 'pal');
|
|
10906
11228
|
const drawSwatches = () => {
|
|
10907
11229
|
swatches.textContent = '';
|
|
@@ -10914,12 +11236,21 @@ function styleForm(style) {
|
|
|
10914
11236
|
field('palette', palette, 'Forced colours where the provider supports them (pixflux). ' + provenance('palette')),
|
|
10915
11237
|
swatches,
|
|
10916
11238
|
);
|
|
11239
|
+
const row = el('div', 'row');
|
|
11240
|
+
row.append(
|
|
11241
|
+
field('view', view, 'PixelLab accepts low top-down, high top-down, side; Retro Diffusion reads sidescroller. ' + provenance('view')),
|
|
11242
|
+
field('background', noBg, (fieldReaches('noBackground', style) ? 'Sent for this style. ' : 'Not sent for ' + style.provider + ' ' + style.generator + '; recorded only. ') + provenance('noBackground')),
|
|
11243
|
+
);
|
|
11244
|
+
form.append(row);
|
|
10917
11245
|
const blast = el('div', 'blast');
|
|
10918
11246
|
const changed = () => {
|
|
10919
11247
|
const out = [];
|
|
10920
11248
|
if (prefix.value !== style.promptPrefix) out.push('promptPrefix');
|
|
10921
11249
|
if (suffix.value !== style.promptSuffix) out.push('promptSuffix');
|
|
10922
11250
|
if (parsePalette(palette.value).join(',') !== style.palette.map((c) => c.toLowerCase()).join(',')) out.push('palette');
|
|
11251
|
+
if (view.value.trim() !== (style.view || '')) out.push('view');
|
|
11252
|
+
const bgNow = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
|
|
11253
|
+
if (noBg.value !== bgNow) out.push('noBackground');
|
|
10923
11254
|
return out;
|
|
10924
11255
|
};
|
|
10925
11256
|
const updateBlast = () => {
|
|
@@ -10927,6 +11258,11 @@ function styleForm(style) {
|
|
|
10927
11258
|
drawSwatches();
|
|
10928
11259
|
if (!fields.length) { blast.className = 'blast'; blast.textContent = 'No changes yet. A style change alters the request for every asset that uses it.'; return; }
|
|
10929
11260
|
const r = blastRadius(style, fields);
|
|
11261
|
+
if (!r.assets) {
|
|
11262
|
+
blast.className = 'blast';
|
|
11263
|
+
blast.textContent = 'Recorded in the manifest only: nothing in ' + style.id + ' sends this field, so no request changes.';
|
|
11264
|
+
return;
|
|
11265
|
+
}
|
|
10930
11266
|
blast.className = 'blast hot';
|
|
10931
11267
|
blast.textContent = '';
|
|
10932
11268
|
const others = r.styles.filter((id) => id !== style.id);
|
|
@@ -10936,7 +11272,8 @@ function styleForm(style) {
|
|
|
10936
11272
|
: ' in ' + style.id + '.'));
|
|
10937
11273
|
blast.append(document.createTextNode(' Generated ones become stale; regenerating all of them is about ' + (r.cost || 'nothing') + '. Nothing is spent until you generate.'));
|
|
10938
11274
|
};
|
|
10939
|
-
for (const input of [prefix, suffix, palette]) input.addEventListener('input', updateBlast);
|
|
11275
|
+
for (const input of [prefix, suffix, palette, view]) input.addEventListener('input', updateBlast);
|
|
11276
|
+
noBg.addEventListener('change', updateBlast);
|
|
10940
11277
|
updateBlast();
|
|
10941
11278
|
form.append(blast);
|
|
10942
11279
|
const actions = el('div', 'actions');
|
|
@@ -10955,6 +11292,8 @@ function styleForm(style) {
|
|
|
10955
11292
|
if (fields.includes('promptPrefix')) patch.promptPrefix = prefix.value;
|
|
10956
11293
|
if (fields.includes('promptSuffix')) patch.promptSuffix = suffix.value;
|
|
10957
11294
|
if (fields.includes('palette')) patch.palette = colors;
|
|
11295
|
+
if (fields.includes('view')) patch.view = view.value.trim();
|
|
11296
|
+
if (fields.includes('noBackground')) patch.noBackground = noBg.value === 'default' ? null : noBg.value === 'on';
|
|
10958
11297
|
save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving\u2026';
|
|
10959
11298
|
const r = blastRadius(style, fields);
|
|
10960
11299
|
try {
|
|
@@ -10963,7 +11302,9 @@ function styleForm(style) {
|
|
|
10963
11302
|
snap = await postEdit(body);
|
|
10964
11303
|
ui.editing = null;
|
|
10965
11304
|
ui.notice = { id: 'style:' + (style.project || '') + ':' + style.id,
|
|
10966
|
-
text:
|
|
11305
|
+
text: r.assets
|
|
11306
|
+
? 'Saved. The request for ' + r.assets + (r.assets === 1 ? ' asset' : ' assets') + ' changed' + (r.cost ? ' \u2014 about ' + r.cost + ' to regenerate.' : '.') + ' Nothing is spent until you generate.'
|
|
11307
|
+
: 'Saved. Recorded in the manifest; no request changed.' };
|
|
10967
11308
|
render();
|
|
10968
11309
|
} catch (err) {
|
|
10969
11310
|
save.disabled = false;
|
|
@@ -11314,6 +11655,9 @@ function renderDrawer() {
|
|
|
11314
11655
|
body.append(s);
|
|
11315
11656
|
}
|
|
11316
11657
|
|
|
11658
|
+
const hand = handEditSection(item);
|
|
11659
|
+
if (hand) body.append(hand);
|
|
11660
|
+
|
|
11317
11661
|
if (item.revision || item.revisionParentKey) {
|
|
11318
11662
|
const { s, dl } = section('Lineage');
|
|
11319
11663
|
row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
|
|
@@ -11562,7 +11906,7 @@ if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
|
11562
11906
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
11563
11907
|
|
|
11564
11908
|
// src/gallery/server.ts
|
|
11565
|
-
import { spawn as
|
|
11909
|
+
import { spawn as spawn3 } from "child_process";
|
|
11566
11910
|
import { randomBytes } from "crypto";
|
|
11567
11911
|
import { createServer as createServer2 } from "http";
|
|
11568
11912
|
import { readFile as readFile19 } from "fs/promises";
|
|
@@ -11785,7 +12129,7 @@ async function serveGallery(opts) {
|
|
|
11785
12129
|
});
|
|
11786
12130
|
opts.onReady?.(url);
|
|
11787
12131
|
if (opts.open !== false && process.platform === "darwin") {
|
|
11788
|
-
|
|
12132
|
+
spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
11789
12133
|
}
|
|
11790
12134
|
return {
|
|
11791
12135
|
url,
|
|
@@ -11799,14 +12143,14 @@ async function serveGallery(opts) {
|
|
|
11799
12143
|
|
|
11800
12144
|
// src/gallery/generate.ts
|
|
11801
12145
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
11802
|
-
import { z as
|
|
11803
|
-
var GenerateRequestSchema =
|
|
11804
|
-
project:
|
|
11805
|
-
keys:
|
|
12146
|
+
import { z as z9 } from "zod";
|
|
12147
|
+
var GenerateRequestSchema = z9.object({
|
|
12148
|
+
project: z9.string().min(1).optional(),
|
|
12149
|
+
keys: z9.array(z9.string().min(1)).min(1).max(500),
|
|
11806
12150
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
11807
|
-
force:
|
|
12151
|
+
force: z9.boolean().optional(),
|
|
11808
12152
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
11809
|
-
resume:
|
|
12153
|
+
resume: z9.boolean().optional()
|
|
11810
12154
|
}).strict();
|
|
11811
12155
|
var GenerateRequestError = class extends Error {
|
|
11812
12156
|
constructor(message6, status = 400) {
|
|
@@ -12044,15 +12388,15 @@ function createGenerateHandlers(opts) {
|
|
|
12044
12388
|
|
|
12045
12389
|
// src/pipeline/salvage.ts
|
|
12046
12390
|
import { readFile as readFile20 } from "fs/promises";
|
|
12047
|
-
import { existsSync as
|
|
12048
|
-
import
|
|
12391
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12392
|
+
import path25 from "path";
|
|
12049
12393
|
function providerClaimId(provider, objectId) {
|
|
12050
12394
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12051
12395
|
}
|
|
12052
12396
|
async function loadClaims(lockPaths, opts = {}) {
|
|
12053
12397
|
const claimed = /* @__PURE__ */ new Set();
|
|
12054
12398
|
for (const p of lockPaths) {
|
|
12055
|
-
if (!
|
|
12399
|
+
if (!existsSync20(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12056
12400
|
let parsed;
|
|
12057
12401
|
try {
|
|
12058
12402
|
parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
|
|
@@ -12108,20 +12452,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
12108
12452
|
return matchStyleByPattern(prompt, manifest);
|
|
12109
12453
|
}
|
|
12110
12454
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12111
|
-
const own =
|
|
12455
|
+
const own = path25.resolve(ownManifestPath);
|
|
12112
12456
|
const siblingManifestPaths = [
|
|
12113
12457
|
.../* @__PURE__ */ new Set([
|
|
12114
12458
|
...workspaceManifestPaths,
|
|
12115
|
-
...claimPaths.map((c) =>
|
|
12459
|
+
...claimPaths.map((c) => path25.join(path25.dirname(path25.resolve(c)), "pixelkiln.manifest.json"))
|
|
12116
12460
|
])
|
|
12117
12461
|
];
|
|
12118
12462
|
const siblings = [];
|
|
12119
12463
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12120
|
-
if (
|
|
12121
|
-
if (!
|
|
12464
|
+
if (path25.resolve(siblingManifestPath) === own) continue;
|
|
12465
|
+
if (!existsSync20(siblingManifestPath)) continue;
|
|
12122
12466
|
try {
|
|
12123
12467
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12124
|
-
siblings.push({ label:
|
|
12468
|
+
siblings.push({ label: path25.basename(path25.dirname(siblingManifestPath)), manifest });
|
|
12125
12469
|
} catch {
|
|
12126
12470
|
}
|
|
12127
12471
|
}
|
|
@@ -12237,8 +12581,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
12237
12581
|
}
|
|
12238
12582
|
|
|
12239
12583
|
// src/pick/salvage-server.ts
|
|
12240
|
-
import { mkdir as
|
|
12241
|
-
import
|
|
12584
|
+
import { mkdir as mkdir7, writeFile as writeFile9, readFile as readFile21 } from "fs/promises";
|
|
12585
|
+
import path26 from "path";
|
|
12242
12586
|
|
|
12243
12587
|
// src/pick/salvage-sheet.ts
|
|
12244
12588
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -12403,7 +12747,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12403
12747
|
});
|
|
12404
12748
|
const html = renderSalvageSheet(orphans, {
|
|
12405
12749
|
styleId: ctx.styleId,
|
|
12406
|
-
importDir:
|
|
12750
|
+
importDir: path26.relative(process.cwd(), ctx.importDir) || "."
|
|
12407
12751
|
});
|
|
12408
12752
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
12409
12753
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -12432,13 +12776,14 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12432
12776
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
12433
12777
|
decodePng(buf);
|
|
12434
12778
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
12435
|
-
const rel =
|
|
12436
|
-
const outFile =
|
|
12437
|
-
await
|
|
12779
|
+
const rel = path26.join("_salvaged", `${assetId}.png`);
|
|
12780
|
+
const outFile = path26.resolve(ctx.importDir, rel);
|
|
12781
|
+
await mkdir7(path26.dirname(outFile), { recursive: true });
|
|
12438
12782
|
await writeFile9(outFile, buf);
|
|
12439
12783
|
ctx.manifest.assets[assetId] = {
|
|
12440
12784
|
prompt: orphan.prompt,
|
|
12441
12785
|
promptByStyle: {},
|
|
12786
|
+
sourceByStyle: {},
|
|
12442
12787
|
category: "_salvaged",
|
|
12443
12788
|
file: rel,
|
|
12444
12789
|
tags: ["salvaged"],
|
|
@@ -12466,7 +12811,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12466
12811
|
sourceUrl: durableSource,
|
|
12467
12812
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
12468
12813
|
outputs: [{
|
|
12469
|
-
path: portableOutputPath(outFile,
|
|
12814
|
+
path: portableOutputPath(outFile, path26.dirname(ctx.manifestPath)),
|
|
12470
12815
|
sha256: sha256(buf)
|
|
12471
12816
|
}],
|
|
12472
12817
|
submittedAt: orphan.createdAt,
|