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/index.cjs
CHANGED
|
@@ -8041,7 +8041,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8041
8041
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
8042
8042
|
if (!opts.repair) return false;
|
|
8043
8043
|
const spec = specByKey.get(key);
|
|
8044
|
-
|
|
8044
|
+
if (e.outputs.length === 0 || !spec) return true;
|
|
8045
|
+
return e.outputs.some((output) => {
|
|
8046
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
8047
|
+
if (!(0, import_node_fs15.existsSync)(file)) return true;
|
|
8048
|
+
return Boolean(opts.force) && sha256((0, import_node_fs15.readFileSync)(file)) !== output.sha256;
|
|
8049
|
+
});
|
|
8045
8050
|
});
|
|
8046
8051
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
8047
8052
|
let cursor = 0;
|
|
@@ -10858,6 +10863,8 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10858
10863
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10859
10864
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
10860
10865
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10866
|
+
.drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
|
|
10867
|
+
.drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
|
|
10861
10868
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10862
10869
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
10863
10870
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
@@ -11067,7 +11074,7 @@ function renderJobs() {
|
|
|
11067
11074
|
const row = el('div', 'job');
|
|
11068
11075
|
const ph = el('span', 'ph');
|
|
11069
11076
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
11070
|
-
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
11077
|
+
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') +
|
|
11071
11078
|
(job.project ? ' in ' + job.project : '');
|
|
11072
11079
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
11073
11080
|
last.title = job.keys.join('\\n');
|
|
@@ -11192,13 +11199,16 @@ function budgetLine() {
|
|
|
11192
11199
|
|
|
11193
11200
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
11194
11201
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
11195
|
-
function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
|
|
11202
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
|
|
11196
11203
|
const host = $('dialog-host');
|
|
11197
11204
|
host.textContent = '';
|
|
11198
11205
|
const wrap = el('div', 'dialog');
|
|
11199
11206
|
const form = el('form');
|
|
11200
11207
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
11208
|
+
const free = resume || refresh || restore;
|
|
11209
|
+
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
11201
11210
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
11211
|
+
: restore ? 'Restore ' + noun
|
|
11202
11212
|
: resume ? 'Resume ' + noun
|
|
11203
11213
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
11204
11214
|
const table = el('table');
|
|
@@ -11208,9 +11218,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
11208
11218
|
for (const item of items) {
|
|
11209
11219
|
const tr = el('tr');
|
|
11210
11220
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
11211
|
-
el('td', 'n',
|
|
11221
|
+
el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
11212
11222
|
table.append(tr);
|
|
11213
|
-
if (!
|
|
11223
|
+
if (!free) {
|
|
11214
11224
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
11215
11225
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
11216
11226
|
}
|
|
@@ -11219,6 +11229,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
11219
11229
|
const sum = el('div', 'sum');
|
|
11220
11230
|
if (refresh) {
|
|
11221
11231
|
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.'));
|
|
11232
|
+
} else if (restore) {
|
|
11233
|
+
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.'));
|
|
11234
|
+
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.'));
|
|
11222
11235
|
} else if (resume) {
|
|
11223
11236
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
11224
11237
|
} else {
|
|
@@ -11231,12 +11244,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
11231
11244
|
if (left !== null && g.cost > left) line.className = 'warn';
|
|
11232
11245
|
sum.append(line);
|
|
11233
11246
|
}
|
|
11234
|
-
if (force) sum.append(el('div', 'warn', '
|
|
11247
|
+
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.'));
|
|
11248
|
+
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.'));
|
|
11235
11249
|
sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
|
|
11236
11250
|
}
|
|
11237
11251
|
form.append(sum);
|
|
11238
11252
|
const actions = el('div', 'actions');
|
|
11239
|
-
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
11253
|
+
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');
|
|
11240
11254
|
go.type = 'submit';
|
|
11241
11255
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
11242
11256
|
const msg = el('span', 'msg');
|
|
@@ -11251,6 +11265,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
11251
11265
|
if (force) body.force = true;
|
|
11252
11266
|
if (resume) body.resume = true;
|
|
11253
11267
|
if (refresh) body.refresh = true;
|
|
11268
|
+
if (restore) body.restore = true;
|
|
11254
11269
|
const job = await postGenerate(body);
|
|
11255
11270
|
host.textContent = '';
|
|
11256
11271
|
GEN.jobs.unshift(job);
|
|
@@ -11909,6 +11924,13 @@ function generateActions(item) {
|
|
|
11909
11924
|
};
|
|
11910
11925
|
if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
|
|
11911
11926
|
else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
|
|
11927
|
+
else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
|
|
11928
|
+
// The file changed after download. Putting the recorded bytes back and
|
|
11929
|
+
// regenerating both discard that change, so both say so and ask.
|
|
11930
|
+
add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
|
|
11931
|
+
add('Regenerate' + cost, null, { force: true });
|
|
11932
|
+
} else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
|
|
11933
|
+
else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
|
|
11912
11934
|
if (item.state === 'in-flight' || item.state === 'recoverable') {
|
|
11913
11935
|
// A job already parked this asset in review: go straight to its sheet.
|
|
11914
11936
|
const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
|
|
@@ -12599,6 +12621,15 @@ function renderDrawer() {
|
|
|
12599
12621
|
if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
|
|
12600
12622
|
body.append(stateNode(item.state, item.reason));
|
|
12601
12623
|
if (GENERATION) body.append(generateActions(item));
|
|
12624
|
+
// Writes are opt-in flags on the command line; say so where the buttons would be.
|
|
12625
|
+
if ((!EDITABLE || !GENERATION) && item.declared) {
|
|
12626
|
+
const missing = [];
|
|
12627
|
+
if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
|
|
12628
|
+
if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
|
|
12629
|
+
const hint = el('div', 'state-dim readonly-hint');
|
|
12630
|
+
hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
|
|
12631
|
+
body.append(hint);
|
|
12632
|
+
}
|
|
12602
12633
|
if (canEdit && ui.editing === item.id) body.append(editForm(item));
|
|
12603
12634
|
// Plan already quotes the error as the reason for a failed entry; only a
|
|
12604
12635
|
// stale or superseded failure needs its own line.
|
|
@@ -13487,13 +13518,21 @@ var import_zod10 = require("zod");
|
|
|
13487
13518
|
var GenerateRequestSchema = import_zod10.z.object({
|
|
13488
13519
|
project: import_zod10.z.string().min(1).optional(),
|
|
13489
13520
|
keys: import_zod10.z.array(import_zod10.z.string().min(1)).min(1).max(500),
|
|
13490
|
-
/**
|
|
13521
|
+
/**
|
|
13522
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
13523
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
13524
|
+
* the recorded bytes back over a modified file.
|
|
13525
|
+
*/
|
|
13491
13526
|
force: import_zod10.z.boolean().optional(),
|
|
13492
13527
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
13493
13528
|
resume: import_zod10.z.boolean().optional(),
|
|
13494
13529
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
13495
|
-
refresh: import_zod10.z.boolean().optional()
|
|
13496
|
-
|
|
13530
|
+
refresh: import_zod10.z.boolean().optional(),
|
|
13531
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
13532
|
+
restore: import_zod10.z.boolean().optional()
|
|
13533
|
+
}).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
|
|
13534
|
+
message: "resume, refresh, and restore are exclusive"
|
|
13535
|
+
});
|
|
13497
13536
|
var GenerateRequestError = class extends Error {
|
|
13498
13537
|
constructor(message6, status = 400) {
|
|
13499
13538
|
super(message6);
|
|
@@ -13581,7 +13620,9 @@ function createGenerateHandlers(opts) {
|
|
|
13581
13620
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
13582
13621
|
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
13583
13622
|
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
13584
|
-
onProgress: (line) => say(job, line)
|
|
13623
|
+
onProgress: (line) => say(job, line),
|
|
13624
|
+
// A forced regeneration replaces whatever is on disk, as `gen --force`.
|
|
13625
|
+
force: job.force
|
|
13585
13626
|
});
|
|
13586
13627
|
job.counts.downloaded += res.downloaded;
|
|
13587
13628
|
if (res.downloaded || res.failed) {
|
|
@@ -13596,12 +13637,32 @@ function createGenerateHandlers(opts) {
|
|
|
13596
13637
|
job.finishedAt = now().toISOString();
|
|
13597
13638
|
}
|
|
13598
13639
|
}
|
|
13640
|
+
async function restore(job, ctx, specs) {
|
|
13641
|
+
job.phase = "fetching";
|
|
13642
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
13643
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
13644
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
13645
|
+
onProgress: (line) => say(job, line),
|
|
13646
|
+
repair: true,
|
|
13647
|
+
force: job.force
|
|
13648
|
+
});
|
|
13649
|
+
job.counts.downloaded += res.downloaded;
|
|
13650
|
+
say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
|
|
13651
|
+
if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
|
|
13652
|
+
}
|
|
13653
|
+
job.phase = "done";
|
|
13654
|
+
job.finishedAt = now().toISOString();
|
|
13655
|
+
}
|
|
13599
13656
|
async function run(job, ctx, specs, groups) {
|
|
13600
13657
|
try {
|
|
13601
13658
|
if (job.mode === "refresh") {
|
|
13602
13659
|
await refresh(job, ctx, specs);
|
|
13603
13660
|
return;
|
|
13604
13661
|
}
|
|
13662
|
+
if (job.mode === "restore") {
|
|
13663
|
+
await restore(job, ctx, specs);
|
|
13664
|
+
return;
|
|
13665
|
+
}
|
|
13605
13666
|
if (job.mode === "generate") {
|
|
13606
13667
|
job.phase = "submitting";
|
|
13607
13668
|
for (const group of groups) {
|
|
@@ -13664,8 +13725,16 @@ function createGenerateHandlers(opts) {
|
|
|
13664
13725
|
const holder = busy.get(`${request.project ?? ""}:${key}`);
|
|
13665
13726
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
13666
13727
|
}
|
|
13728
|
+
if (request.restore) {
|
|
13729
|
+
for (const key of request.keys) {
|
|
13730
|
+
const entry = ctx.lock.entries[key];
|
|
13731
|
+
if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
|
|
13732
|
+
throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
|
|
13733
|
+
}
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13667
13736
|
let groups = [];
|
|
13668
|
-
if (!request.resume && !request.refresh) {
|
|
13737
|
+
if (!request.resume && !request.refresh && !request.restore) {
|
|
13669
13738
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
13670
13739
|
if (!plan.actionable.length) {
|
|
13671
13740
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -13685,7 +13754,8 @@ function createGenerateHandlers(opts) {
|
|
|
13685
13754
|
id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
|
|
13686
13755
|
project: request.project ?? null,
|
|
13687
13756
|
keys: request.keys,
|
|
13688
|
-
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
13757
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
|
|
13758
|
+
force: Boolean(request.force),
|
|
13689
13759
|
phase: "queued",
|
|
13690
13760
|
startedAt: now().toISOString(),
|
|
13691
13761
|
finishedAt: null,
|
|
@@ -13697,7 +13767,7 @@ function createGenerateHandlers(opts) {
|
|
|
13697
13767
|
};
|
|
13698
13768
|
jobs.set(job.id, job);
|
|
13699
13769
|
contexts.set(job.id, ctx);
|
|
13700
|
-
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("; "));
|
|
13770
|
+
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("; "));
|
|
13701
13771
|
void run(job, ctx, specs, groups);
|
|
13702
13772
|
return job;
|
|
13703
13773
|
},
|