pixelkiln 0.26.0 → 0.28.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 +1158 -307
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +512 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -3
- package/dist/index.d.ts +101 -3
- package/dist/index.js +525 -44
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/ARTIFACTS.md +25 -0
- package/docs/CLI.md +56 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { readFile as
|
|
4
|
+
import path30 from "path";
|
|
5
|
+
import { existsSync as existsSync23 } from "fs";
|
|
6
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -134,10 +134,10 @@ import { createReadStream } from "fs";
|
|
|
134
134
|
function sha256(data) {
|
|
135
135
|
return createHash("sha256").update(data).digest("hex");
|
|
136
136
|
}
|
|
137
|
-
async function sha256File(
|
|
137
|
+
async function sha256File(path31) {
|
|
138
138
|
return new Promise((resolve, reject) => {
|
|
139
139
|
const hash = createHash("sha256");
|
|
140
|
-
const stream = createReadStream(
|
|
140
|
+
const stream = createReadStream(path31);
|
|
141
141
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
142
142
|
stream.on("error", reject);
|
|
143
143
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -1617,11 +1617,11 @@ var PixelLabClient = class {
|
|
|
1617
1617
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1618
1618
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1619
1619
|
*/
|
|
1620
|
-
async request(
|
|
1620
|
+
async request(path31, init, attempt = 0) {
|
|
1621
1621
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1622
1622
|
let res;
|
|
1623
1623
|
try {
|
|
1624
|
-
res = await fetch(`${BASE}${
|
|
1624
|
+
res = await fetch(`${BASE}${path31}`, {
|
|
1625
1625
|
...init,
|
|
1626
1626
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1627
1627
|
headers: {
|
|
@@ -1633,24 +1633,24 @@ var PixelLabClient = class {
|
|
|
1633
1633
|
} catch (err) {
|
|
1634
1634
|
if (attempt < MAX_RETRIES) {
|
|
1635
1635
|
await sleep(backoffMs(attempt));
|
|
1636
|
-
return this.request(
|
|
1636
|
+
return this.request(path31, init, attempt + 1);
|
|
1637
1637
|
}
|
|
1638
1638
|
throw err;
|
|
1639
1639
|
}
|
|
1640
1640
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1641
1641
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1642
1642
|
await sleep(waitMs);
|
|
1643
|
-
return this.request(
|
|
1643
|
+
return this.request(path31, init, attempt + 1);
|
|
1644
1644
|
}
|
|
1645
1645
|
const text = await res.text();
|
|
1646
1646
|
if (!res.ok) {
|
|
1647
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1647
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path31} \u2192 ${res.status}`, res.status, text);
|
|
1648
1648
|
}
|
|
1649
1649
|
if (!text) return {};
|
|
1650
1650
|
try {
|
|
1651
1651
|
return JSON.parse(text);
|
|
1652
1652
|
} catch {
|
|
1653
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1653
|
+
throw new Error(`${init?.method ?? "GET"} ${path31} returned invalid JSON`);
|
|
1654
1654
|
}
|
|
1655
1655
|
}
|
|
1656
1656
|
async balance() {
|
|
@@ -2626,9 +2626,9 @@ var RetroDiffusionClient = class {
|
|
|
2626
2626
|
}
|
|
2627
2627
|
return balance;
|
|
2628
2628
|
}
|
|
2629
|
-
async call(
|
|
2629
|
+
async call(path31, init = {}) {
|
|
2630
2630
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2631
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2631
|
+
const response = await this.request(`${this.baseUrl}${path31}`, {
|
|
2632
2632
|
...init,
|
|
2633
2633
|
headers: {
|
|
2634
2634
|
"Content-Type": "application/json",
|
|
@@ -6872,10 +6872,10 @@ function isSha256Hash(value) {
|
|
|
6872
6872
|
function parseCache(value) {
|
|
6873
6873
|
return HashCacheSchema.parse(value);
|
|
6874
6874
|
}
|
|
6875
|
-
async function loadCache(
|
|
6876
|
-
if (!existsSync11(
|
|
6875
|
+
async function loadCache(path31) {
|
|
6876
|
+
if (!existsSync11(path31)) return { version: 1, hashes: {} };
|
|
6877
6877
|
try {
|
|
6878
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
6878
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path31, "utf8")));
|
|
6879
6879
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6880
6880
|
return {
|
|
6881
6881
|
version: 1,
|
|
@@ -6887,18 +6887,18 @@ async function loadCache(path29) {
|
|
|
6887
6887
|
return { version: 1, hashes: {} };
|
|
6888
6888
|
}
|
|
6889
6889
|
}
|
|
6890
|
-
async function saveCache(
|
|
6890
|
+
async function saveCache(path31, cache) {
|
|
6891
6891
|
const sorted = {};
|
|
6892
6892
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6893
6893
|
const hash = cache.hashes[key];
|
|
6894
6894
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6895
6895
|
sorted[key] = hash;
|
|
6896
6896
|
}
|
|
6897
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6898
|
-
const tmp = `${
|
|
6897
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path31)), { recursive: true });
|
|
6898
|
+
const tmp = `${path31}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6899
6899
|
try {
|
|
6900
6900
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6901
|
-
await rename4(tmp,
|
|
6901
|
+
await rename4(tmp, path31);
|
|
6902
6902
|
} finally {
|
|
6903
6903
|
await rm5(tmp, { force: true });
|
|
6904
6904
|
}
|
|
@@ -7842,7 +7842,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
7842
7842
|
// src/gallery/snapshot.ts
|
|
7843
7843
|
import { createHash as createHash3 } from "crypto";
|
|
7844
7844
|
import { existsSync as existsSync17 } from "fs";
|
|
7845
|
-
import { readFile as
|
|
7845
|
+
import { readFile as readFile16, stat as stat3 } from "fs/promises";
|
|
7846
7846
|
import path22 from "path";
|
|
7847
7847
|
|
|
7848
7848
|
// src/workspace.ts
|
|
@@ -7980,7 +7980,7 @@ function validateWorkspace(ws, dir) {
|
|
|
7980
7980
|
|
|
7981
7981
|
// src/gallery/edit.ts
|
|
7982
7982
|
import path21 from "path";
|
|
7983
|
-
import { z as
|
|
7983
|
+
import { z as z7 } from "zod";
|
|
7984
7984
|
|
|
7985
7985
|
// src/manifest-edit.ts
|
|
7986
7986
|
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
@@ -8234,8 +8234,9 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8234
8234
|
// src/pipeline/hand-edit.ts
|
|
8235
8235
|
import { spawn as spawn2 } from "child_process";
|
|
8236
8236
|
import { existsSync as existsSync16 } from "fs";
|
|
8237
|
-
import { copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
8237
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
|
|
8238
8238
|
import path20 from "path";
|
|
8239
|
+
import { z as z6 } from "zod";
|
|
8239
8240
|
var HAND_EDIT_DIR = "edits";
|
|
8240
8241
|
function handEditPath(loaded, spec) {
|
|
8241
8242
|
const style = loaded.manifest.styles[spec.styleId];
|
|
@@ -8305,6 +8306,89 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
8305
8306
|
});
|
|
8306
8307
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
8307
8308
|
}
|
|
8309
|
+
var HandEditCompanionSchema = z6.object({
|
|
8310
|
+
version: z6.literal(1),
|
|
8311
|
+
/** sha256 of the generated art the edit started from; null when it started from untracked art. */
|
|
8312
|
+
basedOn: z6.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
8313
|
+
/** sha256 of the edit PNG as saved, so a later change by another tool is visible. */
|
|
8314
|
+
sha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
8315
|
+
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
8316
|
+
editor: z6.string().min(1),
|
|
8317
|
+
protocol: z6.number().int().positive(),
|
|
8318
|
+
savedAt: z6.string().datetime(),
|
|
8319
|
+
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
8320
|
+
project: z6.string().min(1).nullable()
|
|
8321
|
+
}).strict();
|
|
8322
|
+
function handEditCompanionPath(editPath) {
|
|
8323
|
+
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
8324
|
+
}
|
|
8325
|
+
function handEditProjectPath(editPath) {
|
|
8326
|
+
return editPath.replace(/\.png$/i, "") + ".pxo";
|
|
8327
|
+
}
|
|
8328
|
+
async function readHandEditCompanion(editPath) {
|
|
8329
|
+
let text;
|
|
8330
|
+
try {
|
|
8331
|
+
text = await readFile15(handEditCompanionPath(editPath), "utf8");
|
|
8332
|
+
} catch {
|
|
8333
|
+
return null;
|
|
8334
|
+
}
|
|
8335
|
+
try {
|
|
8336
|
+
return HandEditCompanionSchema.parse(JSON.parse(text));
|
|
8337
|
+
} catch {
|
|
8338
|
+
return null;
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
8342
|
+
async function saveHandEdit(loaded, lock, spec, input) {
|
|
8343
|
+
if (input.png.length > MAX_HAND_EDIT_BYTES) {
|
|
8344
|
+
throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
8345
|
+
}
|
|
8346
|
+
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
8347
|
+
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
8348
|
+
}
|
|
8349
|
+
let decoded;
|
|
8350
|
+
try {
|
|
8351
|
+
validateMedia(input.png, MediaType.PNG);
|
|
8352
|
+
decoded = decodePng(input.png);
|
|
8353
|
+
} catch (error) {
|
|
8354
|
+
throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
8355
|
+
}
|
|
8356
|
+
const base = handEditBase(spec, lock);
|
|
8357
|
+
const expected = base ? decodePng(await readFile15(base)) : { width: spec.width, height: spec.height };
|
|
8358
|
+
if (decoded.width !== expected.width || decoded.height !== expected.height) {
|
|
8359
|
+
throw new ManifestEditError(
|
|
8360
|
+
`the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
|
|
8361
|
+
);
|
|
8362
|
+
}
|
|
8363
|
+
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
8364
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8365
|
+
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
8366
|
+
const digest2 = sha256(input.png);
|
|
8367
|
+
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
8368
|
+
const companion = {
|
|
8369
|
+
version: 1,
|
|
8370
|
+
basedOn,
|
|
8371
|
+
sha256: digest2,
|
|
8372
|
+
editor: input.editor,
|
|
8373
|
+
protocol: input.protocol,
|
|
8374
|
+
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8375
|
+
project: projectPath ? path20.basename(projectPath) : null
|
|
8376
|
+
};
|
|
8377
|
+
await replaceFile(started.editPath, input.png);
|
|
8378
|
+
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
8379
|
+
const companionPath = handEditCompanionPath(started.editPath);
|
|
8380
|
+
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
8381
|
+
return { ...started, sha256: digest2, basedOn, companionPath, projectPath };
|
|
8382
|
+
}
|
|
8383
|
+
async function replaceFile(file, bytes) {
|
|
8384
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8385
|
+
try {
|
|
8386
|
+
await writeFile9(tmp, bytes);
|
|
8387
|
+
await rename7(tmp, file);
|
|
8388
|
+
} finally {
|
|
8389
|
+
await rm8(tmp, { force: true });
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8308
8392
|
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
8309
8393
|
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
8310
8394
|
const [program, ...args] = command;
|
|
@@ -8316,22 +8400,37 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
8316
8400
|
}
|
|
8317
8401
|
|
|
8318
8402
|
// src/gallery/edit.ts
|
|
8319
|
-
var
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8403
|
+
var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
8404
|
+
var HandEditRequestSchema = z7.discriminatedUnion("action", [
|
|
8405
|
+
z7.object({
|
|
8406
|
+
action: z7.literal("start-edit"),
|
|
8407
|
+
project: z7.string().min(1).optional(),
|
|
8408
|
+
assetId: z7.string().min(1),
|
|
8409
|
+
styleId: z7.string().min(1),
|
|
8410
|
+
expectedSha256: z7.string().regex(/^[0-9a-f]{64}$/),
|
|
8326
8411
|
/** Also launch the author's editor on the file. */
|
|
8327
|
-
open:
|
|
8412
|
+
open: z7.boolean().optional()
|
|
8328
8413
|
}).strict(),
|
|
8329
|
-
|
|
8330
|
-
action:
|
|
8331
|
-
project:
|
|
8332
|
-
assetId:
|
|
8333
|
-
styleId:
|
|
8334
|
-
expectedSha256:
|
|
8414
|
+
z7.object({
|
|
8415
|
+
action: z7.literal("detach-edit"),
|
|
8416
|
+
project: z7.string().min(1).optional(),
|
|
8417
|
+
assetId: z7.string().min(1),
|
|
8418
|
+
styleId: z7.string().min(1),
|
|
8419
|
+
expectedSha256: z7.string().regex(/^[0-9a-f]{64}$/)
|
|
8420
|
+
}).strict(),
|
|
8421
|
+
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
8422
|
+
z7.object({
|
|
8423
|
+
action: z7.literal("save-edit"),
|
|
8424
|
+
project: z7.string().min(1).optional(),
|
|
8425
|
+
assetId: z7.string().min(1),
|
|
8426
|
+
styleId: z7.string().min(1),
|
|
8427
|
+
expectedSha256: z7.string().regex(/^[0-9a-f]{64}$/),
|
|
8428
|
+
png: Base64.min(1),
|
|
8429
|
+
/** The editor's layered project file, kept beside the edit. */
|
|
8430
|
+
pxo: Base64.optional(),
|
|
8431
|
+
/** Editor identity from the bridge's ready message. */
|
|
8432
|
+
editor: z7.string().min(1).max(200),
|
|
8433
|
+
protocol: z7.number().int().positive()
|
|
8335
8434
|
}).strict()
|
|
8336
8435
|
]);
|
|
8337
8436
|
function createGalleryEditHandler(opts) {
|
|
@@ -8352,6 +8451,15 @@ function createGalleryEditHandler(opts) {
|
|
|
8352
8451
|
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
8353
8452
|
log2(` opened with: ${command}`);
|
|
8354
8453
|
}
|
|
8454
|
+
} else if (request.action === "save-edit") {
|
|
8455
|
+
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
8456
|
+
png: Buffer.from(request.png, "base64"),
|
|
8457
|
+
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
8458
|
+
editor: request.editor,
|
|
8459
|
+
protocol: request.protocol,
|
|
8460
|
+
expectedSha256: request.expectedSha256
|
|
8461
|
+
});
|
|
8462
|
+
log2(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
8355
8463
|
} else {
|
|
8356
8464
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
8357
8465
|
if (result2.changed) log2(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -8382,6 +8490,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8382
8490
|
}
|
|
8383
8491
|
|
|
8384
8492
|
// src/gallery/snapshot.ts
|
|
8493
|
+
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
8385
8494
|
function galleryMediaId(absolutePath) {
|
|
8386
8495
|
return createHash3("sha256").update(path22.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8387
8496
|
}
|
|
@@ -8501,7 +8610,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8501
8610
|
const media = /* @__PURE__ */ new Map();
|
|
8502
8611
|
const plan = await buildPlan(specs, lock);
|
|
8503
8612
|
const items = [];
|
|
8504
|
-
const manifestText = await
|
|
8613
|
+
const manifestText = await readFile16(loaded.path, "utf8");
|
|
8505
8614
|
const manifestSha256 = sha256(manifestText);
|
|
8506
8615
|
let rawStyles = {};
|
|
8507
8616
|
try {
|
|
@@ -8516,6 +8625,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8516
8625
|
let outputs;
|
|
8517
8626
|
let edit = null;
|
|
8518
8627
|
let editStatus = null;
|
|
8628
|
+
let editMeta = null;
|
|
8519
8629
|
if (entry && entry.outputs.length) {
|
|
8520
8630
|
outputs = await Promise.all(
|
|
8521
8631
|
entry.outputs.map(
|
|
@@ -8527,7 +8637,27 @@ async function buildGallerySnapshot(opts) {
|
|
|
8527
8637
|
const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
|
|
8528
8638
|
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
8529
8639
|
const generated = outputs[0];
|
|
8530
|
-
|
|
8640
|
+
const companion = await readHandEditCompanion(editPath);
|
|
8641
|
+
if (companion) {
|
|
8642
|
+
const projectPath = handEditProjectPath(editPath);
|
|
8643
|
+
const project = companion.project && existsSync17(projectPath) ? await fileInfo(projectPath) : null;
|
|
8644
|
+
let projectUrl = null;
|
|
8645
|
+
if (project) {
|
|
8646
|
+
const id = galleryMediaId(projectPath);
|
|
8647
|
+
media.set(id, { path: projectPath, contentType: PROJECT_FILE_TYPE });
|
|
8648
|
+
projectUrl = `${galleryMediaRoute(id)}?v=${project.bytes}-${Date.parse(project.modifiedAt).toString(36)}`;
|
|
8649
|
+
}
|
|
8650
|
+
editMeta = {
|
|
8651
|
+
editor: companion.editor,
|
|
8652
|
+
savedAt: companion.savedAt,
|
|
8653
|
+
basedOn: companion.basedOn,
|
|
8654
|
+
changedSince: editSha !== null && editSha !== companion.sha256,
|
|
8655
|
+
project: project ? portableOutputPath(projectPath, root) : null,
|
|
8656
|
+
projectUrl
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
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);
|
|
8660
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
8531
8661
|
}
|
|
8532
8662
|
} else if (spec.source) {
|
|
8533
8663
|
outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
|
|
@@ -8577,6 +8707,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8577
8707
|
source: spec.source ?? null,
|
|
8578
8708
|
edit,
|
|
8579
8709
|
editStatus,
|
|
8710
|
+
editMeta,
|
|
8580
8711
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8581
8712
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
8582
8713
|
tags: spec.tags,
|
|
@@ -8633,6 +8764,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8633
8764
|
source: null,
|
|
8634
8765
|
edit: null,
|
|
8635
8766
|
editStatus: null,
|
|
8767
|
+
editMeta: null,
|
|
8636
8768
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8637
8769
|
refreshable: false,
|
|
8638
8770
|
tags: [],
|
|
@@ -8774,7 +8906,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8774
8906
|
root: loaded.root,
|
|
8775
8907
|
provider: loaded.manifest.provider,
|
|
8776
8908
|
account: project.account ?? null,
|
|
8777
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
8909
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
|
|
8778
8910
|
entries: Object.keys(lock.entries).length,
|
|
8779
8911
|
items: projectItems.length,
|
|
8780
8912
|
spendByUnit: spendByUnit(lock),
|
|
@@ -8832,7 +8964,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8832
8964
|
import { spawn as spawn3 } from "child_process";
|
|
8833
8965
|
import { randomBytes } from "crypto";
|
|
8834
8966
|
import { createServer as createServer2 } from "http";
|
|
8835
|
-
import { readFile as
|
|
8967
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
8836
8968
|
|
|
8837
8969
|
// src/gallery/page.ts
|
|
8838
8970
|
function renderGallery(snapshot, opts = {}) {
|
|
@@ -8840,6 +8972,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
8840
8972
|
const session = JSON.stringify(opts.session ?? null);
|
|
8841
8973
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
8842
8974
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
8975
|
+
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
8843
8976
|
const title = `pixelkiln \u2014 ${snapshot.project?.name ?? "workspace"}`;
|
|
8844
8977
|
return `<!doctype html>
|
|
8845
8978
|
<html lang="en">
|
|
@@ -9045,6 +9178,15 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9045
9178
|
.job .acts button { padding:3px 9px; font-size:12px; }
|
|
9046
9179
|
.job pre { grid-column:1 / -1; margin:0; padding:8px 10px; border-top:1px solid var(--line);
|
|
9047
9180
|
font:11.5px/1.45 var(--mono); color:var(--dim); max-height:220px; overflow:auto; white-space:pre-wrap; }
|
|
9181
|
+
#tools { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; }
|
|
9182
|
+
#tools:empty { display:none; }
|
|
9183
|
+
.tool { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
|
|
9184
|
+
grid-template-columns:auto 1fr auto; gap:6px 14px; align-items:center; font-size:12.5px; }
|
|
9185
|
+
.tool .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1 var(--mono); white-space:nowrap; }
|
|
9186
|
+
.tool .last { color:var(--dim); min-width:0; }
|
|
9187
|
+
.tool .acts { display:flex; gap:6px; }
|
|
9188
|
+
.tool .acts button { padding:3px 9px; font-size:12px; }
|
|
9189
|
+
.tool progress { grid-column:1 / -1; width:100%; height:6px; accent-color:var(--accent); }
|
|
9048
9190
|
.budget { color:var(--dim); }
|
|
9049
9191
|
.budget b { color:var(--text); }
|
|
9050
9192
|
.dialog { position:fixed; inset:0; z-index:30; display:grid; place-items:center; background:rgba(0,0,0,.55); }
|
|
@@ -9065,6 +9207,16 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9065
9207
|
border-left:1px solid var(--line-strong); display:grid; grid-template-rows:auto 1fr; min-width:0; }
|
|
9066
9208
|
.sheet.review-host { width:min(1180px, 94vw); }
|
|
9067
9209
|
.sheet.compare-host { width:min(1500px, 96vw); }
|
|
9210
|
+
.sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
|
|
9211
|
+
.sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
|
|
9212
|
+
.sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
|
9213
|
+
.sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
|
|
9214
|
+
.sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
|
|
9215
|
+
.sheet.editor-host .stage { position:relative; min-height:0; }
|
|
9216
|
+
.sheet.editor-host .stage iframe { position:absolute; inset:0; }
|
|
9217
|
+
.sheet.editor-host .loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--bg);
|
|
9218
|
+
color:var(--dim); text-align:center; padding:24px; }
|
|
9219
|
+
.sheet.editor-host .loading b { display:block; color:var(--text); margin-bottom:6px; }
|
|
9068
9220
|
.sheet .rbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line-strong);
|
|
9069
9221
|
background:var(--panel); font-size:13px; flex-wrap:wrap; }
|
|
9070
9222
|
.sheet .rbar span { color:var(--dim); }
|
|
@@ -9103,7 +9255,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9103
9255
|
.tray .pick img { width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
|
|
9104
9256
|
.tray .pick button { padding:0 5px; border-color:transparent; color:var(--dim); font-size:12px; }
|
|
9105
9257
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
9106
|
-
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
9258
|
+
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
9107
9259
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9108
9260
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9109
9261
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
@@ -9151,6 +9303,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9151
9303
|
<div class="totals" id="totals"></div>
|
|
9152
9304
|
<div class="chips" id="chips"></div>
|
|
9153
9305
|
<div id="jobs"></div>
|
|
9306
|
+
<div id="tools"></div>
|
|
9154
9307
|
</header>
|
|
9155
9308
|
<main id="root"></main>
|
|
9156
9309
|
<footer>
|
|
@@ -9169,6 +9322,7 @@ const INITIAL = ${data};
|
|
|
9169
9322
|
const SESSION = ${session};
|
|
9170
9323
|
const EDITABLE = ${editable};
|
|
9171
9324
|
const GENERATION = ${generation};
|
|
9325
|
+
const EDITOR = ${editor};
|
|
9172
9326
|
let snap = INITIAL;
|
|
9173
9327
|
const STATE_TONE = {
|
|
9174
9328
|
ok: 'ok', stale: 'warn', orphaned: 'warn', untracked: 'warn', blocked: 'warn',
|
|
@@ -9336,6 +9490,92 @@ function renderJobs() {
|
|
|
9336
9490
|
host.append(row);
|
|
9337
9491
|
}
|
|
9338
9492
|
}
|
|
9493
|
+
// ---- the in-browser editor (only when the server serves one) -------------
|
|
9494
|
+
// The build is a 46 MB web export fetched once into a user-level cache and
|
|
9495
|
+
// verified against hashes this PixelKiln release carries; the page only
|
|
9496
|
+
// reports and asks, the server downloads. Nothing is fetched without a click.
|
|
9497
|
+
|
|
9498
|
+
let ED = null;
|
|
9499
|
+
let edTimer = null;
|
|
9500
|
+
const fmtMb = (bytes) => (bytes / 1e6).toFixed(1) + ' MB';
|
|
9501
|
+
async function pollEditor() {
|
|
9502
|
+
if (!EDITOR) return;
|
|
9503
|
+
try {
|
|
9504
|
+
const res = await fetch('/api/editor', { cache: 'no-store' });
|
|
9505
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9506
|
+
ED = await res.json();
|
|
9507
|
+
} catch (err) {
|
|
9508
|
+
ED = { error: 'status unavailable: ' + err.message, installed: false, installing: null, missing: [], totalBytes: 0 };
|
|
9509
|
+
}
|
|
9510
|
+
renderTools();
|
|
9511
|
+
clearTimeout(edTimer);
|
|
9512
|
+
if (ED.installing) edTimer = setTimeout(pollEditor, 1000);
|
|
9513
|
+
else if (ui.open) renderDrawer();
|
|
9514
|
+
}
|
|
9515
|
+
async function installEditor() {
|
|
9516
|
+
try {
|
|
9517
|
+
const res = await fetch('/api/editor/install', {
|
|
9518
|
+
method: 'POST',
|
|
9519
|
+
headers: { 'Content-Type': 'application/json', 'X-Pixelkiln-Session': SESSION },
|
|
9520
|
+
body: '{}',
|
|
9521
|
+
});
|
|
9522
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9523
|
+
ED = await res.json();
|
|
9524
|
+
} catch (err) {
|
|
9525
|
+
ED = Object.assign({}, ED, { error: err.message });
|
|
9526
|
+
}
|
|
9527
|
+
renderTools();
|
|
9528
|
+
clearTimeout(edTimer);
|
|
9529
|
+
edTimer = setTimeout(pollEditor, 500);
|
|
9530
|
+
}
|
|
9531
|
+
function renderTools() {
|
|
9532
|
+
const host = $('tools');
|
|
9533
|
+
host.textContent = '';
|
|
9534
|
+
if (!EDITOR || !ED) return;
|
|
9535
|
+
const row = el('div', 'tool');
|
|
9536
|
+
const ph = el('span', 'ph');
|
|
9537
|
+
const last = el('span', 'last');
|
|
9538
|
+
const acts = el('div', 'acts');
|
|
9539
|
+
const name = 'Browser editor' + (ED.pixelorama ? ' (Pixelorama ' + ED.pixelorama + ')' : '');
|
|
9540
|
+
if (ED.installing) {
|
|
9541
|
+
const p = ED.installing;
|
|
9542
|
+
ph.append(el('i', 'dot cool'), document.createTextNode('installing'));
|
|
9543
|
+
last.textContent = name + ': fetching ' + (p.file || 'the build') + ' \u2014 ' + fmtMb(p.fetchedBytes) + ' of ' + fmtMb(p.totalBytes);
|
|
9544
|
+
row.append(ph, last, acts);
|
|
9545
|
+
const bar = el('progress');
|
|
9546
|
+
if (p.totalBytes) { bar.max = p.totalBytes; bar.value = p.fetchedBytes; }
|
|
9547
|
+
row.append(bar);
|
|
9548
|
+
} else if (ED.installed) {
|
|
9549
|
+
ph.append(el('i', 'dot ok'), document.createTextNode('ready'));
|
|
9550
|
+
last.textContent = name + ' is installed and verified; a record\u2019s Hand edit section opens sprites in it.';
|
|
9551
|
+
last.title = ED.dir || '';
|
|
9552
|
+
const b = el('button', null, 'Open editor');
|
|
9553
|
+
b.type = 'button'; b.onclick = () => window.open(ED.url, '_blank', 'noopener');
|
|
9554
|
+
acts.append(b);
|
|
9555
|
+
row.append(ph, last, acts);
|
|
9556
|
+
} else if (ED.error) {
|
|
9557
|
+
ph.append(el('i', 'dot bad'), document.createTextNode('failed'));
|
|
9558
|
+
last.textContent = name + ': ' + ED.error;
|
|
9559
|
+
const b = el('button', 'primary', 'Retry install');
|
|
9560
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
9561
|
+
acts.append(b);
|
|
9562
|
+
row.append(ph, last, acts);
|
|
9563
|
+
} else if (!ED.release) {
|
|
9564
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('unavailable'));
|
|
9565
|
+
last.textContent = name + ': this PixelKiln version pins no published build.';
|
|
9566
|
+
row.append(ph, last, acts);
|
|
9567
|
+
} else {
|
|
9568
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('not installed'));
|
|
9569
|
+
last.textContent = name + ': ' + fmtMb(ED.totalBytes - (ED.installedBytes || 0)) + ' fetched once from the PixelKiln release ' +
|
|
9570
|
+
ED.release + ', verified against the hashes this version carries, and kept outside the project.';
|
|
9571
|
+
const b = el('button', 'primary', 'Install editor');
|
|
9572
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
9573
|
+
acts.append(b);
|
|
9574
|
+
row.append(ph, last, acts);
|
|
9575
|
+
}
|
|
9576
|
+
host.append(row);
|
|
9577
|
+
}
|
|
9578
|
+
|
|
9339
9579
|
function budgetLine() {
|
|
9340
9580
|
const providers = new Set([...Object.keys(GEN.budget.byProvider), ...Object.keys(GEN.spent)]);
|
|
9341
9581
|
const parts = [];
|
|
@@ -9442,8 +9682,183 @@ function openReview(jobId) {
|
|
|
9442
9682
|
host.append(scrim, panel);
|
|
9443
9683
|
}
|
|
9444
9684
|
window.addEventListener('message', (e) => {
|
|
9445
|
-
if (e.origin !== location.origin || !e.data || e.data.type !== '
|
|
9446
|
-
|
|
9685
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== 'string') return;
|
|
9686
|
+
if (e.data.type === 'pixelkiln:review-applied') {
|
|
9687
|
+
setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
|
|
9688
|
+
return;
|
|
9689
|
+
}
|
|
9690
|
+
if (SHEET && e.source === SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
9691
|
+
});
|
|
9692
|
+
|
|
9693
|
+
// ---- the in-browser editor sheet -------------------------------------------
|
|
9694
|
+
// The page is the protocol host: it hands the editor the PNG and palette,
|
|
9695
|
+
// asks it for the image back, and does the authenticated write itself. The
|
|
9696
|
+
// iframe never sees the session token, and the server validates, sizes, and
|
|
9697
|
+
// declares the file the same way pixelkiln edit does.
|
|
9698
|
+
|
|
9699
|
+
let SHEET = null;
|
|
9700
|
+
const editorSource = (item) => item.edit && item.edit.exists ? item.edit
|
|
9701
|
+
: item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
|
|
9702
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
|
|
9703
|
+
!(item.outputs.length > 1);
|
|
9704
|
+
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
9705
|
+
const r = new FileReader();
|
|
9706
|
+
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
9707
|
+
r.onerror = () => reject(r.error);
|
|
9708
|
+
r.readAsDataURL(new Blob([buf]));
|
|
9709
|
+
});
|
|
9710
|
+
function sheetStatus(tone, text) {
|
|
9711
|
+
if (!SHEET) return;
|
|
9712
|
+
SHEET.status.textContent = '';
|
|
9713
|
+
SHEET.status.append(el('i', 'dot ' + tone), document.createTextNode(text));
|
|
9714
|
+
}
|
|
9715
|
+
function sheetButtons() {
|
|
9716
|
+
if (!SHEET) return;
|
|
9717
|
+
const busy = !SHEET.opened || SHEET.pending !== null;
|
|
9718
|
+
SHEET.save.disabled = busy || !SHEET.dirty;
|
|
9719
|
+
SHEET.saveClose.disabled = busy;
|
|
9720
|
+
}
|
|
9721
|
+
function openEditorSheet(item) {
|
|
9722
|
+
if (SHEET) return;
|
|
9723
|
+
const src = editorSource(item);
|
|
9724
|
+
const host = $('dialog-host');
|
|
9725
|
+
host.textContent = '';
|
|
9726
|
+
const scrim = el('div', 'sheet-scrim');
|
|
9727
|
+
scrim.onclick = () => closeEditorSheet();
|
|
9728
|
+
const panel = el('div', 'sheet editor-host');
|
|
9729
|
+
const bar = el('div', 'rbar');
|
|
9730
|
+
const close = el('button', null, 'Close'); close.type = 'button';
|
|
9731
|
+
close.onclick = () => closeEditorSheet();
|
|
9732
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
9733
|
+
const status = el('span', 'st');
|
|
9734
|
+
const acts = el('div', 'acts');
|
|
9735
|
+
const msg = el('span', 'msg');
|
|
9736
|
+
const save = el('button', 'primary', 'Save to project'); save.type = 'button';
|
|
9737
|
+
save.onclick = () => requestEditorSave(false);
|
|
9738
|
+
const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
|
|
9739
|
+
saveClose.onclick = () => requestEditorSave(true);
|
|
9740
|
+
acts.append(msg, save, saveClose);
|
|
9741
|
+
bar.append(close, title, status, acts);
|
|
9742
|
+
const stage = el('div', 'stage');
|
|
9743
|
+
const frame = el('iframe');
|
|
9744
|
+
frame.title = 'Pixelorama';
|
|
9745
|
+
frame.src = ED.url;
|
|
9746
|
+
const loading = el('div', 'loading');
|
|
9747
|
+
const loadText = el('div');
|
|
9748
|
+
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.'));
|
|
9749
|
+
loading.append(loadText);
|
|
9750
|
+
stage.append(frame, loading);
|
|
9751
|
+
panel.append(bar, stage);
|
|
9752
|
+
host.append(scrim, panel);
|
|
9753
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false };
|
|
9754
|
+
sheetStatus('cool', 'loading');
|
|
9755
|
+
sheetButtons();
|
|
9756
|
+
close.focus({ preventScroll: true });
|
|
9757
|
+
}
|
|
9758
|
+
// The edit file is what the editor gets; when a browser save kept the layered
|
|
9759
|
+
// project beside it, that goes along too and the editor restores the layers,
|
|
9760
|
+
// falling back to the flattened PNG if the file cannot be read.
|
|
9761
|
+
async function sendOpen() {
|
|
9762
|
+
const { item, source } = SHEET;
|
|
9763
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
9764
|
+
const projectUrl = source === item.edit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
|
|
9765
|
+
let png, pxo = null;
|
|
9766
|
+
try {
|
|
9767
|
+
const res = await fetch(source.url, { cache: 'no-store' });
|
|
9768
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9769
|
+
png = await res.arrayBuffer();
|
|
9770
|
+
if (projectUrl) {
|
|
9771
|
+
const p = await fetch(projectUrl, { cache: 'no-store' });
|
|
9772
|
+
if (p.ok) pxo = await p.arrayBuffer();
|
|
9773
|
+
}
|
|
9774
|
+
} catch (err) {
|
|
9775
|
+
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
9776
|
+
return;
|
|
9777
|
+
}
|
|
9778
|
+
if (!SHEET) return;
|
|
9779
|
+
const request = 'open-' + (++SHEET.requests);
|
|
9780
|
+
SHEET.sentProject = !!pxo;
|
|
9781
|
+
const message = {
|
|
9782
|
+
type: 'pixelkiln:open', request,
|
|
9783
|
+
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
9784
|
+
png, palette: style ? style.palette : [],
|
|
9785
|
+
};
|
|
9786
|
+
const transfer = [png];
|
|
9787
|
+
if (pxo) { message.pxo = pxo; transfer.push(pxo); }
|
|
9788
|
+
SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
|
|
9789
|
+
sheetStatus('cool', 'opening');
|
|
9790
|
+
}
|
|
9791
|
+
function requestEditorSave(close) {
|
|
9792
|
+
if (!SHEET || !SHEET.opened || SHEET.pending) return;
|
|
9793
|
+
const request = 'save-' + (++SHEET.requests);
|
|
9794
|
+
SHEET.pending = { request, close };
|
|
9795
|
+
SHEET.msg.textContent = '';
|
|
9796
|
+
sheetStatus('cool', 'saving');
|
|
9797
|
+
sheetButtons();
|
|
9798
|
+
SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:request-save', request }, location.origin);
|
|
9799
|
+
}
|
|
9800
|
+
async function onEditorMessage(m) {
|
|
9801
|
+
switch (m.type) {
|
|
9802
|
+
case 'pixelkiln:ready':
|
|
9803
|
+
SHEET.ready = m;
|
|
9804
|
+
if (m.version !== ED.protocol) {
|
|
9805
|
+
SHEET.msg.textContent = 'The editor speaks protocol ' + m.version + '; this gallery expects ' + ED.protocol + '. Reinstall it with pixelkiln tools install editor.';
|
|
9806
|
+
sheetStatus('bad', 'mismatch');
|
|
9807
|
+
return;
|
|
9808
|
+
}
|
|
9809
|
+
sendOpen();
|
|
9810
|
+
break;
|
|
9811
|
+
case 'pixelkiln:opened':
|
|
9812
|
+
SHEET.opened = true;
|
|
9813
|
+
SHEET.loading.remove();
|
|
9814
|
+
if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file with its ' + (m.layers === 1 ? 'layer' : m.layers + ' layers') + ' restored');
|
|
9815
|
+
else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file; its layer file could not be opened');
|
|
9816
|
+
else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
9817
|
+
sheetButtons();
|
|
9818
|
+
break;
|
|
9819
|
+
case 'pixelkiln:dirty':
|
|
9820
|
+
SHEET.dirty = !!m.dirty;
|
|
9821
|
+
if (SHEET.opened && !SHEET.pending) sheetStatus(SHEET.dirty ? 'warn' : 'ok', SHEET.dirty ? 'unsaved changes' : 'saved');
|
|
9822
|
+
sheetButtons();
|
|
9823
|
+
break;
|
|
9824
|
+
case 'pixelkiln:save': {
|
|
9825
|
+
const pending = SHEET.pending;
|
|
9826
|
+
if (!pending || pending.request !== m.request) return;
|
|
9827
|
+
try {
|
|
9828
|
+
const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
9829
|
+
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
9830
|
+
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
9831
|
+
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
9832
|
+
if (saved) SHEET.item = saved;
|
|
9833
|
+
SHEET.dirty = false;
|
|
9834
|
+
SHEET.pending = null;
|
|
9835
|
+
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.' };
|
|
9836
|
+
render();
|
|
9837
|
+
if (pending.close) { closeEditorSheet(true); return; }
|
|
9838
|
+
sheetStatus('ok', 'saved');
|
|
9839
|
+
} catch (err) {
|
|
9840
|
+
SHEET.pending = null;
|
|
9841
|
+
SHEET.msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh, then save again.' : '');
|
|
9842
|
+
sheetStatus('bad', 'not saved');
|
|
9843
|
+
}
|
|
9844
|
+
sheetButtons();
|
|
9845
|
+
break;
|
|
9846
|
+
}
|
|
9847
|
+
case 'pixelkiln:error':
|
|
9848
|
+
SHEET.msg.textContent = m.message || 'The editor reported an error.';
|
|
9849
|
+
if (SHEET.pending) { SHEET.pending = null; sheetStatus('bad', 'not saved'); sheetButtons(); }
|
|
9850
|
+
break;
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
function closeEditorSheet(force) {
|
|
9854
|
+
if (!SHEET) return;
|
|
9855
|
+
if (SHEET.dirty && !force && !confirm('Discard unsaved changes in the editor?')) return;
|
|
9856
|
+
SHEET = null;
|
|
9857
|
+
$('dialog-host').textContent = '';
|
|
9858
|
+
refresh();
|
|
9859
|
+
}
|
|
9860
|
+
window.addEventListener('beforeunload', (e) => {
|
|
9861
|
+
if (SHEET && SHEET.dirty) { e.preventDefault(); e.returnValue = ''; }
|
|
9447
9862
|
});
|
|
9448
9863
|
|
|
9449
9864
|
function visibleItems() {
|
|
@@ -10019,6 +10434,11 @@ function handEditSection(item) {
|
|
|
10019
10434
|
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
10020
10435
|
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
10021
10436
|
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
10437
|
+
if (item.editMeta) {
|
|
10438
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
|
|
10439
|
+
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
10440
|
+
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
10441
|
+
}
|
|
10022
10442
|
s.append(dl);
|
|
10023
10443
|
} else {
|
|
10024
10444
|
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.'));
|
|
@@ -10027,7 +10447,7 @@ function handEditSection(item) {
|
|
|
10027
10447
|
const acts = el('div', 'hand-actions');
|
|
10028
10448
|
const msg = el('span', 'msg');
|
|
10029
10449
|
const go = async (label, action, extra, done) => {
|
|
10030
|
-
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
10450
|
+
const b = el('button', label === 'Detach edit' || (EDITOR && ED && ED.installed) ? null : 'primary', label); b.type = 'button';
|
|
10031
10451
|
b.onclick = async () => {
|
|
10032
10452
|
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
10033
10453
|
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
@@ -10035,8 +10455,20 @@ function handEditSection(item) {
|
|
|
10035
10455
|
};
|
|
10036
10456
|
acts.append(b);
|
|
10037
10457
|
};
|
|
10458
|
+
if (EDITOR && ED && (item.edit || canStart) && editorSource(item)) {
|
|
10459
|
+
if (ED.installed) {
|
|
10460
|
+
const b = el('button', 'primary', 'Edit in browser'); b.type = 'button';
|
|
10461
|
+
b.title = 'Open in Pixelorama here; Save writes the edit file and declares it';
|
|
10462
|
+
b.onclick = () => openEditorSheet(item);
|
|
10463
|
+
acts.append(b);
|
|
10464
|
+
} else if (ED.release && !ED.installing) {
|
|
10465
|
+
const b = el('button', null, 'Install editor to edit in browser'); b.type = 'button';
|
|
10466
|
+
b.onclick = () => { installEditor(); window.scrollTo({ top: 0 }); };
|
|
10467
|
+
acts.append(b);
|
|
10468
|
+
}
|
|
10469
|
+
}
|
|
10038
10470
|
if (item.edit) {
|
|
10039
|
-
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10471
|
+
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10040
10472
|
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
10041
10473
|
} else if (canStart) {
|
|
10042
10474
|
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.');
|
|
@@ -10763,6 +11195,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
10763
11195
|
if (e.key === '/' && !typing) { e.preventDefault(); $('q').focus(); return; }
|
|
10764
11196
|
if (e.key === 'Escape') {
|
|
10765
11197
|
if (typing && document.activeElement.id === 'q') { document.activeElement.blur(); return; }
|
|
11198
|
+
if (SHEET) { e.preventDefault(); closeEditorSheet(); return; }
|
|
10766
11199
|
if ($('dialog-host').childNodes.length) { e.preventDefault(); $('dialog-host').textContent = ''; return; }
|
|
10767
11200
|
if (ui.open) { e.preventDefault(); closeItem(); }
|
|
10768
11201
|
return;
|
|
@@ -10784,6 +11217,7 @@ ui.compare = ui.compare.filter((id) => snap.items.some((i) => i.id === id));
|
|
|
10784
11217
|
render();
|
|
10785
11218
|
if (ui.open) document.querySelector('.card.active')?.scrollIntoView({ block: 'center' });
|
|
10786
11219
|
if (GENERATION) pollJobs();
|
|
11220
|
+
if (EDITOR) pollEditor();
|
|
10787
11221
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
10788
11222
|
</script>
|
|
10789
11223
|
</body>
|
|
@@ -10792,7 +11226,16 @@ if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
|
10792
11226
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
10793
11227
|
|
|
10794
11228
|
// src/gallery/server.ts
|
|
10795
|
-
var MAX_EDIT_BYTES =
|
|
11229
|
+
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
11230
|
+
var EDITOR_CSP = [
|
|
11231
|
+
"default-src 'self'",
|
|
11232
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
|
11233
|
+
"style-src 'self' 'unsafe-inline'",
|
|
11234
|
+
"img-src 'self' data: blob:",
|
|
11235
|
+
"worker-src 'self' blob:",
|
|
11236
|
+
"connect-src 'self'",
|
|
11237
|
+
"frame-ancestors 'self'"
|
|
11238
|
+
].join("; ");
|
|
10796
11239
|
async function readJsonBody(req) {
|
|
10797
11240
|
const chunks = [];
|
|
10798
11241
|
let bytes = 0;
|
|
@@ -10810,7 +11253,7 @@ async function readJsonBody(req) {
|
|
|
10810
11253
|
async function serveGallery(opts) {
|
|
10811
11254
|
const log2 = opts.onProgress ?? (() => {
|
|
10812
11255
|
});
|
|
10813
|
-
const session = opts.edit || opts.generate ? randomBytes(16).toString("hex") : null;
|
|
11256
|
+
const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
|
|
10814
11257
|
let media = /* @__PURE__ */ new Map();
|
|
10815
11258
|
let loading = null;
|
|
10816
11259
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -10885,6 +11328,17 @@ async function serveGallery(opts) {
|
|
|
10885
11328
|
}
|
|
10886
11329
|
return;
|
|
10887
11330
|
}
|
|
11331
|
+
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
11332
|
+
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
11333
|
+
try {
|
|
11334
|
+
const body = await guardedBody("editor installs");
|
|
11335
|
+
if (body === void 0) return;
|
|
11336
|
+
json(202, await opts.editor.install());
|
|
11337
|
+
} catch (err) {
|
|
11338
|
+
reportError("editor install", err);
|
|
11339
|
+
}
|
|
11340
|
+
return;
|
|
11341
|
+
}
|
|
10888
11342
|
const reviewMatch = /^\/review\/([0-9a-f]{16})(\/.*)?$/.exec(url2.pathname);
|
|
10889
11343
|
if (reviewMatch && opts.generate) {
|
|
10890
11344
|
const [, jobId, rest = ""] = reviewMatch;
|
|
@@ -10930,7 +11384,7 @@ async function serveGallery(opts) {
|
|
|
10930
11384
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
10931
11385
|
if (!asset) return fail(404, "no such review asset");
|
|
10932
11386
|
try {
|
|
10933
|
-
const bytes = await
|
|
11387
|
+
const bytes = await readFile17(asset.path);
|
|
10934
11388
|
res.writeHead(200, {
|
|
10935
11389
|
"Content-Type": asset.contentType,
|
|
10936
11390
|
"Cache-Control": "no-store",
|
|
@@ -10948,6 +11402,32 @@ async function serveGallery(opts) {
|
|
|
10948
11402
|
if (!opts.generate) return fail(404, "generation is off");
|
|
10949
11403
|
return json(200, opts.generate.status());
|
|
10950
11404
|
}
|
|
11405
|
+
if (url2.pathname === "/api/editor") {
|
|
11406
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
11407
|
+
try {
|
|
11408
|
+
return json(200, await opts.editor.status());
|
|
11409
|
+
} catch (err) {
|
|
11410
|
+
return reportError("editor status", err);
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
const editorMatch = /^\/editor\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/.exec(url2.pathname);
|
|
11414
|
+
if (editorMatch) {
|
|
11415
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
11416
|
+
const file = await opts.editor.file(editorMatch[1], editorMatch[2]);
|
|
11417
|
+
if (!file) return fail(404, "that editor file is not installed; check /api/editor");
|
|
11418
|
+
res.writeHead(200, {
|
|
11419
|
+
"Content-Type": file.contentType,
|
|
11420
|
+
"Content-Length": file.bytes.length,
|
|
11421
|
+
// The release tag is in the path, so these bytes never change under
|
|
11422
|
+
// this URL; the 40 MB wasm should come from the browser cache.
|
|
11423
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
11424
|
+
"X-Content-Type-Options": "nosniff",
|
|
11425
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
11426
|
+
...editorMatch[2] === "index.html" ? { "Content-Security-Policy": EDITOR_CSP } : {}
|
|
11427
|
+
});
|
|
11428
|
+
res.end(req.method === "HEAD" ? void 0 : file.bytes);
|
|
11429
|
+
return;
|
|
11430
|
+
}
|
|
10951
11431
|
try {
|
|
10952
11432
|
if (url2.pathname === "/") {
|
|
10953
11433
|
const { snapshot } = await load();
|
|
@@ -10959,7 +11439,8 @@ async function serveGallery(opts) {
|
|
|
10959
11439
|
res.end(renderGallery(snapshot, {
|
|
10960
11440
|
...session ? { session } : {},
|
|
10961
11441
|
editable: Boolean(opts.edit),
|
|
10962
|
-
generation: Boolean(opts.generate)
|
|
11442
|
+
generation: Boolean(opts.generate),
|
|
11443
|
+
editor: Boolean(opts.editor)
|
|
10963
11444
|
}));
|
|
10964
11445
|
return;
|
|
10965
11446
|
}
|
|
@@ -10979,7 +11460,7 @@ async function serveGallery(opts) {
|
|
|
10979
11460
|
if (!asset) return fail(404, "no such gallery media");
|
|
10980
11461
|
let bytes;
|
|
10981
11462
|
try {
|
|
10982
|
-
bytes = await
|
|
11463
|
+
bytes = await readFile17(asset.path);
|
|
10983
11464
|
} catch {
|
|
10984
11465
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
10985
11466
|
}
|
|
@@ -11025,16 +11506,16 @@ async function serveGallery(opts) {
|
|
|
11025
11506
|
|
|
11026
11507
|
// src/gallery/generate.ts
|
|
11027
11508
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
11028
|
-
import { z as
|
|
11029
|
-
var GenerateRequestSchema =
|
|
11030
|
-
project:
|
|
11031
|
-
keys:
|
|
11509
|
+
import { z as z8 } from "zod";
|
|
11510
|
+
var GenerateRequestSchema = z8.object({
|
|
11511
|
+
project: z8.string().min(1).optional(),
|
|
11512
|
+
keys: z8.array(z8.string().min(1)).min(1).max(500),
|
|
11032
11513
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
11033
|
-
force:
|
|
11514
|
+
force: z8.boolean().optional(),
|
|
11034
11515
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
11035
|
-
resume:
|
|
11516
|
+
resume: z8.boolean().optional(),
|
|
11036
11517
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11037
|
-
refresh:
|
|
11518
|
+
refresh: z8.boolean().optional()
|
|
11038
11519
|
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
11039
11520
|
var GenerateRequestError = class extends Error {
|
|
11040
11521
|
constructor(message6, status = 400) {
|
|
@@ -11288,20 +11769,318 @@ function createGenerateHandlers(opts) {
|
|
|
11288
11769
|
};
|
|
11289
11770
|
}
|
|
11290
11771
|
|
|
11291
|
-
// src/
|
|
11292
|
-
import { readFile as
|
|
11772
|
+
// src/gallery/editor.ts
|
|
11773
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
11774
|
+
import path24 from "path";
|
|
11775
|
+
|
|
11776
|
+
// src/editor/install.ts
|
|
11777
|
+
import { createHash as createHash4 } from "crypto";
|
|
11293
11778
|
import { existsSync as existsSync18 } from "fs";
|
|
11779
|
+
import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
|
|
11780
|
+
import os2 from "os";
|
|
11294
11781
|
import path23 from "path";
|
|
11782
|
+
|
|
11783
|
+
// tools/pixelorama-bridge/pin.json
|
|
11784
|
+
var pin_default = {
|
|
11785
|
+
pixelorama: "v1.2.2",
|
|
11786
|
+
godot: "4.7.2",
|
|
11787
|
+
extensionsApi: 9,
|
|
11788
|
+
protocol: 2,
|
|
11789
|
+
release: "editor-pixelorama-v1.2.2-pk.10",
|
|
11790
|
+
files: {
|
|
11791
|
+
"index.apple-touch-icon.png": {
|
|
11792
|
+
sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
|
|
11793
|
+
bytes: 1036
|
|
11794
|
+
},
|
|
11795
|
+
"index.audio.position.worklet.js": {
|
|
11796
|
+
sha256: "be33985bc7160d6bf9646f259cd86b259cd67b02ccb297ee5c44f8ac84327bc8",
|
|
11797
|
+
bytes: 2973
|
|
11798
|
+
},
|
|
11799
|
+
"index.audio.worklet.js": {
|
|
11800
|
+
sha256: "5b476a9c9ce642c0ee4256436d1bc31d9c38f868aca0f9a8e2a57c18d2dec2a3",
|
|
11801
|
+
bytes: 7298
|
|
11802
|
+
},
|
|
11803
|
+
"index.html": {
|
|
11804
|
+
sha256: "ad777659ca39a22872437d44595b749c1637f83994d5f82d50dca861f2186b9c",
|
|
11805
|
+
bytes: 5442
|
|
11806
|
+
},
|
|
11807
|
+
"index.icon.png": {
|
|
11808
|
+
sha256: "42a327d23178ddc26cc97e3560ebca6bc7c575d2a1b12a4837f2462049ca5745",
|
|
11809
|
+
bytes: 1070
|
|
11810
|
+
},
|
|
11811
|
+
"index.js": {
|
|
11812
|
+
sha256: "33c94cb3175f3333b82e2a3be5e8e86f77986f0aa2042b1631f6367a4e5bb6ba",
|
|
11813
|
+
bytes: 279815
|
|
11814
|
+
},
|
|
11815
|
+
"index.pck": {
|
|
11816
|
+
sha256: "546a3f9851df0854c9f6052f29f1e559de79deb53b936d44a72c3475234c26ff",
|
|
11817
|
+
bytes: 6427868
|
|
11818
|
+
},
|
|
11819
|
+
"index.png": {
|
|
11820
|
+
sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
|
|
11821
|
+
bytes: 13558
|
|
11822
|
+
},
|
|
11823
|
+
"index.wasm": {
|
|
11824
|
+
sha256: "fc74679e3b97f76878947fcd4fbe1268cbfa6188182a2e33bbc3f5dc9bfa57d0",
|
|
11825
|
+
bytes: 39514754
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
};
|
|
11829
|
+
|
|
11830
|
+
// src/editor/pin.ts
|
|
11831
|
+
var EDITOR_PIN = pin_default;
|
|
11832
|
+
var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
|
|
11833
|
+
|
|
11834
|
+
// src/editor/install.ts
|
|
11835
|
+
var EditorInstallError = class extends Error {
|
|
11836
|
+
constructor(message6, file) {
|
|
11837
|
+
super(message6);
|
|
11838
|
+
this.file = file;
|
|
11839
|
+
this.name = "EditorInstallError";
|
|
11840
|
+
}
|
|
11841
|
+
file;
|
|
11842
|
+
};
|
|
11843
|
+
function editorToolsRoot() {
|
|
11844
|
+
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
11845
|
+
if (override) return path23.resolve(override);
|
|
11846
|
+
if (process.platform === "darwin") return path23.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
11847
|
+
if (process.platform === "win32") {
|
|
11848
|
+
return path23.join(process.env.LOCALAPPDATA ?? path23.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
11849
|
+
}
|
|
11850
|
+
return path23.join(process.env.XDG_CACHE_HOME ?? path23.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
11851
|
+
}
|
|
11852
|
+
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
11853
|
+
return path23.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
11854
|
+
}
|
|
11855
|
+
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
11856
|
+
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
11857
|
+
if (override) return override.replace(/\/+$/, "");
|
|
11858
|
+
if (!pin.release) return null;
|
|
11859
|
+
return `${EDITOR_RELEASE_BASE}/${encodeURIComponent(pin.release)}`;
|
|
11860
|
+
}
|
|
11861
|
+
async function sha256Of(file) {
|
|
11862
|
+
try {
|
|
11863
|
+
return createHash4("sha256").update(await readFile18(file)).digest("hex");
|
|
11864
|
+
} catch {
|
|
11865
|
+
return null;
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
async function editorStatus(opts = {}) {
|
|
11869
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11870
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11871
|
+
const missing = [];
|
|
11872
|
+
let installedBytes = 0;
|
|
11873
|
+
let totalBytes = 0;
|
|
11874
|
+
for (const [name, expected] of Object.entries(pin.files)) {
|
|
11875
|
+
totalBytes += expected.bytes;
|
|
11876
|
+
const file = path23.join(dir, name);
|
|
11877
|
+
const info = existsSync18(file) ? await stat4(file).catch(() => null) : null;
|
|
11878
|
+
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
11879
|
+
installedBytes += expected.bytes;
|
|
11880
|
+
} else {
|
|
11881
|
+
missing.push(name);
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
const pinned = Object.keys(pin.files).length > 0 && pin.release !== null;
|
|
11885
|
+
return {
|
|
11886
|
+
release: pin.release,
|
|
11887
|
+
dir,
|
|
11888
|
+
installed: pinned && missing.length === 0,
|
|
11889
|
+
missing: pinned ? missing : Object.keys(pin.files),
|
|
11890
|
+
totalBytes,
|
|
11891
|
+
installedBytes
|
|
11892
|
+
};
|
|
11893
|
+
}
|
|
11894
|
+
async function installEditor(opts = {}) {
|
|
11895
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11896
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11897
|
+
const baseUrl = opts.baseUrl ?? editorBaseUrl(pin);
|
|
11898
|
+
const doFetch = opts.fetch ?? fetch;
|
|
11899
|
+
const before = await editorStatus({ pin, dir });
|
|
11900
|
+
if (!pin.release || !Object.keys(pin.files).length) {
|
|
11901
|
+
throw new EditorInstallError(
|
|
11902
|
+
"this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust"
|
|
11903
|
+
);
|
|
11904
|
+
}
|
|
11905
|
+
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
11906
|
+
await mkdir7(dir, { recursive: true });
|
|
11907
|
+
const downloaded = [];
|
|
11908
|
+
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
11909
|
+
let fetchedBytes = before.installedBytes;
|
|
11910
|
+
const names = before.missing;
|
|
11911
|
+
for (const [index, name] of names.entries()) {
|
|
11912
|
+
const expected = pin.files[name];
|
|
11913
|
+
const report = (phase, received) => opts.onProgress?.({
|
|
11914
|
+
file: name,
|
|
11915
|
+
index,
|
|
11916
|
+
count: names.length,
|
|
11917
|
+
bytes: expected.bytes,
|
|
11918
|
+
totalBytes: before.totalBytes,
|
|
11919
|
+
fetchedBytes: fetchedBytes + received,
|
|
11920
|
+
phase
|
|
11921
|
+
});
|
|
11922
|
+
report("start", 0);
|
|
11923
|
+
const url = `${baseUrl}/${encodeURIComponent(name)}`;
|
|
11924
|
+
const response = await doFetch(url, { signal: opts.signal, redirect: "follow" });
|
|
11925
|
+
if (!response.ok) throw new EditorInstallError(`${name}: ${response.status} ${response.statusText} from ${url}`, name);
|
|
11926
|
+
const bytes = await readBody(response, expected.bytes, (received) => report("progress", received));
|
|
11927
|
+
if (bytes.length !== expected.bytes) {
|
|
11928
|
+
throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
|
|
11929
|
+
}
|
|
11930
|
+
const digest2 = createHash4("sha256").update(bytes).digest("hex");
|
|
11931
|
+
if (digest2 !== expected.sha256) {
|
|
11932
|
+
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
11933
|
+
}
|
|
11934
|
+
const final = path23.join(dir, name);
|
|
11935
|
+
const tmp = `${final}.${process.pid}.part`;
|
|
11936
|
+
try {
|
|
11937
|
+
await writeFile10(tmp, bytes);
|
|
11938
|
+
await rename8(tmp, final);
|
|
11939
|
+
} finally {
|
|
11940
|
+
await rm9(tmp, { force: true });
|
|
11941
|
+
}
|
|
11942
|
+
fetchedBytes += bytes.length;
|
|
11943
|
+
downloaded.push(name);
|
|
11944
|
+
report("done", 0);
|
|
11945
|
+
}
|
|
11946
|
+
const after = await editorStatus({ pin, dir });
|
|
11947
|
+
if (!after.installed) {
|
|
11948
|
+
throw new EditorInstallError(`editor install is incomplete: ${after.missing.join(", ")}`);
|
|
11949
|
+
}
|
|
11950
|
+
return { ...after, downloaded, skipped };
|
|
11951
|
+
}
|
|
11952
|
+
async function readBody(response, expectedBytes, onBytes) {
|
|
11953
|
+
if (!response.body) return Buffer.from(await response.arrayBuffer());
|
|
11954
|
+
const chunks = [];
|
|
11955
|
+
let received = 0;
|
|
11956
|
+
const reader = response.body.getReader();
|
|
11957
|
+
for (; ; ) {
|
|
11958
|
+
const { done, value } = await reader.read();
|
|
11959
|
+
if (done) break;
|
|
11960
|
+
chunks.push(value);
|
|
11961
|
+
received += value.length;
|
|
11962
|
+
if (received > expectedBytes) {
|
|
11963
|
+
await reader.cancel().catch(() => {
|
|
11964
|
+
});
|
|
11965
|
+
break;
|
|
11966
|
+
}
|
|
11967
|
+
onBytes(received);
|
|
11968
|
+
}
|
|
11969
|
+
return Buffer.concat(chunks);
|
|
11970
|
+
}
|
|
11971
|
+
var EDITOR_CONTENT_TYPES = {
|
|
11972
|
+
".html": "text/html; charset=utf-8",
|
|
11973
|
+
".js": "text/javascript; charset=utf-8",
|
|
11974
|
+
".wasm": "application/wasm",
|
|
11975
|
+
".pck": "application/octet-stream",
|
|
11976
|
+
".png": "image/png",
|
|
11977
|
+
".json": "application/json; charset=utf-8"
|
|
11978
|
+
};
|
|
11979
|
+
function editorContentType(name) {
|
|
11980
|
+
return EDITOR_CONTENT_TYPES[path23.extname(name).toLowerCase()] ?? null;
|
|
11981
|
+
}
|
|
11982
|
+
|
|
11983
|
+
// src/gallery/editor.ts
|
|
11984
|
+
function editorRoute(release, name = "index.html") {
|
|
11985
|
+
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
11986
|
+
}
|
|
11987
|
+
function createGalleryEditorHandlers(opts = {}) {
|
|
11988
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11989
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11990
|
+
const log2 = opts.onProgress ?? (() => {
|
|
11991
|
+
});
|
|
11992
|
+
let verified = null;
|
|
11993
|
+
let checking = null;
|
|
11994
|
+
let installing = null;
|
|
11995
|
+
let running = null;
|
|
11996
|
+
let error = null;
|
|
11997
|
+
const check = () => {
|
|
11998
|
+
if (!checking) {
|
|
11999
|
+
checking = editorStatus({ pin, dir }).then((status) => {
|
|
12000
|
+
verified = status;
|
|
12001
|
+
return status;
|
|
12002
|
+
}).finally(() => {
|
|
12003
|
+
checking = null;
|
|
12004
|
+
});
|
|
12005
|
+
}
|
|
12006
|
+
return checking;
|
|
12007
|
+
};
|
|
12008
|
+
const describe = (status) => ({
|
|
12009
|
+
...status,
|
|
12010
|
+
pixelorama: pin.pixelorama,
|
|
12011
|
+
protocol: pin.protocol,
|
|
12012
|
+
url: status.installed && pin.release ? editorRoute(pin.release) : null,
|
|
12013
|
+
installing,
|
|
12014
|
+
error
|
|
12015
|
+
});
|
|
12016
|
+
return {
|
|
12017
|
+
release: pin.release,
|
|
12018
|
+
status: async () => describe(verified ?? await check()),
|
|
12019
|
+
install: async () => {
|
|
12020
|
+
if (!running) {
|
|
12021
|
+
error = null;
|
|
12022
|
+
installing = { file: "", index: 0, count: 0, bytes: 0, totalBytes: 0, fetchedBytes: 0, phase: "start" };
|
|
12023
|
+
log2(` installing the editor (Pixelorama ${pin.pixelorama}) into ${dir}`);
|
|
12024
|
+
running = installEditor({
|
|
12025
|
+
pin,
|
|
12026
|
+
dir,
|
|
12027
|
+
baseUrl: opts.baseUrl ?? editorBaseUrl(pin) ?? void 0,
|
|
12028
|
+
...opts.fetch ? { fetch: opts.fetch } : {},
|
|
12029
|
+
onProgress: (progress) => {
|
|
12030
|
+
installing = progress;
|
|
12031
|
+
}
|
|
12032
|
+
}).then((result) => {
|
|
12033
|
+
verified = result;
|
|
12034
|
+
const mb = (result.totalBytes / 1e6).toFixed(1);
|
|
12035
|
+
log2(` editor ready: ${result.downloaded.length} file(s) fetched, ${result.skipped.length} already present (${mb} MB)`);
|
|
12036
|
+
}).catch((err) => {
|
|
12037
|
+
error = err instanceof Error ? err.message : String(err);
|
|
12038
|
+
verified = null;
|
|
12039
|
+
log2(` editor install failed: ${error}`);
|
|
12040
|
+
}).finally(() => {
|
|
12041
|
+
installing = null;
|
|
12042
|
+
running = null;
|
|
12043
|
+
});
|
|
12044
|
+
}
|
|
12045
|
+
return describe(verified ?? await check());
|
|
12046
|
+
},
|
|
12047
|
+
file: async (release, name) => {
|
|
12048
|
+
if (!pin.release || release !== pin.release) return null;
|
|
12049
|
+
const expected = pin.files[name];
|
|
12050
|
+
const contentType = editorContentType(name);
|
|
12051
|
+
if (!expected || !contentType) return null;
|
|
12052
|
+
const status = verified ?? await check();
|
|
12053
|
+
if (!status.installed) return null;
|
|
12054
|
+
let bytes;
|
|
12055
|
+
try {
|
|
12056
|
+
bytes = await readFile19(path24.join(dir, name));
|
|
12057
|
+
} catch {
|
|
12058
|
+
verified = null;
|
|
12059
|
+
return null;
|
|
12060
|
+
}
|
|
12061
|
+
if (bytes.length !== expected.bytes) {
|
|
12062
|
+
verified = null;
|
|
12063
|
+
return null;
|
|
12064
|
+
}
|
|
12065
|
+
return { bytes, contentType };
|
|
12066
|
+
}
|
|
12067
|
+
};
|
|
12068
|
+
}
|
|
12069
|
+
|
|
12070
|
+
// src/pipeline/salvage.ts
|
|
12071
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
12072
|
+
import { existsSync as existsSync19 } from "fs";
|
|
12073
|
+
import path25 from "path";
|
|
11295
12074
|
function providerClaimId(provider, objectId) {
|
|
11296
12075
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
11297
12076
|
}
|
|
11298
12077
|
async function loadClaims(lockPaths, opts = {}) {
|
|
11299
12078
|
const claimed = /* @__PURE__ */ new Set();
|
|
11300
12079
|
for (const p of lockPaths) {
|
|
11301
|
-
if (!
|
|
12080
|
+
if (!existsSync19(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
11302
12081
|
let parsed;
|
|
11303
12082
|
try {
|
|
11304
|
-
parsed = parseLock(JSON.parse(await
|
|
12083
|
+
parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
|
|
11305
12084
|
} catch {
|
|
11306
12085
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
11307
12086
|
}
|
|
@@ -11349,20 +12128,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
11349
12128
|
return null;
|
|
11350
12129
|
}
|
|
11351
12130
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
11352
|
-
const own =
|
|
12131
|
+
const own = path25.resolve(ownManifestPath);
|
|
11353
12132
|
const siblingManifestPaths = [
|
|
11354
12133
|
.../* @__PURE__ */ new Set([
|
|
11355
12134
|
...workspaceManifestPaths,
|
|
11356
|
-
...claimPaths.map((c) =>
|
|
12135
|
+
...claimPaths.map((c) => path25.join(path25.dirname(path25.resolve(c)), "pixelkiln.manifest.json"))
|
|
11357
12136
|
])
|
|
11358
12137
|
];
|
|
11359
12138
|
const siblings = [];
|
|
11360
12139
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
11361
|
-
if (
|
|
11362
|
-
if (!
|
|
12140
|
+
if (path25.resolve(siblingManifestPath) === own) continue;
|
|
12141
|
+
if (!existsSync19(siblingManifestPath)) continue;
|
|
11363
12142
|
try {
|
|
11364
12143
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
11365
|
-
siblings.push({ label:
|
|
12144
|
+
siblings.push({ label: path25.basename(path25.dirname(siblingManifestPath)), manifest });
|
|
11366
12145
|
} catch {
|
|
11367
12146
|
}
|
|
11368
12147
|
}
|
|
@@ -11581,15 +12360,15 @@ async function workspaceStatus(ws, dir) {
|
|
|
11581
12360
|
}
|
|
11582
12361
|
|
|
11583
12362
|
// src/pipeline/cache-health.ts
|
|
11584
|
-
import { existsSync as
|
|
11585
|
-
import { readFile as
|
|
11586
|
-
import
|
|
12363
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12364
|
+
import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
12365
|
+
import path26 from "path";
|
|
11587
12366
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11588
|
-
if (options.prune && !
|
|
11589
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
12367
|
+
if (options.prune && !existsSync20(lockPath)) {
|
|
12368
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path26.resolve(lockPath)}`);
|
|
11590
12369
|
}
|
|
11591
|
-
const contentDir =
|
|
11592
|
-
const remotePath =
|
|
12370
|
+
const contentDir = path26.resolve(path26.dirname(lockPath), ".pixelkiln", "cache");
|
|
12371
|
+
const remotePath = path26.resolve(cachePathFor(lockPath));
|
|
11593
12372
|
const referenced = new Set(
|
|
11594
12373
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
11595
12374
|
);
|
|
@@ -11603,7 +12382,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11603
12382
|
]);
|
|
11604
12383
|
for (const name of names) {
|
|
11605
12384
|
try {
|
|
11606
|
-
await
|
|
12385
|
+
await rm10(path26.join(contentDir, name), { force: true });
|
|
11607
12386
|
removed.contentFiles++;
|
|
11608
12387
|
} catch {
|
|
11609
12388
|
}
|
|
@@ -11612,7 +12391,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11612
12391
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
11613
12392
|
removed.resetRemoteHashCache = true;
|
|
11614
12393
|
} else if (remoteHashes.invalidIds.length) {
|
|
11615
|
-
const cache = parseCache(JSON.parse(await
|
|
12394
|
+
const cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
11616
12395
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
11617
12396
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
11618
12397
|
await saveCache(remotePath, cache);
|
|
@@ -11630,7 +12409,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11630
12409
|
async function inspectContentCache(contentDir, referenced) {
|
|
11631
12410
|
const report = {
|
|
11632
12411
|
path: contentDir,
|
|
11633
|
-
exists:
|
|
12412
|
+
exists: existsSync20(contentDir),
|
|
11634
12413
|
files: 0,
|
|
11635
12414
|
bytes: 0,
|
|
11636
12415
|
valid: 0,
|
|
@@ -11651,12 +12430,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11651
12430
|
continue;
|
|
11652
12431
|
}
|
|
11653
12432
|
report.files++;
|
|
11654
|
-
const file =
|
|
12433
|
+
const file = path26.join(contentDir, entry.name);
|
|
11655
12434
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11656
12435
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11657
12436
|
let bytes;
|
|
11658
12437
|
try {
|
|
11659
|
-
bytes = await
|
|
12438
|
+
bytes = await readFile21(file);
|
|
11660
12439
|
report.bytes += bytes.length;
|
|
11661
12440
|
} catch (err) {
|
|
11662
12441
|
report.invalid.push({
|
|
@@ -11694,7 +12473,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11694
12473
|
async function inspectRemoteHashCache(remotePath) {
|
|
11695
12474
|
const report = {
|
|
11696
12475
|
path: remotePath,
|
|
11697
|
-
exists:
|
|
12476
|
+
exists: existsSync20(remotePath),
|
|
11698
12477
|
entries: 0,
|
|
11699
12478
|
valid: 0,
|
|
11700
12479
|
invalidIds: [],
|
|
@@ -11703,7 +12482,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
11703
12482
|
if (!report.exists) return report;
|
|
11704
12483
|
let cache;
|
|
11705
12484
|
try {
|
|
11706
|
-
cache = parseCache(JSON.parse(await
|
|
12485
|
+
cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
11707
12486
|
} catch (err) {
|
|
11708
12487
|
report.error = err instanceof Error ? err.message : String(err);
|
|
11709
12488
|
return report;
|
|
@@ -11974,8 +12753,8 @@ function isRecord2(value) {
|
|
|
11974
12753
|
}
|
|
11975
12754
|
|
|
11976
12755
|
// src/pick/salvage-server.ts
|
|
11977
|
-
import { mkdir as
|
|
11978
|
-
import
|
|
12756
|
+
import { mkdir as mkdir8, writeFile as writeFile11, readFile as readFile22 } from "fs/promises";
|
|
12757
|
+
import path27 from "path";
|
|
11979
12758
|
|
|
11980
12759
|
// src/pick/salvage-sheet.ts
|
|
11981
12760
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -12140,7 +12919,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12140
12919
|
});
|
|
12141
12920
|
const html = renderSalvageSheet(orphans, {
|
|
12142
12921
|
styleId: ctx.styleId,
|
|
12143
|
-
importDir:
|
|
12922
|
+
importDir: path27.relative(process.cwd(), ctx.importDir) || "."
|
|
12144
12923
|
});
|
|
12145
12924
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
12146
12925
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -12169,10 +12948,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12169
12948
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
12170
12949
|
decodePng(buf);
|
|
12171
12950
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
12172
|
-
const rel =
|
|
12173
|
-
const outFile =
|
|
12174
|
-
await
|
|
12175
|
-
await
|
|
12951
|
+
const rel = path27.join("_salvaged", `${assetId}.png`);
|
|
12952
|
+
const outFile = path27.resolve(ctx.importDir, rel);
|
|
12953
|
+
await mkdir8(path27.dirname(outFile), { recursive: true });
|
|
12954
|
+
await writeFile11(outFile, buf);
|
|
12176
12955
|
ctx.manifest.assets[assetId] = {
|
|
12177
12956
|
prompt: orphan.prompt,
|
|
12178
12957
|
promptByStyle: {},
|
|
@@ -12204,7 +12983,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12204
12983
|
sourceUrl: durableSource,
|
|
12205
12984
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
12206
12985
|
outputs: [{
|
|
12207
|
-
path: portableOutputPath(outFile,
|
|
12986
|
+
path: portableOutputPath(outFile, path27.dirname(ctx.manifestPath)),
|
|
12208
12987
|
sha256: sha256(buf)
|
|
12209
12988
|
}],
|
|
12210
12989
|
submittedAt: orphan.createdAt,
|
|
@@ -12228,9 +13007,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12228
13007
|
}
|
|
12229
13008
|
}
|
|
12230
13009
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
12231
|
-
const raw = JSON.parse(await
|
|
13010
|
+
const raw = JSON.parse(await readFile22(ctx.manifestPath, "utf8"));
|
|
12232
13011
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
12233
|
-
await
|
|
13012
|
+
await writeFile11(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12234
13013
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
12235
13014
|
return result;
|
|
12236
13015
|
}
|
|
@@ -12238,51 +13017,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12238
13017
|
}
|
|
12239
13018
|
|
|
12240
13019
|
// src/recipes.ts
|
|
12241
|
-
import
|
|
12242
|
-
import { existsSync as
|
|
12243
|
-
import { readdir as readdir3, readFile as
|
|
13020
|
+
import path28 from "path";
|
|
13021
|
+
import { existsSync as existsSync21 } from "fs";
|
|
13022
|
+
import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
|
|
12244
13023
|
import { fileURLToPath } from "url";
|
|
12245
|
-
import { z as
|
|
13024
|
+
import { z as z9 } from "zod";
|
|
12246
13025
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
12247
13026
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
12248
13027
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
12249
13028
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
12250
13029
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
12251
|
-
var PortablePathSchema =
|
|
12252
|
-
if (value.includes("\\") ||
|
|
13030
|
+
var PortablePathSchema = z9.string().min(1).refine((value) => {
|
|
13031
|
+
if (value.includes("\\") || path28.posix.isAbsolute(value)) return false;
|
|
12253
13032
|
const parts = value.split("/");
|
|
12254
13033
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
12255
13034
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
12256
|
-
var RecipeBindingSchema =
|
|
12257
|
-
nodeId:
|
|
12258
|
-
input:
|
|
13035
|
+
var RecipeBindingSchema = z9.object({
|
|
13036
|
+
nodeId: z9.string().min(1),
|
|
13037
|
+
input: z9.string().min(1)
|
|
12259
13038
|
}).strict();
|
|
12260
|
-
var RecipeSchema =
|
|
12261
|
-
$schema:
|
|
12262
|
-
format:
|
|
12263
|
-
schemaVersion:
|
|
12264
|
-
id:
|
|
12265
|
-
version:
|
|
12266
|
-
provider:
|
|
12267
|
-
summary:
|
|
12268
|
-
files:
|
|
13039
|
+
var RecipeSchema = z9.object({
|
|
13040
|
+
$schema: z9.string().url().optional(),
|
|
13041
|
+
format: z9.literal("pixelkiln-recipe"),
|
|
13042
|
+
schemaVersion: z9.literal(1),
|
|
13043
|
+
id: z9.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
13044
|
+
version: z9.string().regex(VERSION_RE, "expected x.y.z"),
|
|
13045
|
+
provider: z9.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
13046
|
+
summary: z9.string().min(1).max(240),
|
|
13047
|
+
files: z9.array(z9.object({
|
|
12269
13048
|
path: PortablePathSchema,
|
|
12270
|
-
role:
|
|
12271
|
-
sha256:
|
|
13049
|
+
role: z9.enum(["workflow", "reference"]),
|
|
13050
|
+
sha256: z9.string().regex(SHA256_RE)
|
|
12272
13051
|
}).strict()).default([]),
|
|
12273
|
-
models:
|
|
13052
|
+
models: z9.array(z9.object({
|
|
12274
13053
|
path: PortablePathSchema,
|
|
12275
|
-
sha256:
|
|
12276
|
-
source:
|
|
12277
|
-
license:
|
|
13054
|
+
sha256: z9.string().regex(SHA256_RE),
|
|
13055
|
+
source: z9.string().url(),
|
|
13056
|
+
license: z9.string().min(1)
|
|
12278
13057
|
}).strict()).default([]),
|
|
12279
|
-
styleId:
|
|
13058
|
+
styleId: z9.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
12280
13059
|
style: StyleSchema,
|
|
12281
|
-
workflow:
|
|
13060
|
+
workflow: z9.object({
|
|
12282
13061
|
path: PortablePathSchema,
|
|
12283
|
-
outputNodeId:
|
|
12284
|
-
numImages:
|
|
12285
|
-
bindings:
|
|
13062
|
+
outputNodeId: z9.string().min(1),
|
|
13063
|
+
numImages: z9.number().int().min(1).max(16),
|
|
13064
|
+
bindings: z9.object({
|
|
12286
13065
|
prompt: RecipeBindingSchema,
|
|
12287
13066
|
width: RecipeBindingSchema.optional(),
|
|
12288
13067
|
height: RecipeBindingSchema.optional(),
|
|
@@ -12293,67 +13072,67 @@ var RecipeSchema = z8.object({
|
|
|
12293
13072
|
strength: RecipeBindingSchema.optional()
|
|
12294
13073
|
}).strict()
|
|
12295
13074
|
}).strict().optional(),
|
|
12296
|
-
quality:
|
|
12297
|
-
stage:
|
|
12298
|
-
workingCanvas:
|
|
12299
|
-
width:
|
|
12300
|
-
height:
|
|
13075
|
+
quality: z9.object({
|
|
13076
|
+
stage: z9.enum(["composition-source", "production-candidate"]),
|
|
13077
|
+
workingCanvas: z9.object({
|
|
13078
|
+
width: z9.number().int().min(16),
|
|
13079
|
+
height: z9.number().int().min(16)
|
|
12301
13080
|
}).strict(),
|
|
12302
|
-
recommendedNativeSize:
|
|
12303
|
-
min:
|
|
12304
|
-
max:
|
|
13081
|
+
recommendedNativeSize: z9.object({
|
|
13082
|
+
min: z9.number().int().min(1),
|
|
13083
|
+
max: z9.number().int().min(1)
|
|
12305
13084
|
}).strict(),
|
|
12306
|
-
paletteColors:
|
|
12307
|
-
min:
|
|
12308
|
-
max:
|
|
13085
|
+
paletteColors: z9.object({
|
|
13086
|
+
min: z9.number().int().min(1),
|
|
13087
|
+
max: z9.number().int().min(1)
|
|
12309
13088
|
}).strict(),
|
|
12310
|
-
background:
|
|
12311
|
-
checks:
|
|
13089
|
+
background: z9.enum(["opaque", "transparent", "full-bleed"]),
|
|
13090
|
+
checks: z9.array(z9.enum([
|
|
12312
13091
|
"prompt-coverage",
|
|
12313
13092
|
"native-grid",
|
|
12314
13093
|
"final-palette",
|
|
12315
13094
|
"alpha-edge",
|
|
12316
13095
|
"human-1x"
|
|
12317
13096
|
])).min(1),
|
|
12318
|
-
notes:
|
|
13097
|
+
notes: z9.array(z9.string().min(1)).default([])
|
|
12319
13098
|
}).strict(),
|
|
12320
|
-
integrity:
|
|
12321
|
-
algorithm:
|
|
12322
|
-
digest:
|
|
13099
|
+
integrity: z9.object({
|
|
13100
|
+
algorithm: z9.literal("sha256"),
|
|
13101
|
+
digest: z9.string().regex(SHA256_RE)
|
|
12323
13102
|
}).strict()
|
|
12324
13103
|
}).strict().superRefine((recipe, context) => {
|
|
12325
13104
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
12326
13105
|
context.addIssue({
|
|
12327
|
-
code:
|
|
13106
|
+
code: z9.ZodIssueCode.custom,
|
|
12328
13107
|
path: ["style", "provider"],
|
|
12329
13108
|
message: "must match the recipe provider"
|
|
12330
13109
|
});
|
|
12331
13110
|
}
|
|
12332
13111
|
const filePaths = recipe.files.map((file) => file.path);
|
|
12333
13112
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
12334
|
-
context.addIssue({ code:
|
|
13113
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
12335
13114
|
}
|
|
12336
13115
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
12337
13116
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
12338
|
-
context.addIssue({ code:
|
|
13117
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
12339
13118
|
}
|
|
12340
13119
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
12341
13120
|
context.addIssue({
|
|
12342
|
-
code:
|
|
13121
|
+
code: z9.ZodIssueCode.custom,
|
|
12343
13122
|
path: ["files"],
|
|
12344
13123
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
12345
13124
|
});
|
|
12346
13125
|
}
|
|
12347
13126
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
12348
13127
|
context.addIssue({
|
|
12349
|
-
code:
|
|
13128
|
+
code: z9.ZodIssueCode.custom,
|
|
12350
13129
|
path: ["quality", "recommendedNativeSize"],
|
|
12351
13130
|
message: "min cannot exceed max"
|
|
12352
13131
|
});
|
|
12353
13132
|
}
|
|
12354
13133
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
12355
13134
|
context.addIssue({
|
|
12356
|
-
code:
|
|
13135
|
+
code: z9.ZodIssueCode.custom,
|
|
12357
13136
|
path: ["quality", "paletteColors"],
|
|
12358
13137
|
message: "min cannot exceed max"
|
|
12359
13138
|
});
|
|
@@ -12362,7 +13141,7 @@ var RecipeSchema = z8.object({
|
|
|
12362
13141
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
12363
13142
|
)) {
|
|
12364
13143
|
context.addIssue({
|
|
12365
|
-
code:
|
|
13144
|
+
code: z9.ZodIssueCode.custom,
|
|
12366
13145
|
path: ["workflow", "path"],
|
|
12367
13146
|
message: "must name a file whose role is workflow"
|
|
12368
13147
|
});
|
|
@@ -12370,7 +13149,7 @@ var RecipeSchema = z8.object({
|
|
|
12370
13149
|
if (recipe.provider === "comfyui") {
|
|
12371
13150
|
if (!recipe.workflow) {
|
|
12372
13151
|
context.addIssue({
|
|
12373
|
-
code:
|
|
13152
|
+
code: z9.ZodIssueCode.custom,
|
|
12374
13153
|
path: ["workflow"],
|
|
12375
13154
|
message: "ComfyUI recipes require workflow metadata"
|
|
12376
13155
|
});
|
|
@@ -12379,7 +13158,7 @@ var RecipeSchema = z8.object({
|
|
|
12379
13158
|
const options = recipe.style.providerOptions.comfyui;
|
|
12380
13159
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
12381
13160
|
context.addIssue({
|
|
12382
|
-
code:
|
|
13161
|
+
code: z9.ZodIssueCode.custom,
|
|
12383
13162
|
path: ["style", "providerOptions", "comfyui"],
|
|
12384
13163
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
12385
13164
|
});
|
|
@@ -12393,7 +13172,7 @@ var RecipeSchema = z8.object({
|
|
|
12393
13172
|
};
|
|
12394
13173
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
12395
13174
|
context.addIssue({
|
|
12396
|
-
code:
|
|
13175
|
+
code: z9.ZodIssueCode.custom,
|
|
12397
13176
|
path: ["style", "providerOptions", "comfyui"],
|
|
12398
13177
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
12399
13178
|
});
|
|
@@ -12425,10 +13204,10 @@ function message4(error) {
|
|
|
12425
13204
|
return error instanceof Error ? error.message : String(error);
|
|
12426
13205
|
}
|
|
12427
13206
|
async function readRecipeFile(recipePath, bundled) {
|
|
12428
|
-
const absolute =
|
|
13207
|
+
const absolute = path28.resolve(recipePath);
|
|
12429
13208
|
let raw;
|
|
12430
13209
|
try {
|
|
12431
|
-
raw = JSON.parse(await
|
|
13210
|
+
raw = JSON.parse(await readFile23(absolute, "utf8"));
|
|
12432
13211
|
} catch (error) {
|
|
12433
13212
|
throw new Error(`Could not read recipe ${absolute}: ${message4(error)}`, { cause: error });
|
|
12434
13213
|
}
|
|
@@ -12439,13 +13218,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12439
13218
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12440
13219
|
);
|
|
12441
13220
|
}
|
|
12442
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
13221
|
+
return { recipe: parsed.data, path: absolute, dir: path28.dirname(absolute), bundled };
|
|
12443
13222
|
}
|
|
12444
13223
|
async function walkRecipeFiles(root) {
|
|
12445
|
-
if (!
|
|
13224
|
+
if (!existsSync21(root)) return [];
|
|
12446
13225
|
const out = [];
|
|
12447
13226
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
12448
|
-
const resolved =
|
|
13227
|
+
const resolved = path28.join(root, entry.name);
|
|
12449
13228
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12450
13229
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12451
13230
|
}
|
|
@@ -12456,7 +13235,7 @@ function bundledRecipeRoot() {
|
|
|
12456
13235
|
}
|
|
12457
13236
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12458
13237
|
const recipes = await Promise.all(
|
|
12459
|
-
(await walkRecipeFiles(
|
|
13238
|
+
(await walkRecipeFiles(path28.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12460
13239
|
);
|
|
12461
13240
|
return recipes.sort(
|
|
12462
13241
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12479,9 +13258,9 @@ function parseSelector(selector) {
|
|
|
12479
13258
|
return { id: match[1], version: match[2] };
|
|
12480
13259
|
}
|
|
12481
13260
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12482
|
-
const local =
|
|
12483
|
-
if (
|
|
12484
|
-
const recipePath =
|
|
13261
|
+
const local = path28.resolve(target);
|
|
13262
|
+
if (existsSync21(local)) {
|
|
13263
|
+
const recipePath = path28.basename(local) === RECIPE_FILE ? local : path28.join(local, RECIPE_FILE);
|
|
12485
13264
|
return readRecipeFile(recipePath, false);
|
|
12486
13265
|
}
|
|
12487
13266
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12495,8 +13274,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12495
13274
|
return matches[0];
|
|
12496
13275
|
}
|
|
12497
13276
|
async function verifyFile(root, file) {
|
|
12498
|
-
const absolute =
|
|
12499
|
-
if (!
|
|
13277
|
+
const absolute = path28.resolve(root, ...file.path.split("/"));
|
|
13278
|
+
if (!existsSync21(absolute)) {
|
|
12500
13279
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12501
13280
|
}
|
|
12502
13281
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12511,7 +13290,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12511
13290
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12512
13291
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
12513
13292
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12514
|
-
const modelRoot = options.modelRoot ?
|
|
13293
|
+
const modelRoot = options.modelRoot ? path28.resolve(options.modelRoot) : null;
|
|
12515
13294
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12516
13295
|
...await verifyFile(modelRoot, model),
|
|
12517
13296
|
source: model.source,
|
|
@@ -12545,7 +13324,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12545
13324
|
};
|
|
12546
13325
|
}
|
|
12547
13326
|
function renderStyle(style, destination, cwd) {
|
|
12548
|
-
const portableDir =
|
|
13327
|
+
const portableDir = path28.relative(cwd, destination).split(path28.sep).join("/") || ".";
|
|
12549
13328
|
const replace = (value) => {
|
|
12550
13329
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12551
13330
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12557,25 +13336,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
12557
13336
|
return replace(style);
|
|
12558
13337
|
}
|
|
12559
13338
|
async function installRecipe(target, options = {}) {
|
|
12560
|
-
const cwd =
|
|
13339
|
+
const cwd = path28.resolve(options.cwd ?? process.cwd());
|
|
12561
13340
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12562
13341
|
const verification = await verifyRecipe(loaded.path);
|
|
12563
13342
|
if (!verification.ok) {
|
|
12564
13343
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12565
13344
|
}
|
|
12566
|
-
const destination = options.out ?
|
|
13345
|
+
const destination = options.out ? path28.resolve(cwd, options.out) : path28.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12567
13346
|
const sources = [
|
|
12568
13347
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12569
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
13348
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path28.join(loaded.dir, ...file.path.split("/")) }))
|
|
12570
13349
|
];
|
|
12571
13350
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12572
|
-
path:
|
|
12573
|
-
data: await
|
|
13351
|
+
path: path28.join(destination, ...file.path.split("/")),
|
|
13352
|
+
data: await readFile23(file.source)
|
|
12574
13353
|
})));
|
|
12575
13354
|
if (!options.force) {
|
|
12576
13355
|
for (const file of files) {
|
|
12577
|
-
if (!
|
|
12578
|
-
const current = await
|
|
13356
|
+
if (!existsSync21(file.path)) continue;
|
|
13357
|
+
const current = await readFile23(file.path);
|
|
12579
13358
|
if (!current.equals(file.data)) {
|
|
12580
13359
|
throw new Error(
|
|
12581
13360
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
|
|
@@ -12598,15 +13377,15 @@ async function installRecipe(target, options = {}) {
|
|
|
12598
13377
|
}
|
|
12599
13378
|
|
|
12600
13379
|
// src/pipeline/quality-regression.ts
|
|
12601
|
-
import
|
|
12602
|
-
import { existsSync as
|
|
12603
|
-
import { readFile as
|
|
12604
|
-
import { z as
|
|
13380
|
+
import path29 from "path";
|
|
13381
|
+
import { existsSync as existsSync22 } from "fs";
|
|
13382
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
13383
|
+
import { z as z10 } from "zod";
|
|
12605
13384
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
12606
13385
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
12607
13386
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
12608
|
-
var QualityPathSchema =
|
|
12609
|
-
(value) => !
|
|
13387
|
+
var QualityPathSchema = z10.string().min(1).refine(
|
|
13388
|
+
(value) => !path29.posix.isAbsolute(value) && !value.includes("\\"),
|
|
12610
13389
|
"expected a portable relative path"
|
|
12611
13390
|
);
|
|
12612
13391
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -12619,53 +13398,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
12619
13398
|
maxEdgeContrastDrop: 0.03,
|
|
12620
13399
|
maxIsolatedPixelIncrease: 5e-3
|
|
12621
13400
|
};
|
|
12622
|
-
var QualityToleranceSchema =
|
|
12623
|
-
requireExactHash:
|
|
12624
|
-
maxColorCountIncrease:
|
|
12625
|
-
maxNewColors:
|
|
12626
|
-
maxTransparencyDelta:
|
|
12627
|
-
maxPartialAlphaIncrease:
|
|
12628
|
-
maxEdgeDensityDelta:
|
|
12629
|
-
maxEdgeContrastDrop:
|
|
12630
|
-
maxIsolatedPixelIncrease:
|
|
13401
|
+
var QualityToleranceSchema = z10.object({
|
|
13402
|
+
requireExactHash: z10.boolean(),
|
|
13403
|
+
maxColorCountIncrease: z10.number().int().min(0),
|
|
13404
|
+
maxNewColors: z10.number().int().min(0),
|
|
13405
|
+
maxTransparencyDelta: z10.number().min(0).max(1),
|
|
13406
|
+
maxPartialAlphaIncrease: z10.number().min(0).max(1),
|
|
13407
|
+
maxEdgeDensityDelta: z10.number().min(0).max(1),
|
|
13408
|
+
maxEdgeContrastDrop: z10.number().min(0).max(1),
|
|
13409
|
+
maxIsolatedPixelIncrease: z10.number().min(0).max(1)
|
|
12631
13410
|
}).strict();
|
|
12632
|
-
var ImageQualityMetricsSchema =
|
|
12633
|
-
sha256:
|
|
12634
|
-
width:
|
|
12635
|
-
height:
|
|
12636
|
-
colorCount:
|
|
12637
|
-
colors:
|
|
12638
|
-
transparency:
|
|
12639
|
-
partialAlpha:
|
|
12640
|
-
edgeDensity:
|
|
12641
|
-
meanEdgeContrast:
|
|
12642
|
-
isolatedPixelRatio:
|
|
13411
|
+
var ImageQualityMetricsSchema = z10.object({
|
|
13412
|
+
sha256: z10.string().regex(SHA256_RE2),
|
|
13413
|
+
width: z10.number().int().min(1),
|
|
13414
|
+
height: z10.number().int().min(1),
|
|
13415
|
+
colorCount: z10.number().int().min(0),
|
|
13416
|
+
colors: z10.array(z10.string().regex(HEX_RE)),
|
|
13417
|
+
transparency: z10.number().min(0).max(1),
|
|
13418
|
+
partialAlpha: z10.number().min(0).max(1),
|
|
13419
|
+
edgeDensity: z10.number().min(0).max(1),
|
|
13420
|
+
meanEdgeContrast: z10.number().min(0).max(1),
|
|
13421
|
+
isolatedPixelRatio: z10.number().min(0).max(1)
|
|
12643
13422
|
}).strict();
|
|
12644
|
-
var QualityBaselineCaseSchema =
|
|
12645
|
-
id:
|
|
13423
|
+
var QualityBaselineCaseSchema = z10.object({
|
|
13424
|
+
id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12646
13425
|
file: QualityPathSchema,
|
|
12647
|
-
record:
|
|
13426
|
+
record: z10.object({
|
|
12648
13427
|
path: QualityPathSchema,
|
|
12649
|
-
sha256:
|
|
13428
|
+
sha256: z10.string().regex(SHA256_RE2)
|
|
12650
13429
|
}).strict().optional(),
|
|
12651
13430
|
expected: ImageQualityMetricsSchema,
|
|
12652
13431
|
tolerances: QualityToleranceSchema
|
|
12653
13432
|
}).strict();
|
|
12654
|
-
var QualityBaselineSchema =
|
|
12655
|
-
$schema:
|
|
12656
|
-
format:
|
|
12657
|
-
schemaVersion:
|
|
12658
|
-
cases:
|
|
13433
|
+
var QualityBaselineSchema = z10.object({
|
|
13434
|
+
$schema: z10.string().url().optional(),
|
|
13435
|
+
format: z10.literal("pixelkiln-quality-baseline"),
|
|
13436
|
+
schemaVersion: z10.literal(1),
|
|
13437
|
+
cases: z10.array(QualityBaselineCaseSchema).min(1)
|
|
12659
13438
|
}).strict().superRefine((baseline, context) => {
|
|
12660
13439
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
12661
13440
|
if (new Set(ids).size !== ids.length) {
|
|
12662
|
-
context.addIssue({ code:
|
|
13441
|
+
context.addIssue({ code: z10.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
12663
13442
|
}
|
|
12664
13443
|
});
|
|
12665
|
-
var QualityInputSchema =
|
|
12666
|
-
id:
|
|
12667
|
-
path:
|
|
12668
|
-
record:
|
|
13444
|
+
var QualityInputSchema = z10.object({
|
|
13445
|
+
id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
13446
|
+
path: z10.string().min(1),
|
|
13447
|
+
record: z10.string().min(1).optional(),
|
|
12669
13448
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
12670
13449
|
}).strict();
|
|
12671
13450
|
function message5(error) {
|
|
@@ -12753,10 +13532,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
12753
13532
|
};
|
|
12754
13533
|
}
|
|
12755
13534
|
async function measureImageQuality(file) {
|
|
12756
|
-
const absolute =
|
|
13535
|
+
const absolute = path29.resolve(file);
|
|
12757
13536
|
let bytes;
|
|
12758
13537
|
try {
|
|
12759
|
-
bytes = await
|
|
13538
|
+
bytes = await readFile24(absolute);
|
|
12760
13539
|
} catch (error) {
|
|
12761
13540
|
throw new Error(`Cannot read quality image ${absolute}: ${message5(error)}`, { cause: error });
|
|
12762
13541
|
}
|
|
@@ -12767,7 +13546,7 @@ async function measureImageQuality(file) {
|
|
|
12767
13546
|
}
|
|
12768
13547
|
}
|
|
12769
13548
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
12770
|
-
const parsed =
|
|
13549
|
+
const parsed = z10.array(QualityInputSchema).min(1).safeParse(raw);
|
|
12771
13550
|
if (!parsed.success) {
|
|
12772
13551
|
throw new Error(
|
|
12773
13552
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -12775,11 +13554,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12775
13554
|
}
|
|
12776
13555
|
const ids = parsed.data.map((entry) => entry.id);
|
|
12777
13556
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
12778
|
-
const root =
|
|
13557
|
+
const root = path29.dirname(path29.resolve(inputsFilePath));
|
|
12779
13558
|
return parsed.data.map((entry) => ({
|
|
12780
13559
|
id: entry.id,
|
|
12781
|
-
file:
|
|
12782
|
-
...entry.record ? { record:
|
|
13560
|
+
file: path29.resolve(root, entry.path),
|
|
13561
|
+
...entry.record ? { record: path29.resolve(root, entry.record) } : {},
|
|
12783
13562
|
tolerances: QualityToleranceSchema.parse({
|
|
12784
13563
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
12785
13564
|
...entry.tolerances
|
|
@@ -12787,7 +13566,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12787
13566
|
}));
|
|
12788
13567
|
}
|
|
12789
13568
|
function portableRelative2(from, to) {
|
|
12790
|
-
return
|
|
13569
|
+
return path29.relative(from, path29.resolve(to)).split(path29.sep).join("/") || ".";
|
|
12791
13570
|
}
|
|
12792
13571
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
12793
13572
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -12802,20 +13581,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
12802
13581
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
12803
13582
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
12804
13583
|
}
|
|
12805
|
-
const outputs = record.outputs.map((output) =>
|
|
12806
|
-
if (!outputs.includes(
|
|
13584
|
+
const outputs = record.outputs.map((output) => path29.resolve(path29.dirname(recordPath), output.path));
|
|
13585
|
+
if (!outputs.includes(path29.resolve(imagePath))) {
|
|
12807
13586
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
12808
13587
|
}
|
|
12809
13588
|
}
|
|
12810
13589
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
12811
13590
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
12812
|
-
const absolute =
|
|
13591
|
+
const absolute = path29.resolve(baselinePath);
|
|
12813
13592
|
for (const input of inputs) {
|
|
12814
|
-
if (
|
|
13593
|
+
if (path29.resolve(input.file) === absolute || input.record && path29.resolve(input.record) === absolute) {
|
|
12815
13594
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
12816
13595
|
}
|
|
12817
13596
|
}
|
|
12818
|
-
const root =
|
|
13597
|
+
const root = path29.dirname(absolute);
|
|
12819
13598
|
const cases = [];
|
|
12820
13599
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
12821
13600
|
const expected = await measureImageQuality(input.file);
|
|
@@ -12842,8 +13621,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12842
13621
|
cases
|
|
12843
13622
|
});
|
|
12844
13623
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
12845
|
-
if (
|
|
12846
|
-
const current = await
|
|
13624
|
+
if (existsSync22(absolute) && !options.force) {
|
|
13625
|
+
const current = await readFile24(absolute);
|
|
12847
13626
|
if (!current.equals(data)) {
|
|
12848
13627
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
12849
13628
|
}
|
|
@@ -12852,10 +13631,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12852
13631
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
12853
13632
|
}
|
|
12854
13633
|
async function readQualityBaseline(baselinePath) {
|
|
12855
|
-
const absolute =
|
|
13634
|
+
const absolute = path29.resolve(baselinePath);
|
|
12856
13635
|
let raw;
|
|
12857
13636
|
try {
|
|
12858
|
-
raw = JSON.parse(await
|
|
13637
|
+
raw = JSON.parse(await readFile24(absolute, "utf8"));
|
|
12859
13638
|
} catch (error) {
|
|
12860
13639
|
throw new Error(`Could not read quality baseline ${absolute}: ${message5(error)}`, { cause: error });
|
|
12861
13640
|
}
|
|
@@ -12925,12 +13704,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
12925
13704
|
return { violations, warnings };
|
|
12926
13705
|
}
|
|
12927
13706
|
async function checkQualityBaseline(baselinePath) {
|
|
12928
|
-
const absolute =
|
|
13707
|
+
const absolute = path29.resolve(baselinePath);
|
|
12929
13708
|
const baseline = await readQualityBaseline(absolute);
|
|
12930
|
-
const root =
|
|
13709
|
+
const root = path29.dirname(absolute);
|
|
12931
13710
|
const cases = [];
|
|
12932
13711
|
for (const entry of baseline.cases) {
|
|
12933
|
-
const file =
|
|
13712
|
+
const file = path29.resolve(root, entry.file);
|
|
12934
13713
|
let actual = null;
|
|
12935
13714
|
const violations = [];
|
|
12936
13715
|
const warnings = [];
|
|
@@ -12943,7 +13722,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12943
13722
|
violations.push(message5(error));
|
|
12944
13723
|
}
|
|
12945
13724
|
if (entry.record) {
|
|
12946
|
-
const recordPath =
|
|
13725
|
+
const recordPath = path29.resolve(root, entry.record.path);
|
|
12947
13726
|
try {
|
|
12948
13727
|
const recordHash = await sha256File(recordPath);
|
|
12949
13728
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -13029,13 +13808,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13029
13808
|
};
|
|
13030
13809
|
const loadProject = async (project) => {
|
|
13031
13810
|
const ctx = await access2.loadProject(project);
|
|
13032
|
-
const dir =
|
|
13811
|
+
const dir = path30.dirname(ctx.loaded.path);
|
|
13033
13812
|
const declared = readEnvFiles(dir);
|
|
13034
13813
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
13035
13814
|
for (const [name, value] of Object.entries(declared)) {
|
|
13036
13815
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
13037
13816
|
throw new Error(
|
|
13038
|
-
`${
|
|
13817
|
+
`${path30.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
13039
13818
|
);
|
|
13040
13819
|
}
|
|
13041
13820
|
}
|
|
@@ -13062,7 +13841,9 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13062
13841
|
budget ? describeBudget(budget) : null
|
|
13063
13842
|
),
|
|
13064
13843
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
13065
|
-
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
|
|
13844
|
+
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
13845
|
+
// The editor exists to write hand edits back, so it follows the write gate.
|
|
13846
|
+
...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
|
|
13066
13847
|
});
|
|
13067
13848
|
await new Promise((resolve) => {
|
|
13068
13849
|
const stop = () => {
|
|
@@ -13077,11 +13858,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13077
13858
|
log("\n gallery closed");
|
|
13078
13859
|
}
|
|
13079
13860
|
async function provenanceFile(id, file) {
|
|
13080
|
-
const absolute =
|
|
13861
|
+
const absolute = path30.resolve(file);
|
|
13081
13862
|
return {
|
|
13082
13863
|
id,
|
|
13083
13864
|
path: absolute,
|
|
13084
|
-
sha256:
|
|
13865
|
+
sha256: existsSync23(absolute) ? await sha256File(absolute) : null,
|
|
13085
13866
|
included: true
|
|
13086
13867
|
};
|
|
13087
13868
|
}
|
|
@@ -13131,7 +13912,8 @@ var BOOL_FLAGS = [
|
|
|
13131
13912
|
"--primary-only",
|
|
13132
13913
|
"--prune",
|
|
13133
13914
|
"--edit",
|
|
13134
|
-
"--refresh"
|
|
13915
|
+
"--refresh",
|
|
13916
|
+
"--no-editor"
|
|
13135
13917
|
];
|
|
13136
13918
|
var COMMANDS = [
|
|
13137
13919
|
"init",
|
|
@@ -13158,6 +13940,7 @@ var COMMANDS = [
|
|
|
13158
13940
|
"status",
|
|
13159
13941
|
"gallery",
|
|
13160
13942
|
"edit",
|
|
13943
|
+
"tools",
|
|
13161
13944
|
"quality",
|
|
13162
13945
|
"refine",
|
|
13163
13946
|
"recipe",
|
|
@@ -13173,6 +13956,8 @@ var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
|
13173
13956
|
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
13174
13957
|
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
13175
13958
|
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
13959
|
+
var TOOLS_SUBCOMMANDS = ["status", "install"];
|
|
13960
|
+
var TOOLS = ["editor"];
|
|
13176
13961
|
function parseArgs(argv) {
|
|
13177
13962
|
const [command = "help"] = argv;
|
|
13178
13963
|
if (!COMMANDS.includes(command)) {
|
|
@@ -13236,6 +14021,22 @@ function parseArgs(argv) {
|
|
|
13236
14021
|
throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
|
|
13237
14022
|
}
|
|
13238
14023
|
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
14024
|
+
} else if (command === "tools") {
|
|
14025
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "status" : rest[0];
|
|
14026
|
+
if (!TOOLS_SUBCOMMANDS.includes(subcommand)) {
|
|
14027
|
+
throw new Error(`Unknown tools subcommand "${subcommand}". Known: ${TOOLS_SUBCOMMANDS.join(", ")}`);
|
|
14028
|
+
}
|
|
14029
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
14030
|
+
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
14031
|
+
if (subcommand === "install" && target === void 0) {
|
|
14032
|
+
throw new Error(`tools install needs a tool name: ${TOOLS.join(", ")}`);
|
|
14033
|
+
}
|
|
14034
|
+
if (target !== void 0) {
|
|
14035
|
+
if (!TOOLS.includes(target)) {
|
|
14036
|
+
throw new Error(`Unknown tool "${target}". Known: ${TOOLS.join(", ")}`);
|
|
14037
|
+
}
|
|
14038
|
+
rest = rest.slice(1);
|
|
14039
|
+
}
|
|
13239
14040
|
} else if (command === "refine") {
|
|
13240
14041
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
13241
14042
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -13349,7 +14150,7 @@ function parseArgs(argv) {
|
|
|
13349
14150
|
return {
|
|
13350
14151
|
command,
|
|
13351
14152
|
manifest,
|
|
13352
|
-
lock: get("--lock") ??
|
|
14153
|
+
lock: get("--lock") ?? path30.join(path30.dirname(path30.resolve(manifest)), "pixelkiln.lock.json"),
|
|
13353
14154
|
explicitLock: get("--lock"),
|
|
13354
14155
|
styles: list("--style"),
|
|
13355
14156
|
assets: list("--only"),
|
|
@@ -13364,6 +14165,7 @@ function parseArgs(argv) {
|
|
|
13364
14165
|
noOpen: rest.includes("--no-open"),
|
|
13365
14166
|
tag: rest.includes("--tag"),
|
|
13366
14167
|
edit: rest.includes("--edit"),
|
|
14168
|
+
noEditor: rest.includes("--no-editor"),
|
|
13367
14169
|
refresh: rest.includes("--refresh"),
|
|
13368
14170
|
from: get("--from"),
|
|
13369
14171
|
out: get("--out"),
|
|
@@ -13397,6 +14199,45 @@ function parseArgs(argv) {
|
|
|
13397
14199
|
account: get("--account")
|
|
13398
14200
|
};
|
|
13399
14201
|
}
|
|
14202
|
+
async function runTools(args) {
|
|
14203
|
+
const mb = (bytes) => bytes < 1e6 ? `${(bytes / 1e3).toFixed(0)} kB` : `${(bytes / 1e6).toFixed(1)} MB`;
|
|
14204
|
+
if (args.subcommand === "status") {
|
|
14205
|
+
const status2 = await editorStatus();
|
|
14206
|
+
if (args.json) {
|
|
14207
|
+
log(JSON.stringify({ version: 1, editor: { ...status2, pixelorama: EDITOR_PIN.pixelorama, protocol: EDITOR_PIN.protocol } }, null, 2));
|
|
14208
|
+
return;
|
|
14209
|
+
}
|
|
14210
|
+
log(` editor Pixelorama ${EDITOR_PIN.pixelorama} (bridge protocol ${EDITOR_PIN.protocol})`);
|
|
14211
|
+
if (!status2.release) {
|
|
14212
|
+
log(" no published build is pinned by this PixelKiln version");
|
|
14213
|
+
return;
|
|
14214
|
+
}
|
|
14215
|
+
log(` release ${status2.release}`);
|
|
14216
|
+
log(` ${status2.installed ? "installed and verified" : status2.installedBytes ? `partial: ${status2.missing.length} of ${Object.keys(EDITOR_PIN.files).length} files missing` : "not installed"} \u2014 ${mb(status2.totalBytes)} in ${status2.dir}`);
|
|
14217
|
+
if (!status2.installed) log(" run: pixelkiln tools install editor");
|
|
14218
|
+
return;
|
|
14219
|
+
}
|
|
14220
|
+
const status = await editorStatus();
|
|
14221
|
+
if (status.installed) {
|
|
14222
|
+
log(` editor is already installed and verified in ${status.dir}`);
|
|
14223
|
+
return;
|
|
14224
|
+
}
|
|
14225
|
+
if (!status.release) {
|
|
14226
|
+
throw new Error("this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust");
|
|
14227
|
+
}
|
|
14228
|
+
log(` fetching ${status.missing.length} file(s), ${mb(status.totalBytes - status.installedBytes)}, from release ${status.release}`);
|
|
14229
|
+
try {
|
|
14230
|
+
const result = await installEditor({
|
|
14231
|
+
onProgress: (p) => {
|
|
14232
|
+
if (p.phase === "start") log(` ${p.file.padEnd(34)} ${mb(p.bytes).padStart(9)}`);
|
|
14233
|
+
}
|
|
14234
|
+
});
|
|
14235
|
+
log(` editor ready in ${result.dir} (${result.downloaded.length} fetched, ${result.skipped.length} already present, every hash verified)`);
|
|
14236
|
+
} catch (err) {
|
|
14237
|
+
if (err instanceof EditorInstallError) throw new Error(`editor install failed: ${err.message}`);
|
|
14238
|
+
throw err;
|
|
14239
|
+
}
|
|
14240
|
+
}
|
|
13400
14241
|
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
13401
14242
|
|
|
13402
14243
|
pixelkiln <command> [options]
|
|
@@ -13437,8 +14278,12 @@ Commands
|
|
|
13437
14278
|
gallery Open a local read-only gallery of every generation and its
|
|
13438
14279
|
provenance: prompt, provider, cost, outputs, lineage, quality.
|
|
13439
14280
|
--workspace <catalog> shows every registered project at once;
|
|
13440
|
-
--edit lets the page change prompts, sizes, tags, and add assets
|
|
14281
|
+
--edit lets the page change prompts, sizes, tags, and add assets,
|
|
14282
|
+
and offers the in-browser pixel editor (--no-editor hides it);
|
|
13441
14283
|
--budget enables Generate, Regenerate, and review under that ceiling.
|
|
14284
|
+
tools status/install editor: the in-browser editor (Pixelorama) is a
|
|
14285
|
+
46 MB web build fetched once per release into a user cache and
|
|
14286
|
+
verified against pinned hashes. Offline once installed.
|
|
13442
14287
|
workspace Register sibling projects and derive account-wide claims/status.
|
|
13443
14288
|
add/remove/list/status/claims. Offline.
|
|
13444
14289
|
|
|
@@ -13478,6 +14323,7 @@ Options
|
|
|
13478
14323
|
--yes, -y Skip the confirmation prompt
|
|
13479
14324
|
--no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
|
|
13480
14325
|
--edit gallery: allow manifest edits from the page (never spends)
|
|
14326
|
+
--no-editor gallery: do not offer or serve the in-browser editor
|
|
13481
14327
|
--tag Also push tags upstream after fetch
|
|
13482
14328
|
--refresh fetch: re-download and replace files whose object changed
|
|
13483
14329
|
upstream (e.g. edited in PixelLab's editor); no generation
|
|
@@ -13496,6 +14342,7 @@ Examples
|
|
|
13496
14342
|
pixelkiln gallery --workspace pixelkiln.workspace.json
|
|
13497
14343
|
pixelkiln gallery --edit --budget 80
|
|
13498
14344
|
pixelkiln edit --style base --only anvil
|
|
14345
|
+
pixelkiln tools install editor
|
|
13499
14346
|
PIXELKILN_EDITOR="open -a Aseprite" pixelkiln edit --only anvil --style base
|
|
13500
14347
|
pixelkiln adopt --tag
|
|
13501
14348
|
pixelkiln pack --style heybud-premium
|
|
@@ -13655,10 +14502,10 @@ async function confirm(question, auto) {
|
|
|
13655
14502
|
return answer === "y" || answer === "yes";
|
|
13656
14503
|
}
|
|
13657
14504
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13658
|
-
if (!
|
|
14505
|
+
if (!existsSync23(workspacePath)) {
|
|
13659
14506
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13660
14507
|
}
|
|
13661
|
-
const dir =
|
|
14508
|
+
const dir = path30.dirname(path30.resolve(workspacePath));
|
|
13662
14509
|
const ws = await loadWorkspace(workspacePath);
|
|
13663
14510
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13664
14511
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13679,26 +14526,30 @@ async function main() {
|
|
|
13679
14526
|
}
|
|
13680
14527
|
if (args.command === "--version" || args.command === "-v") {
|
|
13681
14528
|
const pkg = JSON.parse(
|
|
13682
|
-
await
|
|
14529
|
+
await readFile25(new URL("../package.json", import.meta.url), "utf8")
|
|
13683
14530
|
);
|
|
13684
14531
|
log(`${pkg.name} ${pkg.version}`);
|
|
13685
14532
|
return;
|
|
13686
14533
|
}
|
|
14534
|
+
if (args.command === "tools") {
|
|
14535
|
+
await runTools(args);
|
|
14536
|
+
return;
|
|
14537
|
+
}
|
|
13687
14538
|
if (args.command === "quality") {
|
|
13688
14539
|
if (args.subcommand === "snapshot") {
|
|
13689
14540
|
if (!args.inputs) throw new Error("quality snapshot needs --inputs <quality-inputs.json>.");
|
|
13690
14541
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
13691
14542
|
let raw;
|
|
13692
14543
|
try {
|
|
13693
|
-
raw = JSON.parse(await
|
|
14544
|
+
raw = JSON.parse(await readFile25(path30.resolve(args.inputs), "utf8"));
|
|
13694
14545
|
} catch (error) {
|
|
13695
14546
|
throw new Error(
|
|
13696
|
-
`Could not read quality inputs ${
|
|
14547
|
+
`Could not read quality inputs ${path30.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13697
14548
|
{ cause: error }
|
|
13698
14549
|
);
|
|
13699
14550
|
}
|
|
13700
14551
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
13701
|
-
if (
|
|
14552
|
+
if (path30.resolve(args.inputs) === path30.resolve(args.out)) {
|
|
13702
14553
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
13703
14554
|
}
|
|
13704
14555
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -13711,7 +14562,7 @@ async function main() {
|
|
|
13711
14562
|
}, null, 2));
|
|
13712
14563
|
} else {
|
|
13713
14564
|
log(
|
|
13714
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
14565
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path30.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
13715
14566
|
);
|
|
13716
14567
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
13717
14568
|
}
|
|
@@ -13806,14 +14657,14 @@ async function main() {
|
|
|
13806
14657
|
}, null, 2));
|
|
13807
14658
|
} else {
|
|
13808
14659
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
13809
|
-
log(` ${
|
|
14660
|
+
log(` ${path30.relative(process.cwd(), result.destination) || "."}`);
|
|
13810
14661
|
log(`
|
|
13811
14662
|
Add this entry under your manifest's styles object:`);
|
|
13812
14663
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
13813
14664
|
if (result.recipe.models.length) {
|
|
13814
14665
|
log(`
|
|
13815
14666
|
Models are not downloaded automatically. Verify them with:`);
|
|
13816
|
-
log(` pixelkiln recipe verify ${
|
|
14667
|
+
log(` pixelkiln recipe verify ${path30.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
13817
14668
|
}
|
|
13818
14669
|
}
|
|
13819
14670
|
return;
|
|
@@ -13847,12 +14698,12 @@ async function main() {
|
|
|
13847
14698
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
13848
14699
|
);
|
|
13849
14700
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
13850
|
-
log(` wrote ${
|
|
13851
|
-
log(` quality record: ${
|
|
14701
|
+
log(` wrote ${path30.relative(process.cwd(), result2.output)}`);
|
|
14702
|
+
log(` quality record: ${path30.relative(process.cwd(), result2.record)}`);
|
|
13852
14703
|
log(`
|
|
13853
14704
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
13854
14705
|
log(
|
|
13855
|
-
` pixelkiln refine approve --from ${
|
|
14706
|
+
` pixelkiln refine approve --from ${path30.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
13856
14707
|
);
|
|
13857
14708
|
}
|
|
13858
14709
|
return;
|
|
@@ -13880,7 +14731,7 @@ async function main() {
|
|
|
13880
14731
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
13881
14732
|
else {
|
|
13882
14733
|
log(
|
|
13883
|
-
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${
|
|
14734
|
+
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path30.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path30.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
|
|
13884
14735
|
);
|
|
13885
14736
|
}
|
|
13886
14737
|
return;
|
|
@@ -13889,14 +14740,14 @@ async function main() {
|
|
|
13889
14740
|
if (args.json) {
|
|
13890
14741
|
log(JSON.stringify(result, null, 2));
|
|
13891
14742
|
} else {
|
|
13892
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
14743
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path30.relative(process.cwd(), result.output || result.record)}`);
|
|
13893
14744
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
13894
14745
|
}
|
|
13895
14746
|
if (!result.safe) process.exitCode = 1;
|
|
13896
14747
|
return;
|
|
13897
14748
|
}
|
|
13898
14749
|
if (args.command === "balance") {
|
|
13899
|
-
loadEnvFiles(
|
|
14750
|
+
loadEnvFiles(path30.dirname(path30.resolve(args.manifest)));
|
|
13900
14751
|
loadEnvFiles(process.cwd());
|
|
13901
14752
|
const loaded2 = await loadManifest(args.manifest);
|
|
13902
14753
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -13909,31 +14760,31 @@ async function main() {
|
|
|
13909
14760
|
}
|
|
13910
14761
|
if (args.command === "init") {
|
|
13911
14762
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13912
|
-
const root =
|
|
13913
|
-
if (!
|
|
14763
|
+
const root = path30.resolve(args.from);
|
|
14764
|
+
if (!existsSync23(root)) throw new Error(`No directory at ${root}`);
|
|
13914
14765
|
const generator = args.generator ?? "map";
|
|
13915
14766
|
if (generator !== "1dir" && generator !== "map") {
|
|
13916
14767
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13917
14768
|
}
|
|
13918
|
-
const target =
|
|
14769
|
+
const target = path30.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13919
14770
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13920
14771
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13921
14772
|
const manifest = buildManifest(
|
|
13922
|
-
args.name ??
|
|
14773
|
+
args.name ?? path30.basename(path30.dirname(target)),
|
|
13923
14774
|
args.styles[0] ?? "base",
|
|
13924
14775
|
generator,
|
|
13925
|
-
|
|
14776
|
+
path30.relative(path30.dirname(target), root) || ".",
|
|
13926
14777
|
assets
|
|
13927
14778
|
);
|
|
13928
14779
|
await writeManifestFile(target, manifest);
|
|
13929
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
14780
|
+
log(` scanned ${assets.length} PNG(s) under ${path30.relative(process.cwd(), root)}`);
|
|
13930
14781
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13931
|
-
log(` wrote ${
|
|
14782
|
+
log(` wrote ${path30.relative(process.cwd(), target)}`);
|
|
13932
14783
|
log(`
|
|
13933
14784
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13934
14785
|
log(` PixelLab account instead of inventing them:`);
|
|
13935
14786
|
log(`
|
|
13936
|
-
pixelkiln adopt --manifest ${
|
|
14787
|
+
pixelkiln adopt --manifest ${path30.relative(process.cwd(), target)} --write-prompts
|
|
13937
14788
|
`);
|
|
13938
14789
|
return;
|
|
13939
14790
|
}
|
|
@@ -13981,10 +14832,10 @@ async function main() {
|
|
|
13981
14832
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
13982
14833
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
13983
14834
|
}
|
|
13984
|
-
const raw = JSON.parse(await
|
|
14835
|
+
const raw = JSON.parse(await readFile25(path30.resolve(args.inputs), "utf8"));
|
|
13985
14836
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13986
14837
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13987
|
-
const base =
|
|
14838
|
+
const base = path30.resolve(args.out.replace(/\.png$/, ""));
|
|
13988
14839
|
const outputs = [
|
|
13989
14840
|
{ path: `${base}.png`, data: png },
|
|
13990
14841
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -13997,17 +14848,17 @@ async function main() {
|
|
|
13997
14848
|
log(
|
|
13998
14849
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
13999
14850
|
);
|
|
14000
|
-
log(` ${
|
|
14851
|
+
log(` ${path30.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14001
14852
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14002
14853
|
return;
|
|
14003
14854
|
}
|
|
14004
14855
|
if (args.command === "workspace") {
|
|
14005
|
-
const workspacePath =
|
|
14006
|
-
const dir =
|
|
14856
|
+
const workspacePath = path30.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
14857
|
+
const dir = path30.dirname(workspacePath);
|
|
14007
14858
|
if (args.subcommand === "add") {
|
|
14008
|
-
const manifestPath =
|
|
14859
|
+
const manifestPath = path30.resolve(args.target);
|
|
14009
14860
|
const loadedTarget = await loadManifest(manifestPath);
|
|
14010
|
-
const lockPath = args.explicitLock ?
|
|
14861
|
+
const lockPath = args.explicitLock ? path30.resolve(args.explicitLock) : path30.join(path30.dirname(manifestPath), "pixelkiln.lock.json");
|
|
14011
14862
|
const ws = await loadWorkspace(workspacePath);
|
|
14012
14863
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
14013
14864
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -14027,10 +14878,10 @@ async function main() {
|
|
|
14027
14878
|
...args.account ? { account: args.account } : {}
|
|
14028
14879
|
};
|
|
14029
14880
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
14030
|
-
log(` registered "${id}" in ${
|
|
14881
|
+
log(` registered "${id}" in ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14031
14882
|
log(` manifest: ${project.manifest}`);
|
|
14032
14883
|
log(` lock: ${project.lock}`);
|
|
14033
|
-
if (!
|
|
14884
|
+
if (!existsSync23(lockPath)) {
|
|
14034
14885
|
log(
|
|
14035
14886
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
14036
14887
|
);
|
|
@@ -14039,7 +14890,7 @@ async function main() {
|
|
|
14039
14890
|
}
|
|
14040
14891
|
if (args.subcommand === "remove") {
|
|
14041
14892
|
const ws = await loadWorkspace(workspacePath);
|
|
14042
|
-
const resolvedTarget =
|
|
14893
|
+
const resolvedTarget = path30.resolve(args.target);
|
|
14043
14894
|
const match = ws.projects.find(
|
|
14044
14895
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
14045
14896
|
);
|
|
@@ -14050,11 +14901,11 @@ async function main() {
|
|
|
14050
14901
|
version: 1,
|
|
14051
14902
|
projects: ws.projects.filter((p) => p !== match)
|
|
14052
14903
|
});
|
|
14053
|
-
log(` removed "${match.id}" from ${
|
|
14904
|
+
log(` removed "${match.id}" from ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14054
14905
|
return;
|
|
14055
14906
|
}
|
|
14056
14907
|
if (args.subcommand === "list") {
|
|
14057
|
-
if (!
|
|
14908
|
+
if (!existsSync23(workspacePath)) {
|
|
14058
14909
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14059
14910
|
}
|
|
14060
14911
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14062,9 +14913,9 @@ async function main() {
|
|
|
14062
14913
|
if (args.json) {
|
|
14063
14914
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
14064
14915
|
} else if (!ws.projects.length) {
|
|
14065
|
-
log(` no projects registered in ${
|
|
14916
|
+
log(` no projects registered in ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14066
14917
|
} else {
|
|
14067
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
14918
|
+
log(` ${ws.projects.length} project(s) in ${path30.relative(process.cwd(), workspacePath)}:`);
|
|
14068
14919
|
for (const p of ws.projects) {
|
|
14069
14920
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
14070
14921
|
}
|
|
@@ -14074,7 +14925,7 @@ async function main() {
|
|
|
14074
14925
|
return;
|
|
14075
14926
|
}
|
|
14076
14927
|
if (args.subcommand === "status") {
|
|
14077
|
-
if (!
|
|
14928
|
+
if (!existsSync23(workspacePath)) {
|
|
14078
14929
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14079
14930
|
}
|
|
14080
14931
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14082,7 +14933,7 @@ async function main() {
|
|
|
14082
14933
|
if (args.json) {
|
|
14083
14934
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
14084
14935
|
} else {
|
|
14085
|
-
log(` workspace: ${
|
|
14936
|
+
log(` workspace: ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14086
14937
|
for (const p of report.projects) {
|
|
14087
14938
|
if (p.error) {
|
|
14088
14939
|
log(`
|
|
@@ -14129,8 +14980,8 @@ async function main() {
|
|
|
14129
14980
|
}
|
|
14130
14981
|
}
|
|
14131
14982
|
if (args.command === "gallery" && args.workspace) {
|
|
14132
|
-
const workspacePath =
|
|
14133
|
-
if (!
|
|
14983
|
+
const workspacePath = path30.resolve(args.workspace);
|
|
14984
|
+
if (!existsSync23(workspacePath)) {
|
|
14134
14985
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14135
14986
|
}
|
|
14136
14987
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -14150,7 +15001,7 @@ async function main() {
|
|
|
14150
15001
|
const registered = async (projectId) => {
|
|
14151
15002
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
14152
15003
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
14153
|
-
return resolveProject(
|
|
15004
|
+
return resolveProject(path30.dirname(workspacePath), project);
|
|
14154
15005
|
};
|
|
14155
15006
|
await runGallery(initial, build, args, {
|
|
14156
15007
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -14165,14 +15016,14 @@ async function main() {
|
|
|
14165
15016
|
});
|
|
14166
15017
|
return;
|
|
14167
15018
|
}
|
|
14168
|
-
if (!
|
|
15019
|
+
if (!existsSync23(path30.resolve(args.manifest))) {
|
|
14169
15020
|
throw new Error(
|
|
14170
|
-
`No manifest at ${
|
|
15021
|
+
`No manifest at ${path30.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
14171
15022
|
);
|
|
14172
15023
|
}
|
|
14173
|
-
const manifestDir =
|
|
15024
|
+
const manifestDir = path30.dirname(path30.resolve(args.manifest));
|
|
14174
15025
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
14175
|
-
if (
|
|
15026
|
+
if (path30.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
14176
15027
|
const loaded = await loadManifest(args.manifest);
|
|
14177
15028
|
let specs = await resolveSpecs(loaded, {
|
|
14178
15029
|
styles: args.styles,
|
|
@@ -14245,7 +15096,7 @@ async function main() {
|
|
|
14245
15096
|
});
|
|
14246
15097
|
};
|
|
14247
15098
|
await runGallery(await build(), reload, args, {
|
|
14248
|
-
manifestFor: () =>
|
|
15099
|
+
manifestFor: () => path30.resolve(args.manifest),
|
|
14249
15100
|
loadProject: async () => {
|
|
14250
15101
|
const freshLoaded = await loadManifest(args.manifest);
|
|
14251
15102
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -14320,7 +15171,7 @@ async function main() {
|
|
|
14320
15171
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
14321
15172
|
if (item.state === "needs-approval") {
|
|
14322
15173
|
log(
|
|
14323
|
-
` pixelkiln refine approve --from ${
|
|
15174
|
+
` pixelkiln refine approve --from ${path30.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
14324
15175
|
);
|
|
14325
15176
|
}
|
|
14326
15177
|
}
|
|
@@ -14411,7 +15262,7 @@ async function main() {
|
|
|
14411
15262
|
let intact = true;
|
|
14412
15263
|
for (const output of entry.outputs) {
|
|
14413
15264
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
14414
|
-
if (!
|
|
15265
|
+
if (!existsSync23(file) || await sha256File(file) !== output.sha256) {
|
|
14415
15266
|
intact = false;
|
|
14416
15267
|
break;
|
|
14417
15268
|
}
|
|
@@ -14469,7 +15320,7 @@ async function main() {
|
|
|
14469
15320
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14470
15321
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14471
15322
|
}
|
|
14472
|
-
const manifestDir2 =
|
|
15323
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14473
15324
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14474
15325
|
for (const styleId of styleIds) {
|
|
14475
15326
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14482,7 +15333,7 @@ async function main() {
|
|
|
14482
15333
|
sources: manifestSources(loaded.manifest, styleId)
|
|
14483
15334
|
});
|
|
14484
15335
|
const style = loaded.manifest.styles[styleId];
|
|
14485
|
-
const base = args.out ?
|
|
15336
|
+
const base = args.out ? path30.resolve(args.out.replace(/\.png$/, "")) : path30.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
14486
15337
|
const outputs = [
|
|
14487
15338
|
{ path: `${base}.png`, data: png },
|
|
14488
15339
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -14512,13 +15363,13 @@ async function main() {
|
|
|
14512
15363
|
log(
|
|
14513
15364
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
14514
15365
|
);
|
|
14515
|
-
log(` ${
|
|
15366
|
+
log(` ${path30.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14516
15367
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14517
15368
|
}
|
|
14518
15369
|
return;
|
|
14519
15370
|
}
|
|
14520
15371
|
if (args.command === "mount") {
|
|
14521
|
-
const manifestDir2 =
|
|
15372
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14522
15373
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14523
15374
|
for (const styleId of styleIds) {
|
|
14524
15375
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14557,7 +15408,7 @@ async function main() {
|
|
|
14557
15408
|
sources,
|
|
14558
15409
|
outputRoles
|
|
14559
15410
|
);
|
|
14560
|
-
const out =
|
|
15411
|
+
const out = path30.resolve(manifestDir2, style.mount.out);
|
|
14561
15412
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
14562
15413
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14563
15414
|
const outputs = [
|
|
@@ -14577,7 +15428,7 @@ async function main() {
|
|
|
14577
15428
|
await provenanceFile("$lock", args.lock),
|
|
14578
15429
|
...qualityRecords,
|
|
14579
15430
|
...artifactSources.filter(
|
|
14580
|
-
(source) => source.id !== "$base" ||
|
|
15431
|
+
(source) => source.id !== "$base" || path30.resolve(source.path) !== out
|
|
14581
15432
|
)
|
|
14582
15433
|
],
|
|
14583
15434
|
options: {
|
|
@@ -14590,7 +15441,7 @@ async function main() {
|
|
|
14590
15441
|
log(
|
|
14591
15442
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14592
15443
|
);
|
|
14593
|
-
log(` ${
|
|
15444
|
+
log(` ${path30.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14594
15445
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14595
15446
|
}
|
|
14596
15447
|
return;
|
|
@@ -14646,7 +15497,7 @@ async function main() {
|
|
|
14646
15497
|
}
|
|
14647
15498
|
if (args.command === "export") {
|
|
14648
15499
|
const format = args.format ?? "generic";
|
|
14649
|
-
const manifestDir2 =
|
|
15500
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14650
15501
|
const selected = specs.filter((spec) => {
|
|
14651
15502
|
if (spec.generator !== "tiles") return false;
|
|
14652
15503
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14662,12 +15513,12 @@ async function main() {
|
|
|
14662
15513
|
for (const spec of selected) {
|
|
14663
15514
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14664
15515
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14665
|
-
const defaultBase =
|
|
14666
|
-
const base = args.out ?
|
|
15516
|
+
const defaultBase = path30.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
15517
|
+
const base = args.out ? path30.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14667
15518
|
const result = exportTileset(entry, spec, {
|
|
14668
15519
|
format,
|
|
14669
15520
|
manifestDir: manifestDir2,
|
|
14670
|
-
imageName:
|
|
15521
|
+
imageName: path30.basename(`${base}.png`),
|
|
14671
15522
|
columns: args.columns
|
|
14672
15523
|
});
|
|
14673
15524
|
const outputs = [
|
|
@@ -14685,7 +15536,7 @@ async function main() {
|
|
|
14685
15536
|
asset: spec.assetId,
|
|
14686
15537
|
columns: args.columns ?? null,
|
|
14687
15538
|
format,
|
|
14688
|
-
image:
|
|
15539
|
+
image: path30.basename(`${base}.png`),
|
|
14689
15540
|
providerRules: result.generic.providerRules,
|
|
14690
15541
|
style: spec.styleId,
|
|
14691
15542
|
tileType: spec.tileType ?? null
|
|
@@ -14695,7 +15546,7 @@ async function main() {
|
|
|
14695
15546
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14696
15547
|
);
|
|
14697
15548
|
log(
|
|
14698
|
-
` ${
|
|
15549
|
+
` ${path30.relative(process.cwd(), base)}.png + ${path30.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14699
15550
|
);
|
|
14700
15551
|
}
|
|
14701
15552
|
return;
|
|
@@ -14745,12 +15596,12 @@ async function main() {
|
|
|
14745
15596
|
tagged ${n} object(s) upstream`);
|
|
14746
15597
|
}
|
|
14747
15598
|
if (args.writePrompts) {
|
|
14748
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
15599
|
+
const { filled, stillEmpty } = await writePromptsBack(path30.resolve(args.manifest), lock, {
|
|
14749
15600
|
onProgress: log,
|
|
14750
15601
|
provider: provider.id,
|
|
14751
15602
|
assetIds: specs.map((spec) => spec.assetId)
|
|
14752
15603
|
});
|
|
14753
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
15604
|
+
log(` recovered ${filled} prompt(s) into ${path30.relative(process.cwd(), args.manifest)}`);
|
|
14754
15605
|
const reloaded = await loadManifest(args.manifest);
|
|
14755
15606
|
const rebased = (await resolveSpecs(reloaded, {
|
|
14756
15607
|
styles: args.styles,
|
|
@@ -14779,12 +15630,12 @@ async function main() {
|
|
|
14779
15630
|
const provider = providerFor(selectedAccountProvider);
|
|
14780
15631
|
const jsonMode = args.dryRun && args.json;
|
|
14781
15632
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
14782
|
-
const ownLock =
|
|
15633
|
+
const ownLock = path30.resolve(args.lock);
|
|
14783
15634
|
let workspaceProjects = [];
|
|
14784
15635
|
let workspaceDir = "";
|
|
14785
15636
|
if (args.workspace) {
|
|
14786
|
-
const workspacePath =
|
|
14787
|
-
workspaceDir =
|
|
15637
|
+
const workspacePath = path30.resolve(args.workspace);
|
|
15638
|
+
workspaceDir = path30.dirname(workspacePath);
|
|
14788
15639
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
14789
15640
|
workspaceProjects = complete.ws.projects;
|
|
14790
15641
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -14793,12 +15644,12 @@ async function main() {
|
|
|
14793
15644
|
const lockPaths = [
|
|
14794
15645
|
.../* @__PURE__ */ new Set([
|
|
14795
15646
|
...workspaceLockPaths,
|
|
14796
|
-
...
|
|
14797
|
-
...args.claims.map((c) =>
|
|
15647
|
+
...existsSync23(ownLock) ? [ownLock] : [],
|
|
15648
|
+
...args.claims.map((c) => path30.resolve(c))
|
|
14798
15649
|
])
|
|
14799
15650
|
];
|
|
14800
15651
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
14801
|
-
for (const p of lockPaths) diag(` ${
|
|
15652
|
+
for (const p of lockPaths) diag(` ${path30.relative(process.cwd(), p)}`);
|
|
14802
15653
|
if (!args.claims.length && !args.workspace) {
|
|
14803
15654
|
diag(
|
|
14804
15655
|
`
|
|
@@ -14806,7 +15657,7 @@ async function main() {
|
|
|
14806
15657
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
14807
15658
|
register every project in a workspace catalog and pass --workspace.`
|
|
14808
15659
|
);
|
|
14809
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
15660
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path30.resolve(args.manifest))) {
|
|
14810
15661
|
diag(
|
|
14811
15662
|
`
|
|
14812
15663
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14890,7 +15741,7 @@ async function main() {
|
|
|
14890
15741
|
manifestPath: loaded.path,
|
|
14891
15742
|
manifest: accountManifest,
|
|
14892
15743
|
styleId,
|
|
14893
|
-
importDir:
|
|
15744
|
+
importDir: path30.resolve(loaded.root, style.outDir),
|
|
14894
15745
|
lock,
|
|
14895
15746
|
lockPath: args.lock
|
|
14896
15747
|
},
|