pixelkiln 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SECURITY.md +12 -2
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +1135 -307
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +489 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.js +502 -44
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/ARTIFACTS.md +25 -0
- package/docs/CLI.md +54 -1
- package/package.json +1 -1
package/dist/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)`);
|
|
@@ -8501,7 +8609,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8501
8609
|
const media = /* @__PURE__ */ new Map();
|
|
8502
8610
|
const plan = await buildPlan(specs, lock);
|
|
8503
8611
|
const items = [];
|
|
8504
|
-
const manifestText = await
|
|
8612
|
+
const manifestText = await readFile16(loaded.path, "utf8");
|
|
8505
8613
|
const manifestSha256 = sha256(manifestText);
|
|
8506
8614
|
let rawStyles = {};
|
|
8507
8615
|
try {
|
|
@@ -8516,6 +8624,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8516
8624
|
let outputs;
|
|
8517
8625
|
let edit = null;
|
|
8518
8626
|
let editStatus = null;
|
|
8627
|
+
let editMeta = null;
|
|
8519
8628
|
if (entry && entry.outputs.length) {
|
|
8520
8629
|
outputs = await Promise.all(
|
|
8521
8630
|
entry.outputs.map(
|
|
@@ -8527,7 +8636,19 @@ async function buildGallerySnapshot(opts) {
|
|
|
8527
8636
|
const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
|
|
8528
8637
|
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
8529
8638
|
const generated = outputs[0];
|
|
8530
|
-
|
|
8639
|
+
const companion = await readHandEditCompanion(editPath);
|
|
8640
|
+
if (companion) {
|
|
8641
|
+
const projectPath = handEditProjectPath(editPath);
|
|
8642
|
+
editMeta = {
|
|
8643
|
+
editor: companion.editor,
|
|
8644
|
+
savedAt: companion.savedAt,
|
|
8645
|
+
basedOn: companion.basedOn,
|
|
8646
|
+
changedSince: editSha !== null && editSha !== companion.sha256,
|
|
8647
|
+
project: companion.project && existsSync17(projectPath) ? portableOutputPath(projectPath, root) : null
|
|
8648
|
+
};
|
|
8649
|
+
}
|
|
8650
|
+
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);
|
|
8651
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
8531
8652
|
}
|
|
8532
8653
|
} else if (spec.source) {
|
|
8533
8654
|
outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
|
|
@@ -8577,6 +8698,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8577
8698
|
source: spec.source ?? null,
|
|
8578
8699
|
edit,
|
|
8579
8700
|
editStatus,
|
|
8701
|
+
editMeta,
|
|
8580
8702
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8581
8703
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
8582
8704
|
tags: spec.tags,
|
|
@@ -8633,6 +8755,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8633
8755
|
source: null,
|
|
8634
8756
|
edit: null,
|
|
8635
8757
|
editStatus: null,
|
|
8758
|
+
editMeta: null,
|
|
8636
8759
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8637
8760
|
refreshable: false,
|
|
8638
8761
|
tags: [],
|
|
@@ -8774,7 +8897,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8774
8897
|
root: loaded.root,
|
|
8775
8898
|
provider: loaded.manifest.provider,
|
|
8776
8899
|
account: project.account ?? null,
|
|
8777
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
8900
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
|
|
8778
8901
|
entries: Object.keys(lock.entries).length,
|
|
8779
8902
|
items: projectItems.length,
|
|
8780
8903
|
spendByUnit: spendByUnit(lock),
|
|
@@ -8832,7 +8955,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8832
8955
|
import { spawn as spawn3 } from "child_process";
|
|
8833
8956
|
import { randomBytes } from "crypto";
|
|
8834
8957
|
import { createServer as createServer2 } from "http";
|
|
8835
|
-
import { readFile as
|
|
8958
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
8836
8959
|
|
|
8837
8960
|
// src/gallery/page.ts
|
|
8838
8961
|
function renderGallery(snapshot, opts = {}) {
|
|
@@ -8840,6 +8963,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
8840
8963
|
const session = JSON.stringify(opts.session ?? null);
|
|
8841
8964
|
const editable = JSON.stringify(Boolean(opts.editable && opts.session));
|
|
8842
8965
|
const generation = JSON.stringify(Boolean(opts.generation && opts.session));
|
|
8966
|
+
const editor = JSON.stringify(Boolean(opts.editor && opts.session));
|
|
8843
8967
|
const title = `pixelkiln \u2014 ${snapshot.project?.name ?? "workspace"}`;
|
|
8844
8968
|
return `<!doctype html>
|
|
8845
8969
|
<html lang="en">
|
|
@@ -9045,6 +9169,15 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9045
9169
|
.job .acts button { padding:3px 9px; font-size:12px; }
|
|
9046
9170
|
.job pre { grid-column:1 / -1; margin:0; padding:8px 10px; border-top:1px solid var(--line);
|
|
9047
9171
|
font:11.5px/1.45 var(--mono); color:var(--dim); max-height:220px; overflow:auto; white-space:pre-wrap; }
|
|
9172
|
+
#tools { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; }
|
|
9173
|
+
#tools:empty { display:none; }
|
|
9174
|
+
.tool { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
|
|
9175
|
+
grid-template-columns:auto 1fr auto; gap:6px 14px; align-items:center; font-size:12.5px; }
|
|
9176
|
+
.tool .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1 var(--mono); white-space:nowrap; }
|
|
9177
|
+
.tool .last { color:var(--dim); min-width:0; }
|
|
9178
|
+
.tool .acts { display:flex; gap:6px; }
|
|
9179
|
+
.tool .acts button { padding:3px 9px; font-size:12px; }
|
|
9180
|
+
.tool progress { grid-column:1 / -1; width:100%; height:6px; accent-color:var(--accent); }
|
|
9048
9181
|
.budget { color:var(--dim); }
|
|
9049
9182
|
.budget b { color:var(--text); }
|
|
9050
9183
|
.dialog { position:fixed; inset:0; z-index:30; display:grid; place-items:center; background:rgba(0,0,0,.55); }
|
|
@@ -9065,6 +9198,16 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9065
9198
|
border-left:1px solid var(--line-strong); display:grid; grid-template-rows:auto 1fr; min-width:0; }
|
|
9066
9199
|
.sheet.review-host { width:min(1180px, 94vw); }
|
|
9067
9200
|
.sheet.compare-host { width:min(1500px, 96vw); }
|
|
9201
|
+
.sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
|
|
9202
|
+
.sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
|
|
9203
|
+
.sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
|
9204
|
+
.sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
|
|
9205
|
+
.sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
|
|
9206
|
+
.sheet.editor-host .stage { position:relative; min-height:0; }
|
|
9207
|
+
.sheet.editor-host .stage iframe { position:absolute; inset:0; }
|
|
9208
|
+
.sheet.editor-host .loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--bg);
|
|
9209
|
+
color:var(--dim); text-align:center; padding:24px; }
|
|
9210
|
+
.sheet.editor-host .loading b { display:block; color:var(--text); margin-bottom:6px; }
|
|
9068
9211
|
.sheet .rbar { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line-strong);
|
|
9069
9212
|
background:var(--panel); font-size:13px; flex-wrap:wrap; }
|
|
9070
9213
|
.sheet .rbar span { color:var(--dim); }
|
|
@@ -9103,7 +9246,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9103
9246
|
.tray .pick img { width:22px; height:22px; object-fit:contain; image-rendering:pixelated; }
|
|
9104
9247
|
.tray .pick button { padding:0 5px; border-color:transparent; color:var(--dim); font-size:12px; }
|
|
9105
9248
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
9106
|
-
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
9249
|
+
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
9107
9250
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9108
9251
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9109
9252
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
@@ -9151,6 +9294,7 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9151
9294
|
<div class="totals" id="totals"></div>
|
|
9152
9295
|
<div class="chips" id="chips"></div>
|
|
9153
9296
|
<div id="jobs"></div>
|
|
9297
|
+
<div id="tools"></div>
|
|
9154
9298
|
</header>
|
|
9155
9299
|
<main id="root"></main>
|
|
9156
9300
|
<footer>
|
|
@@ -9169,6 +9313,7 @@ const INITIAL = ${data};
|
|
|
9169
9313
|
const SESSION = ${session};
|
|
9170
9314
|
const EDITABLE = ${editable};
|
|
9171
9315
|
const GENERATION = ${generation};
|
|
9316
|
+
const EDITOR = ${editor};
|
|
9172
9317
|
let snap = INITIAL;
|
|
9173
9318
|
const STATE_TONE = {
|
|
9174
9319
|
ok: 'ok', stale: 'warn', orphaned: 'warn', untracked: 'warn', blocked: 'warn',
|
|
@@ -9336,6 +9481,92 @@ function renderJobs() {
|
|
|
9336
9481
|
host.append(row);
|
|
9337
9482
|
}
|
|
9338
9483
|
}
|
|
9484
|
+
// ---- the in-browser editor (only when the server serves one) -------------
|
|
9485
|
+
// The build is a 46 MB web export fetched once into a user-level cache and
|
|
9486
|
+
// verified against hashes this PixelKiln release carries; the page only
|
|
9487
|
+
// reports and asks, the server downloads. Nothing is fetched without a click.
|
|
9488
|
+
|
|
9489
|
+
let ED = null;
|
|
9490
|
+
let edTimer = null;
|
|
9491
|
+
const fmtMb = (bytes) => (bytes / 1e6).toFixed(1) + ' MB';
|
|
9492
|
+
async function pollEditor() {
|
|
9493
|
+
if (!EDITOR) return;
|
|
9494
|
+
try {
|
|
9495
|
+
const res = await fetch('/api/editor', { cache: 'no-store' });
|
|
9496
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9497
|
+
ED = await res.json();
|
|
9498
|
+
} catch (err) {
|
|
9499
|
+
ED = { error: 'status unavailable: ' + err.message, installed: false, installing: null, missing: [], totalBytes: 0 };
|
|
9500
|
+
}
|
|
9501
|
+
renderTools();
|
|
9502
|
+
clearTimeout(edTimer);
|
|
9503
|
+
if (ED.installing) edTimer = setTimeout(pollEditor, 1000);
|
|
9504
|
+
else if (ui.open) renderDrawer();
|
|
9505
|
+
}
|
|
9506
|
+
async function installEditor() {
|
|
9507
|
+
try {
|
|
9508
|
+
const res = await fetch('/api/editor/install', {
|
|
9509
|
+
method: 'POST',
|
|
9510
|
+
headers: { 'Content-Type': 'application/json', 'X-Pixelkiln-Session': SESSION },
|
|
9511
|
+
body: '{}',
|
|
9512
|
+
});
|
|
9513
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9514
|
+
ED = await res.json();
|
|
9515
|
+
} catch (err) {
|
|
9516
|
+
ED = Object.assign({}, ED, { error: err.message });
|
|
9517
|
+
}
|
|
9518
|
+
renderTools();
|
|
9519
|
+
clearTimeout(edTimer);
|
|
9520
|
+
edTimer = setTimeout(pollEditor, 500);
|
|
9521
|
+
}
|
|
9522
|
+
function renderTools() {
|
|
9523
|
+
const host = $('tools');
|
|
9524
|
+
host.textContent = '';
|
|
9525
|
+
if (!EDITOR || !ED) return;
|
|
9526
|
+
const row = el('div', 'tool');
|
|
9527
|
+
const ph = el('span', 'ph');
|
|
9528
|
+
const last = el('span', 'last');
|
|
9529
|
+
const acts = el('div', 'acts');
|
|
9530
|
+
const name = 'Browser editor' + (ED.pixelorama ? ' (Pixelorama ' + ED.pixelorama + ')' : '');
|
|
9531
|
+
if (ED.installing) {
|
|
9532
|
+
const p = ED.installing;
|
|
9533
|
+
ph.append(el('i', 'dot cool'), document.createTextNode('installing'));
|
|
9534
|
+
last.textContent = name + ': fetching ' + (p.file || 'the build') + ' \u2014 ' + fmtMb(p.fetchedBytes) + ' of ' + fmtMb(p.totalBytes);
|
|
9535
|
+
row.append(ph, last, acts);
|
|
9536
|
+
const bar = el('progress');
|
|
9537
|
+
if (p.totalBytes) { bar.max = p.totalBytes; bar.value = p.fetchedBytes; }
|
|
9538
|
+
row.append(bar);
|
|
9539
|
+
} else if (ED.installed) {
|
|
9540
|
+
ph.append(el('i', 'dot ok'), document.createTextNode('ready'));
|
|
9541
|
+
last.textContent = name + ' is installed and verified; a record\u2019s Hand edit section opens sprites in it.';
|
|
9542
|
+
last.title = ED.dir || '';
|
|
9543
|
+
const b = el('button', null, 'Open editor');
|
|
9544
|
+
b.type = 'button'; b.onclick = () => window.open(ED.url, '_blank', 'noopener');
|
|
9545
|
+
acts.append(b);
|
|
9546
|
+
row.append(ph, last, acts);
|
|
9547
|
+
} else if (ED.error) {
|
|
9548
|
+
ph.append(el('i', 'dot bad'), document.createTextNode('failed'));
|
|
9549
|
+
last.textContent = name + ': ' + ED.error;
|
|
9550
|
+
const b = el('button', 'primary', 'Retry install');
|
|
9551
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
9552
|
+
acts.append(b);
|
|
9553
|
+
row.append(ph, last, acts);
|
|
9554
|
+
} else if (!ED.release) {
|
|
9555
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('unavailable'));
|
|
9556
|
+
last.textContent = name + ': this PixelKiln version pins no published build.';
|
|
9557
|
+
row.append(ph, last, acts);
|
|
9558
|
+
} else {
|
|
9559
|
+
ph.append(el('i', 'dot dim'), document.createTextNode('not installed'));
|
|
9560
|
+
last.textContent = name + ': ' + fmtMb(ED.totalBytes - (ED.installedBytes || 0)) + ' fetched once from the PixelKiln release ' +
|
|
9561
|
+
ED.release + ', verified against the hashes this version carries, and kept outside the project.';
|
|
9562
|
+
const b = el('button', 'primary', 'Install editor');
|
|
9563
|
+
b.type = 'button'; b.onclick = installEditor;
|
|
9564
|
+
acts.append(b);
|
|
9565
|
+
row.append(ph, last, acts);
|
|
9566
|
+
}
|
|
9567
|
+
host.append(row);
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9339
9570
|
function budgetLine() {
|
|
9340
9571
|
const providers = new Set([...Object.keys(GEN.budget.byProvider), ...Object.keys(GEN.spent)]);
|
|
9341
9572
|
const parts = [];
|
|
@@ -9442,8 +9673,169 @@ function openReview(jobId) {
|
|
|
9442
9673
|
host.append(scrim, panel);
|
|
9443
9674
|
}
|
|
9444
9675
|
window.addEventListener('message', (e) => {
|
|
9445
|
-
if (e.origin !== location.origin || !e.data || e.data.type !== '
|
|
9446
|
-
|
|
9676
|
+
if (e.origin !== location.origin || !e.data || typeof e.data.type !== 'string') return;
|
|
9677
|
+
if (e.data.type === 'pixelkiln:review-applied') {
|
|
9678
|
+
setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
|
|
9679
|
+
return;
|
|
9680
|
+
}
|
|
9681
|
+
if (SHEET && e.source === SHEET.frame.contentWindow) onEditorMessage(e.data);
|
|
9682
|
+
});
|
|
9683
|
+
|
|
9684
|
+
// ---- the in-browser editor sheet -------------------------------------------
|
|
9685
|
+
// The page is the protocol host: it hands the editor the PNG and palette,
|
|
9686
|
+
// asks it for the image back, and does the authenticated write itself. The
|
|
9687
|
+
// iframe never sees the session token, and the server validates, sizes, and
|
|
9688
|
+
// declares the file the same way pixelkiln edit does.
|
|
9689
|
+
|
|
9690
|
+
let SHEET = null;
|
|
9691
|
+
const editorSource = (item) => item.edit && item.edit.exists ? item.edit
|
|
9692
|
+
: item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
|
|
9693
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
|
|
9694
|
+
!(item.outputs.length > 1);
|
|
9695
|
+
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
9696
|
+
const r = new FileReader();
|
|
9697
|
+
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
9698
|
+
r.onerror = () => reject(r.error);
|
|
9699
|
+
r.readAsDataURL(new Blob([buf]));
|
|
9700
|
+
});
|
|
9701
|
+
function sheetStatus(tone, text) {
|
|
9702
|
+
if (!SHEET) return;
|
|
9703
|
+
SHEET.status.textContent = '';
|
|
9704
|
+
SHEET.status.append(el('i', 'dot ' + tone), document.createTextNode(text));
|
|
9705
|
+
}
|
|
9706
|
+
function sheetButtons() {
|
|
9707
|
+
if (!SHEET) return;
|
|
9708
|
+
const busy = !SHEET.opened || SHEET.pending !== null;
|
|
9709
|
+
SHEET.save.disabled = busy || !SHEET.dirty;
|
|
9710
|
+
SHEET.saveClose.disabled = busy;
|
|
9711
|
+
}
|
|
9712
|
+
function openEditorSheet(item) {
|
|
9713
|
+
if (SHEET) return;
|
|
9714
|
+
const src = editorSource(item);
|
|
9715
|
+
const host = $('dialog-host');
|
|
9716
|
+
host.textContent = '';
|
|
9717
|
+
const scrim = el('div', 'sheet-scrim');
|
|
9718
|
+
scrim.onclick = () => closeEditorSheet();
|
|
9719
|
+
const panel = el('div', 'sheet editor-host');
|
|
9720
|
+
const bar = el('div', 'rbar');
|
|
9721
|
+
const close = el('button', null, 'Close'); close.type = 'button';
|
|
9722
|
+
close.onclick = () => closeEditorSheet();
|
|
9723
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
9724
|
+
const status = el('span', 'st');
|
|
9725
|
+
const acts = el('div', 'acts');
|
|
9726
|
+
const msg = el('span', 'msg');
|
|
9727
|
+
const save = el('button', 'primary', 'Save to project'); save.type = 'button';
|
|
9728
|
+
save.onclick = () => requestEditorSave(false);
|
|
9729
|
+
const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
|
|
9730
|
+
saveClose.onclick = () => requestEditorSave(true);
|
|
9731
|
+
acts.append(msg, save, saveClose);
|
|
9732
|
+
bar.append(close, title, status, acts);
|
|
9733
|
+
const stage = el('div', 'stage');
|
|
9734
|
+
const frame = el('iframe');
|
|
9735
|
+
frame.title = 'Pixelorama';
|
|
9736
|
+
frame.src = ED.url;
|
|
9737
|
+
const loading = el('div', 'loading');
|
|
9738
|
+
const loadText = el('div');
|
|
9739
|
+
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.'));
|
|
9740
|
+
loading.append(loadText);
|
|
9741
|
+
stage.append(frame, loading);
|
|
9742
|
+
panel.append(bar, stage);
|
|
9743
|
+
host.append(scrim, panel);
|
|
9744
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src };
|
|
9745
|
+
sheetStatus('cool', 'loading');
|
|
9746
|
+
sheetButtons();
|
|
9747
|
+
close.focus({ preventScroll: true });
|
|
9748
|
+
}
|
|
9749
|
+
async function sendOpen() {
|
|
9750
|
+
const { item, source } = SHEET;
|
|
9751
|
+
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
9752
|
+
let png;
|
|
9753
|
+
try {
|
|
9754
|
+
const res = await fetch(source.url, { cache: 'no-store' });
|
|
9755
|
+
if (!res.ok) throw new Error(await res.text());
|
|
9756
|
+
png = await res.arrayBuffer();
|
|
9757
|
+
} catch (err) {
|
|
9758
|
+
if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
9761
|
+
if (!SHEET) return;
|
|
9762
|
+
const request = 'open-' + (++SHEET.requests);
|
|
9763
|
+
SHEET.frame.contentWindow.postMessage({
|
|
9764
|
+
type: 'pixelkiln:open', request,
|
|
9765
|
+
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
9766
|
+
png, palette: style ? style.palette : [],
|
|
9767
|
+
}, location.origin, [png]);
|
|
9768
|
+
sheetStatus('cool', 'opening');
|
|
9769
|
+
}
|
|
9770
|
+
function requestEditorSave(close) {
|
|
9771
|
+
if (!SHEET || !SHEET.opened || SHEET.pending) return;
|
|
9772
|
+
const request = 'save-' + (++SHEET.requests);
|
|
9773
|
+
SHEET.pending = { request, close };
|
|
9774
|
+
SHEET.msg.textContent = '';
|
|
9775
|
+
sheetStatus('cool', 'saving');
|
|
9776
|
+
sheetButtons();
|
|
9777
|
+
SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:request-save', request }, location.origin);
|
|
9778
|
+
}
|
|
9779
|
+
async function onEditorMessage(m) {
|
|
9780
|
+
switch (m.type) {
|
|
9781
|
+
case 'pixelkiln:ready':
|
|
9782
|
+
SHEET.ready = m;
|
|
9783
|
+
if (m.version !== ED.protocol) {
|
|
9784
|
+
SHEET.msg.textContent = 'The editor speaks protocol ' + m.version + '; this gallery expects ' + ED.protocol + '. Reinstall it with pixelkiln tools install editor.';
|
|
9785
|
+
sheetStatus('bad', 'mismatch');
|
|
9786
|
+
return;
|
|
9787
|
+
}
|
|
9788
|
+
sendOpen();
|
|
9789
|
+
break;
|
|
9790
|
+
case 'pixelkiln:opened':
|
|
9791
|
+
SHEET.opened = true;
|
|
9792
|
+
SHEET.loading.remove();
|
|
9793
|
+
sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
9794
|
+
sheetButtons();
|
|
9795
|
+
break;
|
|
9796
|
+
case 'pixelkiln:dirty':
|
|
9797
|
+
SHEET.dirty = !!m.dirty;
|
|
9798
|
+
if (SHEET.opened && !SHEET.pending) sheetStatus(SHEET.dirty ? 'warn' : 'ok', SHEET.dirty ? 'unsaved changes' : 'saved');
|
|
9799
|
+
sheetButtons();
|
|
9800
|
+
break;
|
|
9801
|
+
case 'pixelkiln:save': {
|
|
9802
|
+
const pending = SHEET.pending;
|
|
9803
|
+
if (!pending || pending.request !== m.request) return;
|
|
9804
|
+
try {
|
|
9805
|
+
const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
9806
|
+
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
9807
|
+
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
9808
|
+
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
9809
|
+
if (saved) SHEET.item = saved;
|
|
9810
|
+
SHEET.dirty = false;
|
|
9811
|
+
SHEET.pending = null;
|
|
9812
|
+
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.' };
|
|
9813
|
+
render();
|
|
9814
|
+
if (pending.close) { closeEditorSheet(true); return; }
|
|
9815
|
+
sheetStatus('ok', 'saved');
|
|
9816
|
+
} catch (err) {
|
|
9817
|
+
SHEET.pending = null;
|
|
9818
|
+
SHEET.msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh, then save again.' : '');
|
|
9819
|
+
sheetStatus('bad', 'not saved');
|
|
9820
|
+
}
|
|
9821
|
+
sheetButtons();
|
|
9822
|
+
break;
|
|
9823
|
+
}
|
|
9824
|
+
case 'pixelkiln:error':
|
|
9825
|
+
SHEET.msg.textContent = m.message || 'The editor reported an error.';
|
|
9826
|
+
if (SHEET.pending) { SHEET.pending = null; sheetStatus('bad', 'not saved'); sheetButtons(); }
|
|
9827
|
+
break;
|
|
9828
|
+
}
|
|
9829
|
+
}
|
|
9830
|
+
function closeEditorSheet(force) {
|
|
9831
|
+
if (!SHEET) return;
|
|
9832
|
+
if (SHEET.dirty && !force && !confirm('Discard unsaved changes in the editor?')) return;
|
|
9833
|
+
SHEET = null;
|
|
9834
|
+
$('dialog-host').textContent = '';
|
|
9835
|
+
refresh();
|
|
9836
|
+
}
|
|
9837
|
+
window.addEventListener('beforeunload', (e) => {
|
|
9838
|
+
if (SHEET && SHEET.dirty) { e.preventDefault(); e.returnValue = ''; }
|
|
9447
9839
|
});
|
|
9448
9840
|
|
|
9449
9841
|
function visibleItems() {
|
|
@@ -10019,6 +10411,11 @@ function handEditSection(item) {
|
|
|
10019
10411
|
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
10020
10412
|
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
10021
10413
|
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
10414
|
+
if (item.editMeta) {
|
|
10415
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
|
|
10416
|
+
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
10417
|
+
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
10418
|
+
}
|
|
10022
10419
|
s.append(dl);
|
|
10023
10420
|
} else {
|
|
10024
10421
|
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 +10424,7 @@ function handEditSection(item) {
|
|
|
10027
10424
|
const acts = el('div', 'hand-actions');
|
|
10028
10425
|
const msg = el('span', 'msg');
|
|
10029
10426
|
const go = async (label, action, extra, done) => {
|
|
10030
|
-
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
10427
|
+
const b = el('button', label === 'Detach edit' || (EDITOR && ED && ED.installed) ? null : 'primary', label); b.type = 'button';
|
|
10031
10428
|
b.onclick = async () => {
|
|
10032
10429
|
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
10033
10430
|
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
@@ -10035,8 +10432,20 @@ function handEditSection(item) {
|
|
|
10035
10432
|
};
|
|
10036
10433
|
acts.append(b);
|
|
10037
10434
|
};
|
|
10435
|
+
if (EDITOR && ED && (item.edit || canStart) && editorSource(item)) {
|
|
10436
|
+
if (ED.installed) {
|
|
10437
|
+
const b = el('button', 'primary', 'Edit in browser'); b.type = 'button';
|
|
10438
|
+
b.title = 'Open in Pixelorama here; Save writes the edit file and declares it';
|
|
10439
|
+
b.onclick = () => openEditorSheet(item);
|
|
10440
|
+
acts.append(b);
|
|
10441
|
+
} else if (ED.release && !ED.installing) {
|
|
10442
|
+
const b = el('button', null, 'Install editor to edit in browser'); b.type = 'button';
|
|
10443
|
+
b.onclick = () => { installEditor(); window.scrollTo({ top: 0 }); };
|
|
10444
|
+
acts.append(b);
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10038
10447
|
if (item.edit) {
|
|
10039
|
-
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10448
|
+
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10040
10449
|
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
10041
10450
|
} else if (canStart) {
|
|
10042
10451
|
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 +11172,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
10763
11172
|
if (e.key === '/' && !typing) { e.preventDefault(); $('q').focus(); return; }
|
|
10764
11173
|
if (e.key === 'Escape') {
|
|
10765
11174
|
if (typing && document.activeElement.id === 'q') { document.activeElement.blur(); return; }
|
|
11175
|
+
if (SHEET) { e.preventDefault(); closeEditorSheet(); return; }
|
|
10766
11176
|
if ($('dialog-host').childNodes.length) { e.preventDefault(); $('dialog-host').textContent = ''; return; }
|
|
10767
11177
|
if (ui.open) { e.preventDefault(); closeItem(); }
|
|
10768
11178
|
return;
|
|
@@ -10784,6 +11194,7 @@ ui.compare = ui.compare.filter((id) => snap.items.some((i) => i.id === id));
|
|
|
10784
11194
|
render();
|
|
10785
11195
|
if (ui.open) document.querySelector('.card.active')?.scrollIntoView({ block: 'center' });
|
|
10786
11196
|
if (GENERATION) pollJobs();
|
|
11197
|
+
if (EDITOR) pollEditor();
|
|
10787
11198
|
if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
10788
11199
|
</script>
|
|
10789
11200
|
</body>
|
|
@@ -10792,7 +11203,16 @@ if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
|
10792
11203
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
10793
11204
|
|
|
10794
11205
|
// src/gallery/server.ts
|
|
10795
|
-
var MAX_EDIT_BYTES =
|
|
11206
|
+
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
11207
|
+
var EDITOR_CSP = [
|
|
11208
|
+
"default-src 'self'",
|
|
11209
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
|
11210
|
+
"style-src 'self' 'unsafe-inline'",
|
|
11211
|
+
"img-src 'self' data: blob:",
|
|
11212
|
+
"worker-src 'self' blob:",
|
|
11213
|
+
"connect-src 'self'",
|
|
11214
|
+
"frame-ancestors 'self'"
|
|
11215
|
+
].join("; ");
|
|
10796
11216
|
async function readJsonBody(req) {
|
|
10797
11217
|
const chunks = [];
|
|
10798
11218
|
let bytes = 0;
|
|
@@ -10810,7 +11230,7 @@ async function readJsonBody(req) {
|
|
|
10810
11230
|
async function serveGallery(opts) {
|
|
10811
11231
|
const log2 = opts.onProgress ?? (() => {
|
|
10812
11232
|
});
|
|
10813
|
-
const session = opts.edit || opts.generate ? randomBytes(16).toString("hex") : null;
|
|
11233
|
+
const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
|
|
10814
11234
|
let media = /* @__PURE__ */ new Map();
|
|
10815
11235
|
let loading = null;
|
|
10816
11236
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -10885,6 +11305,17 @@ async function serveGallery(opts) {
|
|
|
10885
11305
|
}
|
|
10886
11306
|
return;
|
|
10887
11307
|
}
|
|
11308
|
+
if (req.method === "POST" && url2.pathname === "/api/editor/install") {
|
|
11309
|
+
if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
|
|
11310
|
+
try {
|
|
11311
|
+
const body = await guardedBody("editor installs");
|
|
11312
|
+
if (body === void 0) return;
|
|
11313
|
+
json(202, await opts.editor.install());
|
|
11314
|
+
} catch (err) {
|
|
11315
|
+
reportError("editor install", err);
|
|
11316
|
+
}
|
|
11317
|
+
return;
|
|
11318
|
+
}
|
|
10888
11319
|
const reviewMatch = /^\/review\/([0-9a-f]{16})(\/.*)?$/.exec(url2.pathname);
|
|
10889
11320
|
if (reviewMatch && opts.generate) {
|
|
10890
11321
|
const [, jobId, rest = ""] = reviewMatch;
|
|
@@ -10930,7 +11361,7 @@ async function serveGallery(opts) {
|
|
|
10930
11361
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
10931
11362
|
if (!asset) return fail(404, "no such review asset");
|
|
10932
11363
|
try {
|
|
10933
|
-
const bytes = await
|
|
11364
|
+
const bytes = await readFile17(asset.path);
|
|
10934
11365
|
res.writeHead(200, {
|
|
10935
11366
|
"Content-Type": asset.contentType,
|
|
10936
11367
|
"Cache-Control": "no-store",
|
|
@@ -10948,6 +11379,32 @@ async function serveGallery(opts) {
|
|
|
10948
11379
|
if (!opts.generate) return fail(404, "generation is off");
|
|
10949
11380
|
return json(200, opts.generate.status());
|
|
10950
11381
|
}
|
|
11382
|
+
if (url2.pathname === "/api/editor") {
|
|
11383
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
11384
|
+
try {
|
|
11385
|
+
return json(200, await opts.editor.status());
|
|
11386
|
+
} catch (err) {
|
|
11387
|
+
return reportError("editor status", err);
|
|
11388
|
+
}
|
|
11389
|
+
}
|
|
11390
|
+
const editorMatch = /^\/editor\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/.exec(url2.pathname);
|
|
11391
|
+
if (editorMatch) {
|
|
11392
|
+
if (!opts.editor) return fail(404, "the in-browser editor is off for this gallery");
|
|
11393
|
+
const file = await opts.editor.file(editorMatch[1], editorMatch[2]);
|
|
11394
|
+
if (!file) return fail(404, "that editor file is not installed; check /api/editor");
|
|
11395
|
+
res.writeHead(200, {
|
|
11396
|
+
"Content-Type": file.contentType,
|
|
11397
|
+
"Content-Length": file.bytes.length,
|
|
11398
|
+
// The release tag is in the path, so these bytes never change under
|
|
11399
|
+
// this URL; the 40 MB wasm should come from the browser cache.
|
|
11400
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
11401
|
+
"X-Content-Type-Options": "nosniff",
|
|
11402
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
11403
|
+
...editorMatch[2] === "index.html" ? { "Content-Security-Policy": EDITOR_CSP } : {}
|
|
11404
|
+
});
|
|
11405
|
+
res.end(req.method === "HEAD" ? void 0 : file.bytes);
|
|
11406
|
+
return;
|
|
11407
|
+
}
|
|
10951
11408
|
try {
|
|
10952
11409
|
if (url2.pathname === "/") {
|
|
10953
11410
|
const { snapshot } = await load();
|
|
@@ -10959,7 +11416,8 @@ async function serveGallery(opts) {
|
|
|
10959
11416
|
res.end(renderGallery(snapshot, {
|
|
10960
11417
|
...session ? { session } : {},
|
|
10961
11418
|
editable: Boolean(opts.edit),
|
|
10962
|
-
generation: Boolean(opts.generate)
|
|
11419
|
+
generation: Boolean(opts.generate),
|
|
11420
|
+
editor: Boolean(opts.editor)
|
|
10963
11421
|
}));
|
|
10964
11422
|
return;
|
|
10965
11423
|
}
|
|
@@ -10979,7 +11437,7 @@ async function serveGallery(opts) {
|
|
|
10979
11437
|
if (!asset) return fail(404, "no such gallery media");
|
|
10980
11438
|
let bytes;
|
|
10981
11439
|
try {
|
|
10982
|
-
bytes = await
|
|
11440
|
+
bytes = await readFile17(asset.path);
|
|
10983
11441
|
} catch {
|
|
10984
11442
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
10985
11443
|
}
|
|
@@ -11025,16 +11483,16 @@ async function serveGallery(opts) {
|
|
|
11025
11483
|
|
|
11026
11484
|
// src/gallery/generate.ts
|
|
11027
11485
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
11028
|
-
import { z as
|
|
11029
|
-
var GenerateRequestSchema =
|
|
11030
|
-
project:
|
|
11031
|
-
keys:
|
|
11486
|
+
import { z as z8 } from "zod";
|
|
11487
|
+
var GenerateRequestSchema = z8.object({
|
|
11488
|
+
project: z8.string().min(1).optional(),
|
|
11489
|
+
keys: z8.array(z8.string().min(1)).min(1).max(500),
|
|
11032
11490
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
11033
|
-
force:
|
|
11491
|
+
force: z8.boolean().optional(),
|
|
11034
11492
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
11035
|
-
resume:
|
|
11493
|
+
resume: z8.boolean().optional(),
|
|
11036
11494
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11037
|
-
refresh:
|
|
11495
|
+
refresh: z8.boolean().optional()
|
|
11038
11496
|
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
11039
11497
|
var GenerateRequestError = class extends Error {
|
|
11040
11498
|
constructor(message6, status = 400) {
|
|
@@ -11288,20 +11746,318 @@ function createGenerateHandlers(opts) {
|
|
|
11288
11746
|
};
|
|
11289
11747
|
}
|
|
11290
11748
|
|
|
11291
|
-
// src/
|
|
11292
|
-
import { readFile as
|
|
11749
|
+
// src/gallery/editor.ts
|
|
11750
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
11751
|
+
import path24 from "path";
|
|
11752
|
+
|
|
11753
|
+
// src/editor/install.ts
|
|
11754
|
+
import { createHash as createHash4 } from "crypto";
|
|
11293
11755
|
import { existsSync as existsSync18 } from "fs";
|
|
11756
|
+
import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
|
|
11757
|
+
import os2 from "os";
|
|
11294
11758
|
import path23 from "path";
|
|
11759
|
+
|
|
11760
|
+
// tools/pixelorama-bridge/pin.json
|
|
11761
|
+
var pin_default = {
|
|
11762
|
+
pixelorama: "v1.2.2",
|
|
11763
|
+
godot: "4.7.2",
|
|
11764
|
+
extensionsApi: 9,
|
|
11765
|
+
protocol: 1,
|
|
11766
|
+
release: "editor-pixelorama-v1.2.2-pk.6",
|
|
11767
|
+
files: {
|
|
11768
|
+
"index.apple-touch-icon.png": {
|
|
11769
|
+
sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
|
|
11770
|
+
bytes: 1036
|
|
11771
|
+
},
|
|
11772
|
+
"index.audio.position.worklet.js": {
|
|
11773
|
+
sha256: "be33985bc7160d6bf9646f259cd86b259cd67b02ccb297ee5c44f8ac84327bc8",
|
|
11774
|
+
bytes: 2973
|
|
11775
|
+
},
|
|
11776
|
+
"index.audio.worklet.js": {
|
|
11777
|
+
sha256: "5b476a9c9ce642c0ee4256436d1bc31d9c38f868aca0f9a8e2a57c18d2dec2a3",
|
|
11778
|
+
bytes: 7298
|
|
11779
|
+
},
|
|
11780
|
+
"index.html": {
|
|
11781
|
+
sha256: "43299b159b780809044fc0a484906bb93a280116c22cc87388ee2ab8263ac1f3",
|
|
11782
|
+
bytes: 5442
|
|
11783
|
+
},
|
|
11784
|
+
"index.icon.png": {
|
|
11785
|
+
sha256: "42a327d23178ddc26cc97e3560ebca6bc7c575d2a1b12a4837f2462049ca5745",
|
|
11786
|
+
bytes: 1070
|
|
11787
|
+
},
|
|
11788
|
+
"index.js": {
|
|
11789
|
+
sha256: "33c94cb3175f3333b82e2a3be5e8e86f77986f0aa2042b1631f6367a4e5bb6ba",
|
|
11790
|
+
bytes: 279815
|
|
11791
|
+
},
|
|
11792
|
+
"index.pck": {
|
|
11793
|
+
sha256: "ee10aa3d6e09a1ff2a334d0de7df1ae57f32ce00698948892a46443c2c67ae7a",
|
|
11794
|
+
bytes: 6426700
|
|
11795
|
+
},
|
|
11796
|
+
"index.png": {
|
|
11797
|
+
sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
|
|
11798
|
+
bytes: 13558
|
|
11799
|
+
},
|
|
11800
|
+
"index.wasm": {
|
|
11801
|
+
sha256: "fc74679e3b97f76878947fcd4fbe1268cbfa6188182a2e33bbc3f5dc9bfa57d0",
|
|
11802
|
+
bytes: 39514754
|
|
11803
|
+
}
|
|
11804
|
+
}
|
|
11805
|
+
};
|
|
11806
|
+
|
|
11807
|
+
// src/editor/pin.ts
|
|
11808
|
+
var EDITOR_PIN = pin_default;
|
|
11809
|
+
var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
|
|
11810
|
+
|
|
11811
|
+
// src/editor/install.ts
|
|
11812
|
+
var EditorInstallError = class extends Error {
|
|
11813
|
+
constructor(message6, file) {
|
|
11814
|
+
super(message6);
|
|
11815
|
+
this.file = file;
|
|
11816
|
+
this.name = "EditorInstallError";
|
|
11817
|
+
}
|
|
11818
|
+
file;
|
|
11819
|
+
};
|
|
11820
|
+
function editorToolsRoot() {
|
|
11821
|
+
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
11822
|
+
if (override) return path23.resolve(override);
|
|
11823
|
+
if (process.platform === "darwin") return path23.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
11824
|
+
if (process.platform === "win32") {
|
|
11825
|
+
return path23.join(process.env.LOCALAPPDATA ?? path23.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
11826
|
+
}
|
|
11827
|
+
return path23.join(process.env.XDG_CACHE_HOME ?? path23.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
11828
|
+
}
|
|
11829
|
+
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
11830
|
+
return path23.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
11831
|
+
}
|
|
11832
|
+
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
11833
|
+
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
11834
|
+
if (override) return override.replace(/\/+$/, "");
|
|
11835
|
+
if (!pin.release) return null;
|
|
11836
|
+
return `${EDITOR_RELEASE_BASE}/${encodeURIComponent(pin.release)}`;
|
|
11837
|
+
}
|
|
11838
|
+
async function sha256Of(file) {
|
|
11839
|
+
try {
|
|
11840
|
+
return createHash4("sha256").update(await readFile18(file)).digest("hex");
|
|
11841
|
+
} catch {
|
|
11842
|
+
return null;
|
|
11843
|
+
}
|
|
11844
|
+
}
|
|
11845
|
+
async function editorStatus(opts = {}) {
|
|
11846
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11847
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11848
|
+
const missing = [];
|
|
11849
|
+
let installedBytes = 0;
|
|
11850
|
+
let totalBytes = 0;
|
|
11851
|
+
for (const [name, expected] of Object.entries(pin.files)) {
|
|
11852
|
+
totalBytes += expected.bytes;
|
|
11853
|
+
const file = path23.join(dir, name);
|
|
11854
|
+
const info = existsSync18(file) ? await stat4(file).catch(() => null) : null;
|
|
11855
|
+
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
11856
|
+
installedBytes += expected.bytes;
|
|
11857
|
+
} else {
|
|
11858
|
+
missing.push(name);
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
const pinned = Object.keys(pin.files).length > 0 && pin.release !== null;
|
|
11862
|
+
return {
|
|
11863
|
+
release: pin.release,
|
|
11864
|
+
dir,
|
|
11865
|
+
installed: pinned && missing.length === 0,
|
|
11866
|
+
missing: pinned ? missing : Object.keys(pin.files),
|
|
11867
|
+
totalBytes,
|
|
11868
|
+
installedBytes
|
|
11869
|
+
};
|
|
11870
|
+
}
|
|
11871
|
+
async function installEditor(opts = {}) {
|
|
11872
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11873
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11874
|
+
const baseUrl = opts.baseUrl ?? editorBaseUrl(pin);
|
|
11875
|
+
const doFetch = opts.fetch ?? fetch;
|
|
11876
|
+
const before = await editorStatus({ pin, dir });
|
|
11877
|
+
if (!pin.release || !Object.keys(pin.files).length) {
|
|
11878
|
+
throw new EditorInstallError(
|
|
11879
|
+
"this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust"
|
|
11880
|
+
);
|
|
11881
|
+
}
|
|
11882
|
+
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
11883
|
+
await mkdir7(dir, { recursive: true });
|
|
11884
|
+
const downloaded = [];
|
|
11885
|
+
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
11886
|
+
let fetchedBytes = before.installedBytes;
|
|
11887
|
+
const names = before.missing;
|
|
11888
|
+
for (const [index, name] of names.entries()) {
|
|
11889
|
+
const expected = pin.files[name];
|
|
11890
|
+
const report = (phase, received) => opts.onProgress?.({
|
|
11891
|
+
file: name,
|
|
11892
|
+
index,
|
|
11893
|
+
count: names.length,
|
|
11894
|
+
bytes: expected.bytes,
|
|
11895
|
+
totalBytes: before.totalBytes,
|
|
11896
|
+
fetchedBytes: fetchedBytes + received,
|
|
11897
|
+
phase
|
|
11898
|
+
});
|
|
11899
|
+
report("start", 0);
|
|
11900
|
+
const url = `${baseUrl}/${encodeURIComponent(name)}`;
|
|
11901
|
+
const response = await doFetch(url, { signal: opts.signal, redirect: "follow" });
|
|
11902
|
+
if (!response.ok) throw new EditorInstallError(`${name}: ${response.status} ${response.statusText} from ${url}`, name);
|
|
11903
|
+
const bytes = await readBody(response, expected.bytes, (received) => report("progress", received));
|
|
11904
|
+
if (bytes.length !== expected.bytes) {
|
|
11905
|
+
throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
|
|
11906
|
+
}
|
|
11907
|
+
const digest2 = createHash4("sha256").update(bytes).digest("hex");
|
|
11908
|
+
if (digest2 !== expected.sha256) {
|
|
11909
|
+
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
11910
|
+
}
|
|
11911
|
+
const final = path23.join(dir, name);
|
|
11912
|
+
const tmp = `${final}.${process.pid}.part`;
|
|
11913
|
+
try {
|
|
11914
|
+
await writeFile10(tmp, bytes);
|
|
11915
|
+
await rename8(tmp, final);
|
|
11916
|
+
} finally {
|
|
11917
|
+
await rm9(tmp, { force: true });
|
|
11918
|
+
}
|
|
11919
|
+
fetchedBytes += bytes.length;
|
|
11920
|
+
downloaded.push(name);
|
|
11921
|
+
report("done", 0);
|
|
11922
|
+
}
|
|
11923
|
+
const after = await editorStatus({ pin, dir });
|
|
11924
|
+
if (!after.installed) {
|
|
11925
|
+
throw new EditorInstallError(`editor install is incomplete: ${after.missing.join(", ")}`);
|
|
11926
|
+
}
|
|
11927
|
+
return { ...after, downloaded, skipped };
|
|
11928
|
+
}
|
|
11929
|
+
async function readBody(response, expectedBytes, onBytes) {
|
|
11930
|
+
if (!response.body) return Buffer.from(await response.arrayBuffer());
|
|
11931
|
+
const chunks = [];
|
|
11932
|
+
let received = 0;
|
|
11933
|
+
const reader = response.body.getReader();
|
|
11934
|
+
for (; ; ) {
|
|
11935
|
+
const { done, value } = await reader.read();
|
|
11936
|
+
if (done) break;
|
|
11937
|
+
chunks.push(value);
|
|
11938
|
+
received += value.length;
|
|
11939
|
+
if (received > expectedBytes) {
|
|
11940
|
+
await reader.cancel().catch(() => {
|
|
11941
|
+
});
|
|
11942
|
+
break;
|
|
11943
|
+
}
|
|
11944
|
+
onBytes(received);
|
|
11945
|
+
}
|
|
11946
|
+
return Buffer.concat(chunks);
|
|
11947
|
+
}
|
|
11948
|
+
var EDITOR_CONTENT_TYPES = {
|
|
11949
|
+
".html": "text/html; charset=utf-8",
|
|
11950
|
+
".js": "text/javascript; charset=utf-8",
|
|
11951
|
+
".wasm": "application/wasm",
|
|
11952
|
+
".pck": "application/octet-stream",
|
|
11953
|
+
".png": "image/png",
|
|
11954
|
+
".json": "application/json; charset=utf-8"
|
|
11955
|
+
};
|
|
11956
|
+
function editorContentType(name) {
|
|
11957
|
+
return EDITOR_CONTENT_TYPES[path23.extname(name).toLowerCase()] ?? null;
|
|
11958
|
+
}
|
|
11959
|
+
|
|
11960
|
+
// src/gallery/editor.ts
|
|
11961
|
+
function editorRoute(release, name = "index.html") {
|
|
11962
|
+
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
11963
|
+
}
|
|
11964
|
+
function createGalleryEditorHandlers(opts = {}) {
|
|
11965
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
11966
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
11967
|
+
const log2 = opts.onProgress ?? (() => {
|
|
11968
|
+
});
|
|
11969
|
+
let verified = null;
|
|
11970
|
+
let checking = null;
|
|
11971
|
+
let installing = null;
|
|
11972
|
+
let running = null;
|
|
11973
|
+
let error = null;
|
|
11974
|
+
const check = () => {
|
|
11975
|
+
if (!checking) {
|
|
11976
|
+
checking = editorStatus({ pin, dir }).then((status) => {
|
|
11977
|
+
verified = status;
|
|
11978
|
+
return status;
|
|
11979
|
+
}).finally(() => {
|
|
11980
|
+
checking = null;
|
|
11981
|
+
});
|
|
11982
|
+
}
|
|
11983
|
+
return checking;
|
|
11984
|
+
};
|
|
11985
|
+
const describe = (status) => ({
|
|
11986
|
+
...status,
|
|
11987
|
+
pixelorama: pin.pixelorama,
|
|
11988
|
+
protocol: pin.protocol,
|
|
11989
|
+
url: status.installed && pin.release ? editorRoute(pin.release) : null,
|
|
11990
|
+
installing,
|
|
11991
|
+
error
|
|
11992
|
+
});
|
|
11993
|
+
return {
|
|
11994
|
+
release: pin.release,
|
|
11995
|
+
status: async () => describe(verified ?? await check()),
|
|
11996
|
+
install: async () => {
|
|
11997
|
+
if (!running) {
|
|
11998
|
+
error = null;
|
|
11999
|
+
installing = { file: "", index: 0, count: 0, bytes: 0, totalBytes: 0, fetchedBytes: 0, phase: "start" };
|
|
12000
|
+
log2(` installing the editor (Pixelorama ${pin.pixelorama}) into ${dir}`);
|
|
12001
|
+
running = installEditor({
|
|
12002
|
+
pin,
|
|
12003
|
+
dir,
|
|
12004
|
+
baseUrl: opts.baseUrl ?? editorBaseUrl(pin) ?? void 0,
|
|
12005
|
+
...opts.fetch ? { fetch: opts.fetch } : {},
|
|
12006
|
+
onProgress: (progress) => {
|
|
12007
|
+
installing = progress;
|
|
12008
|
+
}
|
|
12009
|
+
}).then((result) => {
|
|
12010
|
+
verified = result;
|
|
12011
|
+
const mb = (result.totalBytes / 1e6).toFixed(1);
|
|
12012
|
+
log2(` editor ready: ${result.downloaded.length} file(s) fetched, ${result.skipped.length} already present (${mb} MB)`);
|
|
12013
|
+
}).catch((err) => {
|
|
12014
|
+
error = err instanceof Error ? err.message : String(err);
|
|
12015
|
+
verified = null;
|
|
12016
|
+
log2(` editor install failed: ${error}`);
|
|
12017
|
+
}).finally(() => {
|
|
12018
|
+
installing = null;
|
|
12019
|
+
running = null;
|
|
12020
|
+
});
|
|
12021
|
+
}
|
|
12022
|
+
return describe(verified ?? await check());
|
|
12023
|
+
},
|
|
12024
|
+
file: async (release, name) => {
|
|
12025
|
+
if (!pin.release || release !== pin.release) return null;
|
|
12026
|
+
const expected = pin.files[name];
|
|
12027
|
+
const contentType = editorContentType(name);
|
|
12028
|
+
if (!expected || !contentType) return null;
|
|
12029
|
+
const status = verified ?? await check();
|
|
12030
|
+
if (!status.installed) return null;
|
|
12031
|
+
let bytes;
|
|
12032
|
+
try {
|
|
12033
|
+
bytes = await readFile19(path24.join(dir, name));
|
|
12034
|
+
} catch {
|
|
12035
|
+
verified = null;
|
|
12036
|
+
return null;
|
|
12037
|
+
}
|
|
12038
|
+
if (bytes.length !== expected.bytes) {
|
|
12039
|
+
verified = null;
|
|
12040
|
+
return null;
|
|
12041
|
+
}
|
|
12042
|
+
return { bytes, contentType };
|
|
12043
|
+
}
|
|
12044
|
+
};
|
|
12045
|
+
}
|
|
12046
|
+
|
|
12047
|
+
// src/pipeline/salvage.ts
|
|
12048
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
12049
|
+
import { existsSync as existsSync19 } from "fs";
|
|
12050
|
+
import path25 from "path";
|
|
11295
12051
|
function providerClaimId(provider, objectId) {
|
|
11296
12052
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
11297
12053
|
}
|
|
11298
12054
|
async function loadClaims(lockPaths, opts = {}) {
|
|
11299
12055
|
const claimed = /* @__PURE__ */ new Set();
|
|
11300
12056
|
for (const p of lockPaths) {
|
|
11301
|
-
if (!
|
|
12057
|
+
if (!existsSync19(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
11302
12058
|
let parsed;
|
|
11303
12059
|
try {
|
|
11304
|
-
parsed = parseLock(JSON.parse(await
|
|
12060
|
+
parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
|
|
11305
12061
|
} catch {
|
|
11306
12062
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
11307
12063
|
}
|
|
@@ -11349,20 +12105,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
11349
12105
|
return null;
|
|
11350
12106
|
}
|
|
11351
12107
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
11352
|
-
const own =
|
|
12108
|
+
const own = path25.resolve(ownManifestPath);
|
|
11353
12109
|
const siblingManifestPaths = [
|
|
11354
12110
|
.../* @__PURE__ */ new Set([
|
|
11355
12111
|
...workspaceManifestPaths,
|
|
11356
|
-
...claimPaths.map((c) =>
|
|
12112
|
+
...claimPaths.map((c) => path25.join(path25.dirname(path25.resolve(c)), "pixelkiln.manifest.json"))
|
|
11357
12113
|
])
|
|
11358
12114
|
];
|
|
11359
12115
|
const siblings = [];
|
|
11360
12116
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
11361
|
-
if (
|
|
11362
|
-
if (!
|
|
12117
|
+
if (path25.resolve(siblingManifestPath) === own) continue;
|
|
12118
|
+
if (!existsSync19(siblingManifestPath)) continue;
|
|
11363
12119
|
try {
|
|
11364
12120
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
11365
|
-
siblings.push({ label:
|
|
12121
|
+
siblings.push({ label: path25.basename(path25.dirname(siblingManifestPath)), manifest });
|
|
11366
12122
|
} catch {
|
|
11367
12123
|
}
|
|
11368
12124
|
}
|
|
@@ -11581,15 +12337,15 @@ async function workspaceStatus(ws, dir) {
|
|
|
11581
12337
|
}
|
|
11582
12338
|
|
|
11583
12339
|
// src/pipeline/cache-health.ts
|
|
11584
|
-
import { existsSync as
|
|
11585
|
-
import { readFile as
|
|
11586
|
-
import
|
|
12340
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12341
|
+
import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
12342
|
+
import path26 from "path";
|
|
11587
12343
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11588
|
-
if (options.prune && !
|
|
11589
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
12344
|
+
if (options.prune && !existsSync20(lockPath)) {
|
|
12345
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path26.resolve(lockPath)}`);
|
|
11590
12346
|
}
|
|
11591
|
-
const contentDir =
|
|
11592
|
-
const remotePath =
|
|
12347
|
+
const contentDir = path26.resolve(path26.dirname(lockPath), ".pixelkiln", "cache");
|
|
12348
|
+
const remotePath = path26.resolve(cachePathFor(lockPath));
|
|
11593
12349
|
const referenced = new Set(
|
|
11594
12350
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
11595
12351
|
);
|
|
@@ -11603,7 +12359,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11603
12359
|
]);
|
|
11604
12360
|
for (const name of names) {
|
|
11605
12361
|
try {
|
|
11606
|
-
await
|
|
12362
|
+
await rm10(path26.join(contentDir, name), { force: true });
|
|
11607
12363
|
removed.contentFiles++;
|
|
11608
12364
|
} catch {
|
|
11609
12365
|
}
|
|
@@ -11612,7 +12368,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11612
12368
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
11613
12369
|
removed.resetRemoteHashCache = true;
|
|
11614
12370
|
} else if (remoteHashes.invalidIds.length) {
|
|
11615
|
-
const cache = parseCache(JSON.parse(await
|
|
12371
|
+
const cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
11616
12372
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
11617
12373
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
11618
12374
|
await saveCache(remotePath, cache);
|
|
@@ -11630,7 +12386,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11630
12386
|
async function inspectContentCache(contentDir, referenced) {
|
|
11631
12387
|
const report = {
|
|
11632
12388
|
path: contentDir,
|
|
11633
|
-
exists:
|
|
12389
|
+
exists: existsSync20(contentDir),
|
|
11634
12390
|
files: 0,
|
|
11635
12391
|
bytes: 0,
|
|
11636
12392
|
valid: 0,
|
|
@@ -11651,12 +12407,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11651
12407
|
continue;
|
|
11652
12408
|
}
|
|
11653
12409
|
report.files++;
|
|
11654
|
-
const file =
|
|
12410
|
+
const file = path26.join(contentDir, entry.name);
|
|
11655
12411
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11656
12412
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11657
12413
|
let bytes;
|
|
11658
12414
|
try {
|
|
11659
|
-
bytes = await
|
|
12415
|
+
bytes = await readFile21(file);
|
|
11660
12416
|
report.bytes += bytes.length;
|
|
11661
12417
|
} catch (err) {
|
|
11662
12418
|
report.invalid.push({
|
|
@@ -11694,7 +12450,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11694
12450
|
async function inspectRemoteHashCache(remotePath) {
|
|
11695
12451
|
const report = {
|
|
11696
12452
|
path: remotePath,
|
|
11697
|
-
exists:
|
|
12453
|
+
exists: existsSync20(remotePath),
|
|
11698
12454
|
entries: 0,
|
|
11699
12455
|
valid: 0,
|
|
11700
12456
|
invalidIds: [],
|
|
@@ -11703,7 +12459,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
11703
12459
|
if (!report.exists) return report;
|
|
11704
12460
|
let cache;
|
|
11705
12461
|
try {
|
|
11706
|
-
cache = parseCache(JSON.parse(await
|
|
12462
|
+
cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
|
|
11707
12463
|
} catch (err) {
|
|
11708
12464
|
report.error = err instanceof Error ? err.message : String(err);
|
|
11709
12465
|
return report;
|
|
@@ -11974,8 +12730,8 @@ function isRecord2(value) {
|
|
|
11974
12730
|
}
|
|
11975
12731
|
|
|
11976
12732
|
// src/pick/salvage-server.ts
|
|
11977
|
-
import { mkdir as
|
|
11978
|
-
import
|
|
12733
|
+
import { mkdir as mkdir8, writeFile as writeFile11, readFile as readFile22 } from "fs/promises";
|
|
12734
|
+
import path27 from "path";
|
|
11979
12735
|
|
|
11980
12736
|
// src/pick/salvage-sheet.ts
|
|
11981
12737
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -12140,7 +12896,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12140
12896
|
});
|
|
12141
12897
|
const html = renderSalvageSheet(orphans, {
|
|
12142
12898
|
styleId: ctx.styleId,
|
|
12143
|
-
importDir:
|
|
12899
|
+
importDir: path27.relative(process.cwd(), ctx.importDir) || "."
|
|
12144
12900
|
});
|
|
12145
12901
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
12146
12902
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -12169,10 +12925,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12169
12925
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
12170
12926
|
decodePng(buf);
|
|
12171
12927
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
12172
|
-
const rel =
|
|
12173
|
-
const outFile =
|
|
12174
|
-
await
|
|
12175
|
-
await
|
|
12928
|
+
const rel = path27.join("_salvaged", `${assetId}.png`);
|
|
12929
|
+
const outFile = path27.resolve(ctx.importDir, rel);
|
|
12930
|
+
await mkdir8(path27.dirname(outFile), { recursive: true });
|
|
12931
|
+
await writeFile11(outFile, buf);
|
|
12176
12932
|
ctx.manifest.assets[assetId] = {
|
|
12177
12933
|
prompt: orphan.prompt,
|
|
12178
12934
|
promptByStyle: {},
|
|
@@ -12204,7 +12960,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12204
12960
|
sourceUrl: durableSource,
|
|
12205
12961
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
12206
12962
|
outputs: [{
|
|
12207
|
-
path: portableOutputPath(outFile,
|
|
12963
|
+
path: portableOutputPath(outFile, path27.dirname(ctx.manifestPath)),
|
|
12208
12964
|
sha256: sha256(buf)
|
|
12209
12965
|
}],
|
|
12210
12966
|
submittedAt: orphan.createdAt,
|
|
@@ -12228,9 +12984,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12228
12984
|
}
|
|
12229
12985
|
}
|
|
12230
12986
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
12231
|
-
const raw = JSON.parse(await
|
|
12987
|
+
const raw = JSON.parse(await readFile22(ctx.manifestPath, "utf8"));
|
|
12232
12988
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
12233
|
-
await
|
|
12989
|
+
await writeFile11(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12234
12990
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
12235
12991
|
return result;
|
|
12236
12992
|
}
|
|
@@ -12238,51 +12994,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
12238
12994
|
}
|
|
12239
12995
|
|
|
12240
12996
|
// src/recipes.ts
|
|
12241
|
-
import
|
|
12242
|
-
import { existsSync as
|
|
12243
|
-
import { readdir as readdir3, readFile as
|
|
12997
|
+
import path28 from "path";
|
|
12998
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12999
|
+
import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
|
|
12244
13000
|
import { fileURLToPath } from "url";
|
|
12245
|
-
import { z as
|
|
13001
|
+
import { z as z9 } from "zod";
|
|
12246
13002
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
12247
13003
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
12248
13004
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
12249
13005
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
12250
13006
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
12251
|
-
var PortablePathSchema =
|
|
12252
|
-
if (value.includes("\\") ||
|
|
13007
|
+
var PortablePathSchema = z9.string().min(1).refine((value) => {
|
|
13008
|
+
if (value.includes("\\") || path28.posix.isAbsolute(value)) return false;
|
|
12253
13009
|
const parts = value.split("/");
|
|
12254
13010
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
12255
13011
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
12256
|
-
var RecipeBindingSchema =
|
|
12257
|
-
nodeId:
|
|
12258
|
-
input:
|
|
13012
|
+
var RecipeBindingSchema = z9.object({
|
|
13013
|
+
nodeId: z9.string().min(1),
|
|
13014
|
+
input: z9.string().min(1)
|
|
12259
13015
|
}).strict();
|
|
12260
|
-
var RecipeSchema =
|
|
12261
|
-
$schema:
|
|
12262
|
-
format:
|
|
12263
|
-
schemaVersion:
|
|
12264
|
-
id:
|
|
12265
|
-
version:
|
|
12266
|
-
provider:
|
|
12267
|
-
summary:
|
|
12268
|
-
files:
|
|
13016
|
+
var RecipeSchema = z9.object({
|
|
13017
|
+
$schema: z9.string().url().optional(),
|
|
13018
|
+
format: z9.literal("pixelkiln-recipe"),
|
|
13019
|
+
schemaVersion: z9.literal(1),
|
|
13020
|
+
id: z9.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
13021
|
+
version: z9.string().regex(VERSION_RE, "expected x.y.z"),
|
|
13022
|
+
provider: z9.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
13023
|
+
summary: z9.string().min(1).max(240),
|
|
13024
|
+
files: z9.array(z9.object({
|
|
12269
13025
|
path: PortablePathSchema,
|
|
12270
|
-
role:
|
|
12271
|
-
sha256:
|
|
13026
|
+
role: z9.enum(["workflow", "reference"]),
|
|
13027
|
+
sha256: z9.string().regex(SHA256_RE)
|
|
12272
13028
|
}).strict()).default([]),
|
|
12273
|
-
models:
|
|
13029
|
+
models: z9.array(z9.object({
|
|
12274
13030
|
path: PortablePathSchema,
|
|
12275
|
-
sha256:
|
|
12276
|
-
source:
|
|
12277
|
-
license:
|
|
13031
|
+
sha256: z9.string().regex(SHA256_RE),
|
|
13032
|
+
source: z9.string().url(),
|
|
13033
|
+
license: z9.string().min(1)
|
|
12278
13034
|
}).strict()).default([]),
|
|
12279
|
-
styleId:
|
|
13035
|
+
styleId: z9.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
12280
13036
|
style: StyleSchema,
|
|
12281
|
-
workflow:
|
|
13037
|
+
workflow: z9.object({
|
|
12282
13038
|
path: PortablePathSchema,
|
|
12283
|
-
outputNodeId:
|
|
12284
|
-
numImages:
|
|
12285
|
-
bindings:
|
|
13039
|
+
outputNodeId: z9.string().min(1),
|
|
13040
|
+
numImages: z9.number().int().min(1).max(16),
|
|
13041
|
+
bindings: z9.object({
|
|
12286
13042
|
prompt: RecipeBindingSchema,
|
|
12287
13043
|
width: RecipeBindingSchema.optional(),
|
|
12288
13044
|
height: RecipeBindingSchema.optional(),
|
|
@@ -12293,67 +13049,67 @@ var RecipeSchema = z8.object({
|
|
|
12293
13049
|
strength: RecipeBindingSchema.optional()
|
|
12294
13050
|
}).strict()
|
|
12295
13051
|
}).strict().optional(),
|
|
12296
|
-
quality:
|
|
12297
|
-
stage:
|
|
12298
|
-
workingCanvas:
|
|
12299
|
-
width:
|
|
12300
|
-
height:
|
|
13052
|
+
quality: z9.object({
|
|
13053
|
+
stage: z9.enum(["composition-source", "production-candidate"]),
|
|
13054
|
+
workingCanvas: z9.object({
|
|
13055
|
+
width: z9.number().int().min(16),
|
|
13056
|
+
height: z9.number().int().min(16)
|
|
12301
13057
|
}).strict(),
|
|
12302
|
-
recommendedNativeSize:
|
|
12303
|
-
min:
|
|
12304
|
-
max:
|
|
13058
|
+
recommendedNativeSize: z9.object({
|
|
13059
|
+
min: z9.number().int().min(1),
|
|
13060
|
+
max: z9.number().int().min(1)
|
|
12305
13061
|
}).strict(),
|
|
12306
|
-
paletteColors:
|
|
12307
|
-
min:
|
|
12308
|
-
max:
|
|
13062
|
+
paletteColors: z9.object({
|
|
13063
|
+
min: z9.number().int().min(1),
|
|
13064
|
+
max: z9.number().int().min(1)
|
|
12309
13065
|
}).strict(),
|
|
12310
|
-
background:
|
|
12311
|
-
checks:
|
|
13066
|
+
background: z9.enum(["opaque", "transparent", "full-bleed"]),
|
|
13067
|
+
checks: z9.array(z9.enum([
|
|
12312
13068
|
"prompt-coverage",
|
|
12313
13069
|
"native-grid",
|
|
12314
13070
|
"final-palette",
|
|
12315
13071
|
"alpha-edge",
|
|
12316
13072
|
"human-1x"
|
|
12317
13073
|
])).min(1),
|
|
12318
|
-
notes:
|
|
13074
|
+
notes: z9.array(z9.string().min(1)).default([])
|
|
12319
13075
|
}).strict(),
|
|
12320
|
-
integrity:
|
|
12321
|
-
algorithm:
|
|
12322
|
-
digest:
|
|
13076
|
+
integrity: z9.object({
|
|
13077
|
+
algorithm: z9.literal("sha256"),
|
|
13078
|
+
digest: z9.string().regex(SHA256_RE)
|
|
12323
13079
|
}).strict()
|
|
12324
13080
|
}).strict().superRefine((recipe, context) => {
|
|
12325
13081
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
12326
13082
|
context.addIssue({
|
|
12327
|
-
code:
|
|
13083
|
+
code: z9.ZodIssueCode.custom,
|
|
12328
13084
|
path: ["style", "provider"],
|
|
12329
13085
|
message: "must match the recipe provider"
|
|
12330
13086
|
});
|
|
12331
13087
|
}
|
|
12332
13088
|
const filePaths = recipe.files.map((file) => file.path);
|
|
12333
13089
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
12334
|
-
context.addIssue({ code:
|
|
13090
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
12335
13091
|
}
|
|
12336
13092
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
12337
13093
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
12338
|
-
context.addIssue({ code:
|
|
13094
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
12339
13095
|
}
|
|
12340
13096
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
12341
13097
|
context.addIssue({
|
|
12342
|
-
code:
|
|
13098
|
+
code: z9.ZodIssueCode.custom,
|
|
12343
13099
|
path: ["files"],
|
|
12344
13100
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
12345
13101
|
});
|
|
12346
13102
|
}
|
|
12347
13103
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
12348
13104
|
context.addIssue({
|
|
12349
|
-
code:
|
|
13105
|
+
code: z9.ZodIssueCode.custom,
|
|
12350
13106
|
path: ["quality", "recommendedNativeSize"],
|
|
12351
13107
|
message: "min cannot exceed max"
|
|
12352
13108
|
});
|
|
12353
13109
|
}
|
|
12354
13110
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
12355
13111
|
context.addIssue({
|
|
12356
|
-
code:
|
|
13112
|
+
code: z9.ZodIssueCode.custom,
|
|
12357
13113
|
path: ["quality", "paletteColors"],
|
|
12358
13114
|
message: "min cannot exceed max"
|
|
12359
13115
|
});
|
|
@@ -12362,7 +13118,7 @@ var RecipeSchema = z8.object({
|
|
|
12362
13118
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
12363
13119
|
)) {
|
|
12364
13120
|
context.addIssue({
|
|
12365
|
-
code:
|
|
13121
|
+
code: z9.ZodIssueCode.custom,
|
|
12366
13122
|
path: ["workflow", "path"],
|
|
12367
13123
|
message: "must name a file whose role is workflow"
|
|
12368
13124
|
});
|
|
@@ -12370,7 +13126,7 @@ var RecipeSchema = z8.object({
|
|
|
12370
13126
|
if (recipe.provider === "comfyui") {
|
|
12371
13127
|
if (!recipe.workflow) {
|
|
12372
13128
|
context.addIssue({
|
|
12373
|
-
code:
|
|
13129
|
+
code: z9.ZodIssueCode.custom,
|
|
12374
13130
|
path: ["workflow"],
|
|
12375
13131
|
message: "ComfyUI recipes require workflow metadata"
|
|
12376
13132
|
});
|
|
@@ -12379,7 +13135,7 @@ var RecipeSchema = z8.object({
|
|
|
12379
13135
|
const options = recipe.style.providerOptions.comfyui;
|
|
12380
13136
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
12381
13137
|
context.addIssue({
|
|
12382
|
-
code:
|
|
13138
|
+
code: z9.ZodIssueCode.custom,
|
|
12383
13139
|
path: ["style", "providerOptions", "comfyui"],
|
|
12384
13140
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
12385
13141
|
});
|
|
@@ -12393,7 +13149,7 @@ var RecipeSchema = z8.object({
|
|
|
12393
13149
|
};
|
|
12394
13150
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
12395
13151
|
context.addIssue({
|
|
12396
|
-
code:
|
|
13152
|
+
code: z9.ZodIssueCode.custom,
|
|
12397
13153
|
path: ["style", "providerOptions", "comfyui"],
|
|
12398
13154
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
12399
13155
|
});
|
|
@@ -12425,10 +13181,10 @@ function message4(error) {
|
|
|
12425
13181
|
return error instanceof Error ? error.message : String(error);
|
|
12426
13182
|
}
|
|
12427
13183
|
async function readRecipeFile(recipePath, bundled) {
|
|
12428
|
-
const absolute =
|
|
13184
|
+
const absolute = path28.resolve(recipePath);
|
|
12429
13185
|
let raw;
|
|
12430
13186
|
try {
|
|
12431
|
-
raw = JSON.parse(await
|
|
13187
|
+
raw = JSON.parse(await readFile23(absolute, "utf8"));
|
|
12432
13188
|
} catch (error) {
|
|
12433
13189
|
throw new Error(`Could not read recipe ${absolute}: ${message4(error)}`, { cause: error });
|
|
12434
13190
|
}
|
|
@@ -12439,13 +13195,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12439
13195
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12440
13196
|
);
|
|
12441
13197
|
}
|
|
12442
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
13198
|
+
return { recipe: parsed.data, path: absolute, dir: path28.dirname(absolute), bundled };
|
|
12443
13199
|
}
|
|
12444
13200
|
async function walkRecipeFiles(root) {
|
|
12445
|
-
if (!
|
|
13201
|
+
if (!existsSync21(root)) return [];
|
|
12446
13202
|
const out = [];
|
|
12447
13203
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
12448
|
-
const resolved =
|
|
13204
|
+
const resolved = path28.join(root, entry.name);
|
|
12449
13205
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12450
13206
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12451
13207
|
}
|
|
@@ -12456,7 +13212,7 @@ function bundledRecipeRoot() {
|
|
|
12456
13212
|
}
|
|
12457
13213
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12458
13214
|
const recipes = await Promise.all(
|
|
12459
|
-
(await walkRecipeFiles(
|
|
13215
|
+
(await walkRecipeFiles(path28.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12460
13216
|
);
|
|
12461
13217
|
return recipes.sort(
|
|
12462
13218
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12479,9 +13235,9 @@ function parseSelector(selector) {
|
|
|
12479
13235
|
return { id: match[1], version: match[2] };
|
|
12480
13236
|
}
|
|
12481
13237
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12482
|
-
const local =
|
|
12483
|
-
if (
|
|
12484
|
-
const recipePath =
|
|
13238
|
+
const local = path28.resolve(target);
|
|
13239
|
+
if (existsSync21(local)) {
|
|
13240
|
+
const recipePath = path28.basename(local) === RECIPE_FILE ? local : path28.join(local, RECIPE_FILE);
|
|
12485
13241
|
return readRecipeFile(recipePath, false);
|
|
12486
13242
|
}
|
|
12487
13243
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12495,8 +13251,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12495
13251
|
return matches[0];
|
|
12496
13252
|
}
|
|
12497
13253
|
async function verifyFile(root, file) {
|
|
12498
|
-
const absolute =
|
|
12499
|
-
if (!
|
|
13254
|
+
const absolute = path28.resolve(root, ...file.path.split("/"));
|
|
13255
|
+
if (!existsSync21(absolute)) {
|
|
12500
13256
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12501
13257
|
}
|
|
12502
13258
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12511,7 +13267,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12511
13267
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12512
13268
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
12513
13269
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12514
|
-
const modelRoot = options.modelRoot ?
|
|
13270
|
+
const modelRoot = options.modelRoot ? path28.resolve(options.modelRoot) : null;
|
|
12515
13271
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12516
13272
|
...await verifyFile(modelRoot, model),
|
|
12517
13273
|
source: model.source,
|
|
@@ -12545,7 +13301,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12545
13301
|
};
|
|
12546
13302
|
}
|
|
12547
13303
|
function renderStyle(style, destination, cwd) {
|
|
12548
|
-
const portableDir =
|
|
13304
|
+
const portableDir = path28.relative(cwd, destination).split(path28.sep).join("/") || ".";
|
|
12549
13305
|
const replace = (value) => {
|
|
12550
13306
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12551
13307
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12557,25 +13313,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
12557
13313
|
return replace(style);
|
|
12558
13314
|
}
|
|
12559
13315
|
async function installRecipe(target, options = {}) {
|
|
12560
|
-
const cwd =
|
|
13316
|
+
const cwd = path28.resolve(options.cwd ?? process.cwd());
|
|
12561
13317
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12562
13318
|
const verification = await verifyRecipe(loaded.path);
|
|
12563
13319
|
if (!verification.ok) {
|
|
12564
13320
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12565
13321
|
}
|
|
12566
|
-
const destination = options.out ?
|
|
13322
|
+
const destination = options.out ? path28.resolve(cwd, options.out) : path28.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12567
13323
|
const sources = [
|
|
12568
13324
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12569
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
13325
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path28.join(loaded.dir, ...file.path.split("/")) }))
|
|
12570
13326
|
];
|
|
12571
13327
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12572
|
-
path:
|
|
12573
|
-
data: await
|
|
13328
|
+
path: path28.join(destination, ...file.path.split("/")),
|
|
13329
|
+
data: await readFile23(file.source)
|
|
12574
13330
|
})));
|
|
12575
13331
|
if (!options.force) {
|
|
12576
13332
|
for (const file of files) {
|
|
12577
|
-
if (!
|
|
12578
|
-
const current = await
|
|
13333
|
+
if (!existsSync21(file.path)) continue;
|
|
13334
|
+
const current = await readFile23(file.path);
|
|
12579
13335
|
if (!current.equals(file.data)) {
|
|
12580
13336
|
throw new Error(
|
|
12581
13337
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
|
|
@@ -12598,15 +13354,15 @@ async function installRecipe(target, options = {}) {
|
|
|
12598
13354
|
}
|
|
12599
13355
|
|
|
12600
13356
|
// src/pipeline/quality-regression.ts
|
|
12601
|
-
import
|
|
12602
|
-
import { existsSync as
|
|
12603
|
-
import { readFile as
|
|
12604
|
-
import { z as
|
|
13357
|
+
import path29 from "path";
|
|
13358
|
+
import { existsSync as existsSync22 } from "fs";
|
|
13359
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
13360
|
+
import { z as z10 } from "zod";
|
|
12605
13361
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
12606
13362
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
12607
13363
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
12608
|
-
var QualityPathSchema =
|
|
12609
|
-
(value) => !
|
|
13364
|
+
var QualityPathSchema = z10.string().min(1).refine(
|
|
13365
|
+
(value) => !path29.posix.isAbsolute(value) && !value.includes("\\"),
|
|
12610
13366
|
"expected a portable relative path"
|
|
12611
13367
|
);
|
|
12612
13368
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -12619,53 +13375,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
12619
13375
|
maxEdgeContrastDrop: 0.03,
|
|
12620
13376
|
maxIsolatedPixelIncrease: 5e-3
|
|
12621
13377
|
};
|
|
12622
|
-
var QualityToleranceSchema =
|
|
12623
|
-
requireExactHash:
|
|
12624
|
-
maxColorCountIncrease:
|
|
12625
|
-
maxNewColors:
|
|
12626
|
-
maxTransparencyDelta:
|
|
12627
|
-
maxPartialAlphaIncrease:
|
|
12628
|
-
maxEdgeDensityDelta:
|
|
12629
|
-
maxEdgeContrastDrop:
|
|
12630
|
-
maxIsolatedPixelIncrease:
|
|
13378
|
+
var QualityToleranceSchema = z10.object({
|
|
13379
|
+
requireExactHash: z10.boolean(),
|
|
13380
|
+
maxColorCountIncrease: z10.number().int().min(0),
|
|
13381
|
+
maxNewColors: z10.number().int().min(0),
|
|
13382
|
+
maxTransparencyDelta: z10.number().min(0).max(1),
|
|
13383
|
+
maxPartialAlphaIncrease: z10.number().min(0).max(1),
|
|
13384
|
+
maxEdgeDensityDelta: z10.number().min(0).max(1),
|
|
13385
|
+
maxEdgeContrastDrop: z10.number().min(0).max(1),
|
|
13386
|
+
maxIsolatedPixelIncrease: z10.number().min(0).max(1)
|
|
12631
13387
|
}).strict();
|
|
12632
|
-
var ImageQualityMetricsSchema =
|
|
12633
|
-
sha256:
|
|
12634
|
-
width:
|
|
12635
|
-
height:
|
|
12636
|
-
colorCount:
|
|
12637
|
-
colors:
|
|
12638
|
-
transparency:
|
|
12639
|
-
partialAlpha:
|
|
12640
|
-
edgeDensity:
|
|
12641
|
-
meanEdgeContrast:
|
|
12642
|
-
isolatedPixelRatio:
|
|
13388
|
+
var ImageQualityMetricsSchema = z10.object({
|
|
13389
|
+
sha256: z10.string().regex(SHA256_RE2),
|
|
13390
|
+
width: z10.number().int().min(1),
|
|
13391
|
+
height: z10.number().int().min(1),
|
|
13392
|
+
colorCount: z10.number().int().min(0),
|
|
13393
|
+
colors: z10.array(z10.string().regex(HEX_RE)),
|
|
13394
|
+
transparency: z10.number().min(0).max(1),
|
|
13395
|
+
partialAlpha: z10.number().min(0).max(1),
|
|
13396
|
+
edgeDensity: z10.number().min(0).max(1),
|
|
13397
|
+
meanEdgeContrast: z10.number().min(0).max(1),
|
|
13398
|
+
isolatedPixelRatio: z10.number().min(0).max(1)
|
|
12643
13399
|
}).strict();
|
|
12644
|
-
var QualityBaselineCaseSchema =
|
|
12645
|
-
id:
|
|
13400
|
+
var QualityBaselineCaseSchema = z10.object({
|
|
13401
|
+
id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12646
13402
|
file: QualityPathSchema,
|
|
12647
|
-
record:
|
|
13403
|
+
record: z10.object({
|
|
12648
13404
|
path: QualityPathSchema,
|
|
12649
|
-
sha256:
|
|
13405
|
+
sha256: z10.string().regex(SHA256_RE2)
|
|
12650
13406
|
}).strict().optional(),
|
|
12651
13407
|
expected: ImageQualityMetricsSchema,
|
|
12652
13408
|
tolerances: QualityToleranceSchema
|
|
12653
13409
|
}).strict();
|
|
12654
|
-
var QualityBaselineSchema =
|
|
12655
|
-
$schema:
|
|
12656
|
-
format:
|
|
12657
|
-
schemaVersion:
|
|
12658
|
-
cases:
|
|
13410
|
+
var QualityBaselineSchema = z10.object({
|
|
13411
|
+
$schema: z10.string().url().optional(),
|
|
13412
|
+
format: z10.literal("pixelkiln-quality-baseline"),
|
|
13413
|
+
schemaVersion: z10.literal(1),
|
|
13414
|
+
cases: z10.array(QualityBaselineCaseSchema).min(1)
|
|
12659
13415
|
}).strict().superRefine((baseline, context) => {
|
|
12660
13416
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
12661
13417
|
if (new Set(ids).size !== ids.length) {
|
|
12662
|
-
context.addIssue({ code:
|
|
13418
|
+
context.addIssue({ code: z10.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
12663
13419
|
}
|
|
12664
13420
|
});
|
|
12665
|
-
var QualityInputSchema =
|
|
12666
|
-
id:
|
|
12667
|
-
path:
|
|
12668
|
-
record:
|
|
13421
|
+
var QualityInputSchema = z10.object({
|
|
13422
|
+
id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
13423
|
+
path: z10.string().min(1),
|
|
13424
|
+
record: z10.string().min(1).optional(),
|
|
12669
13425
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
12670
13426
|
}).strict();
|
|
12671
13427
|
function message5(error) {
|
|
@@ -12753,10 +13509,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
12753
13509
|
};
|
|
12754
13510
|
}
|
|
12755
13511
|
async function measureImageQuality(file) {
|
|
12756
|
-
const absolute =
|
|
13512
|
+
const absolute = path29.resolve(file);
|
|
12757
13513
|
let bytes;
|
|
12758
13514
|
try {
|
|
12759
|
-
bytes = await
|
|
13515
|
+
bytes = await readFile24(absolute);
|
|
12760
13516
|
} catch (error) {
|
|
12761
13517
|
throw new Error(`Cannot read quality image ${absolute}: ${message5(error)}`, { cause: error });
|
|
12762
13518
|
}
|
|
@@ -12767,7 +13523,7 @@ async function measureImageQuality(file) {
|
|
|
12767
13523
|
}
|
|
12768
13524
|
}
|
|
12769
13525
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
12770
|
-
const parsed =
|
|
13526
|
+
const parsed = z10.array(QualityInputSchema).min(1).safeParse(raw);
|
|
12771
13527
|
if (!parsed.success) {
|
|
12772
13528
|
throw new Error(
|
|
12773
13529
|
"--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 +13531,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12775
13531
|
}
|
|
12776
13532
|
const ids = parsed.data.map((entry) => entry.id);
|
|
12777
13533
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
12778
|
-
const root =
|
|
13534
|
+
const root = path29.dirname(path29.resolve(inputsFilePath));
|
|
12779
13535
|
return parsed.data.map((entry) => ({
|
|
12780
13536
|
id: entry.id,
|
|
12781
|
-
file:
|
|
12782
|
-
...entry.record ? { record:
|
|
13537
|
+
file: path29.resolve(root, entry.path),
|
|
13538
|
+
...entry.record ? { record: path29.resolve(root, entry.record) } : {},
|
|
12783
13539
|
tolerances: QualityToleranceSchema.parse({
|
|
12784
13540
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
12785
13541
|
...entry.tolerances
|
|
@@ -12787,7 +13543,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12787
13543
|
}));
|
|
12788
13544
|
}
|
|
12789
13545
|
function portableRelative2(from, to) {
|
|
12790
|
-
return
|
|
13546
|
+
return path29.relative(from, path29.resolve(to)).split(path29.sep).join("/") || ".";
|
|
12791
13547
|
}
|
|
12792
13548
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
12793
13549
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -12802,20 +13558,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
12802
13558
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
12803
13559
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
12804
13560
|
}
|
|
12805
|
-
const outputs = record.outputs.map((output) =>
|
|
12806
|
-
if (!outputs.includes(
|
|
13561
|
+
const outputs = record.outputs.map((output) => path29.resolve(path29.dirname(recordPath), output.path));
|
|
13562
|
+
if (!outputs.includes(path29.resolve(imagePath))) {
|
|
12807
13563
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
12808
13564
|
}
|
|
12809
13565
|
}
|
|
12810
13566
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
12811
13567
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
12812
|
-
const absolute =
|
|
13568
|
+
const absolute = path29.resolve(baselinePath);
|
|
12813
13569
|
for (const input of inputs) {
|
|
12814
|
-
if (
|
|
13570
|
+
if (path29.resolve(input.file) === absolute || input.record && path29.resolve(input.record) === absolute) {
|
|
12815
13571
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
12816
13572
|
}
|
|
12817
13573
|
}
|
|
12818
|
-
const root =
|
|
13574
|
+
const root = path29.dirname(absolute);
|
|
12819
13575
|
const cases = [];
|
|
12820
13576
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
12821
13577
|
const expected = await measureImageQuality(input.file);
|
|
@@ -12842,8 +13598,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12842
13598
|
cases
|
|
12843
13599
|
});
|
|
12844
13600
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
12845
|
-
if (
|
|
12846
|
-
const current = await
|
|
13601
|
+
if (existsSync22(absolute) && !options.force) {
|
|
13602
|
+
const current = await readFile24(absolute);
|
|
12847
13603
|
if (!current.equals(data)) {
|
|
12848
13604
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
12849
13605
|
}
|
|
@@ -12852,10 +13608,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12852
13608
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
12853
13609
|
}
|
|
12854
13610
|
async function readQualityBaseline(baselinePath) {
|
|
12855
|
-
const absolute =
|
|
13611
|
+
const absolute = path29.resolve(baselinePath);
|
|
12856
13612
|
let raw;
|
|
12857
13613
|
try {
|
|
12858
|
-
raw = JSON.parse(await
|
|
13614
|
+
raw = JSON.parse(await readFile24(absolute, "utf8"));
|
|
12859
13615
|
} catch (error) {
|
|
12860
13616
|
throw new Error(`Could not read quality baseline ${absolute}: ${message5(error)}`, { cause: error });
|
|
12861
13617
|
}
|
|
@@ -12925,12 +13681,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
12925
13681
|
return { violations, warnings };
|
|
12926
13682
|
}
|
|
12927
13683
|
async function checkQualityBaseline(baselinePath) {
|
|
12928
|
-
const absolute =
|
|
13684
|
+
const absolute = path29.resolve(baselinePath);
|
|
12929
13685
|
const baseline = await readQualityBaseline(absolute);
|
|
12930
|
-
const root =
|
|
13686
|
+
const root = path29.dirname(absolute);
|
|
12931
13687
|
const cases = [];
|
|
12932
13688
|
for (const entry of baseline.cases) {
|
|
12933
|
-
const file =
|
|
13689
|
+
const file = path29.resolve(root, entry.file);
|
|
12934
13690
|
let actual = null;
|
|
12935
13691
|
const violations = [];
|
|
12936
13692
|
const warnings = [];
|
|
@@ -12943,7 +13699,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12943
13699
|
violations.push(message5(error));
|
|
12944
13700
|
}
|
|
12945
13701
|
if (entry.record) {
|
|
12946
|
-
const recordPath =
|
|
13702
|
+
const recordPath = path29.resolve(root, entry.record.path);
|
|
12947
13703
|
try {
|
|
12948
13704
|
const recordHash = await sha256File(recordPath);
|
|
12949
13705
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -13029,13 +13785,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13029
13785
|
};
|
|
13030
13786
|
const loadProject = async (project) => {
|
|
13031
13787
|
const ctx = await access2.loadProject(project);
|
|
13032
|
-
const dir =
|
|
13788
|
+
const dir = path30.dirname(ctx.loaded.path);
|
|
13033
13789
|
const declared = readEnvFiles(dir);
|
|
13034
13790
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
13035
13791
|
for (const [name, value] of Object.entries(declared)) {
|
|
13036
13792
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
13037
13793
|
throw new Error(
|
|
13038
|
-
`${
|
|
13794
|
+
`${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
13795
|
);
|
|
13040
13796
|
}
|
|
13041
13797
|
}
|
|
@@ -13062,7 +13818,9 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13062
13818
|
budget ? describeBudget(budget) : null
|
|
13063
13819
|
),
|
|
13064
13820
|
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
13065
|
-
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
|
|
13821
|
+
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
|
|
13822
|
+
// The editor exists to write hand edits back, so it follows the write gate.
|
|
13823
|
+
...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
|
|
13066
13824
|
});
|
|
13067
13825
|
await new Promise((resolve) => {
|
|
13068
13826
|
const stop = () => {
|
|
@@ -13077,11 +13835,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
13077
13835
|
log("\n gallery closed");
|
|
13078
13836
|
}
|
|
13079
13837
|
async function provenanceFile(id, file) {
|
|
13080
|
-
const absolute =
|
|
13838
|
+
const absolute = path30.resolve(file);
|
|
13081
13839
|
return {
|
|
13082
13840
|
id,
|
|
13083
13841
|
path: absolute,
|
|
13084
|
-
sha256:
|
|
13842
|
+
sha256: existsSync23(absolute) ? await sha256File(absolute) : null,
|
|
13085
13843
|
included: true
|
|
13086
13844
|
};
|
|
13087
13845
|
}
|
|
@@ -13131,7 +13889,8 @@ var BOOL_FLAGS = [
|
|
|
13131
13889
|
"--primary-only",
|
|
13132
13890
|
"--prune",
|
|
13133
13891
|
"--edit",
|
|
13134
|
-
"--refresh"
|
|
13892
|
+
"--refresh",
|
|
13893
|
+
"--no-editor"
|
|
13135
13894
|
];
|
|
13136
13895
|
var COMMANDS = [
|
|
13137
13896
|
"init",
|
|
@@ -13158,6 +13917,7 @@ var COMMANDS = [
|
|
|
13158
13917
|
"status",
|
|
13159
13918
|
"gallery",
|
|
13160
13919
|
"edit",
|
|
13920
|
+
"tools",
|
|
13161
13921
|
"quality",
|
|
13162
13922
|
"refine",
|
|
13163
13923
|
"recipe",
|
|
@@ -13173,6 +13933,8 @@ var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
|
13173
13933
|
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
13174
13934
|
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
13175
13935
|
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
13936
|
+
var TOOLS_SUBCOMMANDS = ["status", "install"];
|
|
13937
|
+
var TOOLS = ["editor"];
|
|
13176
13938
|
function parseArgs(argv) {
|
|
13177
13939
|
const [command = "help"] = argv;
|
|
13178
13940
|
if (!COMMANDS.includes(command)) {
|
|
@@ -13236,6 +13998,22 @@ function parseArgs(argv) {
|
|
|
13236
13998
|
throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
|
|
13237
13999
|
}
|
|
13238
14000
|
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
14001
|
+
} else if (command === "tools") {
|
|
14002
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "status" : rest[0];
|
|
14003
|
+
if (!TOOLS_SUBCOMMANDS.includes(subcommand)) {
|
|
14004
|
+
throw new Error(`Unknown tools subcommand "${subcommand}". Known: ${TOOLS_SUBCOMMANDS.join(", ")}`);
|
|
14005
|
+
}
|
|
14006
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
14007
|
+
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
14008
|
+
if (subcommand === "install" && target === void 0) {
|
|
14009
|
+
throw new Error(`tools install needs a tool name: ${TOOLS.join(", ")}`);
|
|
14010
|
+
}
|
|
14011
|
+
if (target !== void 0) {
|
|
14012
|
+
if (!TOOLS.includes(target)) {
|
|
14013
|
+
throw new Error(`Unknown tool "${target}". Known: ${TOOLS.join(", ")}`);
|
|
14014
|
+
}
|
|
14015
|
+
rest = rest.slice(1);
|
|
14016
|
+
}
|
|
13239
14017
|
} else if (command === "refine") {
|
|
13240
14018
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
13241
14019
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -13349,7 +14127,7 @@ function parseArgs(argv) {
|
|
|
13349
14127
|
return {
|
|
13350
14128
|
command,
|
|
13351
14129
|
manifest,
|
|
13352
|
-
lock: get("--lock") ??
|
|
14130
|
+
lock: get("--lock") ?? path30.join(path30.dirname(path30.resolve(manifest)), "pixelkiln.lock.json"),
|
|
13353
14131
|
explicitLock: get("--lock"),
|
|
13354
14132
|
styles: list("--style"),
|
|
13355
14133
|
assets: list("--only"),
|
|
@@ -13364,6 +14142,7 @@ function parseArgs(argv) {
|
|
|
13364
14142
|
noOpen: rest.includes("--no-open"),
|
|
13365
14143
|
tag: rest.includes("--tag"),
|
|
13366
14144
|
edit: rest.includes("--edit"),
|
|
14145
|
+
noEditor: rest.includes("--no-editor"),
|
|
13367
14146
|
refresh: rest.includes("--refresh"),
|
|
13368
14147
|
from: get("--from"),
|
|
13369
14148
|
out: get("--out"),
|
|
@@ -13397,6 +14176,45 @@ function parseArgs(argv) {
|
|
|
13397
14176
|
account: get("--account")
|
|
13398
14177
|
};
|
|
13399
14178
|
}
|
|
14179
|
+
async function runTools(args) {
|
|
14180
|
+
const mb = (bytes) => bytes < 1e6 ? `${(bytes / 1e3).toFixed(0)} kB` : `${(bytes / 1e6).toFixed(1)} MB`;
|
|
14181
|
+
if (args.subcommand === "status") {
|
|
14182
|
+
const status2 = await editorStatus();
|
|
14183
|
+
if (args.json) {
|
|
14184
|
+
log(JSON.stringify({ version: 1, editor: { ...status2, pixelorama: EDITOR_PIN.pixelorama, protocol: EDITOR_PIN.protocol } }, null, 2));
|
|
14185
|
+
return;
|
|
14186
|
+
}
|
|
14187
|
+
log(` editor Pixelorama ${EDITOR_PIN.pixelorama} (bridge protocol ${EDITOR_PIN.protocol})`);
|
|
14188
|
+
if (!status2.release) {
|
|
14189
|
+
log(" no published build is pinned by this PixelKiln version");
|
|
14190
|
+
return;
|
|
14191
|
+
}
|
|
14192
|
+
log(` release ${status2.release}`);
|
|
14193
|
+
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}`);
|
|
14194
|
+
if (!status2.installed) log(" run: pixelkiln tools install editor");
|
|
14195
|
+
return;
|
|
14196
|
+
}
|
|
14197
|
+
const status = await editorStatus();
|
|
14198
|
+
if (status.installed) {
|
|
14199
|
+
log(` editor is already installed and verified in ${status.dir}`);
|
|
14200
|
+
return;
|
|
14201
|
+
}
|
|
14202
|
+
if (!status.release) {
|
|
14203
|
+
throw new Error("this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust");
|
|
14204
|
+
}
|
|
14205
|
+
log(` fetching ${status.missing.length} file(s), ${mb(status.totalBytes - status.installedBytes)}, from release ${status.release}`);
|
|
14206
|
+
try {
|
|
14207
|
+
const result = await installEditor({
|
|
14208
|
+
onProgress: (p) => {
|
|
14209
|
+
if (p.phase === "start") log(` ${p.file.padEnd(34)} ${mb(p.bytes).padStart(9)}`);
|
|
14210
|
+
}
|
|
14211
|
+
});
|
|
14212
|
+
log(` editor ready in ${result.dir} (${result.downloaded.length} fetched, ${result.skipped.length} already present, every hash verified)`);
|
|
14213
|
+
} catch (err) {
|
|
14214
|
+
if (err instanceof EditorInstallError) throw new Error(`editor install failed: ${err.message}`);
|
|
14215
|
+
throw err;
|
|
14216
|
+
}
|
|
14217
|
+
}
|
|
13400
14218
|
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
13401
14219
|
|
|
13402
14220
|
pixelkiln <command> [options]
|
|
@@ -13437,8 +14255,12 @@ Commands
|
|
|
13437
14255
|
gallery Open a local read-only gallery of every generation and its
|
|
13438
14256
|
provenance: prompt, provider, cost, outputs, lineage, quality.
|
|
13439
14257
|
--workspace <catalog> shows every registered project at once;
|
|
13440
|
-
--edit lets the page change prompts, sizes, tags, and add assets
|
|
14258
|
+
--edit lets the page change prompts, sizes, tags, and add assets,
|
|
14259
|
+
and offers the in-browser pixel editor (--no-editor hides it);
|
|
13441
14260
|
--budget enables Generate, Regenerate, and review under that ceiling.
|
|
14261
|
+
tools status/install editor: the in-browser editor (Pixelorama) is a
|
|
14262
|
+
46 MB web build fetched once per release into a user cache and
|
|
14263
|
+
verified against pinned hashes. Offline once installed.
|
|
13442
14264
|
workspace Register sibling projects and derive account-wide claims/status.
|
|
13443
14265
|
add/remove/list/status/claims. Offline.
|
|
13444
14266
|
|
|
@@ -13478,6 +14300,7 @@ Options
|
|
|
13478
14300
|
--yes, -y Skip the confirmation prompt
|
|
13479
14301
|
--no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
|
|
13480
14302
|
--edit gallery: allow manifest edits from the page (never spends)
|
|
14303
|
+
--no-editor gallery: do not offer or serve the in-browser editor
|
|
13481
14304
|
--tag Also push tags upstream after fetch
|
|
13482
14305
|
--refresh fetch: re-download and replace files whose object changed
|
|
13483
14306
|
upstream (e.g. edited in PixelLab's editor); no generation
|
|
@@ -13496,6 +14319,7 @@ Examples
|
|
|
13496
14319
|
pixelkiln gallery --workspace pixelkiln.workspace.json
|
|
13497
14320
|
pixelkiln gallery --edit --budget 80
|
|
13498
14321
|
pixelkiln edit --style base --only anvil
|
|
14322
|
+
pixelkiln tools install editor
|
|
13499
14323
|
PIXELKILN_EDITOR="open -a Aseprite" pixelkiln edit --only anvil --style base
|
|
13500
14324
|
pixelkiln adopt --tag
|
|
13501
14325
|
pixelkiln pack --style heybud-premium
|
|
@@ -13655,10 +14479,10 @@ async function confirm(question, auto) {
|
|
|
13655
14479
|
return answer === "y" || answer === "yes";
|
|
13656
14480
|
}
|
|
13657
14481
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13658
|
-
if (!
|
|
14482
|
+
if (!existsSync23(workspacePath)) {
|
|
13659
14483
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13660
14484
|
}
|
|
13661
|
-
const dir =
|
|
14485
|
+
const dir = path30.dirname(path30.resolve(workspacePath));
|
|
13662
14486
|
const ws = await loadWorkspace(workspacePath);
|
|
13663
14487
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13664
14488
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13679,26 +14503,30 @@ async function main() {
|
|
|
13679
14503
|
}
|
|
13680
14504
|
if (args.command === "--version" || args.command === "-v") {
|
|
13681
14505
|
const pkg = JSON.parse(
|
|
13682
|
-
await
|
|
14506
|
+
await readFile25(new URL("../package.json", import.meta.url), "utf8")
|
|
13683
14507
|
);
|
|
13684
14508
|
log(`${pkg.name} ${pkg.version}`);
|
|
13685
14509
|
return;
|
|
13686
14510
|
}
|
|
14511
|
+
if (args.command === "tools") {
|
|
14512
|
+
await runTools(args);
|
|
14513
|
+
return;
|
|
14514
|
+
}
|
|
13687
14515
|
if (args.command === "quality") {
|
|
13688
14516
|
if (args.subcommand === "snapshot") {
|
|
13689
14517
|
if (!args.inputs) throw new Error("quality snapshot needs --inputs <quality-inputs.json>.");
|
|
13690
14518
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
13691
14519
|
let raw;
|
|
13692
14520
|
try {
|
|
13693
|
-
raw = JSON.parse(await
|
|
14521
|
+
raw = JSON.parse(await readFile25(path30.resolve(args.inputs), "utf8"));
|
|
13694
14522
|
} catch (error) {
|
|
13695
14523
|
throw new Error(
|
|
13696
|
-
`Could not read quality inputs ${
|
|
14524
|
+
`Could not read quality inputs ${path30.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13697
14525
|
{ cause: error }
|
|
13698
14526
|
);
|
|
13699
14527
|
}
|
|
13700
14528
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
13701
|
-
if (
|
|
14529
|
+
if (path30.resolve(args.inputs) === path30.resolve(args.out)) {
|
|
13702
14530
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
13703
14531
|
}
|
|
13704
14532
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -13711,7 +14539,7 @@ async function main() {
|
|
|
13711
14539
|
}, null, 2));
|
|
13712
14540
|
} else {
|
|
13713
14541
|
log(
|
|
13714
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
14542
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path30.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
13715
14543
|
);
|
|
13716
14544
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
13717
14545
|
}
|
|
@@ -13806,14 +14634,14 @@ async function main() {
|
|
|
13806
14634
|
}, null, 2));
|
|
13807
14635
|
} else {
|
|
13808
14636
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
13809
|
-
log(` ${
|
|
14637
|
+
log(` ${path30.relative(process.cwd(), result.destination) || "."}`);
|
|
13810
14638
|
log(`
|
|
13811
14639
|
Add this entry under your manifest's styles object:`);
|
|
13812
14640
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
13813
14641
|
if (result.recipe.models.length) {
|
|
13814
14642
|
log(`
|
|
13815
14643
|
Models are not downloaded automatically. Verify them with:`);
|
|
13816
|
-
log(` pixelkiln recipe verify ${
|
|
14644
|
+
log(` pixelkiln recipe verify ${path30.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
13817
14645
|
}
|
|
13818
14646
|
}
|
|
13819
14647
|
return;
|
|
@@ -13847,12 +14675,12 @@ async function main() {
|
|
|
13847
14675
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
13848
14676
|
);
|
|
13849
14677
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
13850
|
-
log(` wrote ${
|
|
13851
|
-
log(` quality record: ${
|
|
14678
|
+
log(` wrote ${path30.relative(process.cwd(), result2.output)}`);
|
|
14679
|
+
log(` quality record: ${path30.relative(process.cwd(), result2.record)}`);
|
|
13852
14680
|
log(`
|
|
13853
14681
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
13854
14682
|
log(
|
|
13855
|
-
` pixelkiln refine approve --from ${
|
|
14683
|
+
` pixelkiln refine approve --from ${path30.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
13856
14684
|
);
|
|
13857
14685
|
}
|
|
13858
14686
|
return;
|
|
@@ -13880,7 +14708,7 @@ async function main() {
|
|
|
13880
14708
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
13881
14709
|
else {
|
|
13882
14710
|
log(
|
|
13883
|
-
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${
|
|
14711
|
+
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
14712
|
);
|
|
13885
14713
|
}
|
|
13886
14714
|
return;
|
|
@@ -13889,14 +14717,14 @@ async function main() {
|
|
|
13889
14717
|
if (args.json) {
|
|
13890
14718
|
log(JSON.stringify(result, null, 2));
|
|
13891
14719
|
} else {
|
|
13892
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
14720
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path30.relative(process.cwd(), result.output || result.record)}`);
|
|
13893
14721
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
13894
14722
|
}
|
|
13895
14723
|
if (!result.safe) process.exitCode = 1;
|
|
13896
14724
|
return;
|
|
13897
14725
|
}
|
|
13898
14726
|
if (args.command === "balance") {
|
|
13899
|
-
loadEnvFiles(
|
|
14727
|
+
loadEnvFiles(path30.dirname(path30.resolve(args.manifest)));
|
|
13900
14728
|
loadEnvFiles(process.cwd());
|
|
13901
14729
|
const loaded2 = await loadManifest(args.manifest);
|
|
13902
14730
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -13909,31 +14737,31 @@ async function main() {
|
|
|
13909
14737
|
}
|
|
13910
14738
|
if (args.command === "init") {
|
|
13911
14739
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13912
|
-
const root =
|
|
13913
|
-
if (!
|
|
14740
|
+
const root = path30.resolve(args.from);
|
|
14741
|
+
if (!existsSync23(root)) throw new Error(`No directory at ${root}`);
|
|
13914
14742
|
const generator = args.generator ?? "map";
|
|
13915
14743
|
if (generator !== "1dir" && generator !== "map") {
|
|
13916
14744
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13917
14745
|
}
|
|
13918
|
-
const target =
|
|
14746
|
+
const target = path30.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13919
14747
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13920
14748
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13921
14749
|
const manifest = buildManifest(
|
|
13922
|
-
args.name ??
|
|
14750
|
+
args.name ?? path30.basename(path30.dirname(target)),
|
|
13923
14751
|
args.styles[0] ?? "base",
|
|
13924
14752
|
generator,
|
|
13925
|
-
|
|
14753
|
+
path30.relative(path30.dirname(target), root) || ".",
|
|
13926
14754
|
assets
|
|
13927
14755
|
);
|
|
13928
14756
|
await writeManifestFile(target, manifest);
|
|
13929
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
14757
|
+
log(` scanned ${assets.length} PNG(s) under ${path30.relative(process.cwd(), root)}`);
|
|
13930
14758
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13931
|
-
log(` wrote ${
|
|
14759
|
+
log(` wrote ${path30.relative(process.cwd(), target)}`);
|
|
13932
14760
|
log(`
|
|
13933
14761
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13934
14762
|
log(` PixelLab account instead of inventing them:`);
|
|
13935
14763
|
log(`
|
|
13936
|
-
pixelkiln adopt --manifest ${
|
|
14764
|
+
pixelkiln adopt --manifest ${path30.relative(process.cwd(), target)} --write-prompts
|
|
13937
14765
|
`);
|
|
13938
14766
|
return;
|
|
13939
14767
|
}
|
|
@@ -13981,10 +14809,10 @@ async function main() {
|
|
|
13981
14809
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
13982
14810
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
13983
14811
|
}
|
|
13984
|
-
const raw = JSON.parse(await
|
|
14812
|
+
const raw = JSON.parse(await readFile25(path30.resolve(args.inputs), "utf8"));
|
|
13985
14813
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13986
14814
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13987
|
-
const base =
|
|
14815
|
+
const base = path30.resolve(args.out.replace(/\.png$/, ""));
|
|
13988
14816
|
const outputs = [
|
|
13989
14817
|
{ path: `${base}.png`, data: png },
|
|
13990
14818
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -13997,17 +14825,17 @@ async function main() {
|
|
|
13997
14825
|
log(
|
|
13998
14826
|
` ${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
14827
|
);
|
|
14000
|
-
log(` ${
|
|
14828
|
+
log(` ${path30.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14001
14829
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14002
14830
|
return;
|
|
14003
14831
|
}
|
|
14004
14832
|
if (args.command === "workspace") {
|
|
14005
|
-
const workspacePath =
|
|
14006
|
-
const dir =
|
|
14833
|
+
const workspacePath = path30.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
14834
|
+
const dir = path30.dirname(workspacePath);
|
|
14007
14835
|
if (args.subcommand === "add") {
|
|
14008
|
-
const manifestPath =
|
|
14836
|
+
const manifestPath = path30.resolve(args.target);
|
|
14009
14837
|
const loadedTarget = await loadManifest(manifestPath);
|
|
14010
|
-
const lockPath = args.explicitLock ?
|
|
14838
|
+
const lockPath = args.explicitLock ? path30.resolve(args.explicitLock) : path30.join(path30.dirname(manifestPath), "pixelkiln.lock.json");
|
|
14011
14839
|
const ws = await loadWorkspace(workspacePath);
|
|
14012
14840
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
14013
14841
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -14027,10 +14855,10 @@ async function main() {
|
|
|
14027
14855
|
...args.account ? { account: args.account } : {}
|
|
14028
14856
|
};
|
|
14029
14857
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
14030
|
-
log(` registered "${id}" in ${
|
|
14858
|
+
log(` registered "${id}" in ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14031
14859
|
log(` manifest: ${project.manifest}`);
|
|
14032
14860
|
log(` lock: ${project.lock}`);
|
|
14033
|
-
if (!
|
|
14861
|
+
if (!existsSync23(lockPath)) {
|
|
14034
14862
|
log(
|
|
14035
14863
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
14036
14864
|
);
|
|
@@ -14039,7 +14867,7 @@ async function main() {
|
|
|
14039
14867
|
}
|
|
14040
14868
|
if (args.subcommand === "remove") {
|
|
14041
14869
|
const ws = await loadWorkspace(workspacePath);
|
|
14042
|
-
const resolvedTarget =
|
|
14870
|
+
const resolvedTarget = path30.resolve(args.target);
|
|
14043
14871
|
const match = ws.projects.find(
|
|
14044
14872
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
14045
14873
|
);
|
|
@@ -14050,11 +14878,11 @@ async function main() {
|
|
|
14050
14878
|
version: 1,
|
|
14051
14879
|
projects: ws.projects.filter((p) => p !== match)
|
|
14052
14880
|
});
|
|
14053
|
-
log(` removed "${match.id}" from ${
|
|
14881
|
+
log(` removed "${match.id}" from ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14054
14882
|
return;
|
|
14055
14883
|
}
|
|
14056
14884
|
if (args.subcommand === "list") {
|
|
14057
|
-
if (!
|
|
14885
|
+
if (!existsSync23(workspacePath)) {
|
|
14058
14886
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14059
14887
|
}
|
|
14060
14888
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14062,9 +14890,9 @@ async function main() {
|
|
|
14062
14890
|
if (args.json) {
|
|
14063
14891
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
14064
14892
|
} else if (!ws.projects.length) {
|
|
14065
|
-
log(` no projects registered in ${
|
|
14893
|
+
log(` no projects registered in ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14066
14894
|
} else {
|
|
14067
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
14895
|
+
log(` ${ws.projects.length} project(s) in ${path30.relative(process.cwd(), workspacePath)}:`);
|
|
14068
14896
|
for (const p of ws.projects) {
|
|
14069
14897
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
14070
14898
|
}
|
|
@@ -14074,7 +14902,7 @@ async function main() {
|
|
|
14074
14902
|
return;
|
|
14075
14903
|
}
|
|
14076
14904
|
if (args.subcommand === "status") {
|
|
14077
|
-
if (!
|
|
14905
|
+
if (!existsSync23(workspacePath)) {
|
|
14078
14906
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14079
14907
|
}
|
|
14080
14908
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -14082,7 +14910,7 @@ async function main() {
|
|
|
14082
14910
|
if (args.json) {
|
|
14083
14911
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
14084
14912
|
} else {
|
|
14085
|
-
log(` workspace: ${
|
|
14913
|
+
log(` workspace: ${path30.relative(process.cwd(), workspacePath)}`);
|
|
14086
14914
|
for (const p of report.projects) {
|
|
14087
14915
|
if (p.error) {
|
|
14088
14916
|
log(`
|
|
@@ -14129,8 +14957,8 @@ async function main() {
|
|
|
14129
14957
|
}
|
|
14130
14958
|
}
|
|
14131
14959
|
if (args.command === "gallery" && args.workspace) {
|
|
14132
|
-
const workspacePath =
|
|
14133
|
-
if (!
|
|
14960
|
+
const workspacePath = path30.resolve(args.workspace);
|
|
14961
|
+
if (!existsSync23(workspacePath)) {
|
|
14134
14962
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14135
14963
|
}
|
|
14136
14964
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -14150,7 +14978,7 @@ async function main() {
|
|
|
14150
14978
|
const registered = async (projectId) => {
|
|
14151
14979
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
14152
14980
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
14153
|
-
return resolveProject(
|
|
14981
|
+
return resolveProject(path30.dirname(workspacePath), project);
|
|
14154
14982
|
};
|
|
14155
14983
|
await runGallery(initial, build, args, {
|
|
14156
14984
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -14165,14 +14993,14 @@ async function main() {
|
|
|
14165
14993
|
});
|
|
14166
14994
|
return;
|
|
14167
14995
|
}
|
|
14168
|
-
if (!
|
|
14996
|
+
if (!existsSync23(path30.resolve(args.manifest))) {
|
|
14169
14997
|
throw new Error(
|
|
14170
|
-
`No manifest at ${
|
|
14998
|
+
`No manifest at ${path30.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
14171
14999
|
);
|
|
14172
15000
|
}
|
|
14173
|
-
const manifestDir =
|
|
15001
|
+
const manifestDir = path30.dirname(path30.resolve(args.manifest));
|
|
14174
15002
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
14175
|
-
if (
|
|
15003
|
+
if (path30.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
14176
15004
|
const loaded = await loadManifest(args.manifest);
|
|
14177
15005
|
let specs = await resolveSpecs(loaded, {
|
|
14178
15006
|
styles: args.styles,
|
|
@@ -14245,7 +15073,7 @@ async function main() {
|
|
|
14245
15073
|
});
|
|
14246
15074
|
};
|
|
14247
15075
|
await runGallery(await build(), reload, args, {
|
|
14248
|
-
manifestFor: () =>
|
|
15076
|
+
manifestFor: () => path30.resolve(args.manifest),
|
|
14249
15077
|
loadProject: async () => {
|
|
14250
15078
|
const freshLoaded = await loadManifest(args.manifest);
|
|
14251
15079
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -14320,7 +15148,7 @@ async function main() {
|
|
|
14320
15148
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
14321
15149
|
if (item.state === "needs-approval") {
|
|
14322
15150
|
log(
|
|
14323
|
-
` pixelkiln refine approve --from ${
|
|
15151
|
+
` pixelkiln refine approve --from ${path30.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
14324
15152
|
);
|
|
14325
15153
|
}
|
|
14326
15154
|
}
|
|
@@ -14411,7 +15239,7 @@ async function main() {
|
|
|
14411
15239
|
let intact = true;
|
|
14412
15240
|
for (const output of entry.outputs) {
|
|
14413
15241
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
14414
|
-
if (!
|
|
15242
|
+
if (!existsSync23(file) || await sha256File(file) !== output.sha256) {
|
|
14415
15243
|
intact = false;
|
|
14416
15244
|
break;
|
|
14417
15245
|
}
|
|
@@ -14469,7 +15297,7 @@ async function main() {
|
|
|
14469
15297
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14470
15298
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14471
15299
|
}
|
|
14472
|
-
const manifestDir2 =
|
|
15300
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14473
15301
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14474
15302
|
for (const styleId of styleIds) {
|
|
14475
15303
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14482,7 +15310,7 @@ async function main() {
|
|
|
14482
15310
|
sources: manifestSources(loaded.manifest, styleId)
|
|
14483
15311
|
});
|
|
14484
15312
|
const style = loaded.manifest.styles[styleId];
|
|
14485
|
-
const base = args.out ?
|
|
15313
|
+
const base = args.out ? path30.resolve(args.out.replace(/\.png$/, "")) : path30.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
14486
15314
|
const outputs = [
|
|
14487
15315
|
{ path: `${base}.png`, data: png },
|
|
14488
15316
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -14512,13 +15340,13 @@ async function main() {
|
|
|
14512
15340
|
log(
|
|
14513
15341
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
14514
15342
|
);
|
|
14515
|
-
log(` ${
|
|
15343
|
+
log(` ${path30.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14516
15344
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14517
15345
|
}
|
|
14518
15346
|
return;
|
|
14519
15347
|
}
|
|
14520
15348
|
if (args.command === "mount") {
|
|
14521
|
-
const manifestDir2 =
|
|
15349
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14522
15350
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14523
15351
|
for (const styleId of styleIds) {
|
|
14524
15352
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14557,7 +15385,7 @@ async function main() {
|
|
|
14557
15385
|
sources,
|
|
14558
15386
|
outputRoles
|
|
14559
15387
|
);
|
|
14560
|
-
const out =
|
|
15388
|
+
const out = path30.resolve(manifestDir2, style.mount.out);
|
|
14561
15389
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
14562
15390
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14563
15391
|
const outputs = [
|
|
@@ -14577,7 +15405,7 @@ async function main() {
|
|
|
14577
15405
|
await provenanceFile("$lock", args.lock),
|
|
14578
15406
|
...qualityRecords,
|
|
14579
15407
|
...artifactSources.filter(
|
|
14580
|
-
(source) => source.id !== "$base" ||
|
|
15408
|
+
(source) => source.id !== "$base" || path30.resolve(source.path) !== out
|
|
14581
15409
|
)
|
|
14582
15410
|
],
|
|
14583
15411
|
options: {
|
|
@@ -14590,7 +15418,7 @@ async function main() {
|
|
|
14590
15418
|
log(
|
|
14591
15419
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14592
15420
|
);
|
|
14593
|
-
log(` ${
|
|
15421
|
+
log(` ${path30.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14594
15422
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14595
15423
|
}
|
|
14596
15424
|
return;
|
|
@@ -14646,7 +15474,7 @@ async function main() {
|
|
|
14646
15474
|
}
|
|
14647
15475
|
if (args.command === "export") {
|
|
14648
15476
|
const format = args.format ?? "generic";
|
|
14649
|
-
const manifestDir2 =
|
|
15477
|
+
const manifestDir2 = path30.dirname(path30.resolve(args.manifest));
|
|
14650
15478
|
const selected = specs.filter((spec) => {
|
|
14651
15479
|
if (spec.generator !== "tiles") return false;
|
|
14652
15480
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14662,12 +15490,12 @@ async function main() {
|
|
|
14662
15490
|
for (const spec of selected) {
|
|
14663
15491
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14664
15492
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14665
|
-
const defaultBase =
|
|
14666
|
-
const base = args.out ?
|
|
15493
|
+
const defaultBase = path30.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
15494
|
+
const base = args.out ? path30.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14667
15495
|
const result = exportTileset(entry, spec, {
|
|
14668
15496
|
format,
|
|
14669
15497
|
manifestDir: manifestDir2,
|
|
14670
|
-
imageName:
|
|
15498
|
+
imageName: path30.basename(`${base}.png`),
|
|
14671
15499
|
columns: args.columns
|
|
14672
15500
|
});
|
|
14673
15501
|
const outputs = [
|
|
@@ -14685,7 +15513,7 @@ async function main() {
|
|
|
14685
15513
|
asset: spec.assetId,
|
|
14686
15514
|
columns: args.columns ?? null,
|
|
14687
15515
|
format,
|
|
14688
|
-
image:
|
|
15516
|
+
image: path30.basename(`${base}.png`),
|
|
14689
15517
|
providerRules: result.generic.providerRules,
|
|
14690
15518
|
style: spec.styleId,
|
|
14691
15519
|
tileType: spec.tileType ?? null
|
|
@@ -14695,7 +15523,7 @@ async function main() {
|
|
|
14695
15523
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14696
15524
|
);
|
|
14697
15525
|
log(
|
|
14698
|
-
` ${
|
|
15526
|
+
` ${path30.relative(process.cwd(), base)}.png + ${path30.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14699
15527
|
);
|
|
14700
15528
|
}
|
|
14701
15529
|
return;
|
|
@@ -14745,12 +15573,12 @@ async function main() {
|
|
|
14745
15573
|
tagged ${n} object(s) upstream`);
|
|
14746
15574
|
}
|
|
14747
15575
|
if (args.writePrompts) {
|
|
14748
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
15576
|
+
const { filled, stillEmpty } = await writePromptsBack(path30.resolve(args.manifest), lock, {
|
|
14749
15577
|
onProgress: log,
|
|
14750
15578
|
provider: provider.id,
|
|
14751
15579
|
assetIds: specs.map((spec) => spec.assetId)
|
|
14752
15580
|
});
|
|
14753
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
15581
|
+
log(` recovered ${filled} prompt(s) into ${path30.relative(process.cwd(), args.manifest)}`);
|
|
14754
15582
|
const reloaded = await loadManifest(args.manifest);
|
|
14755
15583
|
const rebased = (await resolveSpecs(reloaded, {
|
|
14756
15584
|
styles: args.styles,
|
|
@@ -14779,12 +15607,12 @@ async function main() {
|
|
|
14779
15607
|
const provider = providerFor(selectedAccountProvider);
|
|
14780
15608
|
const jsonMode = args.dryRun && args.json;
|
|
14781
15609
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
14782
|
-
const ownLock =
|
|
15610
|
+
const ownLock = path30.resolve(args.lock);
|
|
14783
15611
|
let workspaceProjects = [];
|
|
14784
15612
|
let workspaceDir = "";
|
|
14785
15613
|
if (args.workspace) {
|
|
14786
|
-
const workspacePath =
|
|
14787
|
-
workspaceDir =
|
|
15614
|
+
const workspacePath = path30.resolve(args.workspace);
|
|
15615
|
+
workspaceDir = path30.dirname(workspacePath);
|
|
14788
15616
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
14789
15617
|
workspaceProjects = complete.ws.projects;
|
|
14790
15618
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -14793,12 +15621,12 @@ async function main() {
|
|
|
14793
15621
|
const lockPaths = [
|
|
14794
15622
|
.../* @__PURE__ */ new Set([
|
|
14795
15623
|
...workspaceLockPaths,
|
|
14796
|
-
...
|
|
14797
|
-
...args.claims.map((c) =>
|
|
15624
|
+
...existsSync23(ownLock) ? [ownLock] : [],
|
|
15625
|
+
...args.claims.map((c) => path30.resolve(c))
|
|
14798
15626
|
])
|
|
14799
15627
|
];
|
|
14800
15628
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
14801
|
-
for (const p of lockPaths) diag(` ${
|
|
15629
|
+
for (const p of lockPaths) diag(` ${path30.relative(process.cwd(), p)}`);
|
|
14802
15630
|
if (!args.claims.length && !args.workspace) {
|
|
14803
15631
|
diag(
|
|
14804
15632
|
`
|
|
@@ -14806,7 +15634,7 @@ async function main() {
|
|
|
14806
15634
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
14807
15635
|
register every project in a workspace catalog and pass --workspace.`
|
|
14808
15636
|
);
|
|
14809
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
15637
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path30.resolve(args.manifest))) {
|
|
14810
15638
|
diag(
|
|
14811
15639
|
`
|
|
14812
15640
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14890,7 +15718,7 @@ async function main() {
|
|
|
14890
15718
|
manifestPath: loaded.path,
|
|
14891
15719
|
manifest: accountManifest,
|
|
14892
15720
|
styleId,
|
|
14893
|
-
importDir:
|
|
15721
|
+
importDir: path30.resolve(loaded.root, style.outDir),
|
|
14894
15722
|
lock,
|
|
14895
15723
|
lockPath: args.lock
|
|
14896
15724
|
},
|