pixelkiln 0.30.0 → 0.32.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
@@ -6431,7 +6431,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
6431
6431
  }
6432
6432
 
6433
6433
  // src/pipeline/fetch.ts
6434
- import { existsSync as existsSync10 } from "fs";
6434
+ import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
6435
6435
  import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
6436
6436
  import path14 from "path";
6437
6437
 
@@ -6487,7 +6487,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6487
6487
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
6488
6488
  if (!opts.repair) return false;
6489
6489
  const spec = specByKey.get(key);
6490
- return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync10(resolveOutputPath(output.path, spec.root)));
6490
+ if (e.outputs.length === 0 || !spec) return true;
6491
+ return e.outputs.some((output) => {
6492
+ const file = resolveOutputPath(output.path, spec.root);
6493
+ if (!existsSync10(file)) return true;
6494
+ return Boolean(opts.force) && sha256(readFileSync4(file)) !== output.sha256;
6495
+ });
6491
6496
  });
6492
6497
  const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
6493
6498
  let cursor = 0;
@@ -9364,6 +9369,7 @@ function renderGallery(snapshot, opts = {}) {
9364
9369
  .sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
9365
9370
  .sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
9366
9371
  .sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
9372
+ .sheet.editor-host .rbar .chip { color:var(--dim); font-size:12.5px; display:inline-flex; align-items:center; gap:5px; margin-right:6px; }
9367
9373
  .sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
9368
9374
  .sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
9369
9375
  .sheet.editor-host .stage { position:relative; min-height:0; }
@@ -9411,6 +9417,8 @@ function renderGallery(snapshot, opts = {}) {
9411
9417
  .tray > button { padding:5px 11px; font-size:12.5px; }
9412
9418
  @media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
9413
9419
  .drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
9420
+ .drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
9421
+ .drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
9414
9422
  .pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
9415
9423
  .pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
9416
9424
  background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
@@ -9620,7 +9628,7 @@ function renderJobs() {
9620
9628
  const row = el('div', 'job');
9621
9629
  const ph = el('span', 'ph');
9622
9630
  ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
9623
- const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
9631
+ const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'restore' ? 'restore ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
9624
9632
  (job.project ? ' in ' + job.project : '');
9625
9633
  const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
9626
9634
  last.title = job.keys.join('\\n');
@@ -9745,13 +9753,16 @@ function budgetLine() {
9745
9753
 
9746
9754
  // The confirm step: what will be sent, what it is estimated to cost, and what
9747
9755
  // this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
9748
- function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
9756
+ function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
9749
9757
  const host = $('dialog-host');
9750
9758
  host.textContent = '';
9751
9759
  const wrap = el('div', 'dialog');
9752
9760
  const form = el('form');
9753
9761
  const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
9762
+ const free = resume || refresh || restore;
9763
+ const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
9754
9764
  form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
9765
+ : restore ? 'Restore ' + noun
9755
9766
  : resume ? 'Resume ' + noun
9756
9767
  : (force ? 'Regenerate ' : 'Generate ') + noun));
9757
9768
  const table = el('table');
@@ -9761,9 +9772,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
9761
9772
  for (const item of items) {
9762
9773
  const tr = el('tr');
9763
9774
  tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
9764
- el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
9775
+ el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
9765
9776
  table.append(tr);
9766
- if (!resume && !refresh) {
9777
+ if (!free) {
9767
9778
  const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
9768
9779
  g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
9769
9780
  }
@@ -9772,6 +9783,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
9772
9783
  const sum = el('div', 'sum');
9773
9784
  if (refresh) {
9774
9785
  sum.append(el('div', null, 'Re-downloads each object from the provider and replaces the local file only when the object changed upstream \u2014 for example after editing it in the provider\u2019s own editor. Unchanged objects are left alone; a file you changed locally is refused. Nothing is submitted.'));
9786
+ } else if (restore) {
9787
+ sum.append(el('div', null, 'Puts the recorded bytes back on disk from the local cache or the provider object. Nothing is generated or submitted.'));
9788
+ if (force && replacing.length) sum.append(el('div', 'warn', 'The file on disk was changed after PixelKiln wrote it. Restoring discards that change. To keep it instead, cancel and use Edit by hand or Edit in browser first: the edit is copied beside the generated file, and the record stays intact.'));
9775
9789
  } else if (resume) {
9776
9790
  sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
9777
9791
  } else {
@@ -9784,12 +9798,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
9784
9798
  if (left !== null && g.cost > left) line.className = 'warn';
9785
9799
  sum.append(line);
9786
9800
  }
9787
- if (force) sum.append(el('div', 'warn', 'Regenerating replaces the current art. The previous file is replaced when the new result is fetched; the provider objects it came from are not deleted.'));
9801
+ if (force && replacing.length) sum.append(el('div', 'warn', (replacing.length === 1 ? 'This file' : replacing.length + ' of these files') + ' changed on disk after PixelKiln wrote it (or was never recorded). Generating replaces it and that change is lost. To keep it, cancel and use Edit by hand or Edit in browser first.'));
9802
+ else if (force) sum.append(el('div', 'warn', 'Regenerating replaces the current art. The previous file is replaced when the new result is fetched; the provider objects it came from are not deleted.'));
9788
9803
  sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
9789
9804
  }
9790
9805
  form.append(sum);
9791
9806
  const actions = el('div', 'actions');
9792
- const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
9807
+ const go = el('button', 'primary', refresh ? 'Pull changes' : restore ? (force && replacing.length ? 'Restore and discard my change' : 'Restore') : resume ? 'Resume' : force && replacing.length ? 'Generate and replace' : 'Generate');
9793
9808
  go.type = 'submit';
9794
9809
  const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
9795
9810
  const msg = el('span', 'msg');
@@ -9804,6 +9819,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
9804
9819
  if (force) body.force = true;
9805
9820
  if (resume) body.resume = true;
9806
9821
  if (refresh) body.refresh = true;
9822
+ if (restore) body.restore = true;
9807
9823
  const job = await postGenerate(body);
9808
9824
  host.textContent = '';
9809
9825
  GEN.jobs.unshift(job);
@@ -9892,11 +9908,16 @@ function openEditorSheet(item) {
9892
9908
  const status = el('span', 'st');
9893
9909
  const acts = el('div', 'acts');
9894
9910
  const msg = el('span', 'msg');
9911
+ const compare = el('label', 'chip'); compare.hidden = true;
9912
+ compare.title = 'The generated art on a locked layer at 50%, for comparison; never part of the saved file';
9913
+ const compareBox = el('input'); compareBox.type = 'checkbox'; compareBox.checked = true;
9914
+ compareBox.onchange = () => showReference(compareBox.checked);
9915
+ compare.append(compareBox, document.createTextNode(' show generated'));
9895
9916
  const save = el('button', 'primary', 'Save to project'); save.type = 'button';
9896
9917
  save.onclick = () => requestEditorSave(false);
9897
9918
  const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
9898
9919
  saveClose.onclick = () => requestEditorSave(true);
9899
- acts.append(msg, save, saveClose);
9920
+ acts.append(msg, compare, save, saveClose);
9900
9921
  bar.append(close, title, status, acts);
9901
9922
  const stage = el('div', 'stage');
9902
9923
  const frame = el('iframe');
@@ -9909,7 +9930,7 @@ function openEditorSheet(item) {
9909
9930
  stage.append(frame, loading);
9910
9931
  panel.append(bar, stage);
9911
9932
  host.append(scrim, panel);
9912
- SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
9933
+ SHEET = { item, frame, loading, status, msg, save, saveClose, compare, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, sentReference: false, reference: false, roles: null };
9913
9934
  sheetStatus('cool', 'loading');
9914
9935
  sheetButtons();
9915
9936
  close.focus({ preventScroll: true });
@@ -9929,7 +9950,11 @@ async function sendOpen() {
9929
9950
  sheetStatus('bad', 'unsupported');
9930
9951
  return;
9931
9952
  }
9953
+ // When an edit is being opened, the generated art rides along as a
9954
+ // reference layer: what the editor shows through at 50% for comparison.
9955
+ const referenceFiles = sources.fromEdit && SHEET.ready.version >= 4 && item.outputs.every((o) => o.exists && o.url) ? item.outputs : null;
9932
9956
  const pngs = [];
9957
+ const reference = [];
9933
9958
  let pxo = null;
9934
9959
  try {
9935
9960
  for (const file of sources.files) {
@@ -9941,6 +9966,10 @@ async function sendOpen() {
9941
9966
  const p = await fetch(projectUrl, { cache: 'no-store' });
9942
9967
  if (p.ok) pxo = await p.arrayBuffer();
9943
9968
  }
9969
+ for (const file of referenceFiles || []) {
9970
+ const res = await fetch(file.url, { cache: 'no-store' });
9971
+ if (res.ok) reference.push({ role: file.role || null, png: await res.arrayBuffer() });
9972
+ }
9944
9973
  } catch (err) {
9945
9974
  if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
9946
9975
  return;
@@ -9965,9 +9994,18 @@ async function sendOpen() {
9965
9994
  transfer.push(message.png);
9966
9995
  }
9967
9996
  if (pxo) { message.pxo = pxo; transfer.push(pxo); }
9997
+ if (reference.length === (referenceFiles || []).length && reference.length) {
9998
+ message.reference = reference;
9999
+ for (const r of reference) transfer.push(r.png);
10000
+ }
10001
+ SHEET.sentReference = !!message.reference;
9968
10002
  SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
9969
10003
  sheetStatus('cool', 'opening');
9970
10004
  }
10005
+ function showReference(visible) {
10006
+ if (!SHEET || !SHEET.opened || !SHEET.reference) return;
10007
+ SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:reference', visible }, location.origin);
10008
+ }
9971
10009
  function requestEditorSave(close) {
9972
10010
  if (!SHEET || !SHEET.opened || SHEET.pending) return;
9973
10011
  const request = 'save-' + (++SHEET.requests);
@@ -9991,10 +10029,13 @@ async function onEditorMessage(m) {
9991
10029
  case 'pixelkiln:opened':
9992
10030
  SHEET.opened = true;
9993
10031
  SHEET.loading.remove();
10032
+ SHEET.reference = !!m.reference;
10033
+ SHEET.compare.hidden = !SHEET.reference;
9994
10034
  if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file' + (m.frames > 1 ? 's' : '') + ' with ' + (m.layers === 1 ? 'the layer' : m.layers + ' layers') + (m.frames > 1 ? ' and ' + m.frames + ' ' + memberNoun(SHEET.item, m.frames) : '') + ' restored');
9995
10035
  else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
9996
10036
  else if (m.source === 'frames') sheetStatus('ok', (SHEET.item.edits.length ? 'editing the edit files' : 'editing a copy of the generated ' + memberNoun(SHEET.item, m.frames)) + ' as ' + m.frames + ' frames' + (isFrameSet(SHEET.item) ? '' : ', one per ' + memberNoun(SHEET.item, 1)));
9997
10037
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
10038
+ if (SHEET.reference) SHEET.status.append(document.createTextNode(' \xB7 generated art shown through at 50%'));
9998
10039
  sheetButtons();
9999
10040
  break;
10000
10041
  case 'pixelkiln:dirty':
@@ -10437,6 +10478,13 @@ function generateActions(item) {
10437
10478
  };
10438
10479
  if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
10439
10480
  else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
10481
+ else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
10482
+ // The file changed after download. Putting the recorded bytes back and
10483
+ // regenerating both discard that change, so both say so and ask.
10484
+ add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
10485
+ add('Regenerate' + cost, null, { force: true });
10486
+ } else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
10487
+ else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
10440
10488
  if (item.state === 'in-flight' || item.state === 'recoverable') {
10441
10489
  // A job already parked this asset in review: go straight to its sheet.
10442
10490
  const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
@@ -11127,6 +11175,15 @@ function renderDrawer() {
11127
11175
  if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
11128
11176
  body.append(stateNode(item.state, item.reason));
11129
11177
  if (GENERATION) body.append(generateActions(item));
11178
+ // Writes are opt-in flags on the command line; say so where the buttons would be.
11179
+ if ((!EDITABLE || !GENERATION) && item.declared) {
11180
+ const missing = [];
11181
+ if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
11182
+ if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
11183
+ const hint = el('div', 'state-dim readonly-hint');
11184
+ hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
11185
+ body.append(hint);
11186
+ }
11130
11187
  if (canEdit && ui.editing === item.id) body.append(editForm(item));
11131
11188
  // Plan already quotes the error as the reason for a failed entry; only a
11132
11189
  // stale or superseded failure needs its own line.
@@ -11717,13 +11774,21 @@ import { z as z8 } from "zod";
11717
11774
  var GenerateRequestSchema = z8.object({
11718
11775
  project: z8.string().min(1).optional(),
11719
11776
  keys: z8.array(z8.string().min(1)).min(1).max(500),
11720
- /** Regenerate work that is up to date, as `gen --force` does. */
11777
+ /**
11778
+ * Regenerate work that is up to date, as `gen --force` does, and let the
11779
+ * fetch replace a locally modified or untracked file. With `restore`, put
11780
+ * the recorded bytes back over a modified file.
11781
+ */
11721
11782
  force: z8.boolean().optional(),
11722
11783
  /** Advance in-flight, review, or selected work without submitting anything. */
11723
11784
  resume: z8.boolean().optional(),
11724
11785
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
11725
- refresh: z8.boolean().optional()
11726
- }).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
11786
+ refresh: z8.boolean().optional(),
11787
+ /** Put recorded outputs back on disk from the cache or the provider, without generating. */
11788
+ restore: z8.boolean().optional()
11789
+ }).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
11790
+ message: "resume, refresh, and restore are exclusive"
11791
+ });
11727
11792
  var GenerateRequestError = class extends Error {
11728
11793
  constructor(message6, status = 400) {
11729
11794
  super(message6);
@@ -11811,7 +11876,9 @@ function createGenerateHandlers(opts) {
11811
11876
  for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
11812
11877
  const provider = opts.providerFor(job.project ?? void 0, providerId);
11813
11878
  const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
11814
- onProgress: (line) => say(job, line)
11879
+ onProgress: (line) => say(job, line),
11880
+ // A forced regeneration replaces whatever is on disk, as `gen --force`.
11881
+ force: job.force
11815
11882
  });
11816
11883
  job.counts.downloaded += res.downloaded;
11817
11884
  if (res.downloaded || res.failed) {
@@ -11826,12 +11893,32 @@ function createGenerateHandlers(opts) {
11826
11893
  job.finishedAt = now().toISOString();
11827
11894
  }
11828
11895
  }
11896
+ async function restore(job, ctx, specs) {
11897
+ job.phase = "fetching";
11898
+ for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
11899
+ const provider = opts.providerFor(job.project ?? void 0, providerId);
11900
+ const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
11901
+ onProgress: (line) => say(job, line),
11902
+ repair: true,
11903
+ force: job.force
11904
+ });
11905
+ job.counts.downloaded += res.downloaded;
11906
+ say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
11907
+ if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
11908
+ }
11909
+ job.phase = "done";
11910
+ job.finishedAt = now().toISOString();
11911
+ }
11829
11912
  async function run(job, ctx, specs, groups) {
11830
11913
  try {
11831
11914
  if (job.mode === "refresh") {
11832
11915
  await refresh(job, ctx, specs);
11833
11916
  return;
11834
11917
  }
11918
+ if (job.mode === "restore") {
11919
+ await restore(job, ctx, specs);
11920
+ return;
11921
+ }
11835
11922
  if (job.mode === "generate") {
11836
11923
  job.phase = "submitting";
11837
11924
  for (const group of groups) {
@@ -11894,8 +11981,16 @@ function createGenerateHandlers(opts) {
11894
11981
  const holder = busy.get(`${request.project ?? ""}:${key}`);
11895
11982
  if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
11896
11983
  }
11984
+ if (request.restore) {
11985
+ for (const key of request.keys) {
11986
+ const entry = ctx.lock.entries[key];
11987
+ if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
11988
+ throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
11989
+ }
11990
+ }
11991
+ }
11897
11992
  let groups = [];
11898
- if (!request.resume && !request.refresh) {
11993
+ if (!request.resume && !request.refresh && !request.restore) {
11899
11994
  const plan = await buildPlan(specs, ctx.lock, { force: request.force });
11900
11995
  if (!plan.actionable.length) {
11901
11996
  const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
@@ -11915,7 +12010,8 @@ function createGenerateHandlers(opts) {
11915
12010
  id: randomBytes2(8).toString("hex"),
11916
12011
  project: request.project ?? null,
11917
12012
  keys: request.keys,
11918
- mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
12013
+ mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
12014
+ force: Boolean(request.force),
11919
12015
  phase: "queued",
11920
12016
  startedAt: now().toISOString(),
11921
12017
  finishedAt: null,
@@ -11927,7 +12023,7 @@ function createGenerateHandlers(opts) {
11927
12023
  };
11928
12024
  jobs.set(job.id, job);
11929
12025
  contexts.set(job.id, ctx);
11930
- say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
12026
+ say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.restore ? `restoring ${request.keys.length} asset(s) at no cost${request.force ? " (replacing modified files)" : ""}` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
11931
12027
  void run(job, ctx, specs, groups);
11932
12028
  return job;
11933
12029
  },
@@ -11992,8 +12088,8 @@ var pin_default = {
11992
12088
  pixelorama: "v1.2.2",
11993
12089
  godot: "4.7.2",
11994
12090
  extensionsApi: 9,
11995
- protocol: 3,
11996
- release: "editor-pixelorama-v1.2.2-pk.14",
12091
+ protocol: 4,
12092
+ release: "editor-pixelorama-v1.2.2-pk.18",
11997
12093
  files: {
11998
12094
  "index.apple-touch-icon.png": {
11999
12095
  sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
@@ -12008,7 +12104,7 @@ var pin_default = {
12008
12104
  bytes: 7298
12009
12105
  },
12010
12106
  "index.html": {
12011
- sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
12107
+ sha256: "d9adf06036660c4b76ef5386e872bc03dc0d5b0628fd6ac660d480893ee1f47a",
12012
12108
  bytes: 5442
12013
12109
  },
12014
12110
  "index.icon.png": {
@@ -12020,8 +12116,8 @@ var pin_default = {
12020
12116
  bytes: 279815
12021
12117
  },
12022
12118
  "index.pck": {
12023
- sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
12024
- bytes: 6429628
12119
+ sha256: "a41d9c3a39aa99ea5ee49ddcedfeb8b092006aa75c0de67139f13297c17f42e4",
12120
+ bytes: 6431692
12025
12121
  },
12026
12122
  "index.png": {
12027
12123
  sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
@@ -13990,7 +14086,7 @@ function announceGalleryReady(url, count, stdout = process.stdout, stderr = proc
13990
14086
  gallery of ${count} generation${count === 1 ? "" : "s"}: ${url}
13991
14087
  ` + (notes.length ? ` (${notes.join("; ")}; Ctrl+C to stop)
13992
14088
 
13993
- ` : " (read-only; press Ctrl+C to stop)\n\n")
14089
+ ` : " (read-only \u2014 add --edit to change prompts or edit sprites, --budget <n> to generate; Ctrl+C to stop)\n\n")
13994
14090
  );
13995
14091
  }
13996
14092
  function sessionBudget(args) {