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/cli.js +132 -54
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +13 -0
- package/dist/client/gallery.js +375 -14
- package/dist/index.cjs +106 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +516 -8
- package/dist/index.d.ts +516 -8
- package/dist/index.js +101 -23
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +4 -2
- package/docs/CHARACTERS.md +11 -3
- package/docs/CLI.md +7 -0
- package/docs/PIXELLAB.md +9 -1
- package/docs/REVISIONS.md +9 -4
- package/package.json +1 -1
- package/skills/pixelkiln/references/pixellab.md +11 -8
- package/skills/pixelkiln/references/revisions.md +13 -10
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
|
|
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
|
|
15453
|
-
* gallery does not offer
|
|
15454
|
-
*
|
|
15455
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
17285
|
-
await (0,
|
|
17362
|
+
await (0, import_promises26.writeFile)(tmp, bytes);
|
|
17363
|
+
await (0, import_promises26.rename)(tmp, final);
|
|
17286
17364
|
} finally {
|
|
17287
|
-
await (0,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
18571
|
-
await (0,
|
|
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,
|
|
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,
|
|
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;
|