pixelkiln 0.24.0 → 0.26.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
@@ -1515,6 +1515,10 @@ var import_node_fs = require("fs");
1515
1515
  var import_node_crypto = require("crypto");
1516
1516
  var import_node_os = __toESM(require("os"), 1);
1517
1517
  var import_node_path = __toESM(require("path"), 1);
1518
+ function pixelLabObjectUrl(generator, objectId) {
1519
+ if (!objectId || generator !== "map" && generator !== "1dir") return null;
1520
+ return `https://www.pixellab.ai/objects/${encodeURIComponent(objectId)}`;
1521
+ }
1518
1522
  var PixelLabProvider = class _PixelLabProvider {
1519
1523
  constructor(client) {
1520
1524
  this.client = client;
@@ -7967,14 +7971,16 @@ var import_node_path17 = __toESM(require("path"), 1);
7967
7971
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7968
7972
  const log = opts.onProgress ?? (() => {
7969
7973
  });
7970
- const result = { downloaded: 0, skipped: 0, failed: 0 };
7974
+ const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
7971
7975
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
7972
7976
  normalizeLockOutputPaths(lock, specs);
7973
7977
  const cacheDir = opts.cacheDir === false ? null : import_node_path17.default.resolve(opts.cacheDir ?? import_node_path17.default.join(import_node_path17.default.dirname(lockPath), ".pixelkiln", "cache"));
7974
7978
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
7975
7979
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
7976
7980
  if (e.status === "selected" || e.status === "download-failed") return true;
7977
- if (!opts.repair || e.status !== "downloaded") return false;
7981
+ if (e.status !== "downloaded") return false;
7982
+ if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
7983
+ if (!opts.repair) return false;
7978
7984
  const spec = specByKey.get(key);
7979
7985
  return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0, import_node_fs14.existsSync)(resolveOutputPath(output.path, spec.root)));
7980
7986
  });
@@ -8006,6 +8012,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8006
8012
  continue;
8007
8013
  }
8008
8014
  const outputs = [];
