pixelkiln 0.26.0 → 0.27.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/README.md +2 -2
- package/SECURITY.md +12 -2
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +1135 -307
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +489 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.js +502 -44
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/ARTIFACTS.md +25 -0
- package/docs/CLI.md +54 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9030,7 +9030,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
9030
9030
|
// src/gallery/snapshot.ts
|
|
9031
9031
|
import { createHash as createHash4 } from "crypto";
|
|
9032
9032
|
import { existsSync as existsSync19 } from "fs";
|
|
9033
|
-
import { readFile as
|
|
9033
|
+
import { readFile as readFile19, stat as stat3 } from "fs/promises";
|
|
9034
9034
|
import path24 from "path";
|
|
9035
9035
|
|
|
9036
9036
|
// src/workspace.ts
|
|
@@ -9168,7 +9168,7 @@ function validateWorkspace(ws, dir) {
|
|
|
9168
9168
|
|
|
9169
9169
|
// src/gallery/edit.ts
|
|
9170
9170
|
import path23 from "path";
|
|
9171
|
-
import { z as
|
|
9171
|
+
import { z as z9 } from "zod";
|
|
9172
9172
|
|
|
9173
9173
|
// src/manifest-edit.ts
|
|
9174
9174
|
import { readFile as readFile17, rename as rename6, rm as rm8, writeFile as writeFile8 } from "fs/promises";
|
|
@@ -9422,8 +9422,9 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9422
9422
|
// src/pipeline/hand-edit.ts
|
|
9423
9423
|
import { spawn as spawn2 } from "child_process";
|
|
9424
9424
|
import { existsSync as existsSync18 } from "fs";
|
|
9425
|
-
import { copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
9425
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile18, rename as rename7, rm as rm9, writeFile as writeFile9 } from "fs/promises";
|
|
9426
9426
|
import path22 from "path";
|
|
9427
|
+
import { z as z8 } from "zod";
|
|
9427
9428
|
var HAND_EDIT_DIR = "edits";
|
|
9428
9429
|
function handEditPath(loaded, spec) {
|
|
9429
9430
|
const style = loaded.manifest.styles[spec.styleId];
|
|
@@ -9493,6 +9494,89 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
9493
9494
|
});
|
|
9494
9495
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
9495
9496
|
}
|
|
9497
|
+
var HandEditCompanionSchema = z8.object({
|
|
9498
|
+
version: z8.literal(1),
|
|
9499
|
+
/** sha256 of the generated art the edit started from; null when it started from untracked art. */
|
|
9500
|
+
basedOn: z8.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
9501
|
+
/** sha256 of the edit PNG as saved, so a later change by another tool is visible. */
|
|
9502
|
+
sha256: z8.string().regex(/^[0-9a-f]{64}$/),
|
|
9503
|
+
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
9504
|
+
editor: z8.string().min(1),
|
|
9505
|
+
protocol: z8.number().int().positive(),
|
|
9506
|
+
savedAt: z8.string().datetime(),
|
|
9507
|
+
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
9508
|
+
project: z8.string().min(1).nullable()
|
|
9509
|
+
}).strict();
|
|
9510
|
+
function handEditCompanionPath(editPath) {
|
|
9511
|
+
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
9512
|
+
}
|
|
9513
|
+
function handEditProjectPath(editPath) {
|
|
9514
|
+
return editPath.replace(/\.png$/i, "") + ".pxo";
|
|
9515
|
+
}
|
|
9516
|
+
async function readHandEditCompanion(editPath) {
|
|
9517
|
+
let text;
|
|
9518
|
+
try {
|
|
9519
|
+
text = await readFile18(handEditCompanionPath(editPath), "utf8");
|
|
9520
|
+
} catch {
|
|
9521
|
+
return null;
|
|
9522
|
+
}
|
|
9523
|
+
try {
|
|
9524
|
+
return HandEditCompanionSchema.parse(JSON.parse(text));
|
|
9525
|
+
} catch {
|
|
9526
|
+
return null;
|
|
9527
|
+
}
|
|
9528
|
+
}
|
|
9529
|
+
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
9530
|
+
async function saveHandEdit(loaded, lock, spec, input) {
|
|
9531
|
+
if (input.png.length > MAX_HAND_EDIT_BYTES) {
|
|
9532
|
+
throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9533
|
+
}
|
|
9534
|
+
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
9535
|
+
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9536
|
+
}
|
|
9537
|
+
let decoded;
|
|
9538
|
+
try {
|
|
9539
|
+
validateMedia(input.png, MediaType.PNG);
|
|
9540
|
+
decoded = decodePng(input.png);
|
|
9541
|
+
} catch (error) {
|
|
9542
|
+
throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
9543
|
+
}
|
|
9544
|
+
const base = handEditBase(spec, lock);
|
|
9545
|
+
const expected = base ? decodePng(await readFile18(base)) : { width: spec.width, height: spec.height };
|
|
9546
|
+
if (decoded.width !== expected.width || decoded.height !== expected.height) {
|
|
9547
|
+
throw new ManifestEditError(
|
|
9548
|
+
`the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
|
|
9549
|
+
);
|
|
9550
|
+
}
|
|
9551
|
+
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
9552
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9553
|
+
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
9554
|
+
const digest2 = sha256(input.png);
|
|
9555
|
+
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
9556
|
+
const companion = {
|
|
9557
|
+
version: 1,
|
|
9558
|
+
basedOn,
|
|
9559
|
+
sha256: digest2,
|
|
9560
|
+
editor: input.editor,
|
|
9561
|
+
protocol: input.protocol,
|
|
9562
|
+
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9563
|
+
project: projectPath ? path22.basename(projectPath) : null
|
|
9564
|
+
};
|
|
9565
|
+
await replaceFile(started.editPath, input.png);
|
|
9566
|
+
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
9567
|
+
const companionPath = handEditCompanionPath(started.editPath);
|
|
9568
|
+
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
9569
|
+
return { ...started, sha256: digest2, basedOn, companionPath, projectPath };
|
|
9570
|
+
}
|
|
9571
|
+
async function replaceFile(file, bytes) {
|
|
9572
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9573
|
+
try {
|
|
9574
|
+
await writeFile9(tmp, bytes);
|
|
9575
|
+
await rename7(tmp, file);
|
|
9576
|
+
} finally {
|
|
9577
|
+
await rm9(tmp, { force: true });
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9496
9580
|
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
9497
9581
|
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
9498
9582
|
const [program, ...args] = command;
|
|
@@ -9504,22 +9588,37 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
9504
9588
|
}
|
|
9505
9589
|
|
|
9506
9590
|
// src/gallery/edit.ts
|
|
9507
|
-
var
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9591
|
+
var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
9592
|
+
var HandEditRequestSchema = z9.discriminatedUnion("action", [
|
|
9593
|
+
z9.object({
|
|
9594
|
+
action: z9.literal("start-edit"),
|
|
9595
|
+
project: z9.string().min(1).optional(),
|
|
9596
|
+
assetId: z9.string().min(1),
|
|
9597
|
+
styleId: z9.string().min(1),
|
|
9598
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
9514
9599
|
/** Also launch the author's editor on the file. */
|
|
9515
|
-
open:
|
|
9600
|
+
open: z9.boolean().optional()
|
|
9516
9601
|
}).strict(),
|
|
9517
|
-
|
|
9518
|
-
action:
|
|
9519
|
-
project:
|
|
9520
|
-
assetId:
|
|
9521
|
-
styleId:
|
|
9522
|
-
expectedSha256:
|
|
9602
|
+
z9.object({
|
|
9603
|
+
action: z9.literal("detach-edit"),
|
|
9604
|
+
project: z9.string().min(1).optional(),
|
|
9605
|
+
assetId: z9.string().min(1),
|
|
9606
|
+
styleId: z9.string().min(1),
|
|
9607
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/)
|
|
9608
|
+
}).strict(),
|
|
9609
|
+
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
9610
|
+
z9.object({
|
|
9611
|
+
action: z9.literal("save-edit"),
|
|
9612
|
+
project: z9.string().min(1).optional(),
|
|
9613
|
+
assetId: z9.string().min(1),
|
|
9614
|
+
styleId: z9.string().min(1),
|
|
9615
|
+
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
9616
|
+
png: Base64.min(1),
|
|
9617
|
+
/** The editor's layered project file, kept beside the edit. */
|
|
9618
|
+
pxo: Base64.optional(),
|
|
9619
|
+
/** Editor identity from the bridge's ready message. */
|
|
9620
|
+
editor: z9.string().min(1).max(200),
|
|
9621
|
+
protocol: z9.number().int().positive()
|
|
9523
9622
|
}).strict()
|
|
9524
9623
|
]);
|
|
9525
9624
|
function createGalleryEditHandler(opts) {
|
|
@@ -9540,6 +9639,15 @@ function createGalleryEditHandler(opts) {
|
|
|
9540
9639
|
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
9541
9640
|
log(` opened with: ${command}`);
|
|
9542
9641
|
}
|
|
9642
|
+
} else if (request.action === "save-edit") {
|
|
9643
|
+
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
9644
|
+
png: Buffer.from(request.png, "base64"),
|
|
9645
|
+
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
9646
|
+
editor: request.editor,
|
|
9647
|
+
protocol: request.protocol,
|
|
9648
|
+
expectedSha256: request.expectedSha256
|
|
9649
|
+
});
|
|
9650
|
+
log(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
9543
9651
|
} else {
|
|
9544
9652
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
9545
9653
|
if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -9689,7 +9797,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9689
9797
|
const media = /* @__PURE__ */ new Map();
|
|
9690
9798
|
const plan = await buildPlan(specs, lock);
|
|
9691
9799
|
const items = [];
|
|
9692
|
-
const manifestText = await
|
|
9800
|
+
const manifestText = await readFile19(loaded.path, "utf8");
|
|
9693
9801
|
const manifestSha256 = sha256(manifestText);
|
|
9694
9802
|
let rawStyles = {};
|
|
9695
9803
|
try {
|
|
@@ -9704,6 +9812,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9704
9812
|
let outputs;
|
|
9705
9813
|
let edit = null;
|
|
9706
9814
|
let editStatus = null;
|
|
9815
|
+
let editMeta = null;
|
|
9707
9816
|
if (entry && entry.outputs.length) {
|
|
9708
9817
|
outputs = await Promise.all(
|
|
9709
9818
|
entry.outputs.map(
|
|
@@ -9715,7 +9824,19 @@ async function buildGallerySnapshot(opts) {
|
|
|
9715
9824
|
const editSha = existsSync19(editPath) ? await sha256File(editPath) : null;
|
|
9716
9825
|
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
9717
9826
|
const generated = outputs[0];
|
|
9718
|
-
|
|
9827
|
+
const companion = await readHandEditCompanion(editPath);
|
|
9828
|
+
if (companion) {
|
|
9829
|
+
const projectPath = handEditProjectPath(editPath);
|
|
9830
|
+
editMeta = {
|
|
9831
|
+
editor: companion.editor,
|
|
9832
|
+
savedAt: companion.savedAt,
|
|
9833
|
+
basedOn: companion.basedOn,
|
|
9834
|
+
changedSince: editSha !== null && editSha !== companion.sha256,
|
|
9835
|
+
project: companion.project && existsSync19(projectPath) ? portableOutputPath(projectPath, root) : null
|
|
9836
|
+
};
|
|
9837
|
+
}
|
|
9838
|
+
const regenerated = companion ? generated?.sha256 !== void 0 && generated.sha256 !== null && companion.basedOn !== generated.sha256 : Boolean(generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists);
|
|
9839
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
9719
9840
|
}
|
|
9720
9841
|
} else if (spec.source) {
|
|
9721
9842
|
outputs = [await describeOutput(media, root, path24.resolve(root, spec.source))];
|
|
@@ -9765,6 +9886,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9765
9886
|
source: spec.source ?? null,
|
|
9766
9887
|
edit,
|
|
9767
9888
|
editStatus,
|
|
9889
|
+
editMeta,
|
|
9768
9890
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9769
9891
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9770
9892
|
tags: spec.tags,
|
|
@@ -9821,6 +9943,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9821
9943
|
source: null,
|
|
9822
9944
|
edit: null,
|
|
9823
9945
|
editStatus: null,
|
|
9946
|
+
editMeta: null,
|
|
9824
9947
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9825
9948
|
refreshable: false,
|
|
9826
9949
|
tags: [],
|
|
@@ -9962,7 +10085,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9962
10085
|
root: loaded.root,
|
|
9963
10086
|
provider: loaded.manifest.provider,
|
|
9964
10087
|
account: project.account ?? null,
|
|
9965
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
10088
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
|
|
9966
10089
|
entries: Object.keys(lock.entries).length,
|
|
9967
10090
|
items: projectItems.length,
|
|
9968
10091
|
spendByUnit: spendByUnit(lock),
|
|
@@ -10022,6 +10145,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10022
10145
|
const session = JSON.stringify(opts.session ?? null);
|
|
10023
10146
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
10024
10147
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
10148
|
+
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
10025
10149
|
const title = `pixelkiln \u2014 ${snapshot.project?.name ?? "workspace"}`;
|
|
10026
10150
|
return `<!doctype html>
|
|
10027
10151
|
<html lang="en">
|
|
@@ -10227,6 +10351,15 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10227
10351
|
.job .acts button { padding:3px 9px; font-size:12px; }
|
|
10228
10352
|
.job pre { grid-column:1 / -1; margin:0; padding:8px 10px; border-top:1px solid var(--line);
|
|
10229
10353
|
font:11.5px/1.45 var(--mono); color:var(--dim); max-height:220px; overflow:auto; white-space:pre-wrap; }
|
|
10354
|
+
#tools { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; }
|
|
10355
|
+
#tools:empty { display:none; }
|
|
10356
|
+
.tool { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
|
|
10357
|
+
grid-template-columns:auto 1fr auto; gap:6px 14px; align-items:center; font-size:12.5px; }
|
|
10358
|
+
.tool .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1 var(--mono); white-space:nowrap; }
|
|
10359
|
+
.tool .last { color:var(--dim); min-width:0; }
|
|
10360
|
+
.tool .acts { display:flex; gap:6px; }
|
|
10361
|
+
.tool .acts button { padding:3px 9px; font-size:12px; }
|
|
10362
|
+
.tool progress { grid-column:1 / -1; width:100%; height:6px; accent-color:var(--accent); }
|
|
10230
10363
|
.budget { color:var(--dim); }
|
|
10231
10364
|
.budget b { color:var(--text); }
|
|
10232
10365
|
.dialog { position:fixed; inset:0; z-index:30; display:grid; place-items:center; background:rgba(0,0,0,.55); }
|
|
@@ -10247,6 +10380,16 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10247
10380
|
border-left:1px solid var(--line-strong); display:grid; grid-template-rows:auto 1fr; min-width:0; }
|
|
10248
10381
|
.sheet.review-host { width:min(1180px, 94vw); }
|
|
10249
10382
|
.sheet.compare-host { width:min(1500px, 96vw); }
|
|
10383
|
+
.sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
|
|
10384
|
+
.sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
|
|
10385
|
+
.sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
|
10386
|
+
.sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
|
|
10387
|
+
.sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
|
|
10388
|
+
.sheet.editor-host .stage { position:relative; min-height:0; }
|
|
10389
|
+
.sheet.editor-host .stage iframe { position:absolute; inset:0; }
|
|
10390
|
+
.sheet.editor-host .loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--bg);
|
|
10391
|
+
color:var(--dim); text-align:center; padding:24px; }
|
|
10392
|
+
.sheet.editor-host .loading b { display:block; color:var(--text); margin-bottom:6px; }
|
|
10250
10393
|
.sheet .rbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line-strong);
|
|
10251
10394
|
background:var(--panel); font-size:13px; flex-wrap:wrap; }
|
|
10252
10395
|
.sheet .rbar span { color:var(--dim); }
|
|
@@ -10285,7 +10428,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10285
10428
|
.tray .pick img { width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
|
|
10286
10429
|
.tray .pick button { padding:0 5px; border-color:transparent; color:var(--dim); font-size:12px; }
|
|
10287
10430
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10288
|
-
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
10431
|
+
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
10289
10432
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10290
10433
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10291
10434
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
@@ -10333,6 +10476,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10333
10476
|
<div class="totals" id="totals"></div>
|
|
10334
10477
|
<div class="chips" id="chips"></div>
|
|
10335
10478
|
<div id="jobs"></div>
|
|
10479
|
+
<div id="tools"></div>
|
|
10336
10480
|
</header>
|
|
10337
10481
|
<main id="root"></main>
|
|
10338
10482
|
<footer>
|
|
@@ -10351,6 +10495,7 @@ const INITIAL = ${data};
|
|
|
10351
10495
|
const SESSION = ${session};
|
|
10352
10496
|
const EDITABLE = ${editable};
|
|
10353
10497
|
const GENERATION = ${generation};
|
|
10498
|
+
const EDITOR = ${editor};
|
|
10354
10499
|
let snap = INITIAL;
|
|
10355
10500
|
const STATE_TONE = {
|
|
10356
10501
|
ok: 'ok', stale: 'warn', orphaned: 'warn', untracked: 'warn', blocked: 'warn',
|
|
@@ -10518,6 +10663,92 @@ function renderJobs() {
|
|
|
10518
10663
|
host.append(row);
|
|
10519
10664
|
}
|
|
10520
10665
|
}
|
|
10666
|
+
// ---- the in-browser editor (only when the server serves one) -------------
|
|
10667
|
+
// The build is a 46 MB web export fetched once into a user-level cache and
|
|
10668
|
+
// verified against hashes this PixelKiln release carries; the page only
|
|
10669
|
+
// reports and asks, the server downloads. Nothing is fetched without a click.
|
|
10670
|
+
|
|
10671
|
+
let ED = null;
|
|
10672
|
+
let edTimer = null;
|
|
10673
|
+
const fmtMb = (bytes) => (bytes / 1e6).toFixed(1) + ' MB';
|
|
10674
|
+
async function pollEditor() {
|
|
10675
|
+
if (!EDITOR) return;
|
|
10676
|
+
try {
|
|
10677
|
+
const res = await fetch('/api/editor', { cache: 'no-store' });
|
|
10678
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10679
|
+
ED = await res.json();
|
|
10680
|
+
} catch (err) {
|
|
10681
|
+
ED = { error: 'status unavailable: ' + err.message, installed: false, installing: null, missing: [], totalBytes: 0 };
|
|
10682
|
+
}
|
|
10683
|
+
renderTools();
|
|
10684
|
+
clearTimeout(edTimer);
|
|
10685
|
+
if (ED.installing) edTimer = setTimeout(pollEditor, 1000);
|
|
10686
|
+
else if (ui.open) renderDrawer();
|
|
10687
|
+
}
|
|
10688
|
+
async function installEditor() {
|
|
10689
|
+
try {
|
|
10690
|
+
const res = await fetch('/api/editor/install', {
|
|
10691
|
+
method: 'POST',
|
|
10692
|
+
headers: { 'Content-Type': 'application/json', 'X-Pixelkiln-Session': SESSION },
|
|
10693
|
+
body: '{}',
|
|
10694
|
+
});
|
|
10695
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10696
|
+
ED = await res.json();
|
|
10697
|
+
} catch (err) {
|
|
10698
|
+
ED = Object.assign({}, ED, { error: err.message });
|
|
10699
|
+
}
|
|
10700
|
+
renderTools();
|
|
10701
|
+
clearTimeout(edTimer);
|
|
10702
|
+
edTimer = setTimeout(pollEditor, 500);
|
|
10703
|
+
}
|
|
10704
|
+
function renderTools() {
|
|
10705
|
+
const host = $('tools');
|
|
10706
|
+
host.textContent = '';
|
|
10707
|
+
if (!EDITOR || !ED) return;
|
|
10708
|
+
const row = el('div', 'tool');
|
|
10709
|
+
const ph = el('span', 'ph');
|
|
10710
|
+
const last = el('span', 'last');
|
|
10711
|
+
const acts = el('div', 'acts');
|
|
10712
|
+
const name = 'Browser editor' + (ED.pixelorama ? ' (Pixelorama ' + ED.pixelorama + ')' : '');
|
|
10713
|
+
if (ED.installing) {
|
|
10714
|
+
const p = ED.installing;
|
|
10715
|
+
ph.append(el('i', 'dot cool'), document.createTextNode('installing'));
|
|
10716
|
+
last.textContent = name + ': fetching ' + (p.file || 'the build') + ' \u2014 ' + fmtMb(p.fetchedBytes) + ' of ' + fmtMb(p.totalBytes);
|
|
10717
|
+
row.append(ph, last, acts);
|
|
10718
|
+
const bar = el('progress');
|
|
10719
|
+
if (p.totalBytes) { bar.max = p.totalBytes; bar.value = p.fetchedBytes; }
|
|
10720
|
+
row.append(bar);
|
|
10721
|
+
} else if (ED.installed) {
|
|
10722
|
+
ph.append(el('i', 'dot ok'), document.createTextNode('ready'));
|
|
10723
|
+
last.textContent = name + ' is installed and verified; a record\u2019s Hand edit section opens sprites in it.';
|
|
10724
|
+
last.title = ED.dir || '';
|
|
10725
|
+
const b = el('button', null, 'Open editor');
|
|
10726
|
+
b.type = 'button'; b.onclick = () => window.open(ED.url, '_blank', 'noopener');
|
|
10727
|
+
acts.append(b);
|
|
10728
|
+
row.append(ph, last, acts);
|
|
10729
|
+
} else if (ED.error) {
|
|
10730
|
+
ph.append(el('i', 'dot bad'), document.createTextNode('failed'));
|
|
10731
|
+
last.textContent = name + ': ' + ED.error;
|
|
10732
|
+
const b = el('button', 'primary', 'Retry install');
|
|
10733
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10734
|
+
acts.append(b);
|
|
10735
|
+
row.append(ph, last, acts);
|
|
10736
|
+
} else if (!ED.release) {
|
|
10737
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('unavailable'));
|
|
10738
|
+
last.textContent = name + ': this PixelKiln version pins no published build.';
|
|
10739
|
+
row.append(ph, last, acts);
|
|
10740
|
+
} else {
|
|
10741
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('not installed'));
|
|
10742
|
+
last.textContent = name + ': ' + fmtMb(ED.totalBytes - (ED.installedBytes || 0)) + ' fetched once from the PixelKiln release ' +
|
|
10743
|
+
ED.release + ', verified against the hashes this version carries, and kept outside the project.';
|
|
10744
|
+
const b = el('button', 'primary', 'Install editor');
|
|
10745
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10746
|
+
acts.append(b);
|
|
10747
|
+
row.append(ph, last, acts);
|
|
10748
|
+
}
|
|
10749
|
+
host.append(row);
|
|
10750
|
+
}
|
|
10751
|
+
|
|
10521
10752
|
function budgetLine() {
|
|
10522
10753
|
const providers = new Set([...Object.keys(GEN.budget.byProvider), ...Object.keys(GEN.spent)]);
|
|
10523
10754
|
const parts = [];
|
|
@@ -10624,8 +10855,169 @@ function openReview(jobId) {
|
|
|
10624
10855
|
host.append(scrim, panel);
|
|
10625
10856
|
}
|
|
10626
10857
|
window.addEventListener('message', (e) => {
|
|
10627
|
-
if (e.origin !== location.origin || !e.data || e.data.type !== '
|
|
10628
|
-
|
|
10858
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== 'string') return;
|
|
10859
|
+
if (e.data.type === 'pixelkiln:review-applied') {
|
|
10860
|
+
setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
|
|
10861
|
+
return;
|
|
10862
|
+
}
|
|
10863
|
+
if (SHEET && e.source === SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
10864
|
+
});
|
|
10865
|
+
|
|
10866
|
+
// ---- the in-browser editor sheet -------------------------------------------
|
|
10867
|
+
// The page is the protocol host: it hands the editor the PNG and palette,
|
|
10868
|
+
// asks it for the image back, and does the authenticated write itself. The
|
|
10869
|
+
// iframe never sees the session token, and the server validates, sizes, and
|
|
10870
|
+
// declares the file the same way pixelkiln edit does.
|
|
10871
|
+
|
|
10872
|
+
let SHEET = null;
|
|
10873
|
+
const editorSource = (item) => item.edit && item.edit.exists ? item.edit
|
|
10874
|
+
: item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
|
|
10875
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
|
|
10876
|
+
!(item.outputs.length > 1);
|
|
10877
|
+
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
10878
|
+
const r = new FileReader();
|
|
10879
|
+
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
10880
|
+
r.onerror = () => reject(r.error);
|
|
10881
|
+
r.readAsDataURL(new Blob([buf]));
|
|
10882
|
+
});
|
|
10883
|
+
function sheetStatus(tone, text) {
|
|
10884
|
+
if (!SHEET) return;
|
|
10885
|
+
SHEET.status.textContent = '';
|
|
10886
|
+
SHEET.status.append(el('i', 'dot ' + tone), document.createTextNode(text));
|
|
10887
|
+
}
|
|
10888
|
+
function sheetButtons() {
|
|
10889
|
+
if (!SHEET) return;
|
|
10890
|
+
const busy = !SHEET.opened || SHEET.pending !== null;
|
|
10891
|
+
SHEET.save.disabled = busy || !SHEET.dirty;
|
|
10892
|
+
SHEET.saveClose.disabled = busy;
|
|
10893
|
+
}
|
|
10894
|
+
function openEditorSheet(item) {
|
|
10895
|
+
if (SHEET) return;
|
|
10896
|
+
const src = editorSource(item);
|
|
10897
|
+
const host = $('dialog-host');
|
|
10898
|
+
host.textContent = '';
|
|
10899
|
+
const scrim = el('div', 'sheet-scrim');
|
|
10900
|
+
scrim.onclick = () => closeEditorSheet();
|
|
10901
|
+
const panel = el('div', 'sheet editor-host');
|
|
10902
|
+
const bar = el('div', 'rbar');
|
|
10903
|
+
const close = el('button', null, 'Close'); close.type = 'button';
|
|
10904
|
+
close.onclick = () => closeEditorSheet();
|
|
10905
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
10906
|
+
const status = el('span', 'st');
|
|
10907
|
+
const acts = el('div', 'acts');
|
|
10908
|
+
const msg = el('span', 'msg');
|
|
10909
|
+
const save = el('button', 'primary', 'Save to project'); save.type = 'button';
|
|
10910
|
+
save.onclick = () => requestEditorSave(false);
|
|
10911
|
+
const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
|
|
10912
|
+
saveClose.onclick = () => requestEditorSave(true);
|
|
10913
|
+
acts.append(msg, save, saveClose);
|
|
10914
|
+
bar.append(close, title, status, acts);
|
|
10915
|
+
const stage = el('div', 'stage');
|
|
10916
|
+
const frame = el('iframe');
|
|
10917
|
+
frame.title = 'Pixelorama';
|
|
10918
|
+
frame.src = ED.url;
|
|
10919
|
+
const loading = el('div', 'loading');
|
|
10920
|
+
const loadText = el('div');
|
|
10921
|
+
loadText.append(el('b', null, 'Loading the editor'), document.createTextNode('The first open compiles a 40 MB build; later opens come from the browser cache.'));
|
|
10922
|
+
loading.append(loadText);
|
|
10923
|
+
stage.append(frame, loading);
|
|
10924
|
+
panel.append(bar, stage);
|
|
10925
|
+
host.append(scrim, panel);
|
|
10926
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src };
|
|
10927
|
+
sheetStatus('cool', 'loading');
|
|
10928
|
+
sheetButtons();
|
|
10929
|
+
close.focus({ preventScroll: true });
|
|
10930
|
+
}
|
|
10931
|
+
async function sendOpen() {
|
|
10932
|
+
const { item, source } = SHEET;
|
|
10933
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
10934
|
+
let png;
|
|
10935
|
+
try {
|
|
10936
|
+
const res = await fetch(source.url, { cache: 'no-store' });
|
|
10937
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10938
|
+
png = await res.arrayBuffer();
|
|
10939
|
+
} catch (err) {
|
|
10940
|
+
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
10941
|
+
return;
|
|
10942
|
+
}
|
|
10943
|
+
if (!SHEET) return;
|
|
10944
|
+
const request = 'open-' + (++SHEET.requests);
|
|
10945
|
+
SHEET.frame.contentWindow.postMessage({
|
|
10946
|
+
type: 'pixelkiln:open', request,
|
|
10947
|
+
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
10948
|
+
png, palette: style ? style.palette : [],
|
|
10949
|
+
}, location.origin, [png]);
|
|
10950
|
+
sheetStatus('cool', 'opening');
|
|
10951
|
+
}
|
|
10952
|
+
function requestEditorSave(close) {
|
|
10953
|
+
if (!SHEET || !SHEET.opened || SHEET.pending) return;
|
|
10954
|
+
const request = 'save-' + (++SHEET.requests);
|
|
10955
|
+
SHEET.pending = { request, close };
|
|
10956
|
+
SHEET.msg.textContent = '';
|
|
10957
|
+
sheetStatus('cool', 'saving');
|
|
10958
|
+
sheetButtons();
|
|
10959
|
+
SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:request-save', request }, location.origin);
|
|
10960
|
+
}
|
|
10961
|
+
async function onEditorMessage(m) {
|
|
10962
|
+
switch (m.type) {
|
|
10963
|
+
case 'pixelkiln:ready':
|
|
10964
|
+
SHEET.ready = m;
|
|
10965
|
+
if (m.version !== ED.protocol) {
|
|
10966
|
+
SHEET.msg.textContent = 'The editor speaks protocol ' + m.version + '; this gallery expects ' + ED.protocol + '. Reinstall it with pixelkiln tools install editor.';
|
|
10967
|
+
sheetStatus('bad', 'mismatch');
|
|
10968
|
+
return;
|
|
10969
|
+
}
|
|
10970
|
+
sendOpen();
|
|
10971
|
+
break;
|
|
10972
|
+
case 'pixelkiln:opened':
|
|
10973
|
+
SHEET.opened = true;
|
|
10974
|
+
SHEET.loading.remove();
|
|
10975
|
+
sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
10976
|
+
sheetButtons();
|
|
10977
|
+
break;
|
|
10978
|
+
case 'pixelkiln:dirty':
|
|
10979
|
+
SHEET.dirty = !!m.dirty;
|
|
10980
|
+
if (SHEET.opened && !SHEET.pending) sheetStatus(SHEET.dirty ? 'warn' : 'ok', SHEET.dirty ? 'unsaved changes' : 'saved');
|
|
10981
|
+
sheetButtons();
|
|
10982
|
+
break;
|
|
10983
|
+
case 'pixelkiln:save': {
|
|
10984
|
+
const pending = SHEET.pending;
|
|
10985
|
+
if (!pending || pending.request !== m.request) return;
|
|
10986
|
+
try {
|
|
10987
|
+
const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
10988
|
+
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
10989
|
+
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
10990
|
+
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
10991
|
+
if (saved) SHEET.item = saved;
|
|
10992
|
+
SHEET.dirty = false;
|
|
10993
|
+
SHEET.pending = null;
|
|
10994
|
+
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place it in place of the generated art.' };
|
|
10995
|
+
render();
|
|
10996
|
+
if (pending.close) { closeEditorSheet(true); return; }
|
|
10997
|
+
sheetStatus('ok', 'saved');
|
|
10998
|
+
} catch (err) {
|
|
10999
|
+
SHEET.pending = null;
|
|
11000
|
+
SHEET.msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh, then save again.' : '');
|
|
11001
|
+
sheetStatus('bad', 'not saved');
|
|
11002
|
+
}
|
|
11003
|
+
sheetButtons();
|
|
11004
|
+
break;
|
|
11005
|
+
}
|
|
11006
|
+
case 'pixelkiln:error':
|
|
11007
|
+
SHEET.msg.textContent = m.message || 'The editor reported an error.';
|
|
11008
|
+
if (SHEET.pending) { SHEET.pending = null; sheetStatus('bad', 'not saved'); sheetButtons(); }
|
|
11009
|
+
break;
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
function closeEditorSheet(force) {
|
|
11013
|
+
if (!SHEET) return;
|
|
11014
|
+
if (SHEET.dirty && !force && !confirm('Discard unsaved changes in the editor?')) return;
|
|
11015
|
+
SHEET = null;
|
|
11016
|
+
$('dialog-host').textContent = '';
|
|
11017
|
+
refresh();
|
|
11018
|
+
}
|
|
11019
|
+
window.addEventListener('beforeunload', (e) => {
|
|
11020
|
+
if (SHEET && SHEET.dirty) { e.preventDefault(); e.returnValue = ''; }
|
|
10629
11021
|
});
|
|
10630
11022
|
|
|
10631
11023
|
function visibleItems() {
|
|
@@ -11201,6 +11593,11 @@ function handEditSection(item) {
|
|
|
11201
11593
|
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
11202
11594
|
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
11203
11595
|
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
11596
|
+
if (item.editMeta) {
|
|
11597
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
|
|
11598
|
+
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
11599
|
+
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
11600
|
+
}
|
|
11204
11601
|
s.append(dl);
|
|
11205
11602
|
} else {
|
|
11206
11603
|
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.'));
|
|
@@ -11209,7 +11606,7 @@ function handEditSection(item) {
|
|
|
11209
11606
|
const acts = el('div', 'hand-actions');
|
|
11210
11607
|
const msg = el('span', 'msg');
|
|
11211
11608
|
const go = async (label, action, extra, done) => {
|
|
11212
|
-
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
11609
|
+
const b = el('button', label === 'Detach edit' || (EDITOR && ED && ED.installed) ? null : 'primary', label); b.type = 'button';
|
|
11213
11610
|
b.onclick = async () => {
|
|
11214
11611
|
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
11215
11612
|
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
@@ -11217,8 +11614,20 @@ function handEditSection(item) {
|
|
|
11217
11614
|
};
|
|
11218
11615
|
acts.append(b);
|
|
11219
11616
|
};
|
|
11617
|
+
if (EDITOR && ED && (item.edit || canStart) && editorSource(item)) {
|
|
11618
|
+
if (ED.installed) {
|
|
11619
|
+
const b = el('button', 'primary', 'Edit in browser'); b.type = 'button';
|
|
11620
|
+
b.title = 'Open in Pixelorama here; Save writes the edit file and declares it';
|
|
11621
|
+
b.onclick = () => openEditorSheet(item);
|
|
11622
|
+
acts.append(b);
|
|
11623
|
+
} else if (ED.release && !ED.installing) {
|
|
11624
|
+
const b = el('button', null, 'Install editor to edit in browser'); b.type = 'button';
|
|
11625
|
+
b.onclick = () => { installEditor(); window.scrollTo({ top: 0 }); };
|
|
11626
|
+
acts.append(b);
|
|
11627
|
+
}
|
|
11628
|
+
}
|
|
11220
11629
|
if (item.edit) {
|
|
11221
|
-
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11630
|
+
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11222
11631
|
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
11223
11632
|
} else if (canStart) {
|
|
11224
11633
|
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.');
|
|
@@ -11945,6 +12354,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
11945
12354
|
if (e.key === '/' && !typing) { e.preventDefault(); $('q').focus(); return; }
|
|
11946
12355
|
if (e.key === 'Escape') {
|
|
11947
12356
|
if (typing && document.activeElement.id === 'q') { document.activeElement.blur(); return; }
|
|
12357
|
+
if (SHEET) { e.preventDefault(); closeEditorSheet(); return; }
|
|
11948
12358
|
if ($('dialog-host').childNodes.length) { e.preventDefault(); $('dialog-host').textContent = ''; return; }
|
|
11949
12359
|
if (ui.open) { e.preventDefault(); closeItem(); }
|
|
11950
12360
|
return;
|
|
@@ -11966,6 +12376,7 @@ ui.compare = ui.compare.filter((id) => snap.items.some((i) => i.id === id));
|
|
|
11966
12376
|
render();
|
|
11967
12377
|
if (ui.open) document.querySelector('.card.active')?.scrollIntoView({ block: 'center' });
|
|
11968
12378
|
if (GENERATION) pollJobs();
|
|
12379
|
+
if (EDITOR) pollEditor();
|
|
11969
12380
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
11970
12381
|
</script>
|
|
11971
12382
|
</body>
|
|
@@ -11977,8 +12388,17 @@ var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<
|
|
|
11977
12388
|
import { spawn as spawn3 } from "child_process";
|
|
11978
12389
|
import { randomBytes } from "crypto";
|
|
11979
12390
|
import { createServer as createServer2 } from "http";
|
|
11980
|
-
import { readFile as
|
|
11981
|
-
var MAX_EDIT_BYTES =
|
|
12391
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
12392
|
+
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
12393
|
+
var EDITOR_CSP = [
|
|
12394
|
+
"default-src 'self'",
|
|
12395
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
|
12396
|
+
"style-src 'self' 'unsafe-inline'",
|
|
12397
|
+
"img-src 'self' data: blob:",
|
|
12398
|
+
"worker-src 'self' blob:",
|
|
12399
|
+
"connect-src 'self'",
|
|
12400
|
+
"frame-ancestors 'self'"
|
|
12401
|
+
].join("; ");
|
|
11982
12402
|
async function readJsonBody(req) {
|
|
11983
12403
|
const chunks = [];
|
|
11984
12404
|
let bytes = 0;
|
|
@@ -11996,7 +12416,7 @@ async function readJsonBody(req) {
|
|
|
11996
12416
|
async function serveGallery(opts) {
|
|
11997
12417
|
const log = opts.onProgress ?? (() => {
|
|
11998
12418
|
});
|
|
11999
|
-
const session = opts.edit || opts.generate ? randomBytes(16).toString("hex") : null;
|
|
12419
|
+
const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
|
|
12000
12420
|
let media = /* @__PURE__ */ new Map();
|
|
12001
12421
|
let loading = null;
|
|
12002
12422
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -12071,6 +12491,17 @@ async function serveGallery(opts) {
|
|
|
12071
12491
|
}
|
|
12072
12492
|
return;
|
|
12073
12493
|
}
|
|
12494
|
+
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
12495
|
+
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
12496
|
+
try {
|
|
12497
|
+
const body = await guardedBody("editor installs");
|
|
12498
|
+
if (body === void 0) return;
|
|
12499
|
+
json(202, await opts.editor.install());
|
|
12500
|
+
} catch (err) {
|
|
12501
|
+
reportError("editor install", err);
|
|
12502
|
+
}
|
|
12503
|
+
return;
|
|
12504
|
+
}
|
|
12074
12505
|
const reviewMatch = /^\/review\/([0-9a-f]{16})(\/.*)?$/.exec(url2.pathname);
|
|
12075
12506
|
if (reviewMatch && opts.generate) {
|
|
12076
12507
|
const [, jobId, rest = ""] = reviewMatch;
|
|
@@ -12116,7 +12547,7 @@ async function serveGallery(opts) {
|
|
|
12116
12547
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
12117
12548
|
if (!asset) return fail(404, "no such review asset");
|
|
12118
12549
|
try {
|
|
12119
|
-
const bytes = await
|
|
12550
|
+
const bytes = await readFile20(asset.path);
|
|
12120
12551
|
res.writeHead(200, {
|
|
12121
12552
|
"Content-Type": asset.contentType,
|
|
12122
12553
|
"Cache-Control": "no-store",
|
|
@@ -12134,6 +12565,32 @@ async function serveGallery(opts) {
|
|
|
12134
12565
|
if (!opts.generate) return fail(404, "generation is off");
|
|
12135
12566
|
return json(200, opts.generate.status());
|
|
12136
12567
|
}
|
|
12568
|
+
if (url2.pathname === "/api/editor") {
|
|
12569
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12570
|
+
try {
|
|
12571
|
+
return json(200, await opts.editor.status());
|
|
12572
|
+
} catch (err) {
|
|
12573
|
+
return reportError("editor status", err);
|
|
12574
|
+
}
|
|
12575
|
+
}
|
|
12576
|
+
const editorMatch = /^\/editor\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/.exec(url2.pathname);
|
|
12577
|
+
if (editorMatch) {
|
|
12578
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12579
|
+
const file = await opts.editor.file(editorMatch[1], editorMatch[2]);
|
|
12580
|
+
if (!file) return fail(404, "that editor file is not installed; check /api/editor");
|
|
12581
|
+
res.writeHead(200, {
|
|
12582
|
+
"Content-Type": file.contentType,
|
|
12583
|
+
"Content-Length": file.bytes.length,
|
|
12584
|
+
// The release tag is in the path, so these bytes never change under
|
|
12585
|
+
// this URL; the 40 MB wasm should come from the browser cache.
|
|
12586
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
12587
|
+
"X-Content-Type-Options": "nosniff",
|
|
12588
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
12589
|
+
...editorMatch[2] === "index.html" ? { "Content-Security-Policy": EDITOR_CSP } : {}
|
|
12590
|
+
});
|
|
12591
|
+
res.end(req.method === "HEAD" ? void 0 : file.bytes);
|
|
12592
|
+
return;
|
|
12593
|
+
}
|
|
12137
12594
|
try {
|
|
12138
12595
|
if (url2.pathname === "/") {
|
|
12139
12596
|
const { snapshot } = await load();
|
|
@@ -12145,7 +12602,8 @@ async function serveGallery(opts) {
|
|
|
12145
12602
|
res.end(renderGallery(snapshot, {
|
|
12146
12603
|
...session ? { session } : {},
|
|
12147
12604
|
editable: Boolean(opts.edit),
|
|
12148
|
-
generation: Boolean(opts.generate)
|
|
12605
|
+
generation: Boolean(opts.generate),
|
|
12606
|
+
editor: Boolean(opts.editor)
|
|
12149
12607
|
}));
|
|
12150
12608
|
return;
|
|
12151
12609
|
}
|
|
@@ -12165,7 +12623,7 @@ async function serveGallery(opts) {
|
|
|
12165
12623
|
if (!asset) return fail(404, "no such gallery media");
|
|
12166
12624
|
let bytes;
|
|
12167
12625
|
try {
|
|
12168
|
-
bytes = await
|
|
12626
|
+
bytes = await readFile20(asset.path);
|
|
12169
12627
|
} catch {
|
|
12170
12628
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
12171
12629
|
}
|
|
@@ -12211,16 +12669,16 @@ async function serveGallery(opts) {
|
|
|
12211
12669
|
|
|
12212
12670
|
// src/gallery/generate.ts
|
|
12213
12671
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
12214
|
-
import { z as
|
|
12215
|
-
var GenerateRequestSchema =
|
|
12216
|
-
project:
|
|
12217
|
-
keys:
|
|
12672
|
+
import { z as z10 } from "zod";
|
|
12673
|
+
var GenerateRequestSchema = z10.object({
|
|
12674
|
+
project: z10.string().min(1).optional(),
|
|
12675
|
+
keys: z10.array(z10.string().min(1)).min(1).max(500),
|
|
12218
12676
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
12219
|
-
force:
|
|
12677
|
+
force: z10.boolean().optional(),
|
|
12220
12678
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
12221
|
-
resume:
|
|
12679
|
+
resume: z10.boolean().optional(),
|
|
12222
12680
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
12223
|
-
refresh:
|
|
12681
|
+
refresh: z10.boolean().optional()
|
|
12224
12682
|
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
12225
12683
|
var GenerateRequestError = class extends Error {
|
|
12226
12684
|
constructor(message6, status = 400) {
|
|
@@ -12475,7 +12933,7 @@ function createGenerateHandlers(opts) {
|
|
|
12475
12933
|
}
|
|
12476
12934
|
|
|
12477
12935
|
// src/pipeline/salvage.ts
|
|
12478
|
-
import { readFile as
|
|
12936
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
12479
12937
|
import { existsSync as existsSync20 } from "fs";
|
|
12480
12938
|
import path25 from "path";
|
|
12481
12939
|
function providerClaimId(provider, objectId) {
|
|
@@ -12487,7 +12945,7 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
12487
12945
|
if (!existsSync20(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12488
12946
|
let parsed;
|
|
12489
12947
|
try {
|
|
12490
|
-
parsed = parseLock(JSON.parse(await
|
|
12948
|
+
parsed = parseLock(JSON.parse(await readFile21(p, "utf8")));
|
|
12491
12949
|
} catch {
|
|
12492
12950
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
12493
12951
|
}
|
|
@@ -12669,7 +13127,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
12669
13127
|
}
|
|
12670
13128
|
|
|
12671
13129
|
// src/pick/salvage-server.ts
|
|
12672
|
-
import { mkdir as mkdir7, writeFile as
|
|
13130
|
+
import { mkdir as mkdir7, writeFile as writeFile10, readFile as readFile22 } from "fs/promises";
|
|
12673
13131
|
import path26 from "path";
|
|
12674
13132
|
|
|
12675
13133
|
// src/pick/salvage-sheet.ts
|
|
@@ -12867,7 +13325,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12867
13325
|
const rel = path26.join("_salvaged", `${assetId}.png`);
|
|
12868
13326
|
const outFile = path26.resolve(ctx.importDir, rel);
|
|
12869
13327
|
await mkdir7(path26.dirname(outFile), { recursive: true });
|
|
12870
|
-
await
|
|
13328
|
+
await writeFile10(outFile, buf);
|
|
12871
13329
|
ctx.manifest.assets[assetId] = {
|
|
12872
13330
|
prompt: orphan.prompt,
|
|
12873
13331
|
promptByStyle: {},
|
|
@@ -12923,9 +13381,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12923
13381
|
}
|
|
12924
13382
|
}
|
|
12925
13383
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
12926
|
-
const raw = JSON.parse(await
|
|
13384
|
+
const raw = JSON.parse(await readFile22(ctx.manifestPath, "utf8"));
|
|
12927
13385
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
12928
|
-
await
|
|
13386
|
+
await writeFile10(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12929
13387
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
12930
13388
|
return result;
|
|
12931
13389
|
}
|