pixelkiln 0.81.0 → 0.83.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
 
@@ -15449,10 +15450,10 @@ var NewAssetSchema = import_zod8.z.object({
15449
15450
  styles: import_zod8.z.array(import_zod8.z.string().min(1)).optional(),
15450
15451
  /**
15451
15452
  * A controlled regeneration of another asset in the same manifest,
15452
- * instead of a fresh generation. `inpaint` needs a mask upload the
15453
- * gallery does not offer yet, so only `image-to-image` and
15454
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
15455
- * be created here; hand-edit the manifest for the other modes.
15453
+ * instead of a fresh generation. `inpaint` needs a mask upload and
15454
+ * `interpolate` an ending-keyframe image the gallery does not offer, so
15455
+ * those two are hand-edited; every other mode can be created here
15456
+ * (`animate-skeleton` after the gallery writes its keypoints file).
15456
15457
  */
15457
15458
  revision: import_zod8.z.discriminatedUnion("mode", [
15458
15459
  import_zod8.z.object({
@@ -15461,6 +15462,29 @@ var NewAssetSchema = import_zod8.z.object({
15461
15462
  from: import_zod8.z.string().min(1),
15462
15463
  strength: import_zod8.z.number().min(0).max(1).optional()
15463
15464
  }).strict(),
15465
+ import_zod8.z.object({
15466
+ mode: import_zod8.z.literal("reduce-colors"),
15467
+ from: import_zod8.z.string().min(1),
15468
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
15469
+ numColors: import_zod8.z.number().int().min(2).max(256).optional(),
15470
+ dithering: RevisionDitheringSchema.optional(),
15471
+ ditheringStrength: import_zod8.z.number().min(0).max(10).optional()
15472
+ }).strict(),
15473
+ import_zod8.z.object({ mode: import_zod8.z.literal("correct-pixelart"), from: import_zod8.z.string().min(1), strength: import_zod8.z.number().min(0).max(1).optional() }).strict(),
15474
+ import_zod8.z.object({
15475
+ mode: import_zod8.z.enum(["animate", "animate-pixminimax"]),
15476
+ from: import_zod8.z.string().min(1),
15477
+ frames: import_zod8.z.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15478
+ fps: import_zod8.z.number().int().min(1).max(60).optional(),
15479
+ enhancePrompt: import_zod8.z.boolean().optional(),
15480
+ /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15481
+ direction: CharacterDirectionSchema.optional()
15482
+ }).strict(),
15483
+ import_zod8.z.object({
15484
+ mode: import_zod8.z.literal("edit-animation"),
15485
+ from: import_zod8.z.string().min(1),
15486
+ fps: import_zod8.z.number().int().min(1).max(60).optional()
15487
+ }).strict(),
15464
15488
  import_zod8.z.object({
15465
15489
  mode: import_zod8.z.literal("animate-skeleton"),
15466
15490
  from: import_zod8.z.string().min(1),
@@ -16248,9 +16272,62 @@ var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
16248
16272
  protocol: import_zod12.z.number().int().positive()
16249
16273
  }).strict()
16250
16274
  ]);
16275
+ var UNDO_DEPTH = 20;
16276
+ var UndoRequestSchema = import_zod12.z.object({
16277
+ action: import_zod12.z.literal("undo-edit"),
16278
+ project: import_zod12.z.string().min(1).optional(),
16279
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
16280
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16281
+ }).strict();
16251
16282
  function createGalleryEditHandler(opts) {
16252
16283
  const log = opts.onProgress ?? (() => {
16253
16284
  });
16285
+ const apply = createEditApplier(opts, log);
16286
+ const history = /* @__PURE__ */ new Map();
16287
+ const manifestOf = async (project) => {
16288
+ try {
16289
+ return import_node_path31.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16290
+ } catch {
16291
+ return null;
16292
+ }
16293
+ };
16294
+ return async (body) => {
16295
+ const undo = UndoRequestSchema.safeParse(body);
16296
+ if (undo.success) {
16297
+ const manifestPath2 = await manifestOf(undo.data.project);
16298
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16299
+ const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
16300
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16301
+ const stack = history.get(manifestPath2) ?? [];
16302
+ const last = stack[stack.length - 1];
16303
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
16304
+ if (last.afterSha !== sha256(current)) {
16305
+ history.delete(manifestPath2);
16306
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16307
+ }
16308
+ const tmp = manifestPath2 + ".pixelkiln-undo";
16309
+ await (0, import_promises24.writeFile)(tmp, last.before);
16310
+ await (0, import_promises24.rename)(tmp, manifestPath2);
16311
+ stack.pop();
16312
+ log(` manifest edit undone in ${import_node_path31.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16313
+ return opts.reload();
16314
+ }
16315
+ const manifestPath = await manifestOf(body?.project);
16316
+ const before = manifestPath ? await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null) : null;
16317
+ const build = await apply(body);
16318
+ if (manifestPath && before !== null) {
16319
+ const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
16320
+ if (after !== null && after !== before) {
16321
+ const stack = history.get(manifestPath) ?? [];
16322
+ stack.push({ before, afterSha: sha256(after) });
16323
+ if (stack.length > UNDO_DEPTH) stack.shift();
16324
+ history.set(manifestPath, stack);
16325
+ }
16326
+ }
16327
+ return build;
16328
+ };
16329
+ }
16330
+ function createEditApplier(opts, log) {
16254
16331
  return async (body) => {
16255
16332
  const handEdit = HandEditRequestSchema.safeParse(body);
16256
16333
  if (handEdit.success) {
@@ -16425,7 +16502,7 @@ function describeHistory(media, entry, cacheDir) {
16425
16502
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16426
16503
  async function samePixels(a, b) {
16427
16504
  try {
16428
- const [left, right] = await Promise.all([(0, import_promises24.readFile)(a), (0, import_promises24.readFile)(b)]);
16505
+ const [left, right] = await Promise.all([(0, import_promises25.readFile)(a), (0, import_promises25.readFile)(b)]);
16429
16506
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16430
16507
  const x = decodePng(left);
16431
16508
  const y = decodePng(right);
@@ -16491,7 +16568,7 @@ function describeCharacter(spec, entry) {
16491
16568
  }
16492
16569
  async function fileInfo(absolutePath) {
16493
16570
  try {
16494
- const info = await (0, import_promises24.stat)(absolutePath);
16571
+ const info = await (0, import_promises25.stat)(absolutePath);
16495
16572
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16496
16573
  } catch {
16497
16574
  return null;
@@ -16583,7 +16660,7 @@ async function buildGallerySnapshot(opts) {
16583
16660
  const media = /* @__PURE__ */ new Map();
16584
16661
  const plan = await buildPlan(specs, lock);
16585
16662
  const items = [];
16586
- const manifestText = await (0, import_promises24.readFile)(loaded.path, "utf8");
16663
+ const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
16587
16664
  const manifestSha256 = sha256(manifestText);
16588
16665
  const cacheDir = import_node_path32.default.resolve(import_node_path32.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16589
16666
  let rawStyles = {};
@@ -16935,7 +17012,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16935
17012
  root: loaded.root,
16936
17013
  provider: loaded.manifest.provider,
16937
17014
  account: project.account ?? null,
16938
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises24.readFile)(manifestPath, "utf8")),
17015
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
16939
17016
  historyLimit: historyLimit(loaded.manifest),
16940
17017
  entries: Object.keys(lock.entries).length,
16941
17018
  items: projectItems.length,
@@ -17100,6 +17177,7 @@ ${css}</style>
17100
17177
  </header>
17101
17178
  <main id="root"></main>
17102
17179
  <div id="selbar" class="selbar" hidden></div>
17180
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
17103
17181
  <footer>
17104
17182
  Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
17105
17183
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
@@ -17176,7 +17254,7 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17176
17254
  // src/editor/install.ts
17177
17255
  var import_node_crypto7 = require("crypto");
17178
17256
  var import_node_fs29 = require("fs");
17179
- var import_promises25 = require("fs/promises");
17257
+ var import_promises26 = require("fs/promises");
17180
17258
  var import_node_os3 = __toESM(require("os"), 1);
17181
17259
  var import_node_path33 = __toESM(require("path"), 1);
17182
17260
  var EditorInstallError = class extends Error {
@@ -17207,7 +17285,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17207
17285
  }
17208
17286
  async function sha256Of(file) {
17209
17287
  try {
17210
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises25.readFile)(file)).digest("hex");
17288
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises26.readFile)(file)).digest("hex");
17211
17289
  } catch {
17212
17290
  return null;
17213
17291
  }
@@ -17221,7 +17299,7 @@ async function editorStatus(opts = {}) {
17221
17299
  for (const [name, expected] of Object.entries(pin.files)) {
17222
17300
  totalBytes += expected.bytes;
17223
17301
  const file = import_node_path33.default.join(dir, name);
17224
- const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises25.stat)(file).catch(() => null) : null;
17302
+ const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises26.stat)(file).catch(() => null) : null;
17225
17303
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17226
17304
  installedBytes += expected.bytes;
17227
17305
  } else {
@@ -17250,7 +17328,7 @@ async function installEditor(opts = {}) {
17250
17328
  );
17251
17329
  }
17252
17330
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17253
- await (0, import_promises25.mkdir)(dir, { recursive: true });
17331
+ await (0, import_promises26.mkdir)(dir, { recursive: true });
17254
17332
  const downloaded = [];
17255
17333
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17256
17334
  let fetchedBytes = before.installedBytes;
@@ -17281,10 +17359,10 @@ async function installEditor(opts = {}) {
17281
17359
  const final = import_node_path33.default.join(dir, name);
17282
17360
  const tmp = `${final}.${process.pid}.part`;
17283
17361
  try {
17284
- await (0, import_promises25.writeFile)(tmp, bytes);
17285
- await (0, import_promises25.rename)(tmp, final);
17362
+ await (0, import_promises26.writeFile)(tmp, bytes);
17363
+ await (0, import_promises26.rename)(tmp, final);
17286
17364
  } finally {
17287
- await (0, import_promises25.rm)(tmp, { force: true });
17365
+ await (0, import_promises26.rm)(tmp, { force: true });
17288
17366
  }
17289
17367
  fetchedBytes += bytes.length;
17290
17368
  downloaded.push(name);
@@ -17328,7 +17406,7 @@ function editorContentType(name) {
17328
17406
  }
17329
17407
 
17330
17408
  // src/gallery/editor.ts
17331
- var import_promises26 = require("fs/promises");
17409
+ var import_promises27 = require("fs/promises");
17332
17410
  var import_node_path34 = __toESM(require("path"), 1);
17333
17411
  function editorRoute(release, name = "index.html") {
17334
17412
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
@@ -17402,7 +17480,7 @@ function createGalleryEditorHandlers(opts = {}) {
17402
17480
  if (!status.installed) return null;
17403
17481
  let bytes;
17404
17482
  try {
17405
- bytes = await (0, import_promises26.readFile)(import_node_path34.default.join(dir, name));
17483
+ bytes = await (0, import_promises27.readFile)(import_node_path34.default.join(dir, name));
17406
17484
  } catch {
17407
17485
  verified = null;
17408
17486
  return null;
@@ -17419,7 +17497,7 @@ function createGalleryEditorHandlers(opts = {}) {
17419
17497
  // src/gallery/server.ts
17420
17498
  var import_node_crypto8 = require("crypto");
17421
17499
  var import_node_http2 = require("http");
17422
- var import_promises27 = require("fs/promises");
17500
+ var import_promises28 = require("fs/promises");
17423
17501
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17424
17502
  var EDITOR_CSP = [
17425
17503
  "default-src 'self'",
@@ -17600,7 +17678,7 @@ async function serveGallery(opts) {
17600
17678
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17601
17679
  if (!asset) return fail(404, "no such review asset");
17602
17680
  try {
17603
- const bytes = await (0, import_promises27.readFile)(asset.path);
17681
+ const bytes = await (0, import_promises28.readFile)(asset.path);
17604
17682
  res.writeHead(200, {
17605
17683
  "Content-Type": asset.contentType,
17606
17684
  "Cache-Control": "no-store",
@@ -17683,7 +17761,7 @@ async function serveGallery(opts) {
17683
17761
  if (!asset) return fail(404, "no such gallery media");
17684
17762
  let bytes;
17685
17763
  try {
17686
- bytes = await (0, import_promises27.readFile)(asset.path);
17764
+ bytes = await (0, import_promises28.readFile)(asset.path);
17687
17765
  } catch {
17688
17766
  return fail(404, "gallery media is no longer on disk; refresh the page");
17689
17767
  }
@@ -18093,7 +18171,7 @@ function createGenerateHandlers(opts) {
18093
18171
  }
18094
18172
 
18095
18173
  // src/pipeline/salvage.ts
18096
- var import_promises28 = require("fs/promises");
18174
+ var import_promises29 = require("fs/promises");
18097
18175
  var import_node_fs30 = require("fs");
18098
18176
  var import_node_path35 = __toESM(require("path"), 1);
18099
18177
  function providerClaimId(provider, objectId) {
@@ -18105,7 +18183,7 @@ async function loadClaims(lockPaths, opts = {}) {
18105
18183
  if (!(0, import_node_fs30.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18106
18184
  let parsed;
18107
18185
  try {
18108
- parsed = parseLock(JSON.parse(await (0, import_promises28.readFile)(p, "utf8")));
18186
+ parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
18109
18187
  } catch {
18110
18188
  throw new Error(`Claim lockfile is malformed: ${p}`);
18111
18189
  }
@@ -18342,7 +18420,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18342
18420
  }
18343
18421
 
18344
18422
  // src/pick/salvage-server.ts
18345
- var import_promises29 = require("fs/promises");
18423
+ var import_promises30 = require("fs/promises");
18346
18424
  var import_node_path36 = __toESM(require("path"), 1);
18347
18425
 
18348
18426
  // src/pick/salvage-sheet.ts
@@ -18567,8 +18645,8 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18567
18645
  const assetId = idFromPrompt(orphan.prompt, taken);
18568
18646
  const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
18569
18647
  const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
18570
- await (0, import_promises29.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18571
- await (0, import_promises29.writeFile)(outFile, buf);
18648
+ await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18649
+ await (0, import_promises30.writeFile)(outFile, buf);
18572
18650
  ctx.manifest.assets[assetId] = {
18573
18651
  prompt: orphan.prompt,
18574
18652
  promptByStyle: {},
@@ -18624,9 +18702,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18624
18702
  }
18625
18703
  }
18626
18704
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18627
- const raw = JSON.parse(await (0, import_promises29.readFile)(ctx.manifestPath, "utf8"));
18705
+ const raw = JSON.parse(await (0, import_promises30.readFile)(ctx.manifestPath, "utf8"));
18628
18706
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18629
- await (0, import_promises29.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18707
+ await (0, import_promises30.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18630
18708
  await saveLock(ctx.lockPath, ctx.lock);
18631
18709
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18632
18710
  return result;