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/cli.js +131 -68
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +55 -1
- package/dist/client/gallery.js +405 -11
- package/dist/index.cjs +105 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +100 -37
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +7 -0
- package/docs/CLI.md +20 -3
- package/package.json +1 -1
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
|
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
<
|
|
17067
|
-
<
|
|
17068
|
-
<
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
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
|
-
<
|
|
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>
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
17277
|
-
await (0,
|
|
17339
|
+
await (0, import_promises26.writeFile)(tmp, bytes);
|
|
17340
|
+
await (0, import_promises26.rename)(tmp, final);
|
|
17278
17341
|
} finally {
|
|
17279
|
-
await (0,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
18563
|
-
await (0,
|
|
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,
|
|
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,
|
|
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;
|