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.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
 
@@ -15124,10 +15125,10 @@ var NewAssetSchema = z8.object({
15124
15125
  styles: z8.array(z8.string().min(1)).optional(),
15125
15126
  /**
15126
15127
  * A controlled regeneration of another asset in the same manifest,
15127
- * instead of a fresh generation. `inpaint` needs a mask upload the
15128
- * gallery does not offer yet, so only `image-to-image` and
15129
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
15130
- * be created here; hand-edit the manifest for the other modes.
15128
+ * instead of a fresh generation. `inpaint` needs a mask upload and
15129
+ * `interpolate` an ending-keyframe image the gallery does not offer, so
15130
+ * those two are hand-edited; every other mode can be created here
15131
+ * (`animate-skeleton` after the gallery writes its keypoints file).
15131
15132
  */
15132
15133
  revision: z8.discriminatedUnion("mode", [
15133
15134
  z8.object({
@@ -15136,6 +15137,29 @@ var NewAssetSchema = z8.object({
15136
15137
  from: z8.string().min(1),
15137
15138
  strength: z8.number().min(0).max(1).optional()
15138
15139
  }).strict(),
15140
+ z8.object({
15141
+ mode: z8.literal("reduce-colors"),
15142
+ from: z8.string().min(1),
15143
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
15144
+ numColors: z8.number().int().min(2).max(256).optional(),
15145
+ dithering: RevisionDitheringSchema.optional(),
15146
+ ditheringStrength: z8.number().min(0).max(10).optional()
15147
+ }).strict(),
15148
+ z8.object({ mode: z8.literal("correct-pixelart"), from: z8.string().min(1), strength: z8.number().min(0).max(1).optional() }).strict(),
15149
+ z8.object({
15150
+ mode: z8.enum(["animate", "animate-pixminimax"]),
15151
+ from: z8.string().min(1),
15152
+ frames: z8.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15153
+ fps: z8.number().int().min(1).max(60).optional(),
15154
+ enhancePrompt: z8.boolean().optional(),
15155
+ /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15156
+ direction: CharacterDirectionSchema.optional()
15157
+ }).strict(),
15158
+ z8.object({
15159
+ mode: z8.literal("edit-animation"),
15160
+ from: z8.string().min(1),
15161
+ fps: z8.number().int().min(1).max(60).optional()
15162
+ }).strict(),
15139
15163
  z8.object({
15140
15164
  mode: z8.literal("animate-skeleton"),
15141
15165
  from: z8.string().min(1),
@@ -15923,9 +15947,62 @@ var HandEditRequestSchema = z12.discriminatedUnion("action", [
15923
15947
  protocol: z12.number().int().positive()
15924
15948
  }).strict()
15925
15949
  ]);
15950
+ var UNDO_DEPTH = 20;
15951
+ var UndoRequestSchema = z12.object({
15952
+ action: z12.literal("undo-edit"),
15953
+ project: z12.string().min(1).optional(),
15954
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
15955
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
15956
+ }).strict();
15926
15957
  function createGalleryEditHandler(opts) {
15927
15958
  const log = opts.onProgress ?? (() => {
15928
15959
  });
15960
+ const apply = createEditApplier(opts, log);
15961
+ const history = /* @__PURE__ */ new Map();
15962
+ const manifestOf = async (project) => {
15963
+ try {
15964
+ return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15965
+ } catch {
15966
+ return null;
15967
+ }
15968
+ };
15969
+ return async (body) => {
15970
+ const undo = UndoRequestSchema.safeParse(body);
15971
+ if (undo.success) {
15972
+ const manifestPath2 = await manifestOf(undo.data.project);
15973
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15974
+ const current = await readFile22(manifestPath2, "utf8");
15975
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15976
+ const stack = history.get(manifestPath2) ?? [];
15977
+ const last = stack[stack.length - 1];
15978
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
15979
+ if (last.afterSha !== sha256(current)) {
15980
+ history.delete(manifestPath2);
15981
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15982
+ }
15983
+ const tmp = manifestPath2 + ".pixelkiln-undo";
15984
+ await writeFile14(tmp, last.before);
15985
+ await rename9(tmp, manifestPath2);
15986
+ stack.pop();
15987
+ log(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15988
+ return opts.reload();
15989
+ }
15990
+ const manifestPath = await manifestOf(body?.project);
15991
+ const before = manifestPath ? await readFile22(manifestPath, "utf8").catch(() => null) : null;
15992
+ const build = await apply(body);
15993
+ if (manifestPath && before !== null) {
15994
+ const after = await readFile22(manifestPath, "utf8").catch(() => null);
15995
+ if (after !== null && after !== before) {
15996
+ const stack = history.get(manifestPath) ?? [];
15997
+ stack.push({ before, afterSha: sha256(after) });
15998
+ if (stack.length > UNDO_DEPTH) stack.shift();
15999
+ history.set(manifestPath, stack);
16000
+ }
16001
+ }
16002
+ return build;
16003
+ };
16004
+ }
16005
+ function createEditApplier(opts, log) {
15929
16006
  return async (body) => {
15930
16007
  const handEdit = HandEditRequestSchema.safeParse(body);
15931
16008
  if (handEdit.success) {
@@ -16100,7 +16177,7 @@ function describeHistory(media, entry, cacheDir) {
16100
16177
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16101
16178
  async function samePixels(a, b) {
16102
16179
  try {
16103
- const [left, right] = await Promise.all([readFile22(a), readFile22(b)]);
16180
+ const [left, right] = await Promise.all([readFile23(a), readFile23(b)]);
16104
16181
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16105
16182
  const x = decodePng(left);
16106
16183
  const y = decodePng(right);
@@ -16258,7 +16335,7 @@ async function buildGallerySnapshot(opts) {
16258
16335
  const media = /* @__PURE__ */ new Map();
16259
16336
  const plan = await buildPlan(specs, lock);
16260
16337
  const items = [];
16261
- const manifestText = await readFile22(loaded.path, "utf8");
16338
+ const manifestText = await readFile23(loaded.path, "utf8");
16262
16339
  const manifestSha256 = sha256(manifestText);
16263
16340
  const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
16264
16341
  let rawStyles = {};
@@ -16610,7 +16687,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16610
16687
  root: loaded.root,
16611
16688
  provider: loaded.manifest.provider,
16612
16689
  account: project.account ?? null,
16613
- manifestSha256: manifestSha256 ?? sha256(await readFile22(manifestPath, "utf8")),
16690
+ manifestSha256: manifestSha256 ?? sha256(await readFile23(manifestPath, "utf8")),
16614
16691
  historyLimit: historyLimit(loaded.manifest),
16615
16692
  entries: Object.keys(lock.entries).length,
16616
16693
  items: projectItems.length,
@@ -16775,6 +16852,7 @@ ${css}</style>
16775
16852
  </header>
16776
16853
  <main id="root"></main>
16777
16854
  <div id="selbar" class="selbar" hidden></div>
16855
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
16778
16856
  <footer>
16779
16857
  Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
16780
16858
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
@@ -16851,7 +16929,7 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16851
16929
  // src/editor/install.ts
16852
16930
  import { createHash as createHash5 } from "crypto";
16853
16931
  import { existsSync as existsSync27 } from "fs";
16854
- import { mkdir as mkdir11, readFile as readFile23, rename as rename9, rm as rm11, stat as stat4, writeFile as writeFile14 } from "fs/promises";
16932
+ import { mkdir as mkdir11, readFile as readFile24, rename as rename10, rm as rm11, stat as stat4, writeFile as writeFile15 } from "fs/promises";
16855
16933
  import os2 from "os";
16856
16934
  import path32 from "path";
16857
16935
  var EditorInstallError = class extends Error {
@@ -16882,7 +16960,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16882
16960
  }
16883
16961
  async function sha256Of(file) {
16884
16962
  try {
16885
- return createHash5("sha256").update(await readFile23(file)).digest("hex");
16963
+ return createHash5("sha256").update(await readFile24(file)).digest("hex");
16886
16964
  } catch {
16887
16965
  return null;
16888
16966
  }
@@ -16956,8 +17034,8 @@ async function installEditor(opts = {}) {
16956
17034
  const final = path32.join(dir, name);
16957
17035
  const tmp = `${final}.${process.pid}.part`;
16958
17036
  try {
16959
- await writeFile14(tmp, bytes);
16960
- await rename9(tmp, final);
17037
+ await writeFile15(tmp, bytes);
17038
+ await rename10(tmp, final);
16961
17039
  } finally {
16962
17040
  await rm11(tmp, { force: true });
16963
17041
  }
@@ -17003,7 +17081,7 @@ function editorContentType(name) {
17003
17081
  }
17004
17082
 
17005
17083
  // src/gallery/editor.ts
17006
- import { readFile as readFile24 } from "fs/promises";
17084
+ import { readFile as readFile25 } from "fs/promises";
17007
17085
  import path33 from "path";
17008
17086
  function editorRoute(release, name = "index.html") {
17009
17087
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
@@ -17077,7 +17155,7 @@ function createGalleryEditorHandlers(opts = {}) {
17077
17155
  if (!status.installed) return null;
17078
17156
  let bytes;
17079
17157
  try {
17080
- bytes = await readFile24(path33.join(dir, name));
17158
+ bytes = await readFile25(path33.join(dir, name));
17081
17159
  } catch {
17082
17160
  verified = null;
17083
17161
  return null;
@@ -17094,7 +17172,7 @@ function createGalleryEditorHandlers(opts = {}) {
17094
17172
  // src/gallery/server.ts
17095
17173
  import { randomBytes } from "crypto";
17096
17174
  import { createServer as createServer2 } from "http";
17097
- import { readFile as readFile25 } from "fs/promises";
17175
+ import { readFile as readFile26 } from "fs/promises";
17098
17176
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17099
17177
  var EDITOR_CSP = [
17100
17178
  "default-src 'self'",
@@ -17275,7 +17353,7 @@ async function serveGallery(opts) {
17275
17353
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17276
17354
  if (!asset) return fail(404, "no such review asset");
17277
17355
  try {
17278
- const bytes = await readFile25(asset.path);
17356
+ const bytes = await readFile26(asset.path);
17279
17357
  res.writeHead(200, {
17280
17358
  "Content-Type": asset.contentType,
17281
17359
  "Cache-Control": "no-store",
@@ -17358,7 +17436,7 @@ async function serveGallery(opts) {
17358
17436
  if (!asset) return fail(404, "no such gallery media");
17359
17437
  let bytes;
17360
17438
  try {
17361
- bytes = await readFile25(asset.path);
17439
+ bytes = await readFile26(asset.path);
17362
17440
  } catch {
17363
17441
  return fail(404, "gallery media is no longer on disk; refresh the page");
17364
17442
  }
@@ -17768,7 +17846,7 @@ function createGenerateHandlers(opts) {
17768
17846
  }
17769
17847
 
17770
17848
  // src/pipeline/salvage.ts
17771
- import { readFile as readFile26 } from "fs/promises";
17849
+ import { readFile as readFile27 } from "fs/promises";
17772
17850
  import { existsSync as existsSync28 } from "fs";
17773
17851
  import path34 from "path";
17774
17852
  function providerClaimId(provider, objectId) {
@@ -17780,7 +17858,7 @@ async function loadClaims(lockPaths, opts = {}) {
17780
17858
  if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
17781
17859
  let parsed;
17782
17860
  try {
17783
- parsed = parseLock(JSON.parse(await readFile26(p, "utf8")));
17861
+ parsed = parseLock(JSON.parse(await readFile27(p, "utf8")));
17784
17862
  } catch {
17785
17863
  throw new Error(`Claim lockfile is malformed: ${p}`);
17786
17864
  }
@@ -18017,7 +18095,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18017
18095
  }
18018
18096
 
18019
18097
  // src/pick/salvage-server.ts
18020
- import { mkdir as mkdir12, writeFile as writeFile15, readFile as readFile27 } from "fs/promises";
18098
+ import { mkdir as mkdir12, writeFile as writeFile16, readFile as readFile28 } from "fs/promises";
18021
18099
  import path35 from "path";
18022
18100
 
18023
18101
  // src/pick/salvage-sheet.ts
@@ -18243,7 +18321,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18243
18321
  const rel = path35.join("_salvaged", `${assetId}.png`);
18244
18322
  const outFile = path35.resolve(ctx.importDir, rel);
18245
18323
  await mkdir12(path35.dirname(outFile), { recursive: true });
18246
- await writeFile15(outFile, buf);
18324
+ await writeFile16(outFile, buf);
18247
18325
  ctx.manifest.assets[assetId] = {
18248
18326
  prompt: orphan.prompt,
18249
18327
  promptByStyle: {},
@@ -18299,9 +18377,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18299
18377
  }
18300
18378
  }
18301
18379
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18302
- const raw = JSON.parse(await readFile27(ctx.manifestPath, "utf8"));
18380
+ const raw = JSON.parse(await readFile28(ctx.manifestPath, "utf8"));
18303
18381
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18304
- await writeFile15(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18382
+ await writeFile16(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18305
18383
  await saveLock(ctx.lockPath, ctx.lock);
18306
18384
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18307
18385
  return result;