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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path28 from "path";
5
- import { existsSync as existsSync22 } from "fs";
6
- import { readFile as readFile22 } from "fs/promises";
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(path29) {
137
+ async function sha256File(path31) {
138
138
  return new Promise((resolve, reject) => {
139
139
  const hash = createHash("sha256");
140
- const stream = createReadStream(path29);
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(path29, init, attempt = 0) {
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}${path29}`, {
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(path29, init, attempt + 1);
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(path29, init, attempt + 1);
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"} ${path29} \u2192 ${res.status}`, res.status, text);
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"} ${path29} returned invalid JSON`);
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(path29, init = {}) {
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}${path29}`, {
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(path29) {
6876
- if (!existsSync11(path29)) return { version: 1, hashes: {} };
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(path29, "utf8")));
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(path29, cache) {
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(path29)), { recursive: true });
6898
- const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.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, path29);
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 readFile15, stat as stat3 } from "fs/promises";
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 z6 } from "zod";
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 HandEditRequestSchema = z6.discriminatedUnion("action", [
8320
- z6.object({
8321
- action: z6.literal("start-edit"),
8322
- project: z6.string().min(1).optional(),
8323
- assetId: z6.string().min(1),
8324
- styleId: z6.string().min(1),
8325
- expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/),
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: z6.boolean().optional()
8412
+ open: z7.boolean().optional()
8328
8413
  }).strict(),
8329
- z6.object({
8330
- action: z6.literal("detach-edit"),
8331
- project: z6.string().min(1).optional(),
8332
- assetId: z6.string().min(1),
8333
- styleId: z6.string().min(1),
8334
- expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/)
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 readFile15(loaded.path, "utf8");
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
- editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists ? "regenerated-since" : "edited";
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 readFile15(manifestPath, "utf8")),
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 readFile16 } from "fs/promises";
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 !== 'pixelkiln:review-applied') return;
9446
- setTimeout(() => { $('dialog-host').textContent = ''; pollJobs(); refresh(); }, 600);
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
10793
11204
 
10794
11205
  // src/gallery/server.ts
10795
- var MAX_EDIT_BYTES = 64 * 1024;
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 readFile16(asset.path);
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 readFile16(asset.path);
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 z7 } from "zod";
11029
- var GenerateRequestSchema = z7.object({
11030
- project: z7.string().min(1).optional(),
11031
- keys: z7.array(z7.string().min(1)).min(1).max(500),
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: z7.boolean().optional(),
11491
+ force: z8.boolean().optional(),
11034
11492
  /** Advance in-flight, review, or selected work without submitting anything. */
11035
- resume: z7.boolean().optional(),
11493
+ resume: z8.boolean().optional(),
11036
11494
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
11037
- refresh: z7.boolean().optional()
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/pipeline/salvage.ts
11292
- import { readFile as readFile17 } from "fs/promises";
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 (!existsSync18(p)) throw new Error(`Claim lockfile not found: ${p}`);
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 readFile17(p, "utf8")));
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 = path23.resolve(ownManifestPath);
12108
+ const own = path25.resolve(ownManifestPath);
11353
12109
  const siblingManifestPaths = [
11354
12110
  .../* @__PURE__ */ new Set([
11355
12111
  ...workspaceManifestPaths,
11356
- ...claimPaths.map((c) => path23.join(path23.dirname(path23.resolve(c)), "pixelkiln.manifest.json"))
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 (path23.resolve(siblingManifestPath) === own) continue;
11362
- if (!existsSync18(siblingManifestPath)) continue;
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: path23.basename(path23.dirname(siblingManifestPath)), manifest });
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 existsSync19 } from "fs";
11585
- import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
11586
- import path24 from "path";
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 && !existsSync19(lockPath)) {
11589
- throw new Error(`Refusing to prune without an existing lockfile at ${path24.resolve(lockPath)}`);
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 = path24.resolve(path24.dirname(lockPath), ".pixelkiln", "cache");
11592
- const remotePath = path24.resolve(cachePathFor(lockPath));
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 rm8(path24.join(contentDir, name), { force: true });
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 readFile18(remotePath, "utf8")));
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: existsSync19(contentDir),
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 = path24.join(contentDir, entry.name);
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 readFile18(file);
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: existsSync19(remotePath),
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 readFile18(remotePath, "utf8")));
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 mkdir7, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11978
- import path25 from "path";
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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: path25.relative(process.cwd(), ctx.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 = path25.join("_salvaged", `${assetId}.png`);
12173
- const outFile = path25.resolve(ctx.importDir, rel);
12174
- await mkdir7(path25.dirname(outFile), { recursive: true });
12175
- await writeFile9(outFile, buf);
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, path25.dirname(ctx.manifestPath)),
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 readFile19(ctx.manifestPath, "utf8"));
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 writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
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 path26 from "path";
12242
- import { existsSync as existsSync20 } from "fs";
12243
- import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
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 z8 } from "zod";
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 = z8.string().min(1).refine((value) => {
12252
- if (value.includes("\\") || path26.posix.isAbsolute(value)) return false;
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 = z8.object({
12257
- nodeId: z8.string().min(1),
12258
- input: z8.string().min(1)
13012
+ var RecipeBindingSchema = z9.object({
13013
+ nodeId: z9.string().min(1),
13014
+ input: z9.string().min(1)
12259
13015
  }).strict();
12260
- var RecipeSchema = z8.object({
12261
- $schema: z8.string().url().optional(),
12262
- format: z8.literal("pixelkiln-recipe"),
12263
- schemaVersion: z8.literal(1),
12264
- id: z8.string().regex(RECIPE_ID_RE, "expected provider/name"),
12265
- version: z8.string().regex(VERSION_RE, "expected x.y.z"),
12266
- provider: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
12267
- summary: z8.string().min(1).max(240),
12268
- files: z8.array(z8.object({
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: z8.enum(["workflow", "reference"]),
12271
- sha256: z8.string().regex(SHA256_RE)
13026
+ role: z9.enum(["workflow", "reference"]),
13027
+ sha256: z9.string().regex(SHA256_RE)
12272
13028
  }).strict()).default([]),
12273
- models: z8.array(z8.object({
13029
+ models: z9.array(z9.object({
12274
13030
  path: PortablePathSchema,
12275
- sha256: z8.string().regex(SHA256_RE),
12276
- source: z8.string().url(),
12277
- license: z8.string().min(1)
13031
+ sha256: z9.string().regex(SHA256_RE),
13032
+ source: z9.string().url(),
13033
+ license: z9.string().min(1)
12278
13034
  }).strict()).default([]),
12279
- styleId: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
13035
+ styleId: z9.string().regex(/^[a-z0-9][a-z0-9-]*$/),
12280
13036
  style: StyleSchema,
12281
- workflow: z8.object({
13037
+ workflow: z9.object({
12282
13038
  path: PortablePathSchema,
12283
- outputNodeId: z8.string().min(1),
12284
- numImages: z8.number().int().min(1).max(16),
12285
- bindings: z8.object({
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: z8.object({
12297
- stage: z8.enum(["composition-source", "production-candidate"]),
12298
- workingCanvas: z8.object({
12299
- width: z8.number().int().min(16),
12300
- height: z8.number().int().min(16)
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: z8.object({
12303
- min: z8.number().int().min(1),
12304
- max: z8.number().int().min(1)
13058
+ recommendedNativeSize: z9.object({
13059
+ min: z9.number().int().min(1),
13060
+ max: z9.number().int().min(1)
12305
13061
  }).strict(),
12306
- paletteColors: z8.object({
12307
- min: z8.number().int().min(1),
12308
- max: z8.number().int().min(1)
13062
+ paletteColors: z9.object({
13063
+ min: z9.number().int().min(1),
13064
+ max: z9.number().int().min(1)
12309
13065
  }).strict(),
12310
- background: z8.enum(["opaque", "transparent", "full-bleed"]),
12311
- checks: z8.array(z8.enum([
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: z8.array(z8.string().min(1)).default([])
13074
+ notes: z9.array(z9.string().min(1)).default([])
12319
13075
  }).strict(),
12320
- integrity: z8.object({
12321
- algorithm: z8.literal("sha256"),
12322
- digest: z8.string().regex(SHA256_RE)
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
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: z8.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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: z8.ZodIssueCode.custom,
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 = path26.resolve(recipePath);
13184
+ const absolute = path28.resolve(recipePath);
12429
13185
  let raw;
12430
13186
  try {
12431
- raw = JSON.parse(await readFile20(absolute, "utf8"));
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: path26.dirname(absolute), bundled };
13198
+ return { recipe: parsed.data, path: absolute, dir: path28.dirname(absolute), bundled };
12443
13199
  }
12444
13200
  async function walkRecipeFiles(root) {
12445
- if (!existsSync20(root)) return [];
13201
+ if (!existsSync21(root)) return [];
12446
13202
  const out = [];
12447
13203
  for (const entry of await readdir3(root, { withFileTypes: true })) {
12448
- const resolved = path26.join(root, entry.name);
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(path26.resolve(root))).map((file) => readRecipeFile(file, true))
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 = path26.resolve(target);
12483
- if (existsSync20(local)) {
12484
- const recipePath = path26.basename(local) === RECIPE_FILE ? local : path26.join(local, RECIPE_FILE);
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 = path26.resolve(root, ...file.path.split("/"));
12499
- if (!existsSync20(absolute)) {
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 ? path26.resolve(options.modelRoot) : null;
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 = path26.relative(cwd, destination).split(path26.sep).join("/") || ".";
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 = path26.resolve(options.cwd ?? process.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 ? path26.resolve(cwd, options.out) : path26.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
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: path26.join(loaded.dir, ...file.path.split("/")) }))
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: path26.join(destination, ...file.path.split("/")),
12573
- data: await readFile20(file.source)
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 (!existsSync20(file.path)) continue;
12578
- const current = await readFile20(file.path);
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 path27 from "path";
12602
- import { existsSync as existsSync21 } from "fs";
12603
- import { readFile as readFile21 } from "fs/promises";
12604
- import { z as z9 } from "zod";
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 = z9.string().min(1).refine(
12609
- (value) => !path27.posix.isAbsolute(value) && !value.includes("\\"),
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 = z9.object({
12623
- requireExactHash: z9.boolean(),
12624
- maxColorCountIncrease: z9.number().int().min(0),
12625
- maxNewColors: z9.number().int().min(0),
12626
- maxTransparencyDelta: z9.number().min(0).max(1),
12627
- maxPartialAlphaIncrease: z9.number().min(0).max(1),
12628
- maxEdgeDensityDelta: z9.number().min(0).max(1),
12629
- maxEdgeContrastDrop: z9.number().min(0).max(1),
12630
- maxIsolatedPixelIncrease: z9.number().min(0).max(1)
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 = z9.object({
12633
- sha256: z9.string().regex(SHA256_RE2),
12634
- width: z9.number().int().min(1),
12635
- height: z9.number().int().min(1),
12636
- colorCount: z9.number().int().min(0),
12637
- colors: z9.array(z9.string().regex(HEX_RE)),
12638
- transparency: z9.number().min(0).max(1),
12639
- partialAlpha: z9.number().min(0).max(1),
12640
- edgeDensity: z9.number().min(0).max(1),
12641
- meanEdgeContrast: z9.number().min(0).max(1),
12642
- isolatedPixelRatio: z9.number().min(0).max(1)
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 = z9.object({
12645
- id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
13400
+ var QualityBaselineCaseSchema = z10.object({
13401
+ id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12646
13402
  file: QualityPathSchema,
12647
- record: z9.object({
13403
+ record: z10.object({
12648
13404
  path: QualityPathSchema,
12649
- sha256: z9.string().regex(SHA256_RE2)
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 = z9.object({
12655
- $schema: z9.string().url().optional(),
12656
- format: z9.literal("pixelkiln-quality-baseline"),
12657
- schemaVersion: z9.literal(1),
12658
- cases: z9.array(QualityBaselineCaseSchema).min(1)
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: z9.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
13418
+ context.addIssue({ code: z10.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
12663
13419
  }
12664
13420
  });
12665
- var QualityInputSchema = z9.object({
12666
- id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12667
- path: z9.string().min(1),
12668
- record: z9.string().min(1).optional(),
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 = path27.resolve(file);
13512
+ const absolute = path29.resolve(file);
12757
13513
  let bytes;
12758
13514
  try {
12759
- bytes = await readFile21(absolute);
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 = z9.array(QualityInputSchema).min(1).safeParse(raw);
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 = path27.dirname(path27.resolve(inputsFilePath));
13534
+ const root = path29.dirname(path29.resolve(inputsFilePath));
12779
13535
  return parsed.data.map((entry) => ({
12780
13536
  id: entry.id,
12781
- file: path27.resolve(root, entry.path),
12782
- ...entry.record ? { record: path27.resolve(root, entry.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 path27.relative(from, path27.resolve(to)).split(path27.sep).join("/") || ".";
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) => path27.resolve(path27.dirname(recordPath), output.path));
12806
- if (!outputs.includes(path27.resolve(imagePath))) {
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 = path27.resolve(baselinePath);
13568
+ const absolute = path29.resolve(baselinePath);
12813
13569
  for (const input of inputs) {
12814
- if (path27.resolve(input.file) === absolute || input.record && path27.resolve(input.record) === absolute) {
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 = path27.dirname(absolute);
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 (existsSync21(absolute) && !options.force) {
12846
- const current = await readFile21(absolute);
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 = path27.resolve(baselinePath);
13611
+ const absolute = path29.resolve(baselinePath);
12856
13612
  let raw;
12857
13613
  try {
12858
- raw = JSON.parse(await readFile21(absolute, "utf8"));
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 = path27.resolve(baselinePath);
13684
+ const absolute = path29.resolve(baselinePath);
12929
13685
  const baseline = await readQualityBaseline(absolute);
12930
- const root = path27.dirname(absolute);
13686
+ const root = path29.dirname(absolute);
12931
13687
  const cases = [];
12932
13688
  for (const entry of baseline.cases) {
12933
- const file = path27.resolve(root, entry.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 = path27.resolve(root, entry.record.path);
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 = path28.dirname(ctx.loaded.path);
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
- `${path28.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.`
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 = path28.resolve(file);
13838
+ const absolute = path30.resolve(file);
13081
13839
  return {
13082
13840
  id,
13083
13841
  path: absolute,
13084
- sha256: existsSync22(absolute) ? await sha256File(absolute) : null,
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") ?? path28.join(path28.dirname(path28.resolve(manifest)), "pixelkiln.lock.json"),
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 (!existsSync22(workspacePath)) {
14482
+ if (!existsSync23(workspacePath)) {
13659
14483
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13660
14484
  }
13661
- const dir = path28.dirname(path28.resolve(workspacePath));
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 readFile22(new URL("../package.json", import.meta.url), "utf8")
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 readFile22(path28.resolve(args.inputs), "utf8"));
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 ${path28.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
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 (path28.resolve(args.inputs) === path28.resolve(args.out)) {
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"} ${path28.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
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(` ${path28.relative(process.cwd(), result.destination) || "."}`);
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 ${path28.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
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 ${path28.relative(process.cwd(), result2.output)}`);
13851
- log(` quality record: ${path28.relative(process.cwd(), result2.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 ${path28.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
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 ${path28.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path28.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
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"}: ${path28.relative(process.cwd(), result.output || result.record)}`);
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(path28.dirname(path28.resolve(args.manifest)));
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 = path28.resolve(args.from);
13913
- if (!existsSync22(root)) throw new Error(`No directory at ${root}`);
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 = path28.resolve(args.out ?? "pixelkiln.manifest.json");
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 ?? path28.basename(path28.dirname(target)),
14750
+ args.name ?? path30.basename(path30.dirname(target)),
13923
14751
  args.styles[0] ?? "base",
13924
14752
  generator,
13925
- path28.relative(path28.dirname(target), root) || ".",
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 ${path28.relative(process.cwd(), root)}`);
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 ${path28.relative(process.cwd(), target)}`);
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 ${path28.relative(process.cwd(), target)} --write-prompts
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 readFile22(path28.resolve(args.inputs), "utf8"));
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 = path28.resolve(args.out.replace(/\.png$/, ""));
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(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
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 = path28.resolve(args.workspace ?? "pixelkiln.workspace.json");
14006
- const dir = path28.dirname(workspacePath);
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 = path28.resolve(args.target);
14836
+ const manifestPath = path30.resolve(args.target);
14009
14837
  const loadedTarget = await loadManifest(manifestPath);
14010
- const lockPath = args.explicitLock ? path28.resolve(args.explicitLock) : path28.join(path28.dirname(manifestPath), "pixelkiln.lock.json");
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 ${path28.relative(process.cwd(), workspacePath)}`);
14858
+ log(` registered "${id}" in ${path30.relative(process.cwd(), workspacePath)}`);
14031
14859
  log(` manifest: ${project.manifest}`);
14032
14860
  log(` lock: ${project.lock}`);
14033
- if (!existsSync22(lockPath)) {
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 = path28.resolve(args.target);
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 ${path28.relative(process.cwd(), workspacePath)}`);
14881
+ log(` removed "${match.id}" from ${path30.relative(process.cwd(), workspacePath)}`);
14054
14882
  return;
14055
14883
  }
