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/index.cjs CHANGED
@@ -8041,7 +8041,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8041
8041
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
8042
8042
  if (!opts.repair) return false;
8043
8043
  const spec = specByKey.get(key);
8044
- return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0, import_node_fs15.existsSync)(resolveOutputPath(output.path, spec.root)));
8044
+ if (e.outputs.length === 0 || !spec) return true;
8045
+ return e.outputs.some((output) => {
8046
+ const file = resolveOutputPath(output.path, spec.root);
8047
+ if (!(0, import_node_fs15.existsSync)(file)) return true;
8048
+ return Boolean(opts.force) && sha256((0, import_node_fs15.readFileSync)(file)) !== output.sha256;
8049
+ });
8045
8050
  });
8046
8051
  const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
8047
8052
  let cursor = 0;
@@ -10810,6 +10815,7 @@ function renderGallery(snapshot, opts = {}) {
10810
10815
  .sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
10811
10816
  .sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
10812
10817
  .sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
10818
+ .sheet.editor-host .rbar .chip { color:var(--dim); font-size:12.5px; display:inline-flex; align-items:center; gap:5px; margin-right:6px; }
10813
10819
  .sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
10814
10820
  .sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
10815
10821
  .sheet.editor-host .stage { position:relative; min-height:0; }
@@ -10857,6 +10863,8 @@ function renderGallery(snapshot, opts = {}) {
10857
10863
  .tray > button { padding:5px 11px; font-size:12.5px; }
10858
10864
  @media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
10859
10865
  .drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
10866
+ .drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
10867
+ .drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
10860
10868
  .pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
10861
10869
  .pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
10862
10870
  background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
@@ -11066,7 +11074,7 @@ function renderJobs() {
11066
11074
  const row = el('div', 'job');
11067
11075
  const ph = el('span', 'ph');
11068
11076
  ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
11069
- const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
11077
+ 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') +
11070
11078
  (job.project ? ' in ' + job.project : '');
11071
11079
  const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
11072
11080
  last.title = job.keys.join('\\n');
@@ -11191,13 +11199,16 @@ function budgetLine() {
11191
11199
 
11192
11200
  // The confirm step: what will be sent, what it is estimated to cost, and what
11193
11201
  // this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
11194
- function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
11202
+ function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
11195
11203
  const host = $('dialog-host');
11196
11204
  host.textContent = '';
11197
11205
  const wrap = el('div', 'dialog');
11198
11206
  const form = el('form');
11199
11207
  const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
11208
+ const free = resume || refresh || restore;
11209
+ const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
11200
11210
  form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
11211
+ : restore ? 'Restore ' + noun
11201
11212
  : resume ? 'Resume ' + noun
11202
11213
  : (force ? 'Regenerate ' : 'Generate ') + noun));
11203
11214
  const table = el('table');
@@ -11207,9 +11218,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
11207
11218
  for (const item of items) {
11208
11219
  const tr = el('tr');
11209
11220
  tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
11210
- el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
11221
+ el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
11211
11222
  table.append(tr);
11212
- if (!resume && !refresh) {
11223
+ if (!free) {
11213
11224
  const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
11214
11225
  g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
11215
11226
  }
@@ -11218,6 +11229,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
11218
11229
  const sum = el('div', 'sum');
11219
11230
  if (refresh) {
11220
11231
  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.'));
11232
+ } else if (restore) {
11233
+ 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.'));
11234
+ 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.'));
11221
11235
  } else if (resume) {
11222
11236
  sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
11223
11237
  } else {
@@ -11230,12 +11244,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
11230
11244
  if (left !== null && g.cost > left) line.className = 'warn';
11231
11245
  sum.append(line);
11232
11246
  }
11233
- 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.'));
11247
+ 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.'));
11248
+ 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.'));
11234
11249
  sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
11235
11250
  }
11236
11251
  form.append(sum);
11237
11252
  const actions = el('div', 'actions');
11238
- const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
11253
+ 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');
11239
11254
  go.type = 'submit';
11240
11255
  const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
11241
11256
  const msg = el('span', 'msg');
@@ -11250,6 +11265,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
11250
11265
  if (force) body.force = true;
11251
11266
  if (resume) body.resume = true;
11252
11267
  if (refresh) body.refresh = true;
11268
+ if (restore) body.restore = true;
11253
11269
  const job = await postGenerate(body);
11254
11270
  host.textContent = '';
11255
11271
  GEN.jobs.unshift(job);
@@ -11338,11 +11354,16 @@ function openEditorSheet(item) {
11338
11354
  const status = el('span', 'st');
11339
11355
  const acts = el('div', 'acts');
11340
11356
  const msg = el('span', 'msg');
11357
+ const compare = el('label', 'chip'); compare.hidden = true;
11358
+ compare.title = 'The generated art on a locked layer at 50%, for comparison; never part of the saved file';
11359
+ const compareBox = el('input'); compareBox.type = 'checkbox'; compareBox.checked = true;
11360
+ compareBox.onchange = () => showReference(compareBox.checked);
11361
+ compare.append(compareBox, document.createTextNode(' show generated'));
11341
11362
  const save = el('button', 'primary', 'Save to project'); save.type = 'button';
11342
11363
  save.onclick = () => requestEditorSave(false);
11343
11364
  const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
11344
11365
  saveClose.onclick = () => requestEditorSave(true);
11345
- acts.append(msg, save, saveClose);
11366
+ acts.append(msg, compare, save, saveClose);
11346
11367
  bar.append(close, title, status, acts);
11347
11368
  const stage = el('div', 'stage');
11348
11369
  const frame = el('iframe');
@@ -11355,7 +11376,7 @@ function openEditorSheet(item) {
11355
11376
  stage.append(frame, loading);
11356
11377
  panel.append(bar, stage);
11357
11378
  host.append(scrim, panel);
11358
- SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
11379
+ 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 };
11359
11380
  sheetStatus('cool', 'loading');
11360
11381
  sheetButtons();
11361
11382
  close.focus({ preventScroll: true });
@@ -11375,7 +11396,11 @@ async function sendOpen() {
11375
11396
  sheetStatus('bad', 'unsupported');
11376
11397
  return;
11377
11398
  }
11399
+ // When an edit is being opened, the generated art rides along as a
11400
+ // reference layer: what the editor shows through at 50% for comparison.
11401
+ const referenceFiles = sources.fromEdit && SHEET.ready.version >= 4 && item.outputs.every((o) => o.exists && o.url) ? item.outputs : null;
11378
11402
  const pngs = [];
11403
+ const reference = [];
11379
11404
  let pxo = null;
11380
11405
  try {
11381
11406
  for (const file of sources.files) {
@@ -11387,6 +11412,10 @@ async function sendOpen() {
11387
11412
  const p = await fetch(projectUrl, { cache: 'no-store' });
11388
11413
  if (p.ok) pxo = await p.arrayBuffer();
11389
11414
  }
11415
+ for (const file of referenceFiles || []) {
11416
+ const res = await fetch(file.url, { cache: 'no-store' });
11417
+ if (res.ok) reference.push({ role: file.role || null, png: await res.arrayBuffer() });
11418
+ }
11390
11419
  } catch (err) {
11391
11420
  if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
11392
11421
  return;
@@ -11411,9 +11440,18 @@ async function sendOpen() {
11411
11440
  transfer.push(message.png);
11412
11441
  }
11413
11442
  if (pxo) { message.pxo = pxo; transfer.push(pxo); }
11443
+ if (reference.length === (referenceFiles || []).length && reference.length) {
11444
+ message.reference = reference;
11445
+ for (const r of reference) transfer.push(r.png);
11446
+ }
11447
+ SHEET.sentReference = !!message.reference;
11414
11448
  SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
11415
11449
  sheetStatus('cool', 'opening');
11416
11450
  }
11451
+ function showReference(visible) {
11452
+ if (!SHEET || !SHEET.opened || !SHEET.reference) return;
11453
+ SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:reference', visible }, location.origin);
11454
+ }
11417
11455
  function requestEditorSave(close) {
11418
11456
  if (!SHEET || !SHEET.opened || SHEET.pending) return;
11419
11457
  const request = 'save-' + (++SHEET.requests);
@@ -11437,10 +11475,13 @@ async function onEditorMessage(m) {
11437
11475
  case 'pixelkiln:opened':
11438
11476
  SHEET.opened = true;
11439
11477
  SHEET.loading.remove();
11478
+ SHEET.reference = !!m.reference;
11479
+ SHEET.compare.hidden = !SHEET.reference;
11440
11480
  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');
11441
11481
  else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
11442
11482
  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)));
11443
11483
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
11484
+ if (SHEET.reference) SHEET.status.append(document.createTextNode(' \xB7 generated art shown through at 50%'));
11444
11485
  sheetButtons();
11445
11486
  break;
11446
11487
  case 'pixelkiln:dirty':
@@ -11883,6 +11924,13 @@ function generateActions(item) {
11883
11924
  };
11884
11925
  if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
11885
11926
  else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
11927
+ else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
11928
+ // The file changed after download. Putting the recorded bytes back and
11929
+ // regenerating both discard that change, so both say so and ask.
11930
+ add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
11931
+ add('Regenerate' + cost, null, { force: true });
11932
+ } else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
11933
+ else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
11886
11934
  if (item.state === 'in-flight' || item.state === 'recoverable') {
11887
11935
  // A job already parked this asset in review: go straight to its sheet.
11888
11936
  const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
@@ -12573,6 +12621,15 @@ function renderDrawer() {
12573
12621
  if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
12574
12622
  body.append(stateNode(item.state, item.reason));
12575
12623
  if (GENERATION) body.append(generateActions(item));
12624
+ // Writes are opt-in flags on the command line; say so where the buttons would be.
12625
+ if ((!EDITABLE || !GENERATION) && item.declared) {
12626
+ const missing = [];
12627
+ if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
12628
+ if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
12629
+ const hint = el('div', 'state-dim readonly-hint');
12630
+ hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
12631
+ body.append(hint);
12632
+ }
12576
12633
  if (canEdit && ui.editing === item.id) body.append(editForm(item));
12577
12634
  // Plan already quotes the error as the reason for a failed entry; only a
12578
12635
  // stale or superseded failure needs its own line.
@@ -12883,8 +12940,8 @@ var pin_default = {
12883
12940
  pixelorama: "v1.2.2",
12884
12941
  godot: "4.7.2",
12885
12942
  extensionsApi: 9,
12886
- protocol: 3,
12887
- release: "editor-pixelorama-v1.2.2-pk.14",
12943
+ protocol: 4,
12944
+ release: "editor-pixelorama-v1.2.2-pk.18",
12888
12945
  files: {
12889
12946
  "index.apple-touch-icon.png": {
12890
12947
  sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
@@ -12899,7 +12956,7 @@ var pin_default = {
12899
12956
  bytes: 7298
12900
12957
  },
12901
12958
  "index.html": {
12902
- sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
12959
+ sha256: "d9adf06036660c4b76ef5386e872bc03dc0d5b0628fd6ac660d480893ee1f47a",
12903
12960
  bytes: 5442
12904
12961
  },
12905
12962
  "index.icon.png": {
@@ -12911,8 +12968,8 @@ var pin_default = {
12911
12968
  bytes: 279815
12912
12969
  },
12913
12970
  "index.pck": {
12914
- sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
12915
- bytes: 6429628
12971
+ sha256: "a41d9c3a39aa99ea5ee49ddcedfeb8b092006aa75c0de67139f13297c17f42e4",
12972
+ bytes: 6431692
12916
12973
  },
12917
12974
  "index.png": {
12918
12975
  sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
@@ -13461,13 +13518,21 @@ var import_zod10 = require("zod");
13461
13518
  var GenerateRequestSchema = import_zod10.z.object({
13462
13519
  project: import_zod10.z.string().min(1).optional(),
13463
13520
  keys: import_zod10.z.array(import_zod10.z.string().min(1)).min(1).max(500),
13464
- /** Regenerate work that is up to date, as `gen --force` does. */
13521
+ /**
13522
+ * Regenerate work that is up to date, as `gen --force` does, and let the
13523
+ * fetch replace a locally modified or untracked file. With `restore`, put
13524
+ * the recorded bytes back over a modified file.
13525
+ */
13465
13526
  force: import_zod10.z.boolean().optional(),
13466
13527
  /** Advance in-flight, review, or selected work without submitting anything. */
13467
13528
  resume: import_zod10.z.boolean().optional(),
13468
13529
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
13469
- refresh: import_zod10.z.boolean().optional()
13470
- }).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
13530
+ refresh: import_zod10.z.boolean().optional(),
13531
+ /** Put recorded outputs back on disk from the cache or the provider, without generating. */
13532
+ restore: import_zod10.z.boolean().optional()
13533
+ }).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
13534
+ message: "resume, refresh, and restore are exclusive"
13535
+ });
13471
13536
  var GenerateRequestError = class extends Error {
13472
13537
  constructor(message6, status = 400) {
13473
13538
  super(message6);
@@ -13555,7 +13620,9 @@ function createGenerateHandlers(opts) {
13555
13620
  for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
13556
13621
  const provider = opts.providerFor(job.project ?? void 0, providerId);
13557
13622
  const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
13558
- onProgress: (line) => say(job, line)
13623
+ onProgress: (line) => say(job, line),
13624
+ // A forced regeneration replaces whatever is on disk, as `gen --force`.
13625
+ force: job.force
13559
13626
  });
13560
13627
  job.counts.downloaded += res.downloaded;
13561
13628
  if (res.downloaded || res.failed) {
@@ -13570,12 +13637,32 @@ function createGenerateHandlers(opts) {
13570
13637
  job.finishedAt = now().toISOString();
13571
13638
  }
13572
13639
  }
13640
+ async function restore(job, ctx, specs) {
13641
+ job.phase = "fetching";
13642
+ for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
13643
+ const provider = opts.providerFor(job.project ?? void 0, providerId);
13644
+ const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
13645
+ onProgress: (line) => say(job, line),
13646
+ repair: true,
13647
+ force: job.force
13648
+ });
13649
+ job.counts.downloaded += res.downloaded;
13650
+ say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
13651
+ if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
13652
+ }
13653
+ job.phase = "done";
13654
+ job.finishedAt = now().toISOString();
13655
+ }
13573
13656
  async function run(job, ctx, specs, groups) {
13574
13657
  try {
13575
13658
  if (job.mode === "refresh") {
13576
13659
  await refresh(job, ctx, specs);
13577
13660
  return;
13578
13661
  }
13662
+ if (job.mode === "restore") {
13663
+ await restore(job, ctx, specs);
13664
+ return;
13665
+ }
13579
13666
  if (job.mode === "generate") {
13580
13667
  job.phase = "submitting";
13581
13668
  for (const group of groups) {
@@ -13638,8 +13725,16 @@ function createGenerateHandlers(opts) {
13638
13725
  const holder = busy.get(`${request.project ?? ""}:${key}`);
13639
13726
  if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
13640
13727
  }
13728
+ if (request.restore) {
13729
+ for (const key of request.keys) {
13730
+ const entry = ctx.lock.entries[key];
13731
+ if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
13732
+ throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
13733
+ }
13734
+ }
13735
+ }
13641
13736
  let groups = [];
13642
- if (!request.resume && !request.refresh) {
13737
+ if (!request.resume && !request.refresh && !request.restore) {
13643
13738
  const plan = await buildPlan(specs, ctx.lock, { force: request.force });
13644
13739
  if (!plan.actionable.length) {
13645
13740
  const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
@@ -13659,7 +13754,8 @@ function createGenerateHandlers(opts) {
13659
13754
  id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
13660
13755
  project: request.project ?? null,
13661
13756
  keys: request.keys,
13662
- mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
13757
+ mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
13758
+ force: Boolean(request.force),
13663
13759
  phase: "queued",
13664
13760
  startedAt: now().toISOString(),
13665
13761
  finishedAt: null,
@@ -13671,7 +13767,7 @@ function createGenerateHandlers(opts) {
13671
13767
  };
13672
13768
  jobs.set(job.id, job);
13673
13769
  contexts.set(job.id, ctx);
13674
- 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("; "));
13770
+ 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("; "));
13675
13771
  void run(job, ctx, specs, groups);
13676
13772
  return job;
13677
13773
  },