8015
+ let wrote = false;
8009
8016
  try {
8010
8017
  for (let index = 0; index < sources.length; index++) {
8011
8018
  const source = sources[index];
@@ -8013,41 +8020,55 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8013
8020
  (o) => source.role ? o.role === source.role : !o.role && sources.length === 1
8014
8021
  );
8015
8022
  const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
8023
+ let fresh = null;
8024
+ if (opts.refresh) {
8025
+ if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
8026
+ fresh = await provider.download(source.url);
8027
+ if (recorded && sha256(fresh) === recorded.sha256) {
8028
+ log(` current ${import_node_path17.default.relative(process.cwd(), target)}`);
8029
+ outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
8030
+ continue;
8031
+ }
8032
+ }
8016
8033
  if ((0, import_node_fs14.existsSync)(target)) {
8017
8034
  const currentHash = await sha256File(target);
8018
8035
  if (recorded && currentHash === recorded.sha256) {
8019
- if (cacheDir) {
8020
- await cacheMedia(
8021
- cacheDir,
8022
- await (0, import_promises11.readFile)(target),
8023
- recorded.mediaType ?? MediaType.PNG,
8024
- recorded.sha256
8025
- );
8036
+ if (!fresh) {
8037
+ if (cacheDir) {
8038
+ await cacheMedia(
8039
+ cacheDir,
8040
+ await (0, import_promises11.readFile)(target),
8041
+ recorded.mediaType ?? MediaType.PNG,
8042
+ recorded.sha256
8043
+ );
8044
+ }
8045
+ outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
8046
+ continue;
8026
8047
  }
8027
- outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
8028
- continue;
8029
- }
8030
- const superseded = (entry.supersededOutputs ?? []).find(
8031
- (output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
8032
- );
8033
- if (!opts.force) {
8034
- if (!recorded && superseded && currentHash === superseded.sha256) {
8035
- } else if (recorded || superseded) {
8036
- throw new Error(
8037
- `refusing to overwrite modified output ${target}; pass --force to replace it`
8038
- );
8039
- } else {
8040
- throw new Error(
8041
- `refusing to overwrite untracked output ${target}; pass --force to replace it`
8042
- );
8048
+ log(` changed ${import_node_path17.default.relative(process.cwd(), target)} (upstream)`);
8049
+ } else {
8050
+ const superseded = (entry.supersededOutputs ?? []).find(
8051
+ (output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
8052
+ );
8053
+ if (!opts.force) {
8054
+ if (!recorded && superseded && currentHash === superseded.sha256) {
8055
+ } else if (recorded || superseded) {
8056
+ throw new Error(
8057
+ `refusing to overwrite modified output ${target}; pass --force to replace it`
8058
+ );
8059
+ } else {
8060
+ throw new Error(
8061
+ `refusing to overwrite untracked output ${target}; pass --force to replace it`
8062
+ );
8063
+ }
8043
8064
  }
8065
+ log(
8066
+ ` replace ${import_node_path17.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8067
+ );
8044
8068
  }
8045
- log(
8046
- ` replace ${import_node_path17.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8047
- );
8048
8069
  }
8049
8070
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
8050
- let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
8071
+ let buf = fresh ?? (recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null);
8051
8072
  if (buf) {
8052
8073
  log(` cached ${import_node_path17.default.relative(process.cwd(), target)}`);
8053
8074
  } else {
@@ -8079,8 +8100,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8079
8100
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
8080
8101
  await saveLock(lockPath, lock);
8081
8102
  await (0, import_promises11.rename)(tmp, target);
8103
+ wrote = true;
8082
8104
  log(` wrote ${import_node_path17.default.relative(process.cwd(), target)}`);
8083
8105
  }
8106
+ if (opts.refresh && !wrote) {
8107
+ result.unchanged = (result.unchanged ?? 0) + 1;
8108
+ continue;
8109
+ }
8084
8110
  const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
8085
8111
  upsert(lock, key, {
8086
8112
  status: "downloaded",
@@ -9964,6 +9990,8 @@ async function buildGallerySnapshot(opts) {
9964
9990
  source: spec.source ?? null,
9965
9991
  edit,
9966
9992
  editStatus,
9993
+ upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
9994
+ refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
9967
9995
  tags: spec.tags,
9968
9996
  category: asset?.category ?? null
9969
9997
  });
@@ -10018,6 +10046,8 @@ async function buildGallerySnapshot(opts) {
10018
10046
  source: null,
10019
10047
  edit: null,
10020
10048
  editStatus: null,
10049
+ upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
10050
+ refreshable: false,
10021
10051
  tags: [],
10022
10052
  category: null
10023
10053
  });
@@ -10689,7 +10719,7 @@ function renderJobs() {
10689
10719
  const row = el('div', 'job');
10690
10720
  const ph = el('span', 'ph');
10691
10721
  ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
10692
- const what = (job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
10722
+ const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
10693
10723
  (job.project ? ' in ' + job.project : '');
10694
10724
  const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
10695
10725
  last.title = job.keys.join('\\n');
@@ -10728,13 +10758,15 @@ function budgetLine() {
10728
10758
 
10729
10759
  // The confirm step: what will be sent, what it is estimated to cost, and what
10730
10760
  // this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
10731
- function generateDialog(items, { project = null, force = false, resume = false } = {}) {
10761
+ function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
10732
10762
  const host = $('dialog-host');
10733
10763
  host.textContent = '';
10734
10764
  const wrap = el('div', 'dialog');
10735
10765
  const form = el('form');
10736
- form.append(el('h3', null, resume ? 'Resume ' + items.length + (items.length === 1 ? ' asset' : ' assets')
10737
- : (force ? 'Regenerate ' : 'Generate ') + items.length + (items.length === 1 ? ' asset' : ' assets')));
10766
+ const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
10767
+ form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
10768
+ : resume ? 'Resume ' + noun
10769
+ : (force ? 'Regenerate ' : 'Generate ') + noun));
10738
10770
  const table = el('table');
10739
10771
  const thead = el('tr'); thead.append(el('th', null, 'asset'), el('th', null, 'state'), el('th', 'n', 'candidates'), el('th', 'n', 'estimate'));
10740
10772
  table.append(thead);
@@ -10742,16 +10774,18 @@ function generateDialog(items, { project = null, force = false, resume = false }
10742
10774
  for (const item of items) {
10743
10775
  const tr = el('tr');
10744
10776
  tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
10745
- el('td', 'n', resume ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
10777
+ el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
10746
10778
  table.append(tr);
10747
- if (!resume) {
10779
+ if (!resume && !refresh) {
10748
10780
  const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
10749
10781
  g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
10750
10782
  }
10751
10783
  }
10752
10784
  form.append(table);
10753
10785
  const sum = el('div', 'sum');
10754
- if (resume) {
10786
+ if (refresh) {
10787
+ 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.'));
10788
+ } else if (resume) {
10755
10789
  sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
10756
10790
  } else {
10757
10791
  for (const [provider, g] of byProvider) {
@@ -10768,7 +10802,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
10768
10802
  }
10769
10803
  form.append(sum);
10770
10804
  const actions = el('div', 'actions');
10771
- const go = el('button', 'primary', resume ? 'Resume' : 'Generate');
10805
+ const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
10772
10806
  go.type = 'submit';
10773
10807
  const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
10774
10808
  const msg = el('span', 'msg');
@@ -10782,6 +10816,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
10782
10816
  if (project) body.project = project;
10783
10817
  if (force) body.force = true;
10784
10818
  if (resume) body.resume = true;
10819
+ if (refresh) body.refresh = true;
10785
10820
  const job = await postGenerate(body);
10786
10821
  host.textContent = '';
10787
10822
  GEN.jobs.unshift(job);
@@ -11071,6 +11106,13 @@ function renderMain(items) {
11071
11106
  g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
11072
11107
  tools.append(g);
11073
11108
  }
11109
+ const refreshable = snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
11110
+ if (GENERATION && refreshable.length) {
11111
+ const r = el('button', null, 'Pull upstream ' + refreshable.length); r.type = 'button';
11112
+ r.title = 'Re-download objects edited in the provider\u2019s own editor; no cost';
11113
+ r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
11114
+ tools.append(r);
11115
+ }
11074
11116
  const styleKey = 'style:' + (s.project || '') + ':' + s.id;
11075
11117
  if (EDITABLE && s.outDir) {
11076
11118
  const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
@@ -11164,6 +11206,30 @@ function candidatesControl(style) {
11164
11206
  return wrap;
11165
11207
  }
11166
11208
 
11209
+ function upstreamSection(item) {
11210
+ if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
11211
+ const s = el('section', 'meta');
11212
+ s.append(el('h3', null, 'Upstream'));
11213
+ const dl = el('dl');
11214
+ if (item.upstreamUrl) {
11215
+ const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
11216
+ a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
11217
+ row(dl, 'object', a);
11218
+ row(dl, 'note', item.provider === 'pixellab'
11219
+ ? 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
11220
+ : 'The provider\u2019s page for this object.');
11221
+ }
11222
+ s.append(dl);
11223
+ if (GENERATION && item.refreshable) {
11224
+ const acts = el('div', 'hand-actions');
11225
+ const b = el('button', null, 'Pull upstream changes \xB7 no cost'); b.type = 'button';
11226
+ b.onclick = () => generateDialog([item], { project: item.project, refresh: true });
11227
+ acts.append(b);
11228
+ s.append(acts);
11229
+ }
11230
+ return s;
11231
+ }
11232
+
11167
11233
  function generateActions(item) {
11168
11234
  const row = el('div', 'gen');
11169
11235
  if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
@@ -11882,6 +11948,8 @@ function renderDrawer() {
11882
11948
 
11883
11949
  const hand = handEditSection(item);
11884
11950
  if (hand) body.append(hand);
11951
+ const upstream = upstreamSection(item);
11952
+ if (upstream) body.append(upstream);
11885
11953
 
11886
11954
  if (item.revision || item.revisionParentKey) {
11887
11955
  const { s, dl } = section('Lineage');
@@ -12375,8 +12443,10 @@ var GenerateRequestSchema = import_zod9.z.object({
12375
12443
  /** Regenerate work that is up to date, as `gen --force` does. */
12376
12444
  force: import_zod9.z.boolean().optional(),
12377
12445
  /** Advance in-flight, review, or selected work without submitting anything. */
12378
- resume: import_zod9.z.boolean().optional()
12379
- }).strict();
12446
+ resume: import_zod9.z.boolean().optional(),
12447
+ /** Re-download downloaded outputs and replace files whose object changed upstream. */
12448
+ refresh: import_zod9.z.boolean().optional()
12449
+ }).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
12380
12450
  var GenerateRequestError = class extends Error {
12381
12451
  constructor(message6, status = 400) {
12382
12452
  super(message6);
@@ -12434,6 +12504,20 @@ function createGenerateHandlers(opts) {
12434
12504
  }
12435
12505
  return groups;
12436
12506
  };
12507
+ async function refresh(job, ctx, specs) {
12508
+ job.phase = "fetching";
12509
+ for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
12510
+ const provider = opts.providerFor(job.project ?? void 0, providerId);
12511
+ const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
12512
+ onProgress: (line) => say(job, line),
12513
+ refresh: true
12514
+ });
12515
+ job.counts.downloaded += res.downloaded;
12516
+ say(job, `${providerId}: ${res.downloaded} changed upstream and replaced, ${res.unchanged ?? 0} unchanged, ${res.skipped} skipped, ${res.failed} failed`);
12517
+ }
12518
+ job.phase = "done";
12519
+ job.finishedAt = now().toISOString();
12520
+ }
12437
12521
  async function settle(job, ctx, specs) {
12438
12522
  job.phase = "polling";
12439
12523
  for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
@@ -12467,6 +12551,10 @@ function createGenerateHandlers(opts) {
12467
12551
  }
12468
12552
  async function run(job, ctx, specs, groups) {
12469
12553
  try {
12554
+ if (job.mode === "refresh") {
12555
+ await refresh(job, ctx, specs);
12556
+ return;
12557
+ }
12470
12558
  if (job.mode === "generate") {
12471
12559
  job.phase = "submitting";
12472
12560
  for (const group of groups) {
@@ -12530,7 +12618,7 @@ function createGenerateHandlers(opts) {
12530
12618
  if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
12531
12619
  }
12532
12620
  let groups = [];
12533
- if (!request.resume) {
12621
+ if (!request.resume && !request.refresh) {
12534
12622
  const plan = await buildPlan(specs, ctx.lock, { force: request.force });
12535
12623
  if (!plan.actionable.length) {
12536
12624
  const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
@@ -12550,7 +12638,7 @@ function createGenerateHandlers(opts) {
12550
12638
  id: (0, import_node_crypto8.randomBytes)(8).toString("hex"),
12551
12639
  project: request.project ?? null,
12552
12640
  keys: request.keys,
12553
- mode: request.resume ? "resume" : "generate",
12641
+ mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
12554
12642
  phase: "queued",
12555
12643
  startedAt: now().toISOString(),
12556
12644
  finishedAt: null,
@@ -12562,7 +12650,7 @@ function createGenerateHandlers(opts) {
12562
12650
  };
12563
12651
  jobs.set(job.id, job);
12564
12652
  contexts.set(job.id, ctx);
12565
- say(job, 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("; "));
12653
+ 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("; "));
12566
12654
  void run(job, ctx, specs, groups);
12567
12655
  return job;
12568
12656
  },