pixelkiln 0.31.0 → 0.32.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 +86 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +84 -14
- 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 +85 -15
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +11 -1
- package/docs/RECOVERY.md +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6431,7 +6431,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
6431
6431
|
}
|
|
6432
6432
|
|
|
6433
6433
|
// src/pipeline/fetch.ts
|
|
6434
|
-
import { existsSync as existsSync10 } from "fs";
|
|
6434
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
6435
6435
|
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6436
6436
|
import path14 from "path";
|
|
6437
6437
|
|
|
@@ -6487,7 +6487,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6487
6487
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
6488
6488
|
if (!opts.repair) return false;
|
|
6489
6489
|
const spec = specByKey.get(key);
|
|
6490
|
-
|
|
6490
|
+
if (e.outputs.length === 0 || !spec) return true;
|
|
6491
|
+
return e.outputs.some((output) => {
|
|
6492
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
6493
|
+
if (!existsSync10(file)) return true;
|
|
6494
|
+
return Boolean(opts.force) && sha256(readFileSync4(file)) !== output.sha256;
|
|
6495
|
+
});
|
|
6491
6496
|
});
|
|
6492
6497
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
6493
6498
|
let cursor = 0;
|
|
@@ -9412,6 +9417,8 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9412
9417
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
9413
9418
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
9414
9419
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9420
|
+
.drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
|
|
9421
|
+
.drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
|
|
9415
9422
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9416
9423
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
9417
9424
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
@@ -9621,7 +9628,7 @@ function renderJobs() {
|
|
|
9621
9628
|
const row = el('div', 'job');
|
|
9622
9629
|
const ph = el('span', 'ph');
|
|
9623
9630
|
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') +
|
|
9631
|
+
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
9632
|
(job.project ? ' in ' + job.project : '');
|
|
9626
9633
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
9627
9634
|
last.title = job.keys.join('\\n');
|
|
@@ -9746,13 +9753,16 @@ function budgetLine() {
|
|
|
9746
9753
|
|
|
9747
9754
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
9748
9755
|
// 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 } = {}) {
|
|
9756
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
|
|
9750
9757
|
const host = $('dialog-host');
|
|
9751
9758
|
host.textContent = '';
|
|
9752
9759
|
const wrap = el('div', 'dialog');
|
|
9753
9760
|
const form = el('form');
|
|
9754
9761
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
9762
|
+
const free = resume || refresh || restore;
|
|
9763
|
+
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
9755
9764
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
9765
|
+
: restore ? 'Restore ' + noun
|
|
9756
9766
|
: resume ? 'Resume ' + noun
|
|
9757
9767
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
9758
9768
|
const table = el('table');
|
|
@@ -9762,9 +9772,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9762
9772
|
for (const item of items) {
|
|
9763
9773
|
const tr = el('tr');
|
|
9764
9774
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
9765
|
-
el('td', 'n',
|
|
9775
|
+
el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
9766
9776
|
table.append(tr);
|
|
9767
|
-
if (!
|
|
9777
|
+
if (!free) {
|
|
9768
9778
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
9769
9779
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
9770
9780
|
}
|
|
@@ -9773,6 +9783,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9773
9783
|
const sum = el('div', 'sum');
|
|
9774
9784
|
if (refresh) {
|
|
9775
9785
|
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.'));
|
|
9786
|
+
} else if (restore) {
|
|
9787
|
+
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.'));
|
|
9788
|
+
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
9789
|
} else if (resume) {
|
|
9777
9790
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
9778
9791
|
} else {
|
|
@@ -9785,12 +9798,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9785
9798
|
if (left !== null && g.cost > left) line.className = 'warn';
|
|
9786
9799
|
sum.append(line);
|
|
9787
9800
|
}
|
|
9788
|
-
if (force) sum.append(el('div', 'warn', '
|
|
9801
|
+
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.'));
|
|
9802
|
+
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
9803
|
sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
|
|
9790
9804
|
}
|
|
9791
9805
|
form.append(sum);
|
|
9792
9806
|
const actions = el('div', 'actions');
|
|
9793
|
-
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
9807
|
+
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
9808
|
go.type = 'submit';
|
|
9795
9809
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
9796
9810
|
const msg = el('span', 'msg');
|
|
@@ -9805,6 +9819,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9805
9819
|
if (force) body.force = true;
|
|
9806
9820
|
if (resume) body.resume = true;
|
|
9807
9821
|
if (refresh) body.refresh = true;
|
|
9822
|
+
if (restore) body.restore = true;
|
|
9808
9823
|
const job = await postGenerate(body);
|
|
9809
9824
|
host.textContent = '';
|
|
9810
9825
|
GEN.jobs.unshift(job);
|
|
@@ -10463,6 +10478,13 @@ function generateActions(item) {
|
|
|
10463
10478
|
};
|
|
10464
10479
|
if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
|
|
10465
10480
|
else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
|
|
10481
|
+
else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
|
|
10482
|
+
// The file changed after download. Putting the recorded bytes back and
|
|
10483
|
+
// regenerating both discard that change, so both say so and ask.
|
|
10484
|
+
add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
|
|
10485
|
+
add('Regenerate' + cost, null, { force: true });
|
|
10486
|
+
} else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
|
|
10487
|
+
else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
|
|
10466
10488
|
if (item.state === 'in-flight' || item.state === 'recoverable') {
|
|
10467
10489
|
// A job already parked this asset in review: go straight to its sheet.
|
|
10468
10490
|
const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
|
|
@@ -11153,6 +11175,15 @@ function renderDrawer() {
|
|
|
11153
11175
|
if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
|
|
11154
11176
|
body.append(stateNode(item.state, item.reason));
|
|
11155
11177
|
if (GENERATION) body.append(generateActions(item));
|
|
11178
|
+
// Writes are opt-in flags on the command line; say so where the buttons would be.
|
|
11179
|
+
if ((!EDITABLE || !GENERATION) && item.declared) {
|
|
11180
|
+
const missing = [];
|
|
11181
|
+
if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
|
|
11182
|
+
if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
|
|
11183
|
+
const hint = el('div', 'state-dim readonly-hint');
|
|
11184
|
+
hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
|
|
11185
|
+
body.append(hint);
|
|
11186
|
+
}
|
|
11156
11187
|
if (canEdit && ui.editing === item.id) body.append(editForm(item));
|
|
11157
11188
|
// Plan already quotes the error as the reason for a failed entry; only a
|
|
11158
11189
|
// stale or superseded failure needs its own line.
|
|
@@ -11743,13 +11774,21 @@ import { z as z8 } from "zod";
|
|
|
11743
11774
|
var GenerateRequestSchema = z8.object({
|
|
11744
11775
|
project: z8.string().min(1).optional(),
|
|
11745
11776
|
keys: z8.array(z8.string().min(1)).min(1).max(500),
|
|
11746
|
-
/**
|
|
11777
|
+
/**
|
|
11778
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
11779
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
11780
|
+
* the recorded bytes back over a modified file.
|
|
11781
|
+
*/
|
|
11747
11782
|
force: z8.boolean().optional(),
|
|
11748
11783
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
11749
11784
|
resume: z8.boolean().optional(),
|
|
11750
11785
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11751
|
-
refresh: z8.boolean().optional()
|
|
11752
|
-
|
|
11786
|
+
refresh: z8.boolean().optional(),
|
|
11787
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
11788
|
+
restore: z8.boolean().optional()
|
|
11789
|
+
}).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
|
|
11790
|
+
message: "resume, refresh, and restore are exclusive"
|
|
11791
|
+
});
|
|
11753
11792
|
var GenerateRequestError = class extends Error {
|
|
11754
11793
|
constructor(message6, status = 400) {
|
|
11755
11794
|
super(message6);
|
|
@@ -11837,7 +11876,9 @@ function createGenerateHandlers(opts) {
|
|
|
11837
11876
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
11838
11877
|
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
11839
11878
|
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
11840
|
-
onProgress: (line) => say(job, line)
|
|
11879
|
+
onProgress: (line) => say(job, line),
|
|
11880
|
+
// A forced regeneration replaces whatever is on disk, as `gen --force`.
|
|
11881
|
+
force: job.force
|
|
11841
11882
|
});
|
|
11842
11883
|
job.counts.downloaded += res.downloaded;
|
|
11843
11884
|
if (res.downloaded || res.failed) {
|
|
@@ -11852,12 +11893,32 @@ function createGenerateHandlers(opts) {
|
|
|
11852
11893
|
job.finishedAt = now().toISOString();
|
|
11853
11894
|
}
|
|
11854
11895
|
}
|
|
11896
|
+
async function restore(job, ctx, specs) {
|
|
11897
|
+
job.phase = "fetching";
|
|
11898
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
11899
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
11900
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
11901
|
+
onProgress: (line) => say(job, line),
|
|
11902
|
+
repair: true,
|
|
11903
|
+
force: job.force
|
|
11904
|
+
});
|
|
11905
|
+
job.counts.downloaded += res.downloaded;
|
|
11906
|
+
say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
|
|
11907
|
+
if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
|
|
11908
|
+
}
|
|
11909
|
+
job.phase = "done";
|
|
11910
|
+
job.finishedAt = now().toISOString();
|
|
11911
|
+
}
|
|
11855
11912
|
async function run(job, ctx, specs, groups) {
|
|
11856
11913
|
try {
|
|
11857
11914
|
if (job.mode === "refresh") {
|
|
11858
11915
|
await refresh(job, ctx, specs);
|
|
11859
11916
|
return;
|
|
11860
11917
|
}
|
|
11918
|
+
if (job.mode === "restore") {
|
|
11919
|
+
await restore(job, ctx, specs);
|
|
11920
|
+
return;
|
|
11921
|
+
}
|
|
11861
11922
|
if (job.mode === "generate") {
|
|
11862
11923
|
job.phase = "submitting";
|
|
11863
11924
|
for (const group of groups) {
|
|
@@ -11920,8 +11981,16 @@ function createGenerateHandlers(opts) {
|
|
|
11920
11981
|
const holder = busy.get(`${request.project ?? ""}:${key}`);
|
|
11921
11982
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
11922
11983
|
}
|
|
11984
|
+
if (request.restore) {
|
|
11985
|
+
for (const key of request.keys) {
|
|
11986
|
+
const entry = ctx.lock.entries[key];
|
|
11987
|
+
if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
|
|
11988
|
+
throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
|
|
11989
|
+
}
|
|
11990
|
+
}
|
|
11991
|
+
}
|
|
11923
11992
|
let groups = [];
|
|
11924
|
-
if (!request.resume && !request.refresh) {
|
|
11993
|
+
if (!request.resume && !request.refresh && !request.restore) {
|
|
11925
11994
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
11926
11995
|
if (!plan.actionable.length) {
|
|
11927
11996
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -11941,7 +12010,8 @@ function createGenerateHandlers(opts) {
|
|
|
11941
12010
|
id: randomBytes2(8).toString("hex"),
|
|
11942
12011
|
project: request.project ?? null,
|
|
11943
12012
|
keys: request.keys,
|
|
11944
|
-
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
12013
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
|
|
12014
|
+
force: Boolean(request.force),
|
|
11945
12015
|
phase: "queued",
|
|
11946
12016
|
startedAt: now().toISOString(),
|
|
11947
12017
|
finishedAt: null,
|
|
@@ -11953,7 +12023,7 @@ function createGenerateHandlers(opts) {
|
|
|
11953
12023
|
};
|
|
11954
12024
|
jobs.set(job.id, job);
|
|
11955
12025
|
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("; "));
|
|
12026
|
+
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
12027
|
void run(job, ctx, specs, groups);
|
|
11958
12028
|
return job;
|
|
11959
12029
|
},
|
|
@@ -14016,7 +14086,7 @@ function announceGalleryReady(url, count, stdout = process.stdout, stderr = proc
|
|
|
14016
14086
|
gallery of ${count} generation${count === 1 ? "" : "s"}: ${url}
|
|
14017
14087
|
` + (notes.length ? ` (${notes.join("; ")}; Ctrl+C to stop)
|
|
14018
14088
|
|
|
14019
|
-
` : " (read-only;
|
|
14089
|
+
` : " (read-only \u2014 add --edit to change prompts or edit sprites, --budget <n> to generate; Ctrl+C to stop)\n\n")
|
|
14020
14090
|
);
|
|
14021
14091
|
}
|
|
14022
14092
|
function sessionBudget(args) {
|