pixelkiln 0.31.0 → 0.32.1
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 +99 -20
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +97 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +98 -19
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +11 -1
- package/docs/PIXELLAB.md +2 -1
- package/docs/RECOVERY.md +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2285,8 +2285,15 @@ function lockKey(styleId, assetId) {
|
|
|
2285
2285
|
|
|
2286
2286
|
// src/providers/pixellab.ts
|
|
2287
2287
|
function pixelLabObjectUrl(generator, objectId) {
|
|
2288
|
-
if (!objectId
|
|
2289
|
-
|
|
2288
|
+
if (!objectId) return null;
|
|
2289
|
+
if (generator === "map" || generator === "1dir") {
|
|
2290
|
+
return `https://www.pixellab.ai/create-object/${encodeURIComponent(objectId)}`;
|
|
2291
|
+
}
|
|
2292
|
+
if (generator === "tiles") {
|
|
2293
|
+
const setId = objectId.split("#")[0];
|
|
2294
|
+
return setId ? `https://www.pixellab.ai/maps/tiles/${encodeURIComponent(setId)}` : null;
|
|
2295
|
+
}
|
|
2296
|
+
return null;
|
|
2290
2297
|
}
|
|
2291
2298
|
var PixelLabProvider = class _PixelLabProvider {
|
|
2292
2299
|
constructor(client) {
|
|
@@ -6431,7 +6438,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
6431
6438
|
}
|
|
6432
6439
|
|
|
6433
6440
|
// src/pipeline/fetch.ts
|
|
6434
|
-
import { existsSync as existsSync10 } from "fs";
|
|
6441
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
6435
6442
|
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6436
6443
|
import path14 from "path";
|
|
6437
6444
|
|
|
@@ -6487,7 +6494,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6487
6494
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
6488
6495
|
if (!opts.repair) return false;
|
|
6489
6496
|
const spec = specByKey.get(key);
|
|
6490
|
-
|
|
6497
|
+
if (e.outputs.length === 0 || !spec) return true;
|
|
6498
|
+
return e.outputs.some((output) => {
|
|
6499
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
6500
|
+
if (!existsSync10(file)) return true;
|
|
6501
|
+
return Boolean(opts.force) && sha256(readFileSync4(file)) !== output.sha256;
|
|
6502
|
+
});
|
|
6491
6503
|
});
|
|
6492
6504
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
6493
6505
|
let cursor = 0;
|
|
@@ -9412,6 +9424,8 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9412
9424
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
9413
9425
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
9414
9426
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9427
|
+
.drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
|
|
9428
|
+
.drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
|
|
9415
9429
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9416
9430
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
9417
9431
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
@@ -9621,7 +9635,7 @@ function renderJobs() {
|
|
|
9621
9635
|
const row = el('div', 'job');
|
|
9622
9636
|
const ph = el('span', 'ph');
|
|
9623
9637
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
9624
|
-
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9638
|
+
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'restore' ? 'restore ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9625
9639
|
(job.project ? ' in ' + job.project : '');
|
|
9626
9640
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
9627
9641
|
last.title = job.keys.join('\\n');
|
|
@@ -9746,13 +9760,16 @@ function budgetLine() {
|
|
|
9746
9760
|
|
|
9747
9761
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
9748
9762
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
9749
|
-
function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
|
|
9763
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
|
|
9750
9764
|
const host = $('dialog-host');
|
|
9751
9765
|
host.textContent = '';
|
|
9752
9766
|
const wrap = el('div', 'dialog');
|
|
9753
9767
|
const form = el('form');
|
|
9754
9768
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
9769
|
+
const free = resume || refresh || restore;
|
|
9770
|
+
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
9755
9771
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
9772
|
+
: restore ? 'Restore ' + noun
|
|
9756
9773
|
: resume ? 'Resume ' + noun
|
|
9757
9774
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
9758
9775
|
const table = el('table');
|
|
@@ -9762,9 +9779,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9762
9779
|
for (const item of items) {
|
|
9763
9780
|
const tr = el('tr');
|
|
9764
9781
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
9765
|
-
el('td', 'n',
|
|
9782
|
+
el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
9766
9783
|
table.append(tr);
|
|
9767
|
-
if (!
|
|
9784
|
+
if (!free) {
|
|
9768
9785
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
9769
9786
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
9770
9787
|
}
|
|
@@ -9773,6 +9790,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9773
9790
|
const sum = el('div', 'sum');
|
|
9774
9791
|
if (refresh) {
|
|
9775
9792
|
sum.append(el('div', null, 'Re-downloads each object from the provider and replaces the local file only when the object changed upstream \u2014 for example after editing it in the provider\u2019s own editor. Unchanged objects are left alone; a file you changed locally is refused. Nothing is submitted.'));
|
|
9793
|
+
} else if (restore) {
|
|
9794
|
+
sum.append(el('div', null, 'Puts the recorded bytes back on disk from the local cache or the provider object. Nothing is generated or submitted.'));
|
|
9795
|
+
if (force && replacing.length) sum.append(el('div', 'warn', 'The file on disk was changed after PixelKiln wrote it. Restoring discards that change. To keep it instead, cancel and use Edit by hand or Edit in browser first: the edit is copied beside the generated file, and the record stays intact.'));
|
|
9776
9796
|
} else if (resume) {
|
|
9777
9797
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
9778
9798
|
} else {
|
|
@@ -9785,12 +9805,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9785
9805
|
if (left !== null && g.cost > left) line.className = 'warn';
|
|
9786
9806
|
sum.append(line);
|
|
9787
9807
|
}
|
|
9788
|
-
if (force) sum.append(el('div', 'warn', '
|
|
9808
|
+
if (force && replacing.length) sum.append(el('div', 'warn', (replacing.length === 1 ? 'This file' : replacing.length + ' of these files') + ' changed on disk after PixelKiln wrote it (or was never recorded). Generating replaces it and that change is lost. To keep it, cancel and use Edit by hand or Edit in browser first.'));
|
|
9809
|
+
else if (force) sum.append(el('div', 'warn', 'Regenerating replaces the current art. The previous file is replaced when the new result is fetched; the provider objects it came from are not deleted.'));
|
|
9789
9810
|
sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
|
|
9790
9811
|
}
|
|
9791
9812
|
form.append(sum);
|
|
9792
9813
|
const actions = el('div', 'actions');
|
|
9793
|
-
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
9814
|
+
const go = el('button', 'primary', refresh ? 'Pull changes' : restore ? (force && replacing.length ? 'Restore and discard my change' : 'Restore') : resume ? 'Resume' : force && replacing.length ? 'Generate and replace' : 'Generate');
|
|
9794
9815
|
go.type = 'submit';
|
|
9795
9816
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
9796
9817
|
const msg = el('span', 'msg');
|
|
@@ -9805,6 +9826,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9805
9826
|
if (force) body.force = true;
|
|
9806
9827
|
if (resume) body.resume = true;
|
|
9807
9828
|
if (refresh) body.refresh = true;
|
|
9829
|
+
if (restore) body.restore = true;
|
|
9808
9830
|
const job = await postGenerate(body);
|
|
9809
9831
|
host.textContent = '';
|
|
9810
9832
|
GEN.jobs.unshift(job);
|
|
@@ -10436,9 +10458,11 @@ function upstreamSection(item) {
|
|
|
10436
10458
|
if (item.upstreamUrl) {
|
|
10437
10459
|
const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
|
|
10438
10460
|
a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
|
|
10439
|
-
|
|
10461
|
+
const tiles = item.generator === 'tiles';
|
|
10462
|
+
row(dl, tiles ? 'tile set' : 'object', a);
|
|
10440
10463
|
row(dl, 'note', item.provider === 'pixellab'
|
|
10441
|
-
? 'The
|
|
10464
|
+
? (tiles ? 'The tile set this generation was chosen from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
|
|
10465
|
+
: 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.')
|
|
10442
10466
|
: 'The provider\u2019s page for this object.');
|
|
10443
10467
|
}
|
|
10444
10468
|
s.append(dl);
|
|
@@ -10463,6 +10487,13 @@ function generateActions(item) {
|
|
|
10463
10487
|
};
|
|
10464
10488
|
if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
|
|
10465
10489
|
else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
|
|
10490
|
+
else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
|
|
10491
|
+
// The file changed after download. Putting the recorded bytes back and
|
|
10492
|
+
// regenerating both discard that change, so both say so and ask.
|
|
10493
|
+
add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
|
|
10494
|
+
add('Regenerate' + cost, null, { force: true });
|
|
10495
|
+
} else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
|
|
10496
|
+
else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
|
|
10466
10497
|
if (item.state === 'in-flight' || item.state === 'recoverable') {
|
|
10467
10498
|
// A job already parked this asset in review: go straight to its sheet.
|
|
10468
10499
|
const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
|
|
@@ -11153,6 +11184,15 @@ function renderDrawer() {
|
|
|
11153
11184
|
if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
|
|
11154
11185
|
body.append(stateNode(item.state, item.reason));
|
|
11155
11186
|
if (GENERATION) body.append(generateActions(item));
|
|
11187
|
+
// Writes are opt-in flags on the command line; say so where the buttons would be.
|
|
11188
|
+
if ((!EDITABLE || !GENERATION) && item.declared) {
|
|
11189
|
+
const missing = [];
|
|
11190
|
+
if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
|
|
11191
|
+
if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
|
|
11192
|
+
const hint = el('div', 'state-dim readonly-hint');
|
|
11193
|
+
hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
|
|
11194
|
+
body.append(hint);
|
|
11195
|
+
}
|
|
11156
11196
|
if (canEdit && ui.editing === item.id) body.append(editForm(item));
|
|
11157
11197
|
// Plan already quotes the error as the reason for a failed entry; only a
|
|
11158
11198
|
// stale or superseded failure needs its own line.
|
|
@@ -11743,13 +11783,21 @@ import { z as z8 } from "zod";
|
|
|
11743
11783
|
var GenerateRequestSchema = z8.object({
|
|
11744
11784
|
project: z8.string().min(1).optional(),
|
|
11745
11785
|
keys: z8.array(z8.string().min(1)).min(1).max(500),
|
|
11746
|
-
/**
|
|
11786
|
+
/**
|
|
11787
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
11788
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
11789
|
+
* the recorded bytes back over a modified file.
|
|
11790
|
+
*/
|
|
11747
11791
|
force: z8.boolean().optional(),
|
|
11748
11792
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
11749
11793
|
resume: z8.boolean().optional(),
|
|
11750
11794
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11751
|
-
refresh: z8.boolean().optional()
|
|
11752
|
-
|
|
11795
|
+
refresh: z8.boolean().optional(),
|
|
11796
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
11797
|
+
restore: z8.boolean().optional()
|
|
11798
|
+
}).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
|
|
11799
|
+
message: "resume, refresh, and restore are exclusive"
|
|
11800
|
+
});
|
|
11753
11801
|
var GenerateRequestError = class extends Error {
|
|
11754
11802
|
constructor(message6, status = 400) {
|
|
11755
11803
|
super(message6);
|
|
@@ -11837,7 +11885,9 @@ function createGenerateHandlers(opts) {
|
|
|
11837
11885
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
11838
11886
|
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
11839
11887
|
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
11840
|
-
onProgress: (line) => say(job, line)
|
|
11888
|
+
onProgress: (line) => say(job, line),
|
|
11889
|
+
// A forced regeneration replaces whatever is on disk, as `gen --force`.
|
|
11890
|
+
force: job.force
|
|
11841
11891
|
});
|
|
11842
11892
|
job.counts.downloaded += res.downloaded;
|
|
11843
11893
|
if (res.downloaded || res.failed) {
|
|
@@ -11852,12 +11902,32 @@ function createGenerateHandlers(opts) {
|
|
|
11852
11902
|
job.finishedAt = now().toISOString();
|
|
11853
11903
|
}
|
|
11854
11904
|
}
|
|
11905
|
+
async function restore(job, ctx, specs) {
|
|
11906
|
+
job.phase = "fetching";
|
|
11907
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
11908
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
11909
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
11910
|
+
onProgress: (line) => say(job, line),
|
|
11911
|
+
repair: true,
|
|
11912
|
+
force: job.force
|
|
11913
|
+
});
|
|
11914
|
+
job.counts.downloaded += res.downloaded;
|
|
11915
|
+
say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
|
|
11916
|
+
if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
|
|
11917
|
+
}
|
|
11918
|
+
job.phase = "done";
|
|
11919
|
+
job.finishedAt = now().toISOString();
|
|
11920
|
+
}
|
|
11855
11921
|
async function run(job, ctx, specs, groups) {
|
|
11856
11922
|
try {
|
|
11857
11923
|
if (job.mode === "refresh") {
|
|
11858
11924
|
await refresh(job, ctx, specs);
|
|
11859
11925
|
return;
|
|
11860
11926
|
}
|
|
11927
|
+
if (job.mode === "restore") {
|
|
11928
|
+
await restore(job, ctx, specs);
|
|
11929
|
+
return;
|
|
11930
|
+
}
|
|
11861
11931
|
if (job.mode === "generate") {
|
|
11862
11932
|
job.phase = "submitting";
|
|
11863
11933
|
for (const group of groups) {
|
|
@@ -11920,8 +11990,16 @@ function createGenerateHandlers(opts) {
|
|
|
11920
11990
|
const holder = busy.get(`${request.project ?? ""}:${key}`);
|
|
11921
11991
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
11922
11992
|
}
|
|
11993
|
+
if (request.restore) {
|
|
11994
|
+
for (const key of request.keys) {
|
|
11995
|
+
const entry = ctx.lock.entries[key];
|
|
11996
|
+
if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
|
|
11997
|
+
throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
|
|
11998
|
+
}
|
|
11999
|
+
}
|
|
12000
|
+
}
|
|
11923
12001
|
let groups = [];
|
|
11924
|
-
if (!request.resume && !request.refresh) {
|
|
12002
|
+
if (!request.resume && !request.refresh && !request.restore) {
|
|
11925
12003
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
11926
12004
|
if (!plan.actionable.length) {
|
|
11927
12005
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -11941,7 +12019,8 @@ function createGenerateHandlers(opts) {
|
|
|
11941
12019
|
id: randomBytes2(8).toString("hex"),
|
|
11942
12020
|
project: request.project ?? null,
|
|
11943
12021
|
keys: request.keys,
|
|
11944
|
-
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
12022
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
|
|
12023
|
+
force: Boolean(request.force),
|
|
11945
12024
|
phase: "queued",
|
|
11946
12025
|
startedAt: now().toISOString(),
|
|
11947
12026
|
finishedAt: null,
|
|
@@ -11953,7 +12032,7 @@ function createGenerateHandlers(opts) {
|
|
|
11953
12032
|
};
|
|
11954
12033
|
jobs.set(job.id, job);
|
|
11955
12034
|
contexts.set(job.id, ctx);
|
|
11956
|
-
say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
12035
|
+
say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.restore ? `restoring ${request.keys.length} asset(s) at no cost${request.force ? " (replacing modified files)" : ""}` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
11957
12036
|
void run(job, ctx, specs, groups);
|
|
11958
12037
|
return job;
|
|
11959
12038
|
},
|
|
@@ -14016,7 +14095,7 @@ function announceGalleryReady(url, count, stdout = process.stdout, stderr = proc
|
|
|
14016
14095
|
gallery of ${count} generation${count === 1 ? "" : "s"}: ${url}
|
|
14017
14096
|
` + (notes.length ? ` (${notes.join("; ")}; Ctrl+C to stop)
|
|
14018
14097
|
|
|
14019
|
-
` : " (read-only;
|
|
14098
|
+
` : " (read-only \u2014 add --edit to change prompts or edit sprites, --budget <n> to generate; Ctrl+C to stop)\n\n")
|
|
14020
14099
|
);
|
|
14021
14100
|
}
|
|
14022
14101
|
function sessionBudget(args) {
|