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.cjs
CHANGED
|
@@ -9393,7 +9393,7 @@ function validateWorkspace(ws, dir) {
|
|
|
9393
9393
|
|
|
9394
9394
|
// src/gallery/edit.ts
|
|
9395
9395
|
var import_node_path24 = __toESM(require("path"), 1);
|
|
9396
|
-
var
|
|
9396
|
+
var import_zod9 = require("zod");
|
|
9397
9397
|
|
|
9398
9398
|
// src/manifest-edit.ts
|
|
9399
9399
|
var import_promises18 = require("fs/promises");
|
|
@@ -9649,6 +9649,7 @@ var import_node_child_process3 = require("child_process");
|
|
|
9649
9649
|
var import_node_fs20 = require("fs");
|
|
9650
9650
|
var import_promises19 = require("fs/promises");
|
|
9651
9651
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
9652
|
+
var import_zod8 = require("zod");
|
|
9652
9653
|
var HAND_EDIT_DIR = "edits";
|
|
9653
9654
|
function handEditPath(loaded, spec) {
|
|
9654
9655
|
const style = loaded.manifest.styles[spec.styleId];
|
|
@@ -9718,6 +9719,89 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
9718
9719
|
});
|
|
9719
9720
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
9720
9721
|
}
|
|
9722
|
+
var HandEditCompanionSchema = import_zod8.z.object({
|
|
9723
|
+
version: import_zod8.z.literal(1),
|
|
9724
|
+
/** sha256 of the generated art the edit started from; null when it started from untracked art. */
|
|
9725
|
+
basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
9726
|
+
/** sha256 of the edit PNG as saved, so a later change by another tool is visible. */
|
|
9727
|
+
sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9728
|
+
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
9729
|
+
editor: import_zod8.z.string().min(1),
|
|
9730
|
+
protocol: import_zod8.z.number().int().positive(),
|
|
9731
|
+
savedAt: import_zod8.z.string().datetime(),
|
|
9732
|
+
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
9733
|
+
project: import_zod8.z.string().min(1).nullable()
|
|
9734
|
+
}).strict();
|
|
9735
|
+
function handEditCompanionPath(editPath) {
|
|
9736
|
+
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
9737
|
+
}
|
|
9738
|
+
function handEditProjectPath(editPath) {
|
|
9739
|
+
return editPath.replace(/\.png$/i, "") + ".pxo";
|
|
9740
|
+
}
|
|
9741
|
+
async function readHandEditCompanion(editPath) {
|
|
9742
|
+
let text;
|
|
9743
|
+
try {
|
|
9744
|
+
text = await (0, import_promises19.readFile)(handEditCompanionPath(editPath), "utf8");
|
|
9745
|
+
} catch {
|
|
9746
|
+
return null;
|
|
9747
|
+
}
|
|
9748
|
+
try {
|
|
9749
|
+
return HandEditCompanionSchema.parse(JSON.parse(text));
|
|
9750
|
+
} catch {
|
|
9751
|
+
return null;
|
|
9752
|
+
}
|
|
9753
|
+
}
|
|
9754
|
+
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
9755
|
+
async function saveHandEdit(loaded, lock, spec, input) {
|
|
9756
|
+
if (input.png.length > MAX_HAND_EDIT_BYTES) {
|
|
9757
|
+
throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9758
|
+
}
|
|
9759
|
+
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
9760
|
+
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9761
|
+
}
|
|
9762
|
+
let decoded;
|
|
9763
|
+
try {
|
|
9764
|
+
validateMedia(input.png, MediaType.PNG);
|
|
9765
|
+
decoded = decodePng(input.png);
|
|
9766
|
+
} catch (error) {
|
|
9767
|
+
throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
9768
|
+
}
|
|
9769
|
+
const base = handEditBase(spec, lock);
|
|
9770
|
+
const expected = base ? decodePng(await (0, import_promises19.readFile)(base)) : { width: spec.width, height: spec.height };
|
|
9771
|
+
if (decoded.width !== expected.width || decoded.height !== expected.height) {
|
|
9772
|
+
throw new ManifestEditError(
|
|
9773
|
+
`the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
|
|
9774
|
+
);
|
|
9775
|
+
}
|
|
9776
|
+
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
9777
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9778
|
+
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
9779
|
+
const digest2 = sha256(input.png);
|
|
9780
|
+
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
9781
|
+
const companion = {
|
|
9782
|
+
version: 1,
|
|
9783
|
+
basedOn,
|
|
9784
|
+
sha256: digest2,
|
|
9785
|
+
editor: input.editor,
|
|
9786
|
+
protocol: input.protocol,
|
|
9787
|
+
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9788
|
+
project: projectPath ? import_node_path23.default.basename(projectPath) : null
|
|
9789
|
+
};
|
|
9790
|
+
await replaceFile(started.editPath, input.png);
|
|
9791
|
+
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
9792
|
+
const companionPath = handEditCompanionPath(started.editPath);
|
|
9793
|
+
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
9794
|
+
return { ...started, sha256: digest2, basedOn, companionPath, projectPath };
|
|
9795
|
+
}
|
|
9796
|
+
async function replaceFile(file, bytes) {
|
|
9797
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
9798
|
+
try {
|
|
9799
|
+
await (0, import_promises19.writeFile)(tmp, bytes);
|
|
9800
|
+
await (0, import_promises19.rename)(tmp, file);
|
|
9801
|
+
} finally {
|
|
9802
|
+
await (0, import_promises19.rm)(tmp, { force: true });
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9721
9805
|
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
9722
9806
|
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
9723
9807
|
const [program, ...args] = command;
|
|
@@ -9729,22 +9813,37 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
9729
9813
|
}
|
|
9730
9814
|
|
|
9731
9815
|
// src/gallery/edit.ts
|
|
9732
|
-
var
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9816
|
+
var Base64 = import_zod9.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
9817
|
+
var HandEditRequestSchema = import_zod9.z.discriminatedUnion("action", [
|
|
9818
|
+
import_zod9.z.object({
|
|
9819
|
+
action: import_zod9.z.literal("start-edit"),
|
|
9820
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9821
|
+
assetId: import_zod9.z.string().min(1),
|
|
9822
|
+
styleId: import_zod9.z.string().min(1),
|
|
9823
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9739
9824
|
/** Also launch the author's editor on the file. */
|
|
9740
|
-
open:
|
|
9825
|
+
open: import_zod9.z.boolean().optional()
|
|
9741
9826
|
}).strict(),
|
|
9742
|
-
|
|
9743
|
-
action:
|
|
9744
|
-
project:
|
|
9745
|
-
assetId:
|
|
9746
|
-
styleId:
|
|
9747
|
-
expectedSha256:
|
|
9827
|
+
import_zod9.z.object({
|
|
9828
|
+
action: import_zod9.z.literal("detach-edit"),
|
|
9829
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9830
|
+
assetId: import_zod9.z.string().min(1),
|
|
9831
|
+
styleId: import_zod9.z.string().min(1),
|
|
9832
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/)
|
|
9833
|
+
}).strict(),
|
|
9834
|
+
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
9835
|
+
import_zod9.z.object({
|
|
9836
|
+
action: import_zod9.z.literal("save-edit"),
|
|
9837
|
+
project: import_zod9.z.string().min(1).optional(),
|
|
9838
|
+
assetId: import_zod9.z.string().min(1),
|
|
9839
|
+
styleId: import_zod9.z.string().min(1),
|
|
9840
|
+
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9841
|
+
png: Base64.min(1),
|
|
9842
|
+
/** The editor's layered project file, kept beside the edit. */
|
|
9843
|
+
pxo: Base64.optional(),
|
|
9844
|
+
/** Editor identity from the bridge's ready message. */
|
|
9845
|
+
editor: import_zod9.z.string().min(1).max(200),
|
|
9846
|
+
protocol: import_zod9.z.number().int().positive()
|
|
9748
9847
|
}).strict()
|
|
9749
9848
|
]);
|
|
9750
9849
|
function createGalleryEditHandler(opts) {
|
|
@@ -9765,6 +9864,15 @@ function createGalleryEditHandler(opts) {
|
|
|
9765
9864
|
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
9766
9865
|
log(` opened with: ${command}`);
|
|
9767
9866
|
}
|
|
9867
|
+
} else if (request.action === "save-edit") {
|
|
9868
|
+
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
9869
|
+
png: Buffer.from(request.png, "base64"),
|
|
9870
|
+
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
9871
|
+
editor: request.editor,
|
|
9872
|
+
protocol: request.protocol,
|
|
9873
|
+
expectedSha256: request.expectedSha256
|
|
9874
|
+
});
|
|
9875
|
+
log(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
9768
9876
|
} else {
|
|
9769
9877
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
9770
9878
|
if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -9929,6 +10037,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9929
10037
|
let outputs;
|
|
9930
10038
|
let edit = null;
|
|
9931
10039
|
let editStatus = null;
|
|
10040
|
+
let editMeta = null;
|
|
9932
10041
|
if (entry && entry.outputs.length) {
|
|
9933
10042
|
outputs = await Promise.all(
|
|
9934
10043
|
entry.outputs.map(
|
|
@@ -9940,7 +10049,19 @@ async function buildGallerySnapshot(opts) {
|
|
|
9940
10049
|
const editSha = (0, import_node_fs21.existsSync)(editPath) ? await sha256File(editPath) : null;
|
|
9941
10050
|
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
9942
10051
|
const generated = outputs[0];
|
|
9943
|
-
|
|
10052
|
+
const companion = await readHandEditCompanion(editPath);
|
|
10053
|
+
if (companion) {
|
|
10054
|
+
const projectPath = handEditProjectPath(editPath);
|
|
10055
|
+
editMeta = {
|
|
10056
|
+
editor: companion.editor,
|
|
10057
|
+
savedAt: companion.savedAt,
|
|
10058
|
+
basedOn: companion.basedOn,
|
|
10059
|
+
changedSince: editSha !== null && editSha !== companion.sha256,
|
|
10060
|
+
project: companion.project && (0, import_node_fs21.existsSync)(projectPath) ? portableOutputPath(projectPath, root) : null
|
|
10061
|
+
};
|
|
10062
|
+
}
|
|
10063
|
+
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);
|
|
10064
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
9944
10065
|
}
|
|
9945
10066
|
} else if (spec.source) {
|
|
9946
10067
|
outputs = [await describeOutput(media, root, import_node_path25.default.resolve(root, spec.source))];
|
|
@@ -9990,6 +10111,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9990
10111
|
source: spec.source ?? null,
|
|
9991
10112
|
edit,
|
|
9992
10113
|
editStatus,
|
|
10114
|
+
editMeta,
|
|
9993
10115
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9994
10116
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9995
10117
|
tags: spec.tags,
|
|
@@ -10046,6 +10168,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10046
10168
|
source: null,
|
|
10047
10169
|
edit: null,
|
|
10048
10170
|
editStatus: null,
|
|
10171
|
+
editMeta: null,
|
|
10049
10172
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
10050
10173
|
refreshable: false,
|
|
10051
10174
|
tags: [],
|
|
@@ -10247,6 +10370,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10247
10370
|
const session = JSON.stringify(opts.session ?? null);
|
|
10248
10371
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
10249
10372
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
10373
|
+
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
10250
10374
|
const title = `pixelkiln \u2014 ${snapshot.project?.name ?? "workspace"}`;
|
|
10251
10375
|
return `<!doctype html>
|
|
10252
10376
|
<html lang="en">
|
|
@@ -10452,6 +10576,15 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10452
10576
|
.job .acts button { padding:3px 9px; font-size:12px; }
|
|
10453
10577
|
.job pre { grid-column:1 / -1; margin:0; padding:8px 10px; border-top:1px solid var(--line);
|
|
10454
10578
|
font:11.5px/1.45 var(--mono); color:var(--dim); max-height:220px; overflow:auto; white-space:pre-wrap; }
|
|
10579
|
+
#tools { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; }
|
|
10580
|
+
#tools:empty { display:none; }
|
|
10581
|
+
.tool { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
|
|
10582
|
+
grid-template-columns:auto 1fr auto; gap:6px 14px; align-items:center; font-size:12.5px; }
|
|
10583
|
+
.tool .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1 var(--mono); white-space:nowrap; }
|
|
10584
|
+
.tool .last { color:var(--dim); min-width:0; }
|
|
10585
|
+
.tool .acts { display:flex; gap:6px; }
|
|
10586
|
+
.tool .acts button { padding:3px 9px; font-size:12px; }
|
|
10587
|
+
.tool progress { grid-column:1 / -1; width:100%; height:6px; accent-color:var(--accent); }
|
|
10455
10588
|
.budget { color:var(--dim); }
|
|
10456
10589
|
.budget b { color:var(--text); }
|
|
10457
10590
|
.dialog { position:fixed; inset:0; z-index:30; display:grid; place-items:center; background:rgba(0,0,0,.55); }
|
|
@@ -10472,6 +10605,16 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10472
10605
|
border-left:1px solid var(--line-strong); display:grid; grid-template-rows:auto 1fr; min-width:0; }
|
|
10473
10606
|
.sheet.review-host { width:min(1180px, 94vw); }
|
|
10474
10607
|
.sheet.compare-host { width:min(1500px, 96vw); }
|
|
10608
|
+
.sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
|
|
10609
|
+
.sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
|
|
10610
|
+
.sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
|
10611
|
+
.sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
|
|
10612
|
+
.sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
|
|
10613
|
+
.sheet.editor-host .stage { position:relative; min-height:0; }
|
|
10614
|
+
.sheet.editor-host .stage iframe { position:absolute; inset:0; }
|
|
10615
|
+
.sheet.editor-host .loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--bg);
|
|
10616
|
+
color:var(--dim); text-align:center; padding:24px; }
|
|
10617
|
+
.sheet.editor-host .loading b { display:block; color:var(--text); margin-bottom:6px; }
|
|
10475
10618
|
.sheet .rbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line-strong);
|
|
10476
10619
|
background:var(--panel); font-size:13px; flex-wrap:wrap; }
|
|
10477
10620
|
.sheet .rbar span { color:var(--dim); }
|
|
@@ -10510,7 +10653,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10510
10653
|
.tray .pick img { width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
|
|
10511
10654
|
.tray .pick button { padding:0 5px; border-color:transparent; color:var(--dim); font-size:12px; }
|
|
10512
10655
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10513
|
-
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
10656
|
+
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
10514
10657
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10515
10658
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10516
10659
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
@@ -10558,6 +10701,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10558
10701
|
<div class="totals" id="totals"></div>
|
|
10559
10702
|
<div class="chips" id="chips"></div>
|
|
10560
10703
|
<div id="jobs"></div>
|
|
10704
|
+
<div id="tools"></div>
|
|
10561
10705
|
</header>
|
|
10562
10706
|
<main id="root"></main>
|
|
10563
10707
|
<footer>
|
|
@@ -10576,6 +10720,7 @@ const INITIAL = ${data};
|
|
|
10576
10720
|
const SESSION = ${session};
|
|
10577
10721
|
const EDITABLE = ${editable};
|
|
10578
10722
|
const GENERATION = ${generation};
|
|
10723
|
+
const EDITOR = ${editor};
|
|
10579
10724
|
let snap = INITIAL;
|
|
10580
10725
|
const STATE_TONE = {
|
|
10581
10726
|
ok: 'ok', stale: 'warn', orphaned: 'warn', untracked: 'warn', blocked: 'warn',
|
|
@@ -10743,6 +10888,92 @@ function renderJobs() {
|
|
|
10743
10888
|
host.append(row);
|
|
10744
10889
|
}
|
|
10745
10890
|
}
|
|
10891
|
+
// ---- the in-browser editor (only when the server serves one) -------------
|
|
10892
|
+
// The build is a 46 MB web export fetched once into a user-level cache and
|
|
10893
|
+
// verified against hashes this PixelKiln release carries; the page only
|
|
10894
|
+
// reports and asks, the server downloads. Nothing is fetched without a click.
|
|
10895
|
+
|
|
10896
|
+
let ED = null;
|
|
10897
|
+
let edTimer = null;
|
|
10898
|
+
const fmtMb = (bytes) => (bytes / 1e6).toFixed(1) + ' MB';
|
|
10899
|
+
async function pollEditor() {
|
|
10900
|
+
if (!EDITOR) return;
|
|
10901
|
+
try {
|
|
10902
|
+
const res = await fetch('/api/editor', { cache: 'no-store' });
|
|
10903
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10904
|
+
ED = await res.json();
|
|
10905
|
+
} catch (err) {
|
|
10906
|
+
ED = { error: 'status unavailable: ' + err.message, installed: false, installing: null, missing: [], totalBytes: 0 };
|
|
10907
|
+
}
|
|
10908
|
+
renderTools();
|
|
10909
|
+
clearTimeout(edTimer);
|
|
10910
|
+
if (ED.installing) edTimer = setTimeout(pollEditor, 1000);
|
|
10911
|
+
else if (ui.open) renderDrawer();
|
|
10912
|
+
}
|
|
10913
|
+
async function installEditor() {
|
|
10914
|
+
try {
|
|
10915
|
+
const res = await fetch('/api/editor/install', {
|
|
10916
|
+
method: 'POST',
|
|
10917
|
+
headers: { 'Content-Type': 'application/json', 'X-Pixelkiln-Session': SESSION },
|
|
10918
|
+
body: '{}',
|
|
10919
|
+
});
|
|
10920
|
+
if (!res.ok) throw new Error(await res.text());
|
|
10921
|
+
ED = await res.json();
|
|
10922
|
+
} catch (err) {
|
|
10923
|
+
ED = Object.assign({}, ED, { error: err.message });
|
|
10924
|
+
}
|
|
10925
|
+
renderTools();
|
|
10926
|
+
clearTimeout(edTimer);
|
|
10927
|
+
edTimer = setTimeout(pollEditor, 500);
|
|
10928
|
+
}
|
|
10929
|
+
function renderTools() {
|
|
10930
|
+
const host = $('tools');
|
|
10931
|
+
host.textContent = '';
|
|
10932
|
+
if (!EDITOR || !ED) return;
|
|
10933
|
+
const row = el('div', 'tool');
|
|
10934
|
+
const ph = el('span', 'ph');
|
|
10935
|
+
const last = el('span', 'last');
|
|
10936
|
+
const acts = el('div', 'acts');
|
|
10937
|
+
const name = 'Browser editor' + (ED.pixelorama ? ' (Pixelorama ' + ED.pixelorama + ')' : '');
|
|
10938
|
+
if (ED.installing) {
|
|
10939
|
+
const p = ED.installing;
|
|
10940
|
+
ph.append(el('i', 'dot cool'), document.createTextNode('installing'));
|
|
10941
|
+
last.textContent = name + ': fetching ' + (p.file || 'the build') + ' \u2014 ' + fmtMb(p.fetchedBytes) + ' of ' + fmtMb(p.totalBytes);
|
|
10942
|
+
row.append(ph, last, acts);
|
|
10943
|
+
const bar = el('progress');
|
|
10944
|
+
if (p.totalBytes) { bar.max = p.totalBytes; bar.value = p.fetchedBytes; }
|
|
10945
|
+
row.append(bar);
|
|
10946
|
+
} else if (ED.installed) {
|
|
10947
|
+
ph.append(el('i', 'dot ok'), document.createTextNode('ready'));
|
|
10948
|
+
last.textContent = name + ' is installed and verified; a record\u2019s Hand edit section opens sprites in it.';
|
|
10949
|
+
last.title = ED.dir || '';
|
|
10950
|
+
const b = el('button', null, 'Open editor');
|
|
10951
|
+
b.type = 'button'; b.onclick = () => window.open(ED.url, '_blank', 'noopener');
|
|
10952
|
+
acts.append(b);
|
|
10953
|
+
row.append(ph, last, acts);
|
|
10954
|
+
} else if (ED.error) {
|
|
10955
|
+
ph.append(el('i', 'dot bad'), document.createTextNode('failed'));
|
|
10956
|
+
last.textContent = name + ': ' + ED.error;
|
|
10957
|
+
const b = el('button', 'primary', 'Retry install');
|
|
10958
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10959
|
+
acts.append(b);
|
|
10960
|
+
row.append(ph, last, acts);
|
|
10961
|
+
} else if (!ED.release) {
|
|
10962
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('unavailable'));
|
|
10963
|
+
last.textContent = name + ': this PixelKiln version pins no published build.';
|
|
10964
|
+
row.append(ph, last, acts);
|
|
10965
|
+
} else {
|
|
10966
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('not installed'));
|
|
10967
|
+
last.textContent = name + ': ' + fmtMb(ED.totalBytes - (ED.installedBytes || 0)) + ' fetched once from the PixelKiln release ' +
|
|
10968
|
+
ED.release + ', verified against the hashes this version carries, and kept outside the project.';
|
|
10969
|
+
const b = el('button', 'primary', 'Install editor');
|
|
10970
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
10971
|
+
acts.append(b);
|
|
10972
|
+
row.append(ph, last, acts);
|
|
10973
|
+
}
|
|
10974
|
+
host.append(row);
|
|
10975
|
+
}
|
|
10976
|
+
|
|
10746
10977
|
function budgetLine() {
|
|
10747
10978
|
const providers = new Set([...Object.keys(GEN.budget.byProvider), ...Object.keys(GEN.spent)]);
|
|
10748
10979
|
const parts = [];
|
|
@@ -10849,8 +11080,169 @@ function openReview(jobId) {
|
|
|
10849
11080
|
host.append(scrim, panel);
|
|
10850
11081
|
}
|
|
10851
11082
|
window.addEventListener('message', (e) => {
|
|
10852
|
-
if (e.origin !== location.origin || !e.data || e.data.type !== '
|
|
10853
|
-
|
|
11083
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== 'string') return;
|
|
11084
|
+
if (e.data.type === 'pixelkiln:review-applied') {
|
|
11085
|
+
setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
|
|
11086
|
+
return;
|
|
11087
|
+
}
|
|
11088
|
+
if (SHEET && e.source === SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
11089
|
+
});
|
|
11090
|
+
|
|
11091
|
+
// ---- the in-browser editor sheet -------------------------------------------
|
|
11092
|
+
// The page is the protocol host: it hands the editor the PNG and palette,
|
|
11093
|
+
// asks it for the image back, and does the authenticated write itself. The
|
|
11094
|
+
// iframe never sees the session token, and the server validates, sizes, and
|
|
11095
|
+
// declares the file the same way pixelkiln edit does.
|
|
11096
|
+
|
|
11097
|
+
let SHEET = null;
|
|
11098
|
+
const editorSource = (item) => item.edit && item.edit.exists ? item.edit
|
|
11099
|
+
: item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
|
|
11100
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
|
|
11101
|
+
!(item.outputs.length > 1);
|
|
11102
|
+
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
11103
|
+
const r = new FileReader();
|
|
11104
|
+
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
11105
|
+
r.onerror = () => reject(r.error);
|
|
11106
|
+
r.readAsDataURL(new Blob([buf]));
|
|
11107
|
+
});
|
|
11108
|
+
function sheetStatus(tone, text) {
|
|
11109
|
+
if (!SHEET) return;
|
|
11110
|
+
SHEET.status.textContent = '';
|
|
11111
|
+
SHEET.status.append(el('i', 'dot ' + tone), document.createTextNode(text));
|
|
11112
|
+
}
|
|
11113
|
+
function sheetButtons() {
|
|
11114
|
+
if (!SHEET) return;
|
|
11115
|
+
const busy = !SHEET.opened || SHEET.pending !== null;
|
|
11116
|
+
SHEET.save.disabled = busy || !SHEET.dirty;
|
|
11117
|
+
SHEET.saveClose.disabled = busy;
|
|
11118
|
+
}
|
|
11119
|
+
function openEditorSheet(item) {
|
|
11120
|
+
if (SHEET) return;
|
|
11121
|
+
const src = editorSource(item);
|
|
11122
|
+
const host = $('dialog-host');
|
|
11123
|
+
host.textContent = '';
|
|
11124
|
+
const scrim = el('div', 'sheet-scrim');
|
|
11125
|
+
scrim.onclick = () => closeEditorSheet();
|
|
11126
|
+
const panel = el('div', 'sheet editor-host');
|
|
11127
|
+
const bar = el('div', 'rbar');
|
|
11128
|
+
const close = el('button', null, 'Close'); close.type = 'button';
|
|
11129
|
+
close.onclick = () => closeEditorSheet();
|
|
11130
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
11131
|
+
const status = el('span', 'st');
|
|
11132
|
+
const acts = el('div', 'acts');
|
|
11133
|
+
const msg = el('span', 'msg');
|
|
11134
|
+
const save = el('button', 'primary', 'Save to project'); save.type = 'button';
|
|
11135
|
+
save.onclick = () => requestEditorSave(false);
|
|
11136
|
+
const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
|
|
11137
|
+
saveClose.onclick = () => requestEditorSave(true);
|
|
11138
|
+
acts.append(msg, save, saveClose);
|
|
11139
|
+
bar.append(close, title, status, acts);
|
|
11140
|
+
const stage = el('div', 'stage');
|
|
11141
|
+
const frame = el('iframe');
|
|
11142
|
+
frame.title = 'Pixelorama';
|
|
11143
|
+
frame.src = ED.url;
|
|
11144
|
+
const loading = el('div', 'loading');
|
|
11145
|
+
const loadText = el('div');
|
|
11146
|
+
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.'));
|
|
11147
|
+
loading.append(loadText);
|
|
11148
|
+
stage.append(frame, loading);
|
|
11149
|
+
panel.append(bar, stage);
|
|
11150
|
+
host.append(scrim, panel);
|
|
11151
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src };
|
|
11152
|
+
sheetStatus('cool', 'loading');
|
|
11153
|
+
sheetButtons();
|
|
11154
|
+
close.focus({ preventScroll: true });
|
|
11155
|
+
}
|
|
11156
|
+
async function sendOpen() {
|
|
11157
|
+
const { item, source } = SHEET;
|
|
11158
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
11159
|
+
let png;
|
|
11160
|
+
try {
|
|
11161
|
+
const res = await fetch(source.url, { cache: 'no-store' });
|
|
11162
|
+
if (!res.ok) throw new Error(await res.text());
|
|
11163
|
+
png = await res.arrayBuffer();
|
|
11164
|
+
} catch (err) {
|
|
11165
|
+
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
11166
|
+
return;
|
|
11167
|
+
}
|
|
11168
|
+
if (!SHEET) return;
|
|
11169
|
+
const request = 'open-' + (++SHEET.requests);
|
|
11170
|
+
SHEET.frame.contentWindow.postMessage({
|
|
11171
|
+
type: 'pixelkiln:open', request,
|
|
11172
|
+
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
11173
|
+
png, palette: style ? style.palette : [],
|
|
11174
|
+
}, location.origin, [png]);
|
|
11175
|
+
sheetStatus('cool', 'opening');
|
|
11176
|
+
}
|
|
11177
|
+
function requestEditorSave(close) {
|
|
11178
|
+
if (!SHEET || !SHEET.opened || SHEET.pending) return;
|
|
11179
|
+
const request = 'save-' + (++SHEET.requests);
|
|
11180
|
+
SHEET.pending = { request, close };
|
|
11181
|
+
SHEET.msg.textContent = '';
|
|
11182
|
+
sheetStatus('cool', 'saving');
|
|
11183
|
+
sheetButtons();
|
|
11184
|
+
SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:request-save', request }, location.origin);
|
|
11185
|
+
}
|
|
11186
|
+
async function onEditorMessage(m) {
|
|
11187
|
+
switch (m.type) {
|
|
11188
|
+
case 'pixelkiln:ready':
|
|
11189
|
+
SHEET.ready = m;
|
|
11190
|
+
if (m.version !== ED.protocol) {
|
|
11191
|
+
SHEET.msg.textContent = 'The editor speaks protocol ' + m.version + '; this gallery expects ' + ED.protocol + '. Reinstall it with pixelkiln tools install editor.';
|
|
11192
|
+
sheetStatus('bad', 'mismatch');
|
|
11193
|
+
return;
|
|
11194
|
+
}
|
|
11195
|
+
sendOpen();
|
|
11196
|
+
break;
|
|
11197
|
+
case 'pixelkiln:opened':
|
|
11198
|
+
SHEET.opened = true;
|
|
11199
|
+
SHEET.loading.remove();
|
|
11200
|
+
sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
11201
|
+
sheetButtons();
|
|
11202
|
+
break;
|
|
11203
|
+
case 'pixelkiln:dirty':
|
|
11204
|
+
SHEET.dirty = !!m.dirty;
|
|
11205
|
+
if (SHEET.opened && !SHEET.pending) sheetStatus(SHEET.dirty ? 'warn' : 'ok', SHEET.dirty ? 'unsaved changes' : 'saved');
|
|
11206
|
+
sheetButtons();
|
|
11207
|
+
break;
|
|
11208
|
+
case 'pixelkiln:save': {
|
|
11209
|
+
const pending = SHEET.pending;
|
|
11210
|
+
if (!pending || pending.request !== m.request) return;
|
|
11211
|
+
try {
|
|
11212
|
+
const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
11213
|
+
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
11214
|
+
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
11215
|
+
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
11216
|
+
if (saved) SHEET.item = saved;
|
|
11217
|
+
SHEET.dirty = false;
|
|
11218
|
+
SHEET.pending = null;
|
|
11219
|
+
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.' };
|
|
11220
|
+
render();
|
|
11221
|
+
if (pending.close) { closeEditorSheet(true); return; }
|
|
11222
|
+
sheetStatus('ok', 'saved');
|
|
11223
|
+
} catch (err) {
|
|
11224
|
+
SHEET.pending = null;
|
|
11225
|
+
SHEET.msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh, then save again.' : '');
|
|
11226
|
+
sheetStatus('bad', 'not saved');
|
|
11227
|
+
}
|
|
11228
|
+
sheetButtons();
|
|
11229
|
+
break;
|
|
11230
|
+
}
|
|
11231
|
+
case 'pixelkiln:error':
|
|
11232
|
+
SHEET.msg.textContent = m.message || 'The editor reported an error.';
|
|
11233
|
+
if (SHEET.pending) { SHEET.pending = null; sheetStatus('bad', 'not saved'); sheetButtons(); }
|
|
11234
|
+
break;
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
11237
|
+
function closeEditorSheet(force) {
|
|
11238
|
+
if (!SHEET) return;
|
|
11239
|
+
if (SHEET.dirty && !force && !confirm('Discard unsaved changes in the editor?')) return;
|
|
11240
|
+
SHEET = null;
|
|
11241
|
+
$('dialog-host').textContent = '';
|
|
11242
|
+
refresh();
|
|
11243
|
+
}
|
|
11244
|
+
window.addEventListener('beforeunload', (e) => {
|
|
11245
|
+
if (SHEET && SHEET.dirty) { e.preventDefault(); e.returnValue = ''; }
|
|
10854
11246
|
});
|
|
10855
11247
|
|
|
10856
11248
|
function visibleItems() {
|
|
@@ -11426,6 +11818,11 @@ function handEditSection(item) {
|
|
|
11426
11818
|
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
11427
11819
|
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
11428
11820
|
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
11821
|
+
if (item.editMeta) {
|
|
11822
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
|
|
11823
|
+
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
11824
|
+
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
11825
|
+
}
|
|
11429
11826
|
s.append(dl);
|
|
11430
11827
|
} else {
|
|
11431
11828
|
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.'));
|
|
@@ -11434,7 +11831,7 @@ function handEditSection(item) {
|
|
|
11434
11831
|
const acts = el('div', 'hand-actions');
|
|
11435
11832
|
const msg = el('span', 'msg');
|
|
11436
11833
|
const go = async (label, action, extra, done) => {
|
|
11437
|
-
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
11834
|
+
const b = el('button', label === 'Detach edit' || (EDITOR && ED && ED.installed) ? null : 'primary', label); b.type = 'button';
|
|
11438
11835
|
b.onclick = async () => {
|
|
11439
11836
|
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
11440
11837
|
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
@@ -11442,8 +11839,20 @@ function handEditSection(item) {
|
|
|
11442
11839
|
};
|
|
11443
11840
|
acts.append(b);
|
|
11444
11841
|
};
|
|
11842
|
+
if (EDITOR && ED && (item.edit || canStart) && editorSource(item)) {
|
|
11843
|
+
if (ED.installed) {
|
|
11844
|
+
const b = el('button', 'primary', 'Edit in browser'); b.type = 'button';
|
|
11845
|
+
b.title = 'Open in Pixelorama here; Save writes the edit file and declares it';
|
|
11846
|
+
b.onclick = () => openEditorSheet(item);
|
|
11847
|
+
acts.append(b);
|
|
11848
|
+
} else if (ED.release && !ED.installing) {
|
|
11849
|
+
const b = el('button', null, 'Install editor to edit in browser'); b.type = 'button';
|
|
11850
|
+
b.onclick = () => { installEditor(); window.scrollTo({ top: 0 }); };
|
|
11851
|
+
acts.append(b);
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11445
11854
|
if (item.edit) {
|
|
11446
|
-
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11855
|
+
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11447
11856
|
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
11448
11857
|
} else if (canStart) {
|
|
11449
11858
|
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.');
|
|
@@ -12170,6 +12579,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
12170
12579
|
if (e.key === '/' && !typing) { e.preventDefault(); $('q').focus(); return; }
|
|
12171
12580
|
if (e.key === 'Escape') {
|
|
12172
12581
|
if (typing && document.activeElement.id === 'q') { document.activeElement.blur(); return; }
|
|
12582
|
+
if (SHEET) { e.preventDefault(); closeEditorSheet(); return; }
|
|
12173
12583
|
if ($('dialog-host').childNodes.length) { e.preventDefault(); $('dialog-host').textContent = ''; return; }
|
|
12174
12584
|
if (ui.open) { e.preventDefault(); closeItem(); }
|
|
12175
12585
|
return;
|
|
@@ -12191,6 +12601,7 @@ ui.compare = ui.compare.filter((id) => snap.items.some((i) => i.id === id));
|
|
|
12191
12601
|
render();
|
|
12192
12602
|
if (ui.open) document.querySelector('.card.active')?.scrollIntoView({ block: 'center' });
|
|
12193
12603
|
if (GENERATION) pollJobs();
|
|
12604
|
+
if (EDITOR) pollEditor();
|
|
12194
12605
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
12195
12606
|
</script>
|
|
12196
12607
|
</body>
|
|
@@ -12203,7 +12614,16 @@ var import_node_child_process4 = require("child_process");
|
|
|
12203
12614
|
var import_node_crypto7 = require("crypto");
|
|
12204
12615
|
var import_node_http2 = require("http");
|
|
12205
12616
|
var import_promises21 = require("fs/promises");
|
|
12206
|
-
var MAX_EDIT_BYTES =
|
|
12617
|
+
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
12618
|
+
var EDITOR_CSP = [
|
|
12619
|
+
"default-src 'self'",
|
|
12620
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
|
12621
|
+
"style-src 'self' 'unsafe-inline'",
|
|
12622
|
+
"img-src 'self' data: blob:",
|
|
12623
|
+
"worker-src 'self' blob:",
|
|
12624
|
+
"connect-src 'self'",
|
|
12625
|
+
"frame-ancestors 'self'"
|
|
12626
|
+
].join("; ");
|
|
12207
12627
|
async function readJsonBody(req) {
|
|
12208
12628
|
const chunks = [];
|
|
12209
12629
|
let bytes = 0;
|
|
@@ -12221,7 +12641,7 @@ async function readJsonBody(req) {
|
|
|
12221
12641
|
async function serveGallery(opts) {
|
|
12222
12642
|
const log = opts.onProgress ?? (() => {
|
|
12223
12643
|
});
|
|
12224
|
-
const session = opts.edit || opts.generate ? (0, import_node_crypto7.randomBytes)(16).toString("hex") : null;
|
|
12644
|
+
const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto7.randomBytes)(16).toString("hex") : null;
|
|
12225
12645
|
let media = /* @__PURE__ */ new Map();
|
|
12226
12646
|
let loading = null;
|
|
12227
12647
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -12296,6 +12716,17 @@ async function serveGallery(opts) {
|
|
|
12296
12716
|
}
|
|
12297
12717
|
return;
|
|
12298
12718
|
}
|
|
12719
|
+
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
12720
|
+
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
12721
|
+
try {
|
|
12722
|
+
const body = await guardedBody("editor installs");
|
|
12723
|
+
if (body === void 0) return;
|
|
12724
|
+
json(202, await opts.editor.install());
|
|
12725
|
+
} catch (err) {
|
|
12726
|
+
reportError("editor install", err);
|
|
12727
|
+
}
|
|
12728
|
+
return;
|
|
12729
|
+
}
|
|
12299
12730
|
const reviewMatch = /^\/review\/([0-9a-f]{16})(\/.*)?$/.exec(url2.pathname);
|
|
12300
12731
|
if (reviewMatch && opts.generate) {
|
|
12301
12732
|
const [, jobId, rest = ""] = reviewMatch;
|
|
@@ -12359,6 +12790,32 @@ async function serveGallery(opts) {
|
|
|
12359
12790
|
if (!opts.generate) return fail(404, "generation is off");
|
|
12360
12791
|
return json(200, opts.generate.status());
|
|
12361
12792
|
}
|
|
12793
|
+
if (url2.pathname === "/api/editor") {
|
|
12794
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12795
|
+
try {
|
|
12796
|
+
return json(200, await opts.editor.status());
|
|
12797
|
+
} catch (err) {
|
|
12798
|
+
return reportError("editor status", err);
|
|
12799
|
+
}
|
|
12800
|
+
}
|
|
12801
|
+
const editorMatch = /^\/editor\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/.exec(url2.pathname);
|
|
12802
|
+
if (editorMatch) {
|
|
12803
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
12804
|
+
const file = await opts.editor.file(editorMatch[1], editorMatch[2]);
|
|
12805
|
+
if (!file) return fail(404, "that editor file is not installed; check /api/editor");
|
|
12806
|
+
res.writeHead(200, {
|
|
12807
|
+
"Content-Type": file.contentType,
|
|
12808
|
+
"Content-Length": file.bytes.length,
|
|
12809
|
+
// The release tag is in the path, so these bytes never change under
|
|
12810
|
+
// this URL; the 40 MB wasm should come from the browser cache.
|
|
12811
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
12812
|
+
"X-Content-Type-Options": "nosniff",
|
|
12813
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
12814
|
+
...editorMatch[2] === "index.html" ? { "Content-Security-Policy": EDITOR_CSP } : {}
|
|
12815
|
+
});
|
|
12816
|
+
res.end(req.method === "HEAD" ? void 0 : file.bytes);
|
|
12817
|
+
return;
|
|
12818
|
+
}
|
|
12362
12819
|
try {
|
|
12363
12820
|
if (url2.pathname === "/") {
|
|
12364
12821
|
const { snapshot } = await load();
|
|
@@ -12370,7 +12827,8 @@ async function serveGallery(opts) {
|
|
|
12370
12827
|
res.end(renderGallery(snapshot, {
|
|
12371
12828
|
...session ? { session } : {},
|
|
12372
12829
|
editable: Boolean(opts.edit),
|
|
12373
|
-
generation: Boolean(opts.generate)
|
|
12830
|
+
generation: Boolean(opts.generate),
|
|
12831
|
+
editor: Boolean(opts.editor)
|
|
12374
12832
|
}));
|
|
12375
12833
|
return;
|
|
12376
12834
|
}
|
|
@@ -12436,16 +12894,16 @@ async function serveGallery(opts) {
|
|
|
12436
12894
|
|
|
12437
12895
|
// src/gallery/generate.ts
|
|
12438
12896
|
var import_node_crypto8 = require("crypto");
|
|
12439
|
-
var
|
|
12440
|
-
var GenerateRequestSchema =
|
|
12441
|
-
project:
|
|
12442
|
-
keys:
|
|
12897
|
+
var import_zod10 = require("zod");
|
|
12898
|
+
var GenerateRequestSchema = import_zod10.z.object({
|
|
12899
|
+
project: import_zod10.z.string().min(1).optional(),
|
|
12900
|
+
keys: import_zod10.z.array(import_zod10.z.string().min(1)).min(1).max(500),
|
|
12443
12901
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
12444
|
-
force:
|
|
12902
|
+
force: import_zod10.z.boolean().optional(),
|
|
12445
12903
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
12446
|
-
resume:
|
|
12904
|
+
resume: import_zod10.z.boolean().optional(),
|
|
12447
12905
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
12448
|
-
refresh:
|
|
12906
|
+
refresh: import_zod10.z.boolean().optional()
|
|
12449
12907
|
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
12450
12908
|
var GenerateRequestError = class extends Error {
|
|
12451
12909
|
constructor(message6, status = 400) {
|