pixelkiln 0.24.0 → 0.26.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.d.ts +2 -0
- package/dist/cli.js +140 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +130 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -11
- package/dist/index.d.ts +32 -11
- package/dist/index.js +130 -42
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +18 -0
- package/docs/PIXELLAB.md +12 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -4310,14 +4310,6 @@ declare class PixelLabClient {
|
|
|
4310
4310
|
}
|
|
4311
4311
|
declare function clientFromEnv(): PixelLabClient;
|
|
4312
4312
|
|
|
4313
|
-
/**
|
|
4314
|
-
* PixelLab, the reference implementation.
|
|
4315
|
-
*
|
|
4316
|
-
* All PixelLab-specific knowledge lives here: the three generators and their
|
|
4317
|
-
* very different pricing, the review/candidate flow, the fact that a map
|
|
4318
|
-
* object's job record expires while its image does not, and that pixflux
|
|
4319
|
-
* returns bytes inline instead of a job id.
|
|
4320
|
-
*/
|
|
4321
4313
|
declare class PixelLabProvider implements Provider {
|
|
4322
4314
|
private readonly client;
|
|
4323
4315
|
readonly id = "pixellab";
|
|
@@ -7225,6 +7217,8 @@ interface FetchResult {
|
|
|
7225
7217
|
downloaded: number;
|
|
7226
7218
|
skipped: number;
|
|
7227
7219
|
failed: number;
|
|
7220
|
+
/** `refresh` only: outputs re-downloaded and found byte-identical upstream. */
|
|
7221
|
+
unchanged?: number;
|
|
7228
7222
|
}
|
|
7229
7223
|
/**
|
|
7230
7224
|
* Downloads every selected object to its manifest-defined path and records the
|
|
@@ -7239,6 +7233,13 @@ declare function fetchAssets(provider: Provider, specs: ResolvedSpec[], lock: Lo
|
|
|
7239
7233
|
repair?: boolean;
|
|
7240
7234
|
/** Replace an existing untracked or modified destination. Explicit ownership override. */
|
|
7241
7235
|
force?: boolean;
|
|
7236
|
+
/**
|
|
7237
|
+
* Re-download `downloaded` outputs from their durable provider reference
|
|
7238
|
+
* and replace the local file when the upstream bytes changed — an object
|
|
7239
|
+
* edited in the provider's own editor, say. Unchanged objects are left
|
|
7240
|
+
* alone; a locally modified file is still refused without `force`.
|
|
7241
|
+
*/
|
|
7242
|
+
refresh?: boolean;
|
|
7242
7243
|
/** Content-addressed media cache. Defaults beside the lockfile; false disables it. */
|
|
7243
7244
|
cacheDir?: string | false;
|
|
7244
7245
|
}): Promise<FetchResult>;
|
|
@@ -7670,6 +7671,10 @@ interface GalleryItem {
|
|
|
7670
7671
|
*/
|
|
7671
7672
|
edit: GalleryOutput | null;
|
|
7672
7673
|
editStatus: HandEditStatus | null;
|
|
7674
|
+
/** Where the provider's own app shows this object, when it has one. */
|
|
7675
|
+
upstreamUrl: string | null;
|
|
7676
|
+
/** Downloaded work with a durable provider reference; `fetch --refresh` can re-pull it. */
|
|
7677
|
+
refreshable: boolean;
|
|
7673
7678
|
tags: string[];
|
|
7674
7679
|
category: string | null;
|
|
7675
7680
|
}
|
|
@@ -7849,8 +7854,8 @@ interface GenerateJob {
|
|
|
7849
7854
|
id: string;
|
|
7850
7855
|
project: string | null;
|
|
7851
7856
|
keys: string[];
|
|
7852
|
-
/** `resume` skips submission: poll, review, and fetch existing work at no cost. */
|
|
7853
|
-
mode: "generate" | "resume";
|
|
7857
|
+
/** `resume` skips submission: poll, review, and fetch existing work at no cost; `refresh` re-pulls changed objects. */
|
|
7858
|
+
mode: "generate" | "resume" | "refresh";
|
|
7854
7859
|
phase: GeneratePhase;
|
|
7855
7860
|
startedAt: string;
|
|
7856
7861
|
finishedAt: string | null;
|
|
@@ -7911,23 +7916,39 @@ interface GalleryGenerateHandlers {
|
|
|
7911
7916
|
}): Promise<ReviewSession | null>;
|
|
7912
7917
|
applyReview(jobId: string, body: unknown): Promise<PickResult>;
|
|
7913
7918
|
}
|
|
7914
|
-
declare const GenerateRequestSchema: z.ZodObject<{
|
|
7919
|
+
declare const GenerateRequestSchema: z.ZodEffects<z.ZodObject<{
|
|
7915
7920
|
project: z.ZodOptional<z.ZodString>;
|
|
7916
7921
|
keys: z.ZodArray<z.ZodString, "many">;
|
|
7917
7922
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
7918
7923
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
7919
7924
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
7920
7925
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
7926
|
+
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
7927
|
+
refresh: z.ZodOptional<z.ZodBoolean>;
|
|
7921
7928
|
}, "strict", z.ZodTypeAny, {
|
|
7922
7929
|
keys: string[];
|
|
7923
7930
|
resume?: boolean | undefined;
|
|
7924
7931
|
force?: boolean | undefined;
|
|
7925
7932
|
project?: string | undefined;
|
|
7933
|
+
refresh?: boolean | undefined;
|
|
7934
|
+
}, {
|
|
7935
|
+
keys: string[];
|
|
7936
|
+
resume?: boolean | undefined;
|
|
7937
|
+
force?: boolean | undefined;
|
|
7938
|
+
project?: string | undefined;
|
|
7939
|
+
refresh?: boolean | undefined;
|
|
7940
|
+
}>, {
|
|
7941
|
+
keys: string[];
|
|
7942
|
+
resume?: boolean | undefined;
|
|
7943
|
+
force?: boolean | undefined;
|
|
7944
|
+
project?: string | undefined;
|
|
7945
|
+
refresh?: boolean | undefined;
|
|
7926
7946
|
}, {
|
|
7927
7947
|
keys: string[];
|
|
7928
7948
|
resume?: boolean | undefined;
|
|
7929
7949
|
force?: boolean | undefined;
|
|
7930
7950
|
project?: string | undefined;
|
|
7951
|
+
refresh?: boolean | undefined;
|
|
7931
7952
|
}>;
|
|
7932
7953
|
declare class GenerateRequestError extends Error {
|
|
7933
7954
|
readonly status: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -4310,14 +4310,6 @@ declare class PixelLabClient {
|
|
|
4310
4310
|
}
|
|
4311
4311
|
declare function clientFromEnv(): PixelLabClient;
|
|
4312
4312
|
|
|
4313
|
-
/**
|
|
4314
|
-
* PixelLab, the reference implementation.
|
|
4315
|
-
*
|
|
4316
|
-
* All PixelLab-specific knowledge lives here: the three generators and their
|
|
4317
|
-
* very different pricing, the review/candidate flow, the fact that a map
|
|
4318
|
-
* object's job record expires while its image does not, and that pixflux
|
|
4319
|
-
* returns bytes inline instead of a job id.
|
|
4320
|
-
*/
|
|
4321
4313
|
declare class PixelLabProvider implements Provider {
|
|
4322
4314
|
private readonly client;
|
|
4323
4315
|
readonly id = "pixellab";
|
|
@@ -7225,6 +7217,8 @@ interface FetchResult {
|
|
|
7225
7217
|
downloaded: number;
|
|
7226
7218
|
skipped: number;
|
|
7227
7219
|
failed: number;
|
|
7220
|
+
/** `refresh` only: outputs re-downloaded and found byte-identical upstream. */
|
|
7221
|
+
unchanged?: number;
|
|
7228
7222
|
}
|
|
7229
7223
|
/**
|
|
7230
7224
|
* Downloads every selected object to its manifest-defined path and records the
|
|
@@ -7239,6 +7233,13 @@ declare function fetchAssets(provider: Provider, specs: ResolvedSpec[], lock: Lo
|
|
|
7239
7233
|
repair?: boolean;
|
|
7240
7234
|
/** Replace an existing untracked or modified destination. Explicit ownership override. */
|
|
7241
7235
|
force?: boolean;
|
|
7236
|
+
/**
|
|
7237
|
+
* Re-download `downloaded` outputs from their durable provider reference
|
|
7238
|
+
* and replace the local file when the upstream bytes changed — an object
|
|
7239
|
+
* edited in the provider's own editor, say. Unchanged objects are left
|
|
7240
|
+
* alone; a locally modified file is still refused without `force`.
|
|
7241
|
+
*/
|
|
7242
|
+
refresh?: boolean;
|
|
7242
7243
|
/** Content-addressed media cache. Defaults beside the lockfile; false disables it. */
|
|
7243
7244
|
cacheDir?: string | false;
|
|
7244
7245
|
}): Promise<FetchResult>;
|
|
@@ -7670,6 +7671,10 @@ interface GalleryItem {
|
|
|
7670
7671
|
*/
|
|
7671
7672
|
edit: GalleryOutput | null;
|
|
7672
7673
|
editStatus: HandEditStatus | null;
|
|
7674
|
+
/** Where the provider's own app shows this object, when it has one. */
|
|
7675
|
+
upstreamUrl: string | null;
|
|
7676
|
+
/** Downloaded work with a durable provider reference; `fetch --refresh` can re-pull it. */
|
|
7677
|
+
refreshable: boolean;
|
|
7673
7678
|
tags: string[];
|
|
7674
7679
|
category: string | null;
|
|
7675
7680
|
}
|
|
@@ -7849,8 +7854,8 @@ interface GenerateJob {
|
|
|
7849
7854
|
id: string;
|
|
7850
7855
|
project: string | null;
|
|
7851
7856
|
keys: string[];
|
|
7852
|
-
/** `resume` skips submission: poll, review, and fetch existing work at no cost. */
|
|
7853
|
-
mode: "generate" | "resume";
|
|
7857
|
+
/** `resume` skips submission: poll, review, and fetch existing work at no cost; `refresh` re-pulls changed objects. */
|
|
7858
|
+
mode: "generate" | "resume" | "refresh";
|
|
7854
7859
|
phase: GeneratePhase;
|
|
7855
7860
|
startedAt: string;
|
|
7856
7861
|
finishedAt: string | null;
|
|
@@ -7911,23 +7916,39 @@ interface GalleryGenerateHandlers {
|
|
|
7911
7916
|
}): Promise<ReviewSession | null>;
|
|
7912
7917
|
applyReview(jobId: string, body: unknown): Promise<PickResult>;
|
|
7913
7918
|
}
|
|
7914
|
-
declare const GenerateRequestSchema: z.ZodObject<{
|
|
7919
|
+
declare const GenerateRequestSchema: z.ZodEffects<z.ZodObject<{
|
|
7915
7920
|
project: z.ZodOptional<z.ZodString>;
|
|
7916
7921
|
keys: z.ZodArray<z.ZodString, "many">;
|
|
7917
7922
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
7918
7923
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
7919
7924
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
7920
7925
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
7926
|
+
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
7927
|
+
refresh: z.ZodOptional<z.ZodBoolean>;
|
|
7921
7928
|
}, "strict", z.ZodTypeAny, {
|
|
7922
7929
|
keys: string[];
|
|
7923
7930
|
resume?: boolean | undefined;
|
|
7924
7931
|
force?: boolean | undefined;
|
|
7925
7932
|
project?: string | undefined;
|
|
7933
|
+
refresh?: boolean | undefined;
|
|
7934
|
+
}, {
|
|
7935
|
+
keys: string[];
|
|
7936
|
+
resume?: boolean | undefined;
|
|
7937
|
+
force?: boolean | undefined;
|
|
7938
|
+
project?: string | undefined;
|
|
7939
|
+
refresh?: boolean | undefined;
|
|
7940
|
+
}>, {
|
|
7941
|
+
keys: string[];
|
|
7942
|
+
resume?: boolean | undefined;
|
|
7943
|
+
force?: boolean | undefined;
|
|
7944
|
+
project?: string | undefined;
|
|
7945
|
+
refresh?: boolean | undefined;
|
|
7926
7946
|
}, {
|
|
7927
7947
|
keys: string[];
|
|
7928
7948
|
resume?: boolean | undefined;
|
|
7929
7949
|
force?: boolean | undefined;
|
|
7930
7950
|
project?: string | undefined;
|
|
7951
|
+
refresh?: boolean | undefined;
|
|
7931
7952
|
}>;
|
|
7932
7953
|
declare class GenerateRequestError extends Error {
|
|
7933
7954
|
readonly status: number;
|
package/dist/index.js
CHANGED
|
@@ -1291,6 +1291,10 @@ import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
|
1291
1291
|
import { randomUUID } from "crypto";
|
|
1292
1292
|
import os from "os";
|
|
1293
1293
|
import path from "path";
|
|
1294
|
+
function pixelLabObjectUrl(generator, objectId) {
|
|
1295
|
+
if (!objectId || generator !== "map" && generator !== "1dir") return null;
|
|
1296
|
+
return `https://www.pixellab.ai/objects/${encodeURIComponent(objectId)}`;
|
|
1297
|
+
}
|
|
1294
1298
|
var PixelLabProvider = class _PixelLabProvider {
|
|
1295
1299
|
constructor(client) {
|
|
1296
1300
|
this.client = client;
|
|
@@ -7742,14 +7746,16 @@ import path16 from "path";
|
|
|
7742
7746
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
7743
7747
|
const log = opts.onProgress ?? (() => {
|
|
7744
7748
|
});
|
|
7745
|
-
const result = { downloaded: 0, skipped: 0, failed: 0 };
|
|
7749
|
+
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
7746
7750
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
7747
7751
|
normalizeLockOutputPaths(lock, specs);
|
|
7748
7752
|
const cacheDir = opts.cacheDir === false ? null : path16.resolve(opts.cacheDir ?? path16.join(path16.dirname(lockPath), ".pixelkiln", "cache"));
|
|
7749
7753
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
7750
7754
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
7751
7755
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7752
|
-
if (
|
|
7756
|
+
if (e.status !== "downloaded") return false;
|
|
7757
|
+
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7758
|
+
if (!opts.repair) return false;
|
|
7753
7759
|
const spec = specByKey.get(key);
|
|
7754
7760
|
return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync12(resolveOutputPath(output.path, spec.root)));
|
|
7755
7761
|
});
|
|
@@ -7781,6 +7787,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7781
7787
|
continue;
|
|
7782
7788
|
}
|
|
7783
7789
|
const outputs = [];
|
|
7790
|
+
let wrote = false;
|
|
7784
7791
|
try {
|
|
7785
7792
|
for (let index = 0; index < sources.length; index++) {
|
|
7786
7793
|
const source = sources[index];
|
|
@@ -7788,41 +7795,55 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7788
7795
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
7789
7796
|
);
|
|
7790
7797
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
7798
|
+
let fresh = null;
|
|
7799
|
+
if (opts.refresh) {
|
|
7800
|
+
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7801
|
+
fresh = await provider.download(source.url);
|
|
7802
|
+
if (recorded && sha256(fresh) === recorded.sha256) {
|
|
7803
|
+
log(` current ${path16.relative(process.cwd(), target)}`);
|
|
7804
|
+
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7805
|
+
continue;
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7791
7808
|
if (existsSync12(target)) {
|
|
7792
7809
|
const currentHash = await sha256File(target);
|
|
7793
7810
|
if (recorded && currentHash === recorded.sha256) {
|
|
7794
|
-
if (
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7811
|
+
if (!fresh) {
|
|
7812
|
+
if (cacheDir) {
|
|
7813
|
+
await cacheMedia(
|
|
7814
|
+
cacheDir,
|
|
7815
|
+
await readFile11(target),
|
|
7816
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
7817
|
+
recorded.sha256
|
|
7818
|
+
);
|
|
7819
|
+
}
|
|
7820
|
+
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7821
|
+
continue;
|
|
7801
7822
|
}
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7823
|
+
log(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
|
|
7824
|
+
} else {
|
|
7825
|
+
const superseded = (entry.supersededOutputs ?? []).find(
|
|
7826
|
+
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
7827
|
+
);
|
|
7828
|
+
if (!opts.force) {
|
|
7829
|
+
if (!recorded && superseded && currentHash === superseded.sha256) {
|
|
7830
|
+
} else if (recorded || superseded) {
|
|
7831
|
+
throw new Error(
|
|
7832
|
+
`refusing to overwrite modified output ${target}; pass --force to replace it`
|
|
7833
|
+
);
|
|
7834
|
+
} else {
|
|
7835
|
+
throw new Error(
|
|
7836
|
+
`refusing to overwrite untracked output ${target}; pass --force to replace it`
|
|
7837
|
+
);
|
|
7838
|
+
}
|
|
7818
7839
|
}
|
|
7840
|
+
log(
|
|
7841
|
+
` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
7842
|
+
);
|
|
7819
7843
|
}
|
|
7820
|
-
log(
|
|
7821
|
-
` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
7822
|
-
);
|
|
7823
7844
|
}
|
|
7824
7845
|
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
7825
|
-
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
7846
|
+
let buf = fresh ?? (recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null);
|
|
7826
7847
|
if (buf) {
|
|
7827
7848
|
log(` cached ${path16.relative(process.cwd(), target)}`);
|
|
7828
7849
|
} else {
|
|
@@ -7854,8 +7875,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7854
7875
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7855
7876
|
await saveLock(lockPath, lock);
|
|
7856
7877
|
await rename4(tmp, target);
|
|
7878
|
+
wrote = true;
|
|
7857
7879
|
log(` wrote ${path16.relative(process.cwd(), target)}`);
|
|
7858
7880
|
}
|
|
7881
|
+
if (opts.refresh && !wrote) {
|
|
7882
|
+
result.unchanged = (result.unchanged ?? 0) + 1;
|
|
7883
|
+
continue;
|
|
7884
|
+
}
|
|
7859
7885
|
const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
|
|
7860
7886
|
upsert(lock, key, {
|
|
7861
7887
|
status: "downloaded",
|
|
@@ -9739,6 +9765,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9739
9765
|
source: spec.source ?? null,
|
|
9740
9766
|
edit,
|
|
9741
9767
|
editStatus,
|
|
9768
|
+
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9769
|
+
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9742
9770
|
tags: spec.tags,
|
|
9743
9771
|
category: asset?.category ?? null
|
|
9744
9772
|
});
|
|
@@ -9793,6 +9821,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
9793
9821
|
source: null,
|
|
9794
9822
|
edit: null,
|
|
9795
9823
|
editStatus: null,
|
|
9824
|
+
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
9825
|
+
refreshable: false,
|
|
9796
9826
|
tags: [],
|
|
9797
9827
|
category: null
|
|
9798
9828
|
});
|
|
@@ -10464,7 +10494,7 @@ function renderJobs() {
|
|
|
10464
10494
|
const row = el('div', 'job');
|
|
10465
10495
|
const ph = el('span', 'ph');
|
|
10466
10496
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
10467
|
-
const what = (job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
10497
|
+
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
10468
10498
|
(job.project ? ' in ' + job.project : '');
|
|
10469
10499
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
10470
10500
|
last.title = job.keys.join('\\n');
|
|
@@ -10503,13 +10533,15 @@ function budgetLine() {
|
|
|
10503
10533
|
|
|
10504
10534
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
10505
10535
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
10506
|
-
function generateDialog(items, { project = null, force = false, resume = false } = {}) {
|
|
10536
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
|
|
10507
10537
|
const host = $('dialog-host');
|
|
10508
10538
|
host.textContent = '';
|
|
10509
10539
|
const wrap = el('div', 'dialog');
|
|
10510
10540
|
const form = el('form');
|
|
10511
|
-
|
|
10512
|
-
|
|
10541
|
+
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
10542
|
+
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
10543
|
+
: resume ? 'Resume ' + noun
|
|
10544
|
+
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
10513
10545
|
const table = el('table');
|
|
10514
10546
|
const thead = el('tr'); thead.append(el('th', null, 'asset'), el('th', null, 'state'), el('th', 'n', 'candidates'), el('th', 'n', 'estimate'));
|
|
10515
10547
|
table.append(thead);
|
|
@@ -10517,16 +10549,18 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
10517
10549
|
for (const item of items) {
|
|
10518
10550
|
const tr = el('tr');
|
|
10519
10551
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
10520
|
-
el('td', 'n', resume ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
10552
|
+
el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
10521
10553
|
table.append(tr);
|
|
10522
|
-
if (!resume) {
|
|
10554
|
+
if (!resume && !refresh) {
|
|
10523
10555
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
10524
10556
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
10525
10557
|
}
|
|
10526
10558
|
}
|
|
10527
10559
|
form.append(table);
|
|
10528
10560
|
const sum = el('div', 'sum');
|
|
10529
|
-
if (
|
|
10561
|
+
if (refresh) {
|
|
10562
|
+
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.'));
|
|
10563
|
+
} else if (resume) {
|
|
10530
10564
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
10531
10565
|
} else {
|
|
10532
10566
|
for (const [provider, g] of byProvider) {
|
|
@@ -10543,7 +10577,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
10543
10577
|
}
|
|
10544
10578
|
form.append(sum);
|
|
10545
10579
|
const actions = el('div', 'actions');
|
|
10546
|
-
const go = el('button', 'primary', resume ? 'Resume' : 'Generate');
|
|
10580
|
+
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
10547
10581
|
go.type = 'submit';
|
|
10548
10582
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
10549
10583
|
const msg = el('span', 'msg');
|
|
@@ -10557,6 +10591,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
10557
10591
|
if (project) body.project = project;
|
|
10558
10592
|
if (force) body.force = true;
|
|
10559
10593
|
if (resume) body.resume = true;
|
|
10594
|
+
if (refresh) body.refresh = true;
|
|
10560
10595
|
const job = await postGenerate(body);
|
|
10561
10596
|
host.textContent = '';
|
|
10562
10597
|
GEN.jobs.unshift(job);
|
|
@@ -10846,6 +10881,13 @@ function renderMain(items) {
|
|
|
10846
10881
|
g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
|
|
10847
10882
|
tools.append(g);
|
|
10848
10883
|
}
|
|
10884
|
+
const refreshable = snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
|
|
10885
|
+
if (GENERATION && refreshable.length) {
|
|
10886
|
+
const r = el('button', null, 'Pull upstream ' + refreshable.length); r.type = 'button';
|
|
10887
|
+
r.title = 'Re-download objects edited in the provider\u2019s own editor; no cost';
|
|
10888
|
+
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
10889
|
+
tools.append(r);
|
|
10890
|
+
}
|
|
10849
10891
|
const styleKey = 'style:' + (s.project || '') + ':' + s.id;
|
|
10850
10892
|
if (EDITABLE && s.outDir) {
|
|
10851
10893
|
const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
|
|
@@ -10939,6 +10981,30 @@ function candidatesControl(style) {
|
|
|
10939
10981
|
return wrap;
|
|
10940
10982
|
}
|
|
10941
10983
|
|
|
10984
|
+
function upstreamSection(item) {
|
|
10985
|
+
if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
|
|
10986
|
+
const s = el('section', 'meta');
|
|
10987
|
+
s.append(el('h3', null, 'Upstream'));
|
|
10988
|
+
const dl = el('dl');
|
|
10989
|
+
if (item.upstreamUrl) {
|
|
10990
|
+
const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
|
|
10991
|
+
a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
|
|
10992
|
+
row(dl, 'object', a);
|
|
10993
|
+
row(dl, 'note', item.provider === 'pixellab'
|
|
10994
|
+
? '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.'
|
|
10995
|
+
: 'The provider\u2019s page for this object.');
|
|
10996
|
+
}
|
|
10997
|
+
s.append(dl);
|
|
10998
|
+
if (GENERATION && item.refreshable) {
|
|
10999
|
+
const acts = el('div', 'hand-actions');
|
|
11000
|
+
const b = el('button', null, 'Pull upstream changes \xB7 no cost'); b.type = 'button';
|
|
11001
|
+
b.onclick = () => generateDialog([item], { project: item.project, refresh: true });
|
|
11002
|
+
acts.append(b);
|
|
11003
|
+
s.append(acts);
|
|
11004
|
+
}
|
|
11005
|
+
return s;
|
|
11006
|
+
}
|
|
11007
|
+
|
|
10942
11008
|
function generateActions(item) {
|
|
10943
11009
|
const row = el('div', 'gen');
|
|
10944
11010
|
if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
|
|
@@ -11657,6 +11723,8 @@ function renderDrawer() {
|
|
|
11657
11723
|
|
|
11658
11724
|
const hand = handEditSection(item);
|
|
11659
11725
|
if (hand) body.append(hand);
|
|
11726
|
+
const upstream = upstreamSection(item);
|
|
11727
|
+
if (upstream) body.append(upstream);
|
|
11660
11728
|
|
|
11661
11729
|
if (item.revision || item.revisionParentKey) {
|
|
11662
11730
|
const { s, dl } = section('Lineage');
|
|
@@ -12150,8 +12218,10 @@ var GenerateRequestSchema = z9.object({
|
|
|
12150
12218
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
12151
12219
|
force: z9.boolean().optional(),
|
|
12152
12220
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
12153
|
-
resume: z9.boolean().optional()
|
|
12154
|
-
|
|
12221
|
+
resume: z9.boolean().optional(),
|
|
12222
|
+
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
12223
|
+
refresh: z9.boolean().optional()
|
|
12224
|
+
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
12155
12225
|
var GenerateRequestError = class extends Error {
|
|
12156
12226
|
constructor(message6, status = 400) {
|
|
12157
12227
|
super(message6);
|
|
@@ -12209,6 +12279,20 @@ function createGenerateHandlers(opts) {
|
|
|
12209
12279
|
}
|
|
12210
12280
|
return groups;
|
|
12211
12281
|
};
|
|
12282
|
+
async function refresh(job, ctx, specs) {
|
|
12283
|
+
job.phase = "fetching";
|
|
12284
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
12285
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
12286
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
12287
|
+
onProgress: (line) => say(job, line),
|
|
12288
|
+
refresh: true
|
|
12289
|
+
});
|
|
12290
|
+
job.counts.downloaded += res.downloaded;
|
|
12291
|
+
say(job, `${providerId}: ${res.downloaded} changed upstream and replaced, ${res.unchanged ?? 0} unchanged, ${res.skipped} skipped, ${res.failed} failed`);
|
|
12292
|
+
}
|
|
12293
|
+
job.phase = "done";
|
|
12294
|
+
job.finishedAt = now().toISOString();
|
|
12295
|
+
}
|
|
12212
12296
|
async function settle(job, ctx, specs) {
|
|
12213
12297
|
job.phase = "polling";
|
|
12214
12298
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
@@ -12242,6 +12326,10 @@ function createGenerateHandlers(opts) {
|
|
|
12242
12326
|
}
|
|
12243
12327
|
async function run(job, ctx, specs, groups) {
|
|
12244
12328
|
try {
|
|
12329
|
+
if (job.mode === "refresh") {
|
|
12330
|
+
await refresh(job, ctx, specs);
|
|
12331
|
+
return;
|
|
12332
|
+
}
|
|
12245
12333
|
if (job.mode === "generate") {
|
|
12246
12334
|
job.phase = "submitting";
|
|
12247
12335
|
for (const group of groups) {
|
|
@@ -12305,7 +12393,7 @@ function createGenerateHandlers(opts) {
|
|
|
12305
12393
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
12306
12394
|
}
|
|
12307
12395
|
let groups = [];
|
|
12308
|
-
if (!request.resume) {
|
|
12396
|
+
if (!request.resume && !request.refresh) {
|
|
12309
12397
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
12310
12398
|
if (!plan.actionable.length) {
|
|
12311
12399
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -12325,7 +12413,7 @@ function createGenerateHandlers(opts) {
|
|
|
12325
12413
|
id: randomBytes2(8).toString("hex"),
|
|
12326
12414
|
project: request.project ?? null,
|
|
12327
12415
|
keys: request.keys,
|
|
12328
|
-
mode: request.resume ? "resume" : "generate",
|
|
12416
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
12329
12417
|
phase: "queued",
|
|
12330
12418
|
startedAt: now().toISOString(),
|
|
12331
12419
|
finishedAt: null,
|
|
@@ -12337,7 +12425,7 @@ function createGenerateHandlers(opts) {
|
|
|
12337
12425
|
};
|
|
12338
12426
|
jobs.set(job.id, job);
|
|
12339
12427
|
contexts.set(job.id, ctx);
|
|
12340
|
-
say(job, 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("; "));
|
|
12428
|
+
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("; "));
|
|
12341
12429
|
void run(job, ctx, specs, groups);
|
|
12342
12430
|
return job;
|
|
12343
12431
|
},
|