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.cjs CHANGED
@@ -15256,7 +15256,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15256
15256
  // src/gallery/snapshot.ts
15257
15257
  var import_node_crypto6 = require("crypto");
15258
15258
  var import_node_fs27 = require("fs");
15259
- var import_promises24 = require("fs/promises");
15259
+ var import_promises25 = require("fs/promises");
15260
15260
  var import_node_path32 = __toESM(require("path"), 1);
15261
15261
 
15262
15262
  // src/workspace.ts
@@ -15393,6 +15393,7 @@ function validateWorkspace(ws, dir) {
15393
15393
  }
15394
15394
 
15395
15395
  // src/gallery/edit.ts
15396
+ var import_promises24 = require("fs/promises");
15396
15397
  var import_node_path31 = __toESM(require("path"), 1);
15397
15398
  var import_zod12 = require("zod");
15398
15399
 
@@ -16248,9 +16249,62 @@ var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
16248
16249
  protocol: import_zod12.z.number().int().positive()
16249
16250
  }).strict()
16250
16251
  ]);
16252
+ var UNDO_DEPTH = 20;
16253
+ var UndoRequestSchema = import_zod12.z.object({
16254
+ action: import_zod12.z.literal("undo-edit"),
16255
+ project: import_zod12.z.string().min(1).optional(),
16256
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
16257
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16258
+ }).strict();
16251
16259
  function createGalleryEditHandler(opts) {
16252
16260
  const log = opts.onProgress ?? (() => {
16253
16261
  });
16262
+ const apply = createEditApplier(opts, log);
16263
+ const history = /* @__PURE__ */ new Map();
16264
+ const manifestOf = async (project) => {
16265
+ try {
16266
+ return import_node_path31.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16267
+ } catch {
16268
+ return null;
16269
+ }
16270
+ };
16271
+ return async (body) => {
16272
+ const undo = UndoRequestSchema.safeParse(body);
16273
+ if (undo.success) {
16274
+ const manifestPath2 = await manifestOf(undo.data.project);
16275
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16276
+ const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
16277
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16278
+ const stack = history.get(manifestPath2) ?? [];
16279
+ const last = stack[stack.length - 1];
16280
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
16281
+ if (last.afterSha !== sha256(current)) {
16282
+ history.delete(manifestPath2);
16283
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16284
+ }
16285
+ const tmp = manifestPath2 + ".pixelkiln-undo";
16286
+ await (0, import_promises24.writeFile)(tmp, last.before);
16287
+ await (0, import_promises24.rename)(tmp, manifestPath2);
16288
+ stack.pop();
16289
+ log(` manifest edit undone in ${import_node_path31.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16290
+ return opts.reload();
16291
+ }
16292
+ const manifestPath = await manifestOf(body?.project);
16293
+ const before = manifestPath ? await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null) : null;
16294
+ const build = await apply(body);
16295
+ if (manifestPath && before !== null) {
16296
+ const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
16297
+ if (after !== null && after !== before) {
16298
+ const stack = history.get(manifestPath) ?? [];
16299
+ stack.push({ before, afterSha: sha256(after) });
16300
+ if (stack.length > UNDO_DEPTH) stack.shift();
16301
+ history.set(manifestPath, stack);
16302
+ }
16303
+ }
16304
+ return build;
16305
+ };
16306
+ }
16307
+ function createEditApplier(opts, log) {
16254
16308
  return async (body) => {
16255
16309
  const handEdit = HandEditRequestSchema.safeParse(body);
16256
16310
  if (handEdit.success) {
@@ -16425,7 +16479,7 @@ function describeHistory(media, entry, cacheDir) {
16425
16479
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16426
16480
  async function samePixels(a, b) {
16427
16481
  try {
16428
- const [left, right] = await Promise.all([(0, import_promises24.readFile)(a), (0, import_promises24.readFile)(b)]);
16482
+ const [left, right] = await Promise.all([(0, import_promises25.readFile)(a), (0, import_promises25.readFile)(b)]);
16429
16483
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16430
16484
  const x = decodePng(left);
16431
16485
  const y = decodePng(right);
@@ -16491,7 +16545,7 @@ function describeCharacter(spec, entry) {
16491
16545
  }
16492
16546
  async function fileInfo(absolutePath) {
16493
16547
  try {
16494
- const info = await (0, import_promises24.stat)(absolutePath);
16548
+ const info = await (0, import_promises25.stat)(absolutePath);
16495
16549
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16496
16550
  } catch {
16497
16551
  return null;
@@ -16583,7 +16637,7 @@ async function buildGallerySnapshot(opts) {
16583
16637
  const media = /* @__PURE__ */ new Map();
16584
16638
  const plan = await buildPlan(specs, lock);
16585
16639
  const items = [];
16586
- const manifestText = await (0, import_promises24.readFile)(loaded.path, "utf8");
16640
+ const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
16587
16641
  const manifestSha256 = sha256(manifestText);
16588
16642
  const cacheDir = import_node_path32.default.resolve(import_node_path32.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16589
16643
  let rawStyles = {};
@@ -16935,7 +16989,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16935
16989
  root: loaded.root,
16936
16990
  provider: loaded.manifest.provider,
16937
16991
  account: project.account ?? null,
16938
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises24.readFile)(manifestPath, "utf8")),
16992
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
16939
16993
  historyLimit: historyLimit(loaded.manifest),
16940
16994
  entries: Object.keys(lock.entries).length,
16941
16995
  items: projectItems.length,
@@ -17063,26 +17117,35 @@ ${css}</style>
17063
17117
  <span class="project" id="project"></span>
17064
17118
  <span class="grow"></span>
17065
17119
  <label class="search"><input id="q" type="search" placeholder="Search assets, prompts, ids, hashes" autocomplete="off"><kbd>/</kbd></label>
17066
- <select id="sort" aria-label="Sort">
17067
- <option value="key">Sort: style / asset</option>
17068
- <option value="newest">Sort: newest first</option>
17069
- <option value="oldest">Sort: oldest first</option>
17070
- <option value="cost">Sort: cost</option>
17071
- <option value="size">Sort: size</option>
17072
- </select>
17073
- <select id="group" aria-label="Group">
17074
- <option value="style">Group: by style</option>
17075
- <option value="family">Group: by family</option>
17076
- <option value="none">Group: none</option>
17077
- </select>
17120
+ <details class="menu" id="view-menu">
17121
+ <summary title="Sort, group, card size, backdrop, and playback">View</summary>
17122
+ <div class="menu-panel">
17123
+ <label class="menu-row"><span>Sort</span><select id="sort" aria-label="Sort">
17124
+ <option value="key">style / asset</option>
17125
+ <option value="newest">newest first</option>
17126
+ <option value="oldest">oldest first</option>
17127
+ <option value="cost">cost</option>
17128
+ <option value="size">size</option>
17129
+ </select></label>
17130
+ <label class="menu-row"><span>Group</span><select id="group" aria-label="Group">
17131
+ <option value="style">by style</option>
17132
+ <option value="family">by family</option>
17133
+ <option value="none">none</option>
17134
+ </select></label>
17135
+ <div class="menu-row"><span>Cards</span><span class="seg" id="size" role="group" aria-label="Card size">
17136
+ <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>
17137
+ </span></div>
17138
+ <div class="menu-row"><span>Backdrop</span><span id="backdrop-slot"></span></div>
17139
+ <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>
17140
+ <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>
17141
+ <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>
17142
+ </div>
17143
+ </details>
17078
17144
  <span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
17079
17145
  <button id="select" type="button" title="Pick several records to generate, regenerate, compare, or tag together (or ctrl/\u2318-click a card)">Select</button>
17080
17146
  <button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
17081
17147
  <button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
17082
- <label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
17083
- <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>
17084
- <span id="backdrop-slot"></span>
17085
- <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>
17148
+ <button id="keys" type="button" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">?</button>
17086
17149
  </div>
17087
17150
  <div class="totals" id="totals"></div>
17088
17151
  <div class="chips" id="chips"></div>
@@ -17091,9 +17154,9 @@ ${css}</style>
17091
17154
  </header>
17092
17155
  <main id="root"></main>
17093
17156
  <div id="selbar" class="selbar" hidden></div>
17157
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
17094
17158
  <footer>
17095
- 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
17096
- 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,
17159
+ Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
17097
17160
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
17098
17161
  Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
17099
17162
  Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.
@@ -17168,7 +17231,7 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17168
17231
  // src/editor/install.ts
17169
17232
  var import_node_crypto7 = require("crypto");
17170
17233
  var import_node_fs29 = require("fs");
17171
- var import_promises25 = require("fs/promises");
17234
+ var import_promises26 = require("fs/promises");
17172
17235
  var import_node_os3 = __toESM(require("os"), 1);
17173
17236
  var import_node_path33 = __toESM(require("path"), 1);
17174
17237
  var EditorInstallError = class extends Error {
@@ -17199,7 +17262,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17199
17262
  }
17200
17263
  async function sha256Of(file) {
17201
17264
  try {
17202
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises25.readFile)(file)).digest("hex");
17265
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises26.readFile)(file)).digest("hex");
17203
17266
  } catch {
17204
17267
  return null;
17205
17268
  }
@@ -17213,7 +17276,7 @@ async function editorStatus(opts = {}) {
17213
17276
  for (const [name, expected] of Object.entries(pin.files)) {
17214
17277
  totalBytes += expected.bytes;
17215
17278
  const file = import_node_path33.default.join(dir, name);
17216
- const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises25.stat)(file).catch(() => null) : null;
17279
+ const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises26.stat)(file).catch(() => null) : null;
17217
17280
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17218
17281
  installedBytes += expected.bytes;
17219
17282
  } else {
@@ -17242,7 +17305,7 @@ async function installEditor(opts = {}) {
17242
17305
  );
17243
17306
  }
17244
17307
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17245
- await (0, import_promises25.mkdir)(dir, { recursive: true });
17308
+ await (0, import_promises26.mkdir)(dir, { recursive: true });
17246
17309
  const downloaded = [];
17247
17310
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17248
17311
  let fetchedBytes = before.installedBytes;
@@ -17273,10 +17336,10 @@ async function installEditor(opts = {}) {
17273
17336
  const final = import_node_path33.default.join(dir, name);
17274
17337
  const tmp = `${final}.${process.pid}.part`;
17275
17338
  try {
17276
- await (0, import_promises25.writeFile)(tmp, bytes);
17277
- await (0, import_promises25.rename)(tmp, final);
17339
+ await (0, import_promises26.writeFile)(tmp, bytes);
17340
+ await (0, import_promises26.rename)(tmp, final);
17278
17341
  } finally {
17279
- await (0, import_promises25.rm)(tmp, { force: true });
17342
+ await (0, import_promises26.rm)(tmp, { force: true });
17280
17343
  }
17281
17344
  fetchedBytes += bytes.length;
17282
17345
  downloaded.push(name);
@@ -17320,7 +17383,7 @@ function editorContentType(name) {
17320
17383
  }
17321
17384
 
17322
17385
  // src/gallery/editor.ts
17323
- var import_promises26 = require("fs/promises");
17386
+ var import_promises27 = require("fs/promises");
17324
17387
  var import_node_path34 = __toESM(require("path"), 1);
17325
17388
  function editorRoute(release, name = "index.html") {
17326
17389
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
@@ -17394,7 +17457,7 @@ function createGalleryEditorHandlers(opts = {}) {
17394
17457
  if (!status.installed) return null;
17395
17458
  let bytes;
17396
17459
  try {
17397
- bytes = await (0, import_promises26.readFile)(import_node_path34.default.join(dir, name));
17460
+ bytes = await (0, import_promises27.readFile)(import_node_path34.default.join(dir, name));
17398
17461
  } catch {
17399
17462
  verified = null;
17400
17463
  return null;
@@ -17411,7 +17474,7 @@ function createGalleryEditorHandlers(opts = {}) {
17411
17474
  // src/gallery/server.ts
17412
17475
  var import_node_crypto8 = require("crypto");
17413
17476
  var import_node_http2 = require("http");
17414
- var import_promises27 = require("fs/promises");
17477
+ var import_promises28 = require("fs/promises");
17415
17478
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17416
17479
  var EDITOR_CSP = [
17417
17480
  "default-src 'self'",
@@ -17592,7 +17655,7 @@ async function serveGallery(opts) {
17592
17655
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17593
17656
  if (!asset) return fail(404, "no such review asset");
17594
17657
  try {
17595
- const bytes = await (0, import_promises27.readFile)(asset.path);
17658
+ const bytes = await (0, import_promises28.readFile)(asset.path);
17596
17659
  res.writeHead(200, {
17597
17660
  "Content-Type": asset.contentType,
17598
17661
  "Cache-Control": "no-store",
@@ -17675,7 +17738,7 @@ async function serveGallery(opts) {
17675
17738
  if (!asset) return fail(404, "no such gallery media");
17676
17739
  let bytes;
17677
17740
  try {
17678
- bytes = await (0, import_promises27.readFile)(asset.path);
17741
+ bytes = await (0, import_promises28.readFile)(asset.path);
17679
17742
  } catch {
17680
17743
  return fail(404, "gallery media is no longer on disk; refresh the page");
17681
17744
  }
@@ -18085,7 +18148,7 @@ function createGenerateHandlers(opts) {
18085
18148
  }
18086
18149
 
18087
18150
  // src/pipeline/salvage.ts
18088
- var import_promises28 = require("fs/promises");
18151
+ var import_promises29 = require("fs/promises");
18089
18152
  var import_node_fs30 = require("fs");
18090
18153
  var import_node_path35 = __toESM(require("path"), 1);
18091
18154
  function providerClaimId(provider, objectId) {
@@ -18097,7 +18160,7 @@ async function loadClaims(lockPaths, opts = {}) {
18097
18160
  if (!(0, import_node_fs30.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18098
18161
  let parsed;
18099
18162
  try {
18100
- parsed = parseLock(JSON.parse(await (0, import_promises28.readFile)(p, "utf8")));
18163
+ parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
18101
18164
  } catch {
18102
18165
  throw new Error(`Claim lockfile is malformed: ${p}`);
18103
18166
  }
@@ -18334,7 +18397,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18334
18397
  }
18335
18398
 
18336
18399
  // src/pick/salvage-server.ts
18337
- var import_promises29 = require("fs/promises");
18400
+ var import_promises30 = require("fs/promises");
18338
18401
  var import_node_path36 = __toESM(require("path"), 1);
18339
18402
 
18340
18403
  // src/pick/salvage-sheet.ts
@@ -18559,8 +18622,8 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18559
18622
  const assetId = idFromPrompt(orphan.prompt, taken);
18560
18623
  const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
18561
18624
  const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
18562
- await (0, import_promises29.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18563
- await (0, import_promises29.writeFile)(outFile, buf);
18625
+ await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18626
+ await (0, import_promises30.writeFile)(outFile, buf);
18564
18627
  ctx.manifest.assets[assetId] = {
18565
18628
  prompt: orphan.prompt,
18566
18629
  promptByStyle: {},
@@ -18616,9 +18679,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18616
18679
  }
18617
18680
  }
18618
18681
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18619
- const raw = JSON.parse(await (0, import_promises29.readFile)(ctx.manifestPath, "utf8"));
18682
+ const raw = JSON.parse(await (0, import_promises30.readFile)(ctx.manifestPath, "utf8"));
18620
18683
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18621
- await (0, import_promises29.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18684
+ await (0, import_promises30.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18622
18685
  await saveLock(ctx.lockPath, ctx.lock);
18623
18686
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18624
18687
  return result;