14056
14884
  if (args.subcommand === "list") {
14057
- if (!existsSync22(workspacePath)) {
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 ${path28.relative(process.cwd(), workspacePath)}`);
14893
+ log(` no projects registered in ${path30.relative(process.cwd(), workspacePath)}`);
14066
14894
  } else {
14067
- log(` ${ws.projects.length} project(s) in ${path28.relative(process.cwd(), workspacePath)}:`);
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 (!existsSync22(workspacePath)) {
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: ${path28.relative(process.cwd(), workspacePath)}`);
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 = path28.resolve(args.workspace);
14133
- if (!existsSync22(workspacePath)) {
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(path28.dirname(workspacePath), project);
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 (!existsSync22(path28.resolve(args.manifest))) {
14996
+ if (!existsSync23(path30.resolve(args.manifest))) {
14169
14997
  throw new Error(
14170
- `No manifest at ${path28.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
14998
+ `No manifest at ${path30.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
14171
14999
  );
14172
15000
  }
14173
- const manifestDir = path28.dirname(path28.resolve(args.manifest));
15001
+ const manifestDir = path30.dirname(path30.resolve(args.manifest));
14174
15002
  const envFiles2 = [...loadEnvFiles(manifestDir)];
14175
- if (path28.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
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: () => path28.resolve(args.manifest),
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 ${path28.relative(process.cwd(), item.record)} --reviewer "Your Name"`
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 (!existsSync22(file) || await sha256File(file) !== output.sha256) {
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 = path28.dirname(path28.resolve(args.manifest));
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 ? path28.resolve(args.out.replace(/\.png$/, "")) : path28.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
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(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
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 = path28.dirname(path28.resolve(args.manifest));
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 = path28.resolve(manifestDir2, style.mount.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" || path28.resolve(source.path) !== out
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(` ${path28.relative(process.cwd(), out)} + atlas/provenance JSON`);
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 = path28.dirname(path28.resolve(args.manifest));
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 = path28.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
14666
- const base = args.out ? path28.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
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: path28.basename(`${base}.png`),
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: path28.basename(`${base}.png`),
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
- ` ${path28.relative(process.cwd(), base)}.png + ${path28.basename(base)}${result.extension} + .pixelkiln.json`
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(path28.resolve(args.manifest), lock, {
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 ${path28.relative(process.cwd(), args.manifest)}`);
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 = path28.resolve(args.lock);
15610
+ const ownLock = path30.resolve(args.lock);
14783
15611
  let workspaceProjects = [];
14784
15612
  let workspaceDir = "";
14785
15613
  if (args.workspace) {
14786
- const workspacePath = path28.resolve(args.workspace);
14787
- workspaceDir = path28.dirname(workspacePath);
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
- ...existsSync22(ownLock) ? [ownLock] : [],
14797
- ...args.claims.map((c) => path28.resolve(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(` ${path28.relative(process.cwd(), p)}`);
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 === path28.resolve(args.manifest))) {
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: path28.resolve(loaded.root, style.outDir),
15721
+ importDir: path30.resolve(loaded.root, style.outDir),
14894
15722
  lock,
14895
15723
  lockPath: args.lock
14896
15724
  },