pixelkiln 0.80.0 → 0.82.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.js CHANGED
@@ -14931,7 +14931,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
14931
14931
  // src/gallery/snapshot.ts
14932
14932
  import { createHash as createHash4 } from "crypto";
14933
14933
  import { existsSync as existsSync25 } from "fs";
14934
- import { readFile as readFile22, stat as stat3 } from "fs/promises";
14934
+ import { readFile as readFile23, stat as stat3 } from "fs/promises";
14935
14935
  import path31 from "path";
14936
14936
 
14937
14937
  // src/workspace.ts
@@ -15068,6 +15068,7 @@ function validateWorkspace(ws, dir) {
15068
15068
  }
15069
15069
 
15070
15070
  // src/gallery/edit.ts
15071
+ import { readFile as readFile22, rename as rename9, writeFile as writeFile14 } from "fs/promises";
15071
15072
  import path30 from "path";
15072
15073
  import { z as z12 } from "zod";
15073
15074
 
@@ -15923,9 +15924,62 @@ var HandEditRequestSchema = z12.discriminatedUnion("action", [
15923
15924
  protocol: z12.number().int().positive()
15924
15925
  }).strict()
15925
15926
  ]);
15927
+ var UNDO_DEPTH = 20;
15928
+ var UndoRequestSchema = z12.object({
15929
+ action: z12.literal("undo-edit"),
15930
+ project: z12.string().min(1).optional(),
15931
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
15932
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
15933
+ }).strict();
15926
15934
  function createGalleryEditHandler(opts) {
15927
15935
  const log = opts.onProgress ?? (() => {
15928
15936
  });
15937
+ const apply = createEditApplier(opts, log);
15938
+ const history = /* @__PURE__ */ new Map();
15939
+ const manifestOf = async (project) => {
15940
+ try {
15941
+ return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15942
+ } catch {
15943
+ return null;
15944
+ }
15945
+ };
15946
+ return async (body) => {
15947
+ const undo = UndoRequestSchema.safeParse(body);
15948
+ if (undo.success) {
15949
+ const manifestPath2 = await manifestOf(undo.data.project);
15950
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15951
+ const current = await readFile22(manifestPath2, "utf8");
15952
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15953
+ const stack = history.get(manifestPath2) ?? [];
15954
+ const last = stack[stack.length - 1];
15955
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
15956
+ if (last.afterSha !== sha256(current)) {
15957
+ history.delete(manifestPath2);
15958
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15959
+ }
15960
+ const tmp = manifestPath2 + ".pixelkiln-undo";
15961
+ await writeFile14(tmp, last.before);
15962
+ await rename9(tmp, manifestPath2);
15963
+ stack.pop();
15964
+ log(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15965
+ return opts.reload();
15966
+ }
15967
+ const manifestPath = await manifestOf(body?.project);
15968
+ const before = manifestPath ? await readFile22(manifestPath, "utf8").catch(() => null) : null;
15969
+ const build = await apply(body);
15970
+ if (manifestPath && before !== null) {
15971
+ const after = await readFile22(manifestPath, "utf8").catch(() => null);
15972
+ if (after !== null && after !== before) {
15973
+ const stack = history.get(manifestPath) ?? [];
15974
+ stack.push({ before, afterSha: sha256(after) });
15975
+ if (stack.length > UNDO_DEPTH) stack.shift();
15976
+ history.set(manifestPath, stack);
15977
+ }
15978
+ }
15979
+ return build;
15980
+ };
15981
+ }
15982
+ function createEditApplier(opts, log) {
15929
15983
  return async (body) => {
15930
15984
  const handEdit = HandEditRequestSchema.safeParse(body);
15931
15985
  if (handEdit.success) {
@@ -16100,7 +16154,7 @@ function describeHistory(media, entry, cacheDir) {
16100
16154
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16101
16155
  async function samePixels(a, b) {
16102
16156
  try {
16103
- const [left, right] = await Promise.all([readFile22(a), readFile22(b)]);
16157
+ const [left, right] = await Promise.all([readFile23(a), readFile23(b)]);
16104
16158
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16105
16159
  const x = decodePng(left);
16106
16160
  const y = decodePng(right);
@@ -16258,7 +16312,7 @@ async function buildGallerySnapshot(opts) {
16258
16312
  const media = /* @__PURE__ */ new Map();
16259
16313
  const plan = await buildPlan(specs, lock);
16260
16314
  const items = [];
16261
- const manifestText = await readFile22(loaded.path, "utf8");
16315
+ const manifestText = await readFile23(loaded.path, "utf8");
16262
16316
  const manifestSha256 = sha256(manifestText);
16263
16317
  const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
16264
16318
  let rawStyles = {};
@@ -16610,7 +16664,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16610
16664
  root: loaded.root,
16611
16665
  provider: loaded.manifest.provider,
16612
16666
  account: project.account ?? null,
16613
- manifestSha256: manifestSha256 ?? sha256(await readFile22(manifestPath, "utf8")),
16667
+ manifestSha256: manifestSha256 ?? sha256(await readFile23(manifestPath, "utf8")),
16614
16668
  historyLimit: historyLimit(loaded.manifest),
16615
16669
  entries: Object.keys(lock.entries).length,
16616
16670
  items: projectItems.length,
@@ -16738,26 +16792,35 @@ ${css}</style>
16738
16792
  <span class="project" id="project"></span>
16739
16793
  <span class="grow"></span>
16740
16794
  <label class="search"><input id="q" type="search" placeholder="Search assets, prompts, ids, hashes" autocomplete="off"><kbd>/</kbd></label>
16741
- <select id="sort" aria-label="Sort">
16742
- <option value="key">Sort: style / asset</option>
16743
- <option value="newest">Sort: newest first</option>
16744
- <option value="oldest">Sort: oldest first</option>
16745
- <option value="cost">Sort: cost</option>
16746
- <option value="size">Sort: size</option>
16747
- </select>
16748
- <select id="group" aria-label="Group">
16749
- <option value="style">Group: by style</option>
16750
- <option value="family">Group: by family</option>
16751
- <option value="none">Group: none</option>
16752
- </select>
16795
+ <details class="menu" id="view-menu">
16796
+ <summary title="Sort, group, card size, backdrop, and playback">View</summary>
16797
+ <div class="menu-panel">
16798
+ <label class="menu-row"><span>Sort</span><select id="sort" aria-label="Sort">
16799
+ <option value="key">style / asset</option>
16800
+ <option value="newest">newest first</option>
16801
+ <option value="oldest">oldest first</option>
16802
+ <option value="cost">cost</option>
16803
+ <option value="size">size</option>
16804
+ </select></label>
16805
+ <label class="menu-row"><span>Group</span><select id="group" aria-label="Group">
16806
+ <option value="style">by style</option>
16807
+ <option value="family">by family</option>
16808
+ <option value="none">none</option>
16809
+ </select></label>
16810
+ <div class="menu-row"><span>Cards</span><span class="seg" id="size" role="group" aria-label="Card size">
16811
+ <button type="button" data-size="s" title="Small cards: more on screen">S</button><button type="button" data-size="m" title="Medium cards">M</button><button type="button" data-size="l" title="Large cards: sprites drawn bigger">L</button>
16812
+ </span></div>
16813
+ <div class="menu-row"><span>Backdrop</span><span id="backdrop-slot"></span></div>
16814
+ <label class="menu-row check" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> Play every loop</label>
16815
+ <label class="menu-row check" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> Refresh every 5 seconds</label>
16816
+ <label class="menu-row check" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> Notify when a job finishes</label>
16817
+ </div>
16818
+ </details>
16753
16819
  <span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
16754
16820
  <button id="select" type="button" title="Pick several records to generate, regenerate, compare, or tag together (or ctrl/\u2318-click a card)">Select</button>
16755
16821
  <button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
16756
16822
  <button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
16757
- <label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
16758
- <label class="chip" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> play loops</label>
16759
- <span id="backdrop-slot"></span>
16760
- <label class="chip" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> notify</label>
16823
+ <button id="keys" type="button" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">?</button>
16761
16824
  </div>
16762
16825
  <div class="totals" id="totals"></div>
16763
16826
  <div class="chips" id="chips"></div>
@@ -16766,9 +16829,9 @@ ${css}</style>
16766
16829
  </header>
16767
16830
  <main id="root"></main>
16768
16831
  <div id="selbar" class="selbar" hidden></div>
16832
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
16769
16833
  <footer>
16770
- Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
16771
- is open, <kbd>,</kbd>/<kbd>.</kbd> step a loop's frames and <kbd>space</kbd> plays it, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
16834
+ Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
16772
16835
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
16773
16836
  Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
16774
16837
  Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.
@@ -16843,7 +16906,7 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16843
16906
  // src/editor/install.ts
16844
16907
  import { createHash as createHash5 } from "crypto";
16845
16908
  import { existsSync as existsSync27 } from "fs";
16846
- import { mkdir as mkdir11, readFile as readFile23, rename as rename9, rm as rm11, stat as stat4, writeFile as writeFile14 } from "fs/promises";
16909
+ import { mkdir as mkdir11, readFile as readFile24, rename as rename10, rm as rm11, stat as stat4, writeFile as writeFile15 } from "fs/promises";
16847
16910
  import os2 from "os";
16848
16911
  import path32 from "path";
16849
16912
  var EditorInstallError = class extends Error {
@@ -16874,7 +16937,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16874
16937
  }
16875
16938
  async function sha256Of(file) {
16876
16939
  try {
16877
- return createHash5("sha256").update(await readFile23(file)).digest("hex");
16940
+ return createHash5("sha256").update(await readFile24(file)).digest("hex");
16878
16941
  } catch {
16879
16942
  return null;
16880
16943
  }
@@ -16948,8 +17011,8 @@ async function installEditor(opts = {}) {
16948
17011
  const final = path32.join(dir, name);
16949
17012
  const tmp = `${final}.${process.pid}.part`;
16950
17013
  try {
16951
- await writeFile14(tmp, bytes);
16952
- await rename9(tmp, final);
17014
+ await writeFile15(tmp, bytes);
17015
+ await rename10(tmp, final);
16953
17016
  } finally {
16954
17017
  await rm11(tmp, { force: true });
16955
17018
  }
@@ -16995,7 +17058,7 @@ function editorContentType(name) {
16995
17058
  }
16996
17059
 
16997
17060
  // src/gallery/editor.ts
16998
- import { readFile as readFile24 } from "fs/promises";
17061
+ import { readFile as readFile25 } from "fs/promises";
16999
17062
  import path33 from "path";
17000
17063
  function editorRoute(release, name = "index.html") {
17001
17064
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
@@ -17069,7 +17132,7 @@ function createGalleryEditorHandlers(opts = {}) {
17069
17132
  if (!status.installed) return null;
17070
17133
  let bytes;
17071
17134
  try {
17072
- bytes = await readFile24(path33.join(dir, name));
17135
+ bytes = await readFile25(path33.join(dir, name));
17073
17136
  } catch {
17074
17137
  verified = null;
17075
17138
  return null;
@@ -17086,7 +17149,7 @@ function createGalleryEditorHandlers(opts = {}) {
17086
17149
  // src/gallery/server.ts
17087
17150
  import { randomBytes } from "crypto";
17088
17151
  import { createServer as createServer2 } from "http";
17089
- import { readFile as readFile25 } from "fs/promises";
17152
+ import { readFile as readFile26 } from "fs/promises";
17090
17153
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17091
17154
  var EDITOR_CSP = [
17092
17155
  "default-src 'self'",
@@ -17267,7 +17330,7 @@ async function serveGallery(opts) {
17267
17330
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17268
17331
  if (!asset) return fail(404, "no such review asset");
17269
17332
  try {
17270
- const bytes = await readFile25(asset.path);
17333
+ const bytes = await readFile26(asset.path);
17271
17334
  res.writeHead(200, {
17272
17335
  "Content-Type": asset.contentType,
17273
17336
  "Cache-Control": "no-store",
@@ -17350,7 +17413,7 @@ async function serveGallery(opts) {
17350
17413
  if (!asset) return fail(404, "no such gallery media");
17351
17414
  let bytes;
17352
17415
  try {
17353
- bytes = await readFile25(asset.path);
17416
+ bytes = await readFile26(asset.path);
17354
17417
  } catch {
17355
17418
  return fail(404, "gallery media is no longer on disk; refresh the page");
17356
17419
  }
@@ -17760,7 +17823,7 @@ function createGenerateHandlers(opts) {
17760
17823
  }
17761
17824
 
17762
17825
  // src/pipeline/salvage.ts
17763
- import { readFile as readFile26 } from "fs/promises";
17826
+ import { readFile as readFile27 } from "fs/promises";
17764
17827
  import { existsSync as existsSync28 } from "fs";
17765
17828
  import path34 from "path";
17766
17829
  function providerClaimId(provider, objectId) {
@@ -17772,7 +17835,7 @@ async function loadClaims(lockPaths, opts = {}) {
17772
17835
  if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
17773
17836
  let parsed;
17774
17837
  try {
17775
- parsed = parseLock(JSON.parse(await readFile26(p, "utf8")));
17838
+ parsed = parseLock(JSON.parse(await readFile27(p, "utf8")));
17776
17839
  } catch {
17777
17840
  throw new Error(`Claim lockfile is malformed: ${p}`);
17778
17841
  }
@@ -18009,7 +18072,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18009
18072
  }
18010
18073
 
18011
18074
  // src/pick/salvage-server.ts
18012
- import { mkdir as mkdir12, writeFile as writeFile15, readFile as readFile27 } from "fs/promises";
18075
+ import { mkdir as mkdir12, writeFile as writeFile16, readFile as readFile28 } from "fs/promises";
18013
18076
  import path35 from "path";
18014
18077
 
18015
18078
  // src/pick/salvage-sheet.ts
@@ -18235,7 +18298,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18235
18298
  const rel = path35.join("_salvaged", `${assetId}.png`);
18236
18299
  const outFile = path35.resolve(ctx.importDir, rel);
18237
18300
  await mkdir12(path35.dirname(outFile), { recursive: true });
18238
- await writeFile15(outFile, buf);
18301
+ await writeFile16(outFile, buf);
18239
18302
  ctx.manifest.assets[assetId] = {
18240
18303
  prompt: orphan.prompt,
18241
18304
  promptByStyle: {},
@@ -18291,9 +18354,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18291
18354
  }
18292
18355
  }
18293
18356
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18294
- const raw = JSON.parse(await readFile27(ctx.manifestPath, "utf8"));
18357
+ const raw = JSON.parse(await readFile28(ctx.manifestPath, "utf8"));
18295
18358
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18296
- await writeFile15(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18359
+ await writeFile16(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18297
18360
  await saveLock(ctx.lockPath, ctx.lock);
18298
18361
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18299
18362
  return result;