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.d.cts
CHANGED
|
@@ -8312,8 +8312,14 @@ interface GenerateJob {
|
|
|
8312
8312
|
id: string;
|
|
8313
8313
|
project: string | null;
|
|
8314
8314
|
keys: string[];
|
|
8315
|
-
/**
|
|
8316
|
-
|
|
8315
|
+
/**
|
|
8316
|
+
* `resume` skips submission: poll, review, and fetch existing work at no
|
|
8317
|
+
* cost; `refresh` re-pulls changed objects; `restore` puts the recorded
|
|
8318
|
+
* bytes back on disk from the cache or the provider, as `pixelkiln restore`.
|
|
8319
|
+
*/
|
|
8320
|
+
mode: "generate" | "resume" | "refresh" | "restore";
|
|
8321
|
+
/** The job may replace a locally modified or untracked file, as `--force` does. */
|
|
8322
|
+
force: boolean;
|
|
8317
8323
|
phase: GeneratePhase;
|
|
8318
8324
|
startedAt: string;
|
|
8319
8325
|
finishedAt: string | null;
|
|
@@ -8377,36 +8383,46 @@ interface GalleryGenerateHandlers {
|
|
|
8377
8383
|
declare const GenerateRequestSchema: z.ZodEffects<z.ZodObject<{
|
|
8378
8384
|
project: z.ZodOptional<z.ZodString>;
|
|
8379
8385
|
keys: z.ZodArray<z.ZodString, "many">;
|
|
8380
|
-
/**
|
|
8386
|
+
/**
|
|
8387
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
8388
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
8389
|
+
* the recorded bytes back over a modified file.
|
|
8390
|
+
*/
|
|
8381
8391
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
8382
8392
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
8383
8393
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
8384
8394
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
8385
8395
|
refresh: z.ZodOptional<z.ZodBoolean>;
|
|
8396
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
8397
|
+
restore: z.ZodOptional<z.ZodBoolean>;
|
|
8386
8398
|
}, "strict", z.ZodTypeAny, {
|
|
8387
8399
|
keys: string[];
|
|
8388
8400
|
resume?: boolean | undefined;
|
|
8389
8401
|
force?: boolean | undefined;
|
|
8390
8402
|
project?: string | undefined;
|
|
8391
8403
|
refresh?: boolean | undefined;
|
|
8404
|
+
restore?: boolean | undefined;
|
|
8392
8405
|
}, {
|
|
8393
8406
|
keys: string[];
|
|
8394
8407
|
resume?: boolean | undefined;
|
|
8395
8408
|
force?: boolean | undefined;
|
|
8396
8409
|
project?: string | undefined;
|
|
8397
8410
|
refresh?: boolean | undefined;
|
|
8411
|
+
restore?: boolean | undefined;
|
|
8398
8412
|
}>, {
|
|
8399
8413
|
keys: string[];
|
|
8400
8414
|
resume?: boolean | undefined;
|
|
8401
8415
|
force?: boolean | undefined;
|
|
8402
8416
|
project?: string | undefined;
|
|
8403
8417
|
refresh?: boolean | undefined;
|
|
8418
|
+
restore?: boolean | undefined;
|
|
8404
8419
|
}, {
|
|
8405
8420
|
keys: string[];
|
|
8406
8421
|
resume?: boolean | undefined;
|
|
8407
8422
|
force?: boolean | undefined;
|
|
8408
8423
|
project?: string | undefined;
|
|
8409
8424
|
refresh?: boolean | undefined;
|
|
8425
|
+
restore?: boolean | undefined;
|
|
8410
8426
|
}>;
|
|
8411
8427
|
declare class GenerateRequestError extends Error {
|
|
8412
8428
|
readonly status: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -8312,8 +8312,14 @@ interface GenerateJob {
|
|
|
8312
8312
|
id: string;
|
|
8313
8313
|
project: string | null;
|
|
8314
8314
|
keys: string[];
|
|
8315
|
-
/**
|
|
8316
|
-
|
|
8315
|
+
/**
|
|
8316
|
+
* `resume` skips submission: poll, review, and fetch existing work at no
|
|
8317
|
+
* cost; `refresh` re-pulls changed objects; `restore` puts the recorded
|
|
8318
|
+
* bytes back on disk from the cache or the provider, as `pixelkiln restore`.
|
|
8319
|
+
*/
|
|
8320
|
+
mode: "generate" | "resume" | "refresh" | "restore";
|
|
8321
|
+
/** The job may replace a locally modified or untracked file, as `--force` does. */
|
|
8322
|
+
force: boolean;
|
|
8317
8323
|
phase: GeneratePhase;
|
|
8318
8324
|
startedAt: string;
|
|
8319
8325
|
finishedAt: string | null;
|
|
@@ -8377,36 +8383,46 @@ interface GalleryGenerateHandlers {
|
|
|
8377
8383
|
declare const GenerateRequestSchema: z.ZodEffects<z.ZodObject<{
|
|
8378
8384
|
project: z.ZodOptional<z.ZodString>;
|
|
8379
8385
|
keys: z.ZodArray<z.ZodString, "many">;
|
|
8380
|
-
/**
|
|
8386
|
+
/**
|
|
8387
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
8388
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
8389
|
+
* the recorded bytes back over a modified file.
|
|
8390
|
+
*/
|
|
8381
8391
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
8382
8392
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
8383
8393
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
8384
8394
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
8385
8395
|
refresh: z.ZodOptional<z.ZodBoolean>;
|
|
8396
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
8397
|
+
restore: z.ZodOptional<z.ZodBoolean>;
|
|
8386
8398
|
}, "strict", z.ZodTypeAny, {
|
|
8387
8399
|
keys: string[];
|
|
8388
8400
|
resume?: boolean | undefined;
|
|
8389
8401
|
force?: boolean | undefined;
|
|
8390
8402
|
project?: string | undefined;
|
|
8391
8403
|
refresh?: boolean | undefined;
|
|
8404
|
+
restore?: boolean | undefined;
|
|
8392
8405
|
}, {
|
|
8393
8406
|
keys: string[];
|
|
8394
8407
|
resume?: boolean | undefined;
|
|
8395
8408
|
force?: boolean | undefined;
|
|
8396
8409
|
project?: string | undefined;
|
|
8397
8410
|
refresh?: boolean | undefined;
|
|
8411
|
+
restore?: boolean | undefined;
|
|
8398
8412
|
}>, {
|
|
8399
8413
|
keys: string[];
|
|
8400
8414
|
resume?: boolean | undefined;
|
|
8401
8415
|
force?: boolean | undefined;
|
|
8402
8416
|
project?: string | undefined;
|
|
8403
8417
|
refresh?: boolean | undefined;
|
|
8418
|
+
restore?: boolean | undefined;
|
|
8404
8419
|
}, {
|
|
8405
8420
|
keys: string[];
|
|
8406
8421
|
resume?: boolean | undefined;
|
|
8407
8422
|
force?: boolean | undefined;
|
|
8408
8423
|
project?: string | undefined;
|
|
8409
8424
|
refresh?: boolean | undefined;
|
|
8425
|
+
restore?: boolean | undefined;
|
|
8410
8426
|
}>;
|
|
8411
8427
|
declare class GenerateRequestError extends Error {
|
|
8412
8428
|
readonly status: number;
|
package/dist/index.js
CHANGED
|
@@ -7770,7 +7770,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
7770
7770
|
}
|
|
7771
7771
|
|
|
7772
7772
|
// src/pipeline/fetch.ts
|
|
7773
|
-
import { existsSync as existsSync13 } from "fs";
|
|
7773
|
+
import { existsSync as existsSync13, readFileSync as readFileSync3 } from "fs";
|
|
7774
7774
|
import { mkdir as mkdir4, readFile as readFile11, rename as rename4, rm as rm6, writeFile as writeFile4 } from "fs/promises";
|
|
7775
7775
|
import path16 from "path";
|
|
7776
7776
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
@@ -7787,7 +7787,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7787
7787
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7788
7788
|
if (!opts.repair) return false;
|
|
7789
7789
|
const spec = specByKey.get(key);
|
|
7790
|
-
|
|
7790
|
+
if (e.outputs.length === 0 || !spec) return true;
|
|
7791
|
+
return e.outputs.some((output) => {
|
|
7792
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
7793
|
+
if (!existsSync13(file)) return true;
|
|
7794
|
+
return Boolean(opts.force) && sha256(readFileSync3(file)) !== output.sha256;
|
|
7795
|
+
});
|
|
7791
7796
|
});
|
|
7792
7797
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
7793
7798
|
let cursor = 0;
|
|
@@ -10604,6 +10609,8 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
10604
10609
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
10605
10610
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host, .sheet.editor-host { width:100vw; } }
|
|
10606
10611
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
10612
|
+
.drawer .readonly-hint { margin-top:10px; font-size:12.5px; line-height:1.5; }
|
|
10613
|
+
.drawer .readonly-hint code { font:12px var(--mono); color:var(--text); }
|
|
10607
10614
|
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
10608
10615
|
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
10609
10616
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
@@ -10813,7 +10820,7 @@ function renderJobs() {
|
|
|
10813
10820
|
const row = el('div', 'job');
|
|
10814
10821
|
const ph = el('span', 'ph');
|
|
10815
10822
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
10816
|
-
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
10823
|
+
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') +
|
|
10817
10824
|
(job.project ? ' in ' + job.project : '');
|
|
10818
10825
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
10819
10826
|
last.title = job.keys.join('\\n');
|
|
@@ -10938,13 +10945,16 @@ function budgetLine() {
|
|
|
10938
10945
|
|
|
10939
10946
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
10940
10947
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
10941
|
-
function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
|
|
10948
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
|
|
10942
10949
|
const host = $('dialog-host');
|
|
10943
10950
|
host.textContent = '';
|
|
10944
10951
|
const wrap = el('div', 'dialog');
|
|
10945
10952
|
const form = el('form');
|
|
10946
10953
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
10954
|
+
const free = resume || refresh || restore;
|
|
10955
|
+
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
10947
10956
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
10957
|
+
: restore ? 'Restore ' + noun
|
|
10948
10958
|
: resume ? 'Resume ' + noun
|
|
10949
10959
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
10950
10960
|
const table = el('table');
|
|
@@ -10954,9 +10964,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
10954
10964
|
for (const item of items) {
|
|
10955
10965
|
const tr = el('tr');
|
|
10956
10966
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
10957
|
-
el('td', 'n',
|
|
10967
|
+
el('td', 'n', free ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
10958
10968
|
table.append(tr);
|
|
10959
|
-
if (!
|
|
10969
|
+
if (!free) {
|
|
10960
10970
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
10961
10971
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
10962
10972
|
}
|
|
@@ -10965,6 +10975,9 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
10965
10975
|
const sum = el('div', 'sum');
|
|
10966
10976
|
if (refresh) {
|
|
10967
10977
|
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.'));
|
|
10978
|
+
} else if (restore) {
|
|
10979
|
+
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.'));
|
|
10980
|
+
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.'));
|
|
10968
10981
|
} else if (resume) {
|
|
10969
10982
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
10970
10983
|
} else {
|
|
@@ -10977,12 +10990,13 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
10977
10990
|
if (left !== null && g.cost > left) line.className = 'warn';
|
|
10978
10991
|
sum.append(line);
|
|
10979
10992
|
}
|
|
10980
|
-
if (force) sum.append(el('div', 'warn', '
|
|
10993
|
+
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.'));
|
|
10994
|
+
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.'));
|
|
10981
10995
|
sum.append(el('div', null, 'Candidate sets land in review; you choose from them here before anything is downloaded.'));
|
|
10982
10996
|
}
|
|
10983
10997
|
form.append(sum);
|
|
10984
10998
|
const actions = el('div', 'actions');
|
|
10985
|
-
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
10999
|
+
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');
|
|
10986
11000
|
go.type = 'submit';
|
|
10987
11001
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
10988
11002
|
const msg = el('span', 'msg');
|
|
@@ -10997,6 +11011,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
10997
11011
|
if (force) body.force = true;
|
|
10998
11012
|
if (resume) body.resume = true;
|
|
10999
11013
|
if (refresh) body.refresh = true;
|
|
11014
|
+
if (restore) body.restore = true;
|
|
11000
11015
|
const job = await postGenerate(body);
|
|
11001
11016
|
host.textContent = '';
|
|
11002
11017
|
GEN.jobs.unshift(job);
|
|
@@ -11655,6 +11670,13 @@ function generateActions(item) {
|
|
|
11655
11670
|
};
|
|
11656
11671
|
if (item.state === 'missing' || item.state === 'stale' || item.state === 'failed') add('Generate' + cost, 'primary', {});
|
|
11657
11672
|
else if (item.state === 'ok') add('Regenerate' + cost, null, { force: true });
|
|
11673
|
+
else if (item.state === 'orphaned' && item.outputs.every((o) => o.exists)) {
|
|
11674
|
+
// The file changed after download. Putting the recorded bytes back and
|
|
11675
|
+
// regenerating both discard that change, so both say so and ask.
|
|
11676
|
+
add('Restore recorded bytes \xB7 no cost', null, { restore: true, force: true });
|
|
11677
|
+
add('Regenerate' + cost, null, { force: true });
|
|
11678
|
+
} else if (item.state === 'orphaned') add('Restore \xB7 no cost', 'primary', { restore: true });
|
|
11679
|
+
else if (item.state === 'untracked') add('Generate' + cost + ' \xB7 replaces the file', null, { force: true });
|
|
11658
11680
|
if (item.state === 'in-flight' || item.state === 'recoverable') {
|
|
11659
11681
|
// A job already parked this asset in review: go straight to its sheet.
|
|
11660
11682
|
const holder = GEN.jobs.find((job) => job.phase === 'review' && job.project === item.project && job.review.includes(item.key));
|
|
@@ -12345,6 +12367,15 @@ function renderDrawer() {
|
|
|
12345
12367
|
if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
|
|
12346
12368
|
body.append(stateNode(item.state, item.reason));
|
|
12347
12369
|
if (GENERATION) body.append(generateActions(item));
|
|
12370
|
+
// Writes are opt-in flags on the command line; say so where the buttons would be.
|
|
12371
|
+
if ((!EDITABLE || !GENERATION) && item.declared) {
|
|
12372
|
+
const missing = [];
|
|
12373
|
+
if (!EDITABLE) missing.push('--edit to change its prompt, size, and tags or open it in the browser editor');
|
|
12374
|
+
if (!GENERATION) missing.push('--budget <n> to generate or regenerate it from here');
|
|
12375
|
+
const hint = el('div', 'state-dim readonly-hint');
|
|
12376
|
+
hint.append(document.createTextNode('This gallery is read-only. Restart it with '), el('code', null, 'pixelkiln gallery'), document.createTextNode(' plus ' + missing.join(', and ') + '.'));
|
|
12377
|
+
body.append(hint);
|
|
12378
|
+
}
|
|
12348
12379
|
if (canEdit && ui.editing === item.id) body.append(editForm(item));
|
|
12349
12380
|
// Plan already quotes the error as the reason for a failed entry; only a
|
|
12350
12381
|
// stale or superseded failure needs its own line.
|
|
@@ -13233,13 +13264,21 @@ import { z as z10 } from "zod";
|
|
|
13233
13264
|
var GenerateRequestSchema = z10.object({
|
|
13234
13265
|
project: z10.string().min(1).optional(),
|
|
13235
13266
|
keys: z10.array(z10.string().min(1)).min(1).max(500),
|
|
13236
|
-
/**
|
|
13267
|
+
/**
|
|
13268
|
+
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
13269
|
+
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
13270
|
+
* the recorded bytes back over a modified file.
|
|
13271
|
+
*/
|
|
13237
13272
|
force: z10.boolean().optional(),
|
|
13238
13273
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
13239
13274
|
resume: z10.boolean().optional(),
|
|
13240
13275
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
13241
|
-
refresh: z10.boolean().optional()
|
|
13242
|
-
|
|
13276
|
+
refresh: z10.boolean().optional(),
|
|
13277
|
+
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
13278
|
+
restore: z10.boolean().optional()
|
|
13279
|
+
}).strict().refine((request) => [request.resume, request.refresh, request.restore].filter(Boolean).length <= 1, {
|
|
13280
|
+
message: "resume, refresh, and restore are exclusive"
|
|
13281
|
+
});
|
|
13243
13282
|
var GenerateRequestError = class extends Error {
|
|
13244
13283
|
constructor(message6, status = 400) {
|
|
13245
13284
|
super(message6);
|
|
@@ -13327,7 +13366,9 @@ function createGenerateHandlers(opts) {
|
|
|
13327
13366
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
13328
13367
|
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
13329
13368
|
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
13330
|
-
onProgress: (line) => say(job, line)
|
|
13369
|
+
onProgress: (line) => say(job, line),
|
|
13370
|
+
// A forced regeneration replaces whatever is on disk, as `gen --force`.
|
|
13371
|
+
force: job.force
|
|
13331
13372
|
});
|
|
13332
13373
|
job.counts.downloaded += res.downloaded;
|
|
13333
13374
|
if (res.downloaded || res.failed) {
|
|
@@ -13342,12 +13383,32 @@ function createGenerateHandlers(opts) {
|
|
|
13342
13383
|
job.finishedAt = now().toISOString();
|
|
13343
13384
|
}
|
|
13344
13385
|
}
|
|
13386
|
+
async function restore(job, ctx, specs) {
|
|
13387
|
+
job.phase = "fetching";
|
|
13388
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
13389
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
13390
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
13391
|
+
onProgress: (line) => say(job, line),
|
|
13392
|
+
repair: true,
|
|
13393
|
+
force: job.force
|
|
13394
|
+
});
|
|
13395
|
+
job.counts.downloaded += res.downloaded;
|
|
13396
|
+
say(job, `${providerId}: restored ${res.downloaded}, skipped ${res.skipped}, failed ${res.failed}`);
|
|
13397
|
+
if (res.failed) throw new Error(`${res.failed} file(s) could not be restored`);
|
|
13398
|
+
}
|
|
13399
|
+
job.phase = "done";
|
|
13400
|
+
job.finishedAt = now().toISOString();
|
|
13401
|
+
}
|
|
13345
13402
|
async function run(job, ctx, specs, groups) {
|
|
13346
13403
|
try {
|
|
13347
13404
|
if (job.mode === "refresh") {
|
|
13348
13405
|
await refresh(job, ctx, specs);
|
|
13349
13406
|
return;
|
|
13350
13407
|
}
|
|
13408
|
+
if (job.mode === "restore") {
|
|
13409
|
+
await restore(job, ctx, specs);
|
|
13410
|
+
return;
|
|
13411
|
+
}
|
|
13351
13412
|
if (job.mode === "generate") {
|
|
13352
13413
|
job.phase = "submitting";
|
|
13353
13414
|
for (const group of groups) {
|
|
@@ -13410,8 +13471,16 @@ function createGenerateHandlers(opts) {
|
|
|
13410
13471
|
const holder = busy.get(`${request.project ?? ""}:${key}`);
|
|
13411
13472
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
13412
13473
|
}
|
|
13474
|
+
if (request.restore) {
|
|
13475
|
+
for (const key of request.keys) {
|
|
13476
|
+
const entry = ctx.lock.entries[key];
|
|
13477
|
+
if (!entry || entry.status !== "downloaded" || !entry.outputs.length) {
|
|
13478
|
+
throw new GenerateRequestError(`"${key}" has no downloaded output recorded to restore`);
|
|
13479
|
+
}
|
|
13480
|
+
}
|
|
13481
|
+
}
|
|
13413
13482
|
let groups = [];
|
|
13414
|
-
if (!request.resume && !request.refresh) {
|
|
13483
|
+
if (!request.resume && !request.refresh && !request.restore) {
|
|
13415
13484
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
13416
13485
|
if (!plan.actionable.length) {
|
|
13417
13486
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -13431,7 +13500,8 @@ function createGenerateHandlers(opts) {
|
|
|
13431
13500
|
id: randomBytes2(8).toString("hex"),
|
|
13432
13501
|
project: request.project ?? null,
|
|
13433
13502
|
keys: request.keys,
|
|
13434
|
-
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
13503
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
|
|
13504
|
+
force: Boolean(request.force),
|
|
13435
13505
|
phase: "queued",
|
|
13436
13506
|
startedAt: now().toISOString(),
|
|
13437
13507
|
finishedAt: null,
|
|
@@ -13443,7 +13513,7 @@ function createGenerateHandlers(opts) {
|
|
|
13443
13513
|
};
|
|
13444
13514
|
jobs.set(job.id, job);
|
|
13445
13515
|
contexts.set(job.id, ctx);
|
|
13446
|
-
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("; "));
|
|
13516
|
+
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("; "));
|
|
13447
13517
|
void run(job, ctx, specs, groups);
|
|
13448
13518
|
return job;
|
|
13449
13519
|
},
|