pixelkiln 0.32.1 → 0.34.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/README.md +1 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +1070 -623
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +857 -452
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +681 -46
- package/dist/index.d.ts +681 -46
- package/dist/index.js +851 -455
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +4 -1
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CLI.md +42 -0
- package/docs/GETTING_STARTED.md +9 -3
- package/docs/MANIFEST.md +1 -0
- package/docs/RECOVERY.md +7 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +5 -0
- package/skills/pixelkiln/SKILL.md +4 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import { existsSync as
|
|
4
|
+
import path31 from "path";
|
|
5
|
+
import { existsSync as existsSync25 } from "fs";
|
|
6
6
|
import { readFile as readFile25 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
@@ -134,10 +134,10 @@ import { createReadStream } from "fs";
|
|
|
134
134
|
function sha256(data) {
|
|
135
135
|
return createHash("sha256").update(data).digest("hex");
|
|
136
136
|
}
|
|
137
|
-
async function sha256File(
|
|
137
|
+
async function sha256File(path32) {
|
|
138
138
|
return new Promise((resolve, reject) => {
|
|
139
139
|
const hash = createHash("sha256");
|
|
140
|
-
const stream = createReadStream(
|
|
140
|
+
const stream = createReadStream(path32);
|
|
141
141
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
142
142
|
stream.on("error", reject);
|
|
143
143
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -1612,11 +1612,11 @@ var PixelLabClient = class {
|
|
|
1612
1612
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1613
1613
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1614
1614
|
*/
|
|
1615
|
-
async request(
|
|
1615
|
+
async request(path32, init, attempt = 0) {
|
|
1616
1616
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1617
1617
|
let res;
|
|
1618
1618
|
try {
|
|
1619
|
-
res = await fetch(`${BASE}${
|
|
1619
|
+
res = await fetch(`${BASE}${path32}`, {
|
|
1620
1620
|
...init,
|
|
1621
1621
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1622
1622
|
headers: {
|
|
@@ -1628,24 +1628,24 @@ var PixelLabClient = class {
|
|
|
1628
1628
|
} catch (err) {
|
|
1629
1629
|
if (attempt < MAX_RETRIES) {
|
|
1630
1630
|
await sleep(backoffMs(attempt));
|
|
1631
|
-
return this.request(
|
|
1631
|
+
return this.request(path32, init, attempt + 1);
|
|
1632
1632
|
}
|
|
1633
1633
|
throw err;
|
|
1634
1634
|
}
|
|
1635
1635
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1636
1636
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1637
1637
|
await sleep(waitMs);
|
|
1638
|
-
return this.request(
|
|
1638
|
+
return this.request(path32, init, attempt + 1);
|
|
1639
1639
|
}
|
|
1640
1640
|
const text = await res.text();
|
|
1641
1641
|
if (!res.ok) {
|
|
1642
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1642
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path32} \u2192 ${res.status}`, res.status, text);
|
|
1643
1643
|
}
|
|
1644
1644
|
if (!text) return {};
|
|
1645
1645
|
try {
|
|
1646
1646
|
return JSON.parse(text);
|
|
1647
1647
|
} catch {
|
|
1648
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1648
|
+
throw new Error(`${init?.method ?? "GET"} ${path32} returned invalid JSON`);
|
|
1649
1649
|
}
|
|
1650
1650
|
}
|
|
1651
1651
|
async balance() {
|
|
@@ -2158,11 +2158,15 @@ var AssetSchema = z2.object({
|
|
|
2158
2158
|
});
|
|
2159
2159
|
}
|
|
2160
2160
|
});
|
|
2161
|
+
var DEFAULT_HISTORY_LIMIT = 5;
|
|
2162
|
+
var HistoryLimitSchema = z2.number().int().min(0).max(100);
|
|
2161
2163
|
var ManifestSchema = z2.object({
|
|
2162
2164
|
$schema: z2.string().optional(),
|
|
2163
2165
|
name: z2.string(),
|
|
2164
2166
|
/** Generation backend. Existing manifests remain PixelLab by default. */
|
|
2165
2167
|
provider: z2.string().min(1).default("pixellab"),
|
|
2168
|
+
/** Replaced generations kept per asset for this project; overrides PIXELKILN_HISTORY. 0 keeps none. */
|
|
2169
|
+
history: HistoryLimitSchema.optional(),
|
|
2166
2170
|
styles: z2.record(StyleSchema),
|
|
2167
2171
|
assets: z2.record(AssetSchema)
|
|
2168
2172
|
}).strict();
|
|
@@ -2170,9 +2174,43 @@ var ManifestInputSchema = z2.object({
|
|
|
2170
2174
|
$schema: z2.string().optional(),
|
|
2171
2175
|
name: z2.string(),
|
|
2172
2176
|
provider: z2.string().min(1).default("pixellab"),
|
|
2177
|
+
history: HistoryLimitSchema.optional(),
|
|
2173
2178
|
styles: z2.record(StyleInputSchema),
|
|
2174
2179
|
assets: z2.record(AssetSchema)
|
|
2175
2180
|
}).strict();
|
|
2181
|
+
var LockOutputSchema = z2.object({
|
|
2182
|
+
path: z2.string(),
|
|
2183
|
+
sha256: z2.string(),
|
|
2184
|
+
role: z2.string().optional(),
|
|
2185
|
+
mediaType: MediaTypeSchema.optional()
|
|
2186
|
+
});
|
|
2187
|
+
var LockSourceSchema = z2.object({
|
|
2188
|
+
url: z2.string(),
|
|
2189
|
+
role: z2.string().optional(),
|
|
2190
|
+
mediaType: MediaTypeSchema.optional()
|
|
2191
|
+
});
|
|
2192
|
+
var LockHistoryEntrySchema = z2.object({
|
|
2193
|
+
specHash: z2.string(),
|
|
2194
|
+
generator: GeneratorSchema,
|
|
2195
|
+
tileFeature: z2.string().nullable().default(null),
|
|
2196
|
+
prompt: z2.string(),
|
|
2197
|
+
width: z2.number().int(),
|
|
2198
|
+
height: z2.number().int(),
|
|
2199
|
+
jobId: z2.string().nullable().default(null),
|
|
2200
|
+
objectId: z2.string().nullable().default(null),
|
|
2201
|
+
candidateIndex: z2.number().int().nullable().default(null),
|
|
2202
|
+
sourceUrl: z2.string().nullable().default(null),
|
|
2203
|
+
sourceUrls: z2.array(LockSourceSchema).default([]),
|
|
2204
|
+
outputs: z2.array(LockOutputSchema).default([]),
|
|
2205
|
+
providerMetadata: z2.record(z2.record(z2.unknown())).default({}),
|
|
2206
|
+
submittedAt: z2.string().nullable().default(null),
|
|
2207
|
+
downloadedAt: z2.string().nullable().default(null),
|
|
2208
|
+
cost: z2.number().finite().nonnegative().default(0),
|
|
2209
|
+
costUnit: z2.string().min(1).default("generations"),
|
|
2210
|
+
provider: z2.string().default("pixellab"),
|
|
2211
|
+
/** When the replacement was submitted. */
|
|
2212
|
+
retiredAt: z2.string()
|
|
2213
|
+
});
|
|
2176
2214
|
var LockEntrySchema = z2.object({
|
|
2177
2215
|
styleId: z2.string(),
|
|
2178
2216
|
assetId: z2.string(),
|
|
@@ -2262,7 +2300,9 @@ var LockEntrySchema = z2.object({
|
|
|
2262
2300
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
2263
2301
|
costUnit: z2.string().min(1).default("generations"),
|
|
2264
2302
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
2265
|
-
provider: z2.string().default("pixellab")
|
|
2303
|
+
provider: z2.string().default("pixellab"),
|
|
2304
|
+
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
2305
|
+
history: z2.array(LockHistoryEntrySchema).optional()
|
|
2266
2306
|
});
|
|
2267
2307
|
var LockSchema = z2.object({
|
|
2268
2308
|
version: z2.literal(2),
|
|
@@ -2488,6 +2528,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2488
2528
|
if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
|
|
2489
2529
|
return { status: "processing" };
|
|
2490
2530
|
} catch (err) {
|
|
2531
|
+
if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
|
|
2491
2532
|
if (!(err instanceof PixelLabError) || err.status !== 404) throw err;
|
|
2492
2533
|
const survivor = await this.client.getObject(jobId).catch(() => null);
|
|
2493
2534
|
const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
|
|
@@ -2628,9 +2669,9 @@ var RetroDiffusionClient = class {
|
|
|
2628
2669
|
}
|
|
2629
2670
|
return balance;
|
|
2630
2671
|
}
|
|
2631
|
-
async call(
|
|
2672
|
+
async call(path32, init = {}) {
|
|
2632
2673
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2633
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2674
|
+
const response = await this.request(`${this.baseUrl}${path32}`, {
|
|
2634
2675
|
...init,
|
|
2635
2676
|
headers: {
|
|
2636
2677
|
"Content-Type": "application/json",
|
|
@@ -6174,313 +6215,54 @@ function summarize(plan) {
|
|
|
6174
6215
|
return counts;
|
|
6175
6216
|
}
|
|
6176
6217
|
|
|
6177
|
-
// src/pipeline/
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
}
|
|
6205
|
-
const styleImages = /* @__PURE__ */ new Map();
|
|
6206
|
-
for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
|
|
6207
|
-
styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
|
|
6208
|
-
}
|
|
6209
|
-
let submitted = 0;
|
|
6210
|
-
let failed = 0;
|
|
6211
|
-
let spent = 0;
|
|
6212
|
-
let lastSubmitAt = 0;
|
|
6213
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
6214
|
-
let lastSlotError = null;
|
|
6215
|
-
async function pruneInFlight() {
|
|
6216
|
-
for (const [id, spec] of [...inFlight]) {
|
|
6217
|
-
try {
|
|
6218
|
-
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
6219
|
-
if (state.status !== "processing") inFlight.delete(id);
|
|
6220
|
-
lastSlotError = null;
|
|
6221
|
-
} catch (err) {
|
|
6222
|
-
lastSlotError = err instanceof Error ? err.message : String(err);
|
|
6223
|
-
}
|
|
6224
|
-
}
|
|
6218
|
+
// src/pipeline/history.ts
|
|
6219
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6220
|
+
import path15 from "path";
|
|
6221
|
+
|
|
6222
|
+
// src/pipeline/fetch.ts
|
|
6223
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
6224
|
+
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6225
|
+
import path14 from "path";
|
|
6226
|
+
|
|
6227
|
+
// src/source-url.ts
|
|
6228
|
+
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
6229
|
+
"access_token",
|
|
6230
|
+
"apikey",
|
|
6231
|
+
"api_key",
|
|
6232
|
+
"auth",
|
|
6233
|
+
"authorization",
|
|
6234
|
+
"awsaccesskeyid",
|
|
6235
|
+
"googleaccessid",
|
|
6236
|
+
"key",
|
|
6237
|
+
"sig"
|
|
6238
|
+
]);
|
|
6239
|
+
function isSensitiveSourceUrl(value) {
|
|
6240
|
+
let parsed;
|
|
6241
|
+
try {
|
|
6242
|
+
parsed = new URL(value);
|
|
6243
|
+
} catch {
|
|
6244
|
+
return false;
|
|
6225
6245
|
}
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
throw new Error(
|
|
6231
|
-
`Timed out waiting for a generation slot` + (lastSlotError ? `; last poll error: ${lastSlotError}` : "")
|
|
6232
|
-
);
|
|
6233
|
-
}
|
|
6234
|
-
await sleep2(slotPollMs);
|
|
6235
|
-
await pruneInFlight();
|
|
6236
|
-
}
|
|
6246
|
+
if (parsed.username || parsed.password) return true;
|
|
6247
|
+
for (const key of parsed.searchParams.keys()) {
|
|
6248
|
+
const normalized = key.toLowerCase();
|
|
6249
|
+
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
6237
6250
|
}
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
6247
|
-
);
|
|
6248
|
-
const previousJobId = resumesCheckpoint ? previousEntry.jobId : void 0;
|
|
6249
|
-
const previousMetadata = resumesCheckpoint ? previousEntry.providerMetadata[provider.id] : void 0;
|
|
6250
|
-
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
6251
|
-
upsert(lock, key, {
|
|
6252
|
-
styleId: spec.styleId,
|
|
6253
|
-
assetId: spec.assetId,
|
|
6254
|
-
specHash: spec.specHash,
|
|
6255
|
-
generator: spec.generator,
|
|
6256
|
-
tileFeature: spec.tileFeature ?? null,
|
|
6257
|
-
prompt: spec.prompt,
|
|
6258
|
-
width: spec.width,
|
|
6259
|
-
height: spec.height,
|
|
6260
|
-
revision: spec.revision ? {
|
|
6261
|
-
mode: spec.revision.mode,
|
|
6262
|
-
sourceAssetId: spec.revision.sourceAssetId,
|
|
6263
|
-
sourceSha256: spec.revision.sourceSha256,
|
|
6264
|
-
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
|
|
6265
|
-
...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
|
|
6266
|
-
} : null,
|
|
6267
|
-
status: "pending",
|
|
6268
|
-
jobId: previousJobId ?? null,
|
|
6269
|
-
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
6270
|
-
objectId: null,
|
|
6271
|
-
reviewObjectId: null,
|
|
6272
|
-
candidateIndex: null,
|
|
6273
|
-
error: null,
|
|
6274
|
-
outputs: [],
|
|
6275
|
-
// Keep the old ownership proof while new bytes are pending. Fetch may
|
|
6276
|
-
// replace that file only while its hash still matches this record.
|
|
6277
|
-
supersededOutputs,
|
|
6278
|
-
providerMetadata: resumesCheckpoint ? previousEntry.providerMetadata : {},
|
|
6279
|
-
sourceUrl: null,
|
|
6280
|
-
sourceUrls: [],
|
|
6281
|
-
submittedAt: resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6282
|
-
cost: estimate.amount,
|
|
6283
|
-
costUnit: estimate.unit,
|
|
6284
|
-
// Persist routing before the request starts. If the process stops while
|
|
6285
|
-
// the provider is accepting work, a resumed run must not fall back to
|
|
6286
|
-
// PixelLab (the legacy lock default) for another provider's entry.
|
|
6287
|
-
provider: provider.id,
|
|
6288
|
-
downloadedAt: null
|
|
6289
|
-
});
|
|
6290
|
-
await saveLock(lockPath, lock);
|
|
6291
|
-
lastSubmitAt = Date.now();
|
|
6292
|
-
try {
|
|
6293
|
-
const refs = styleImages.get(spec.styleId) ?? [];
|
|
6294
|
-
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
6295
|
-
...previousJobId ? { previousJobId } : {},
|
|
6296
|
-
...previousMetadata ? { previousMetadata } : {},
|
|
6297
|
-
checkpoint: async (checkpoint) => {
|
|
6298
|
-
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
6299
|
-
const entry = lock.entries[key];
|
|
6300
|
-
upsert(lock, key, {
|
|
6301
|
-
jobId: checkpoint.jobId,
|
|
6302
|
-
submissionComplete: checkpoint.complete,
|
|
6303
|
-
reviewObjectId: checkpoint.complete && (spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature) ? checkpoint.jobId : null,
|
|
6304
|
-
status: checkpoint.complete ? "processing" : "pending",
|
|
6305
|
-
error: null,
|
|
6306
|
-
providerMetadata: checkpoint.metadata ? {
|
|
6307
|
-
...entry.providerMetadata,
|
|
6308
|
-
[provider.id]: {
|
|
6309
|
-
...entry.providerMetadata[provider.id],
|
|
6310
|
-
...checkpoint.metadata
|
|
6311
|
-
}
|
|
6312
|
-
} : entry.providerMetadata
|
|
6313
|
-
});
|
|
6314
|
-
await saveLock(lockPath, lock);
|
|
6315
|
-
}
|
|
6316
|
-
});
|
|
6317
|
-
upsert(lock, key, {
|
|
6318
|
-
jobId,
|
|
6319
|
-
submissionComplete: true,
|
|
6320
|
-
// A multi-candidate generator routes through review; record the parent
|
|
6321
|
-
// so `pick` knows where to look.
|
|
6322
|
-
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6323
|
-
status: "processing",
|
|
6324
|
-
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
6325
|
-
});
|
|
6326
|
-
inFlight.set(jobId, spec);
|
|
6327
|
-
submitted++;
|
|
6328
|
-
spent += estimate.amount;
|
|
6329
|
-
log2(
|
|
6330
|
-
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
6331
|
-
);
|
|
6332
|
-
} catch (err) {
|
|
6333
|
-
const message6 = err instanceof Error ? err.message : String(err);
|
|
6334
|
-
const entry = lock.entries[key];
|
|
6335
|
-
if (entry.jobId && entry.submissionComplete === true) {
|
|
6336
|
-
upsert(lock, key, { status: "processing", error: null });
|
|
6337
|
-
inFlight.set(entry.jobId, spec);
|
|
6338
|
-
submitted++;
|
|
6339
|
-
spent += estimate.amount;
|
|
6340
|
-
log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
6341
|
-
} else {
|
|
6342
|
-
failed++;
|
|
6343
|
-
upsert(lock, key, {
|
|
6344
|
-
status: "failed",
|
|
6345
|
-
error: message6,
|
|
6346
|
-
cost: entry.jobId ? estimate.amount : 0
|
|
6347
|
-
});
|
|
6348
|
-
log2(` FAILED ${key}: ${message6}`);
|
|
6349
|
-
}
|
|
6350
|
-
}
|
|
6351
|
-
await saveLock(lockPath, lock);
|
|
6251
|
+
return false;
|
|
6252
|
+
}
|
|
6253
|
+
function shouldPersistSourceUrl(value) {
|
|
6254
|
+
let parsed;
|
|
6255
|
+
try {
|
|
6256
|
+
parsed = new URL(value);
|
|
6257
|
+
} catch {
|
|
6258
|
+
return true;
|
|
6352
6259
|
}
|
|
6353
|
-
|
|
6260
|
+
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
6261
|
+
return !isSensitiveSourceUrl(value);
|
|
6354
6262
|
}
|
|
6355
6263
|
|
|
6356
|
-
// src/pipeline/
|
|
6357
|
-
|
|
6358
|
-
async function poll(provider, lock, lockPath, opts = {}) {
|
|
6359
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
6360
|
-
const timeout = opts.timeoutMs ?? 15 * 60 * 1e3;
|
|
6361
|
-
const log2 = opts.onProgress ?? (() => {
|
|
6362
|
-
});
|
|
6363
|
-
const started = Date.now();
|
|
6364
|
-
const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6365
|
-
const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
|
|
6366
|
-
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
6367
|
-
const incompleteSubmissions = Object.entries(lock.entries).filter(
|
|
6368
|
-
([key, entry]) => entry.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && entry.jobId && entry.submissionComplete === false && (entry.status === "pending" || entry.status === "processing")
|
|
6369
|
-
);
|
|
6370
|
-
for (const [key] of incompleteSubmissions) {
|
|
6371
|
-
log2(` incomplete submission ${key}; rerun \`pixelkiln submit\` to resume`);
|
|
6372
|
-
}
|
|
6373
|
-
result.stillRunning = incompleteSubmissions.length;
|
|
6374
|
-
const pending = () => Object.entries(lock.entries).filter(
|
|
6375
|
-
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && e.submissionComplete !== false && (e.status === "pending" || e.status === "processing")
|
|
6376
|
-
);
|
|
6377
|
-
while (pending().length > 0) {
|
|
6378
|
-
if (Date.now() - started > timeout) {
|
|
6379
|
-
result.stillRunning += pending().length;
|
|
6380
|
-
log2(` timed out with ${result.stillRunning} job(s) still running \u2014 re-run \`poll\` to resume`);
|
|
6381
|
-
break;
|
|
6382
|
-
}
|
|
6383
|
-
for (const [key, entry] of pending()) {
|
|
6384
|
-
try {
|
|
6385
|
-
const currentSpec = specByKey.get(key);
|
|
6386
|
-
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
6387
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
6388
|
-
spec: currentSpec
|
|
6389
|
-
});
|
|
6390
|
-
if (state.status === "review" || state.status === "review-set") {
|
|
6391
|
-
upsert(lock, key, {
|
|
6392
|
-
status: "review",
|
|
6393
|
-
reviewObjectId: entry.jobId,
|
|
6394
|
-
providerMetadata: state.metadata ? {
|
|
6395
|
-
...entry.providerMetadata,
|
|
6396
|
-
[provider.id]: {
|
|
6397
|
-
...entry.providerMetadata[provider.id],
|
|
6398
|
-
...state.metadata
|
|
6399
|
-
}
|
|
6400
|
-
} : entry.providerMetadata
|
|
6401
|
-
});
|
|
6402
|
-
result.review++;
|
|
6403
|
-
log2(
|
|
6404
|
-
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6405
|
-
);
|
|
6406
|
-
} else if (state.status === "ready") {
|
|
6407
|
-
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
6408
|
-
upsert(lock, key, {
|
|
6409
|
-
status: "selected",
|
|
6410
|
-
objectId: state.objectId,
|
|
6411
|
-
sourceUrl: sourceUrls[0]?.url ?? null,
|
|
6412
|
-
sourceUrls,
|
|
6413
|
-
providerMetadata: state.metadata ? {
|
|
6414
|
-
...entry.providerMetadata,
|
|
6415
|
-
[provider.id]: {
|
|
6416
|
-
...entry.providerMetadata[provider.id],
|
|
6417
|
-
...state.metadata
|
|
6418
|
-
}
|
|
6419
|
-
} : entry.providerMetadata,
|
|
6420
|
-
error: null
|
|
6421
|
-
});
|
|
6422
|
-
result.completed++;
|
|
6423
|
-
log2(` ready ${key}`);
|
|
6424
|
-
} else if (state.status === "failed") {
|
|
6425
|
-
upsert(lock, key, { status: "failed", error: state.error });
|
|
6426
|
-
result.failed++;
|
|
6427
|
-
log2(` FAILED ${key}: ${state.error}`);
|
|
6428
|
-
}
|
|
6429
|
-
} catch (err) {
|
|
6430
|
-
log2(` error polling ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6431
|
-
}
|
|
6432
|
-
}
|
|
6433
|
-
await saveLock(lockPath, lock);
|
|
6434
|
-
if (pending().length > 0) await sleep3(interval);
|
|
6435
|
-
}
|
|
6436
|
-
await saveLock(lockPath, lock);
|
|
6437
|
-
return result;
|
|
6438
|
-
}
|
|
6439
|
-
|
|
6440
|
-
// src/pipeline/fetch.ts
|
|
6441
|
-
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
6442
|
-
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6443
|
-
import path14 from "path";
|
|
6444
|
-
|
|
6445
|
-
// src/source-url.ts
|
|
6446
|
-
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
6447
|
-
"access_token",
|
|
6448
|
-
"apikey",
|
|
6449
|
-
"api_key",
|
|
6450
|
-
"auth",
|
|
6451
|
-
"authorization",
|
|
6452
|
-
"awsaccesskeyid",
|
|
6453
|
-
"googleaccessid",
|
|
6454
|
-
"key",
|
|
6455
|
-
"sig"
|
|
6456
|
-
]);
|
|
6457
|
-
function isSensitiveSourceUrl(value) {
|
|
6458
|
-
let parsed;
|
|
6459
|
-
try {
|
|
6460
|
-
parsed = new URL(value);
|
|
6461
|
-
} catch {
|
|
6462
|
-
return false;
|
|
6463
|
-
}
|
|
6464
|
-
if (parsed.username || parsed.password) return true;
|
|
6465
|
-
for (const key of parsed.searchParams.keys()) {
|
|
6466
|
-
const normalized = key.toLowerCase();
|
|
6467
|
-
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
6468
|
-
}
|
|
6469
|
-
return false;
|
|
6470
|
-
}
|
|
6471
|
-
function shouldPersistSourceUrl(value) {
|
|
6472
|
-
let parsed;
|
|
6473
|
-
try {
|
|
6474
|
-
parsed = new URL(value);
|
|
6475
|
-
} catch {
|
|
6476
|
-
return true;
|
|
6477
|
-
}
|
|
6478
|
-
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
6479
|
-
return !isSensitiveSourceUrl(value);
|
|
6480
|
-
}
|
|
6481
|
-
|
|
6482
|
-
// src/pipeline/fetch.ts
|
|
6483
|
-
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
6264
|
+
// src/pipeline/fetch.ts
|
|
6265
|
+
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
6484
6266
|
const log2 = opts.onProgress ?? (() => {
|
|
6485
6267
|
});
|
|
6486
6268
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
@@ -6498,7 +6280,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6498
6280
|
return e.outputs.some((output) => {
|
|
6499
6281
|
const file = resolveOutputPath(output.path, spec.root);
|
|
6500
6282
|
if (!existsSync10(file)) return true;
|
|
6501
|
-
|
|
6283
|
+
const current = sha256(readFileSync4(file));
|
|
6284
|
+
if (current === output.sha256) return false;
|
|
6285
|
+
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
6502
6286
|
});
|
|
6503
6287
|
});
|
|
6504
6288
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
@@ -6568,7 +6352,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6568
6352
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
6569
6353
|
);
|
|
6570
6354
|
if (!opts.force) {
|
|
6571
|
-
if (
|
|
6355
|
+
if (superseded && currentHash === superseded.sha256) {
|
|
6572
6356
|
} else if (recorded || superseded) {
|
|
6573
6357
|
throw new Error(
|
|
6574
6358
|
`refusing to overwrite modified output ${target}; pass --force to replace it`
|
|
@@ -6642,79 +6426,476 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6642
6426
|
} catch (err) {
|
|
6643
6427
|
const message6 = err instanceof Error ? err.message : String(err);
|
|
6644
6428
|
upsert(lock, key, {
|
|
6645
|
-
status: "download-failed",
|
|
6646
|
-
outputs: mergeOutputs(entry.outputs, outputs),
|
|
6647
|
-
error: `download failed: ${message6}`
|
|
6429
|
+
status: "download-failed",
|
|
6430
|
+
outputs: mergeOutputs(entry.outputs, outputs),
|
|
6431
|
+
error: `download failed: ${message6}`
|
|
6432
|
+
});
|
|
6433
|
+
result.failed++;
|
|
6434
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
6435
|
+
}
|
|
6436
|
+
await saveLock(lockPath, lock);
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
6440
|
+
await saveLock(lockPath, lock);
|
|
6441
|
+
return result;
|
|
6442
|
+
}
|
|
6443
|
+
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
6444
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6445
|
+
if (!existsSync10(file)) return null;
|
|
6446
|
+
try {
|
|
6447
|
+
const buf = await readFile7(file);
|
|
6448
|
+
if (sha256(buf) !== hash) return null;
|
|
6449
|
+
validateMedia(buf, mediaType);
|
|
6450
|
+
return buf;
|
|
6451
|
+
} catch {
|
|
6452
|
+
return null;
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
6456
|
+
const hash = knownHash ?? sha256(buf);
|
|
6457
|
+
validateMedia(buf, mediaType);
|
|
6458
|
+
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6459
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
6460
|
+
await mkdir3(cacheDir, { recursive: true });
|
|
6461
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6462
|
+
await writeFile3(tmp, buf);
|
|
6463
|
+
try {
|
|
6464
|
+
await rename3(tmp, file);
|
|
6465
|
+
} catch (err) {
|
|
6466
|
+
if (!existsSync10(file)) throw err;
|
|
6467
|
+
} finally {
|
|
6468
|
+
await rm4(tmp, { force: true });
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
function mergeOutputs(previous, current) {
|
|
6472
|
+
const merged = /* @__PURE__ */ new Map();
|
|
6473
|
+
for (const output of [...previous, ...current]) {
|
|
6474
|
+
merged.set(output.role ? `role:${output.role}` : `path:${output.path}`, output);
|
|
6475
|
+
}
|
|
6476
|
+
return [...merged.values()];
|
|
6477
|
+
}
|
|
6478
|
+
async function pushTags(provider, specs, lock, opts = {}) {
|
|
6479
|
+
const log2 = opts.onProgress ?? (() => {
|
|
6480
|
+
});
|
|
6481
|
+
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6482
|
+
let tagged = 0;
|
|
6483
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
6484
|
+
const spec = specByKey.get(key);
|
|
6485
|
+
if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
|
|
6486
|
+
if (entry.generator === "pixflux") continue;
|
|
6487
|
+
if (!provider.setTags) return tagged;
|
|
6488
|
+
try {
|
|
6489
|
+
await provider.setTags(entry.objectId, spec.tags);
|
|
6490
|
+
tagged++;
|
|
6491
|
+
} catch (err) {
|
|
6492
|
+
log2(` tag failed ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
return tagged;
|
|
6496
|
+
}
|
|
6497
|
+
|
|
6498
|
+
// src/pipeline/history.ts
|
|
6499
|
+
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
6500
|
+
function historyLimit(manifest, env = process.env) {
|
|
6501
|
+
if (manifest?.history !== void 0) return manifest.history;
|
|
6502
|
+
const raw = env[HISTORY_ENV]?.trim();
|
|
6503
|
+
if (raw === void 0 || raw === "") return DEFAULT_HISTORY_LIMIT;
|
|
6504
|
+
const value = Number(raw);
|
|
6505
|
+
if (!Number.isInteger(value) || value < 0 || value > 100) {
|
|
6506
|
+
throw new Error(`${HISTORY_ENV} must be a whole number from 0 to 100, got "${raw}"`);
|
|
6507
|
+
}
|
|
6508
|
+
return value;
|
|
6509
|
+
}
|
|
6510
|
+
function retireGeneration(entry, retiredAt) {
|
|
6511
|
+
return {
|
|
6512
|
+
specHash: entry.specHash,
|
|
6513
|
+
generator: entry.generator,
|
|
6514
|
+
tileFeature: entry.tileFeature,
|
|
6515
|
+
prompt: entry.prompt,
|
|
6516
|
+
width: entry.width,
|
|
6517
|
+
height: entry.height,
|
|
6518
|
+
jobId: entry.jobId,
|
|
6519
|
+
objectId: entry.objectId,
|
|
6520
|
+
candidateIndex: entry.candidateIndex,
|
|
6521
|
+
sourceUrl: entry.sourceUrl,
|
|
6522
|
+
sourceUrls: entry.sourceUrls,
|
|
6523
|
+
outputs: entry.outputs,
|
|
6524
|
+
providerMetadata: entry.providerMetadata,
|
|
6525
|
+
submittedAt: entry.submittedAt,
|
|
6526
|
+
downloadedAt: entry.downloadedAt,
|
|
6527
|
+
cost: entry.cost,
|
|
6528
|
+
costUnit: entry.costUnit,
|
|
6529
|
+
provider: entry.provider,
|
|
6530
|
+
retiredAt
|
|
6531
|
+
};
|
|
6532
|
+
}
|
|
6533
|
+
function historyAfterReplacing(previous, limit, retiredAt) {
|
|
6534
|
+
if (!previous) return void 0;
|
|
6535
|
+
const kept = previous.history ?? [];
|
|
6536
|
+
const outgoing = previous.status === "downloaded" && previous.outputs.length ? [retireGeneration(previous, retiredAt)] : [];
|
|
6537
|
+
const history = [...outgoing, ...kept].slice(0, limit);
|
|
6538
|
+
return history.length ? history : void 0;
|
|
6539
|
+
}
|
|
6540
|
+
function referencedHashes(lock) {
|
|
6541
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
6542
|
+
for (const entry of Object.values(lock.entries)) {
|
|
6543
|
+
for (const output of entry.outputs) hashes.add(output.sha256);
|
|
6544
|
+
for (const generation of entry.history ?? []) for (const output of generation.outputs) hashes.add(output.sha256);
|
|
6545
|
+
}
|
|
6546
|
+
return hashes;
|
|
6547
|
+
}
|
|
6548
|
+
function pickHistory(entry, selector) {
|
|
6549
|
+
const history = entry.history ?? [];
|
|
6550
|
+
if (!history.length) throw new Error(`${entry.styleId}/${entry.assetId} has no previous generations recorded`);
|
|
6551
|
+
if (typeof selector === "number" || /^\d{1,3}$/.test(selector)) {
|
|
6552
|
+
const index = Number(selector);
|
|
6553
|
+
const generation = history[index - 1];
|
|
6554
|
+
if (!generation) throw new Error(`${entry.styleId}/${entry.assetId} keeps ${history.length} previous generation(s); there is no #${index}`);
|
|
6555
|
+
return { index, generation };
|
|
6556
|
+
}
|
|
6557
|
+
const prefix = selector.toLowerCase();
|
|
6558
|
+
const matches = history.map((generation, i) => ({ index: i + 1, generation })).filter(({ generation }) => generation.outputs.some((output) => output.sha256.startsWith(prefix)));
|
|
6559
|
+
if (matches.length !== 1) {
|
|
6560
|
+
throw new Error(
|
|
6561
|
+
matches.length ? `"${selector}" matches ${matches.length} previous generations of ${entry.styleId}/${entry.assetId}; give more of the hash` : `no previous generation of ${entry.styleId}/${entry.assetId} has an output hash starting with "${selector}"`
|
|
6562
|
+
);
|
|
6563
|
+
}
|
|
6564
|
+
return matches[0];
|
|
6565
|
+
}
|
|
6566
|
+
async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
6567
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
6568
|
+
const entry = lock.entries[key];
|
|
6569
|
+
if (!entry) throw new Error(`${key} is not in the lockfile`);
|
|
6570
|
+
if (entry.status !== "downloaded") {
|
|
6571
|
+
throw new Error(`${key} is ${entry.status}; finish or fetch the current generation before restoring an older one`);
|
|
6572
|
+
}
|
|
6573
|
+
const { index, generation } = pickHistory(entry, opts.generation);
|
|
6574
|
+
if (generation.provider !== provider.id) {
|
|
6575
|
+
throw new Error(`${key}'s previous generation #${index} came from ${generation.provider}, not ${provider.id}`);
|
|
6576
|
+
}
|
|
6577
|
+
for (let i = 0; i < entry.outputs.length; i++) {
|
|
6578
|
+
const file = currentEntryOutputPath(entry, spec, i);
|
|
6579
|
+
if (!existsSync11(file)) continue;
|
|
6580
|
+
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
6581
|
+
throw new Error(
|
|
6582
|
+
`${path15.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`
|
|
6583
|
+
);
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6587
|
+
const remaining = (entry.history ?? []).filter((_, i) => i !== index - 1);
|
|
6588
|
+
const history = [retireGeneration(entry, now), ...remaining];
|
|
6589
|
+
upsert(lock, key, {
|
|
6590
|
+
specHash: generation.specHash,
|
|
6591
|
+
generator: generation.generator,
|
|
6592
|
+
tileFeature: generation.tileFeature,
|
|
6593
|
+
prompt: generation.prompt,
|
|
6594
|
+
width: generation.width,
|
|
6595
|
+
height: generation.height,
|
|
6596
|
+
jobId: generation.jobId,
|
|
6597
|
+
objectId: generation.objectId,
|
|
6598
|
+
candidateIndex: generation.candidateIndex,
|
|
6599
|
+
reviewObjectId: null,
|
|
6600
|
+
status: "downloaded",
|
|
6601
|
+
error: null,
|
|
6602
|
+
sourceUrl: generation.sourceUrl,
|
|
6603
|
+
sourceUrls: generation.sourceUrls,
|
|
6604
|
+
outputs: generation.outputs,
|
|
6605
|
+
// The files on disk are the outgoing generation's; that record authorizes
|
|
6606
|
+
// replacing them without --force, exactly as after a regeneration.
|
|
6607
|
+
supersededOutputs: entry.outputs,
|
|
6608
|
+
providerMetadata: generation.providerMetadata,
|
|
6609
|
+
submittedAt: generation.submittedAt,
|
|
6610
|
+
downloadedAt: generation.downloadedAt,
|
|
6611
|
+
cost: generation.cost,
|
|
6612
|
+
costUnit: generation.costUnit,
|
|
6613
|
+
provider: generation.provider,
|
|
6614
|
+
history
|
|
6615
|
+
});
|
|
6616
|
+
await saveLock(lockPath, lock);
|
|
6617
|
+
const result = await fetchAssets(provider, [spec], lock, lockPath, {
|
|
6618
|
+
onProgress: opts.onProgress,
|
|
6619
|
+
repair: true,
|
|
6620
|
+
force: opts.force,
|
|
6621
|
+
cacheDir: opts.cacheDir
|
|
6622
|
+
});
|
|
6623
|
+
if (result.failed) {
|
|
6624
|
+
throw new Error(`the previous generation's bytes could not be written back; run pixelkiln restore --only ${spec.assetId} --style ${spec.styleId} once the provider is reachable`);
|
|
6625
|
+
}
|
|
6626
|
+
return { restored: generation, index, downloaded: result.downloaded };
|
|
6627
|
+
}
|
|
6628
|
+
|
|
6629
|
+
// src/pipeline/submit.ts
|
|
6630
|
+
var IN_FLIGHT_POLL_MS = 5e3;
|
|
6631
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6632
|
+
async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
6633
|
+
const limits = provider.rateLimit?.() ?? DEFAULT_RATE_LIMIT;
|
|
6634
|
+
const maxInFlight = opts.maxInFlight ?? limits.maxInFlight;
|
|
6635
|
+
const spacing = opts.spacingMs ?? limits.spacingMs;
|
|
6636
|
+
const slotPollMs = opts.slotPollMs ?? IN_FLIGHT_POLL_MS;
|
|
6637
|
+
const slotTimeoutMs = opts.slotTimeoutMs ?? 15 * 60 * 1e3;
|
|
6638
|
+
const log2 = opts.onProgress ?? (() => {
|
|
6639
|
+
});
|
|
6640
|
+
const estimates = new Map(items.map((item) => [
|
|
6641
|
+
item.key,
|
|
6642
|
+
validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
6643
|
+
]));
|
|
6644
|
+
const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
|
|
6645
|
+
if (units.size > 1) {
|
|
6646
|
+
throw new Error(`Provider "${provider.id}" returned mixed cost units for one submission batch`);
|
|
6647
|
+
}
|
|
6648
|
+
const unit = units.values().next().value ?? "free";
|
|
6649
|
+
if (opts.budget != null) {
|
|
6650
|
+
const cost = [...estimates.values()].reduce((sum, estimate) => sum + estimate.amount, 0);
|
|
6651
|
+
if (cost > opts.budget) {
|
|
6652
|
+
throw new Error(
|
|
6653
|
+
`This run would spend ${formatCost(unit, cost)} but the budget is ${formatCost(unit, opts.budget)}. Narrow it with --only/--style, or raise --budget.`
|
|
6654
|
+
);
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
const styleImages = /* @__PURE__ */ new Map();
|
|
6658
|
+
for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
|
|
6659
|
+
styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
|
|
6660
|
+
}
|
|
6661
|
+
let submitted = 0;
|
|
6662
|
+
let failed = 0;
|
|
6663
|
+
let spent = 0;
|
|
6664
|
+
let lastSubmitAt = 0;
|
|
6665
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
6666
|
+
let lastSlotError = null;
|
|
6667
|
+
async function pruneInFlight() {
|
|
6668
|
+
for (const [id, spec] of [...inFlight]) {
|
|
6669
|
+
try {
|
|
6670
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
6671
|
+
if (state.status !== "processing") inFlight.delete(id);
|
|
6672
|
+
lastSlotError = null;
|
|
6673
|
+
} catch (err) {
|
|
6674
|
+
lastSlotError = err instanceof Error ? err.message : String(err);
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
async function waitForSlot() {
|
|
6679
|
+
const started = Date.now();
|
|
6680
|
+
while (inFlight.size >= maxInFlight) {
|
|
6681
|
+
if (Date.now() - started >= slotTimeoutMs) {
|
|
6682
|
+
throw new Error(
|
|
6683
|
+
`Timed out waiting for a generation slot` + (lastSlotError ? `; last poll error: ${lastSlotError}` : "")
|
|
6684
|
+
);
|
|
6685
|
+
}
|
|
6686
|
+
await sleep2(slotPollMs);
|
|
6687
|
+
await pruneInFlight();
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
for (const { spec, key } of items) {
|
|
6691
|
+
const estimate = estimates.get(key);
|
|
6692
|
+
await waitForSlot();
|
|
6693
|
+
const since = Date.now() - lastSubmitAt;
|
|
6694
|
+
if (since < spacing) await sleep2(spacing - since);
|
|
6695
|
+
await requireRevisionReady(spec, lock);
|
|
6696
|
+
const previousEntry = lock.entries[key];
|
|
6697
|
+
const resumesCheckpoint = Boolean(
|
|
6698
|
+
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
6699
|
+
);
|
|
6700
|
+
const previousJobId = resumesCheckpoint ? previousEntry.jobId : void 0;
|
|
6701
|
+
const previousMetadata = resumesCheckpoint ? previousEntry.providerMetadata[provider.id] : void 0;
|
|
6702
|
+
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
6703
|
+
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
6704
|
+
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
6705
|
+
upsert(lock, key, {
|
|
6706
|
+
styleId: spec.styleId,
|
|
6707
|
+
assetId: spec.assetId,
|
|
6708
|
+
specHash: spec.specHash,
|
|
6709
|
+
generator: spec.generator,
|
|
6710
|
+
tileFeature: spec.tileFeature ?? null,
|
|
6711
|
+
prompt: spec.prompt,
|
|
6712
|
+
width: spec.width,
|
|
6713
|
+
height: spec.height,
|
|
6714
|
+
revision: spec.revision ? {
|
|
6715
|
+
mode: spec.revision.mode,
|
|
6716
|
+
sourceAssetId: spec.revision.sourceAssetId,
|
|
6717
|
+
sourceSha256: spec.revision.sourceSha256,
|
|
6718
|
+
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
|
|
6719
|
+
...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
|
|
6720
|
+
} : null,
|
|
6721
|
+
status: "pending",
|
|
6722
|
+
jobId: previousJobId ?? null,
|
|
6723
|
+
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
6724
|
+
objectId: null,
|
|
6725
|
+
reviewObjectId: null,
|
|
6726
|
+
candidateIndex: null,
|
|
6727
|
+
error: null,
|
|
6728
|
+
outputs: [],
|
|
6729
|
+
// Keep the old ownership proof while new bytes are pending. Fetch may
|
|
6730
|
+
// replace that file only while its hash still matches this record.
|
|
6731
|
+
supersededOutputs,
|
|
6732
|
+
providerMetadata: resumesCheckpoint ? previousEntry.providerMetadata : {},
|
|
6733
|
+
sourceUrl: null,
|
|
6734
|
+
sourceUrls: [],
|
|
6735
|
+
submittedAt,
|
|
6736
|
+
history,
|
|
6737
|
+
cost: estimate.amount,
|
|
6738
|
+
costUnit: estimate.unit,
|
|
6739
|
+
// Persist routing before the request starts. If the process stops while
|
|
6740
|
+
// the provider is accepting work, a resumed run must not fall back to
|
|
6741
|
+
// PixelLab (the legacy lock default) for another provider's entry.
|
|
6742
|
+
provider: provider.id,
|
|
6743
|
+
downloadedAt: null
|
|
6744
|
+
});
|
|
6745
|
+
await saveLock(lockPath, lock);
|
|
6746
|
+
lastSubmitAt = Date.now();
|
|
6747
|
+
try {
|
|
6748
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
6749
|
+
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
6750
|
+
...previousJobId ? { previousJobId } : {},
|
|
6751
|
+
...previousMetadata ? { previousMetadata } : {},
|
|
6752
|
+
checkpoint: async (checkpoint) => {
|
|
6753
|
+
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
6754
|
+
const entry = lock.entries[key];
|
|
6755
|
+
upsert(lock, key, {
|
|
6756
|
+
jobId: checkpoint.jobId,
|
|
6757
|
+
submissionComplete: checkpoint.complete,
|
|
6758
|
+
reviewObjectId: checkpoint.complete && (spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature) ? checkpoint.jobId : null,
|
|
6759
|
+
status: checkpoint.complete ? "processing" : "pending",
|
|
6760
|
+
error: null,
|
|
6761
|
+
providerMetadata: checkpoint.metadata ? {
|
|
6762
|
+
...entry.providerMetadata,
|
|
6763
|
+
[provider.id]: {
|
|
6764
|
+
...entry.providerMetadata[provider.id],
|
|
6765
|
+
...checkpoint.metadata
|
|
6766
|
+
}
|
|
6767
|
+
} : entry.providerMetadata
|
|
6768
|
+
});
|
|
6769
|
+
await saveLock(lockPath, lock);
|
|
6770
|
+
}
|
|
6771
|
+
});
|
|
6772
|
+
upsert(lock, key, {
|
|
6773
|
+
jobId,
|
|
6774
|
+
submissionComplete: true,
|
|
6775
|
+
// A multi-candidate generator routes through review; record the parent
|
|
6776
|
+
// so `pick` knows where to look.
|
|
6777
|
+
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6778
|
+
status: "processing",
|
|
6779
|
+
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
6780
|
+
});
|
|
6781
|
+
inFlight.set(jobId, spec);
|
|
6782
|
+
submitted++;
|
|
6783
|
+
spent += estimate.amount;
|
|
6784
|
+
log2(
|
|
6785
|
+
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
6786
|
+
);
|
|
6787
|
+
} catch (err) {
|
|
6788
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
6789
|
+
const entry = lock.entries[key];
|
|
6790
|
+
if (entry.jobId && entry.submissionComplete === true) {
|
|
6791
|
+
upsert(lock, key, { status: "processing", error: null });
|
|
6792
|
+
inFlight.set(entry.jobId, spec);
|
|
6793
|
+
submitted++;
|
|
6794
|
+
spent += estimate.amount;
|
|
6795
|
+
log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
6796
|
+
} else {
|
|
6797
|
+
failed++;
|
|
6798
|
+
upsert(lock, key, {
|
|
6799
|
+
status: "failed",
|
|
6800
|
+
error: message6,
|
|
6801
|
+
cost: entry.jobId ? estimate.amount : 0
|
|
6648
6802
|
});
|
|
6649
|
-
|
|
6650
|
-
log2(` FAILED ${key}: ${message6}`);
|
|
6803
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
6651
6804
|
}
|
|
6652
|
-
await saveLock(lockPath, lock);
|
|
6653
6805
|
}
|
|
6806
|
+
await saveLock(lockPath, lock);
|
|
6654
6807
|
}
|
|
6655
|
-
|
|
6656
|
-
await saveLock(lockPath, lock);
|
|
6657
|
-
return result;
|
|
6658
|
-
}
|
|
6659
|
-
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
6660
|
-
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6661
|
-
if (!existsSync10(file)) return null;
|
|
6662
|
-
try {
|
|
6663
|
-
const buf = await readFile7(file);
|
|
6664
|
-
if (sha256(buf) !== hash) return null;
|
|
6665
|
-
validateMedia(buf, mediaType);
|
|
6666
|
-
return buf;
|
|
6667
|
-
} catch {
|
|
6668
|
-
return null;
|
|
6669
|
-
}
|
|
6670
|
-
}
|
|
6671
|
-
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
6672
|
-
const hash = knownHash ?? sha256(buf);
|
|
6673
|
-
validateMedia(buf, mediaType);
|
|
6674
|
-
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6675
|
-
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
6676
|
-
await mkdir3(cacheDir, { recursive: true });
|
|
6677
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6678
|
-
await writeFile3(tmp, buf);
|
|
6679
|
-
try {
|
|
6680
|
-
await rename3(tmp, file);
|
|
6681
|
-
} catch (err) {
|
|
6682
|
-
if (!existsSync10(file)) throw err;
|
|
6683
|
-
} finally {
|
|
6684
|
-
await rm4(tmp, { force: true });
|
|
6685
|
-
}
|
|
6686
|
-
}
|
|
6687
|
-
function mergeOutputs(previous, current) {
|
|
6688
|
-
const merged = /* @__PURE__ */ new Map();
|
|
6689
|
-
for (const output of [...previous, ...current]) {
|
|
6690
|
-
merged.set(output.role ? `role:${output.role}` : `path:${output.path}`, output);
|
|
6691
|
-
}
|
|
6692
|
-
return [...merged.values()];
|
|
6808
|
+
return { submitted, failed, spent, unit };
|
|
6693
6809
|
}
|
|
6694
|
-
|
|
6810
|
+
|
|
6811
|
+
// src/pipeline/poll.ts
|
|
6812
|
+
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6813
|
+
async function poll(provider, lock, lockPath, opts = {}) {
|
|
6814
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
6815
|
+
const timeout = opts.timeoutMs ?? 15 * 60 * 1e3;
|
|
6695
6816
|
const log2 = opts.onProgress ?? (() => {
|
|
6696
6817
|
});
|
|
6697
|
-
const
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6818
|
+
const started = Date.now();
|
|
6819
|
+
const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6820
|
+
const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
|
|
6821
|
+
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
6822
|
+
const incompleteSubmissions = Object.entries(lock.entries).filter(
|
|
6823
|
+
([key, entry]) => entry.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && entry.jobId && entry.submissionComplete === false && (entry.status === "pending" || entry.status === "processing")
|
|
6824
|
+
);
|
|
6825
|
+
for (const [key] of incompleteSubmissions) {
|
|
6826
|
+
log2(` incomplete submission ${key}; rerun \`pixelkiln submit\` to resume`);
|
|
6827
|
+
}
|
|
6828
|
+
result.stillRunning = incompleteSubmissions.length;
|
|
6829
|
+
const pending = () => Object.entries(lock.entries).filter(
|
|
6830
|
+
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && e.submissionComplete !== false && (e.status === "pending" || e.status === "processing")
|
|
6831
|
+
);
|
|
6832
|
+
while (pending().length > 0) {
|
|
6833
|
+
if (Date.now() - started > timeout) {
|
|
6834
|
+
result.stillRunning += pending().length;
|
|
6835
|
+
log2(` timed out with ${result.stillRunning} job(s) still running \u2014 re-run \`poll\` to resume`);
|
|
6836
|
+
break;
|
|
6837
|
+
}
|
|
6838
|
+
for (const [key, entry] of pending()) {
|
|
6839
|
+
try {
|
|
6840
|
+
const currentSpec = specByKey.get(key);
|
|
6841
|
+
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
6842
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
6843
|
+
spec: currentSpec
|
|
6844
|
+
});
|
|
6845
|
+
if (state.status === "review" || state.status === "review-set") {
|
|
6846
|
+
upsert(lock, key, {
|
|
6847
|
+
status: "review",
|
|
6848
|
+
reviewObjectId: entry.jobId,
|
|
6849
|
+
providerMetadata: state.metadata ? {
|
|
6850
|
+
...entry.providerMetadata,
|
|
6851
|
+
[provider.id]: {
|
|
6852
|
+
...entry.providerMetadata[provider.id],
|
|
6853
|
+
...state.metadata
|
|
6854
|
+
}
|
|
6855
|
+
} : entry.providerMetadata
|
|
6856
|
+
});
|
|
6857
|
+
result.review++;
|
|
6858
|
+
log2(
|
|
6859
|
+
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6860
|
+
);
|
|
6861
|
+
} else if (state.status === "ready") {
|
|
6862
|
+
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
6863
|
+
upsert(lock, key, {
|
|
6864
|
+
status: "selected",
|
|
6865
|
+
objectId: state.objectId,
|
|
6866
|
+
sourceUrl: sourceUrls[0]?.url ?? null,
|
|
6867
|
+
sourceUrls,
|
|
6868
|
+
providerMetadata: state.metadata ? {
|
|
6869
|
+
...entry.providerMetadata,
|
|
6870
|
+
[provider.id]: {
|
|
6871
|
+
...entry.providerMetadata[provider.id],
|
|
6872
|
+
...state.metadata
|
|
6873
|
+
}
|
|
6874
|
+
} : entry.providerMetadata,
|
|
6875
|
+
error: null
|
|
6876
|
+
});
|
|
6877
|
+
result.completed++;
|
|
6878
|
+
log2(` ready ${key}`);
|
|
6879
|
+
} else if (state.status === "failed") {
|
|
6880
|
+
upsert(lock, key, { status: "failed", error: state.error });
|
|
6881
|
+
result.failed++;
|
|
6882
|
+
log2(` FAILED ${key}: ${state.error}`);
|
|
6883
|
+
}
|
|
6884
|
+
} catch (err) {
|
|
6885
|
+
log2(` error polling ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6886
|
+
}
|
|
6709
6887
|
}
|
|
6888
|
+
await saveLock(lockPath, lock);
|
|
6889
|
+
if (pending().length > 0) await sleep3(interval);
|
|
6710
6890
|
}
|
|
6711
|
-
|
|
6891
|
+
await saveLock(lockPath, lock);
|
|
6892
|
+
return result;
|
|
6712
6893
|
}
|
|
6713
6894
|
|
|
6714
6895
|
// src/pipeline/doctor.ts
|
|
6715
|
-
import { constants, existsSync as
|
|
6896
|
+
import { constants, existsSync as existsSync12 } from "fs";
|
|
6716
6897
|
import { access, stat as stat2 } from "fs/promises";
|
|
6717
|
-
import
|
|
6898
|
+
import path16 from "path";
|
|
6718
6899
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
6719
6900
|
const checks = [];
|
|
6720
6901
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -6725,8 +6906,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6725
6906
|
);
|
|
6726
6907
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
6727
6908
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
6728
|
-
|
|
6729
|
-
...spec.quality ? [
|
|
6909
|
+
path16.dirname(spec.outFile),
|
|
6910
|
+
...spec.quality ? [path16.dirname(spec.quality.outFile)] : []
|
|
6730
6911
|
]))];
|
|
6731
6912
|
const unwritable = [];
|
|
6732
6913
|
for (const dir of dirs) {
|
|
@@ -6749,7 +6930,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6749
6930
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
6750
6931
|
const output = entry.outputs[index];
|
|
6751
6932
|
const spec = specByKey.get(key);
|
|
6752
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
6933
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path16.dirname(lockPath));
|
|
6753
6934
|
const owner = outputOwners.get(absolute);
|
|
6754
6935
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
6755
6936
|
outputOwners.set(absolute, key);
|
|
@@ -6764,7 +6945,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6764
6945
|
} else {
|
|
6765
6946
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
6766
6947
|
}
|
|
6767
|
-
const cacheDir =
|
|
6948
|
+
const cacheDir = path16.join(path16.dirname(lockPath), ".pixelkiln", "cache");
|
|
6768
6949
|
const stranded = [];
|
|
6769
6950
|
const invalidState = [];
|
|
6770
6951
|
const staleMapUrls = [];
|
|
@@ -6780,11 +6961,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6780
6961
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
6781
6962
|
let hasCache = false;
|
|
6782
6963
|
for (const output of entry.outputs) {
|
|
6783
|
-
const cached =
|
|
6964
|
+
const cached = path16.join(
|
|
6784
6965
|
cacheDir,
|
|
6785
6966
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
6786
6967
|
);
|
|
6787
|
-
if (
|
|
6968
|
+
if (existsSync12(cached) && await sha256File(cached) === output.sha256) {
|
|
6788
6969
|
hasCache = true;
|
|
6789
6970
|
break;
|
|
6790
6971
|
}
|
|
@@ -6880,20 +7061,20 @@ async function nearestExistingDirectory(start) {
|
|
|
6880
7061
|
if ((await stat2(current)).isDirectory()) return current;
|
|
6881
7062
|
} catch {
|
|
6882
7063
|
}
|
|
6883
|
-
const parent =
|
|
7064
|
+
const parent = path16.dirname(current);
|
|
6884
7065
|
if (parent === current) return current;
|
|
6885
7066
|
current = parent;
|
|
6886
7067
|
}
|
|
6887
7068
|
}
|
|
6888
7069
|
|
|
6889
7070
|
// src/pipeline/adopt.ts
|
|
6890
|
-
import { existsSync as
|
|
7071
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6891
7072
|
import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
6892
|
-
import
|
|
7073
|
+
import path17 from "path";
|
|
6893
7074
|
|
|
6894
7075
|
// src/cache.ts
|
|
6895
7076
|
import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
6896
|
-
import { existsSync as
|
|
7077
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6897
7078
|
import pathModule from "path";
|
|
6898
7079
|
import { z as z3 } from "zod";
|
|
6899
7080
|
var HashCacheSchema = z3.object({
|
|
@@ -6908,10 +7089,10 @@ function isSha256Hash(value) {
|
|
|
6908
7089
|
function parseCache(value) {
|
|
6909
7090
|
return HashCacheSchema.parse(value);
|
|
6910
7091
|
}
|
|
6911
|
-
async function loadCache(
|
|
6912
|
-
if (!
|
|
7092
|
+
async function loadCache(path32) {
|
|
7093
|
+
if (!existsSync13(path32)) return { version: 1, hashes: {} };
|
|
6913
7094
|
try {
|
|
6914
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
7095
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path32, "utf8")));
|
|
6915
7096
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6916
7097
|
return {
|
|
6917
7098
|
version: 1,
|
|
@@ -6923,18 +7104,18 @@ async function loadCache(path31) {
|
|
|
6923
7104
|
return { version: 1, hashes: {} };
|
|
6924
7105
|
}
|
|
6925
7106
|
}
|
|
6926
|
-
async function saveCache(
|
|
7107
|
+
async function saveCache(path32, cache) {
|
|
6927
7108
|
const sorted = {};
|
|
6928
7109
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6929
7110
|
const hash = cache.hashes[key];
|
|
6930
7111
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6931
7112
|
sorted[key] = hash;
|
|
6932
7113
|
}
|
|
6933
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6934
|
-
const tmp = `${
|
|
7114
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path32)), { recursive: true });
|
|
7115
|
+
const tmp = `${path32}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6935
7116
|
try {
|
|
6936
7117
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6937
|
-
await rename4(tmp,
|
|
7118
|
+
await rename4(tmp, path32);
|
|
6938
7119
|
} finally {
|
|
6939
7120
|
await rm5(tmp, { force: true });
|
|
6940
7121
|
}
|
|
@@ -6965,7 +7146,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6965
7146
|
const localByHash = /* @__PURE__ */ new Map();
|
|
6966
7147
|
const unmatchedLocal = [];
|
|
6967
7148
|
for (const spec of specs) {
|
|
6968
|
-
if (!
|
|
7149
|
+
if (!existsSync14(spec.outFile)) {
|
|
6969
7150
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
6970
7151
|
continue;
|
|
6971
7152
|
}
|
|
@@ -7122,13 +7303,13 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7122
7303
|
}
|
|
7123
7304
|
|
|
7124
7305
|
// src/pick/server.ts
|
|
7125
|
-
import { existsSync as
|
|
7306
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7126
7307
|
import { readFile as readFile12 } from "fs/promises";
|
|
7127
7308
|
|
|
7128
7309
|
// src/pipeline/init.ts
|
|
7129
7310
|
import { readdir, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
7130
|
-
import { existsSync as
|
|
7131
|
-
import
|
|
7311
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7312
|
+
import path18 from "path";
|
|
7132
7313
|
function pngSize(buf) {
|
|
7133
7314
|
if (buf.length < 24) return null;
|
|
7134
7315
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -7142,7 +7323,7 @@ function slugify(value) {
|
|
|
7142
7323
|
async function walk(dir, exclude) {
|
|
7143
7324
|
const out = [];
|
|
7144
7325
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
7145
|
-
const full =
|
|
7326
|
+
const full = path18.join(dir, entry.name);
|
|
7146
7327
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
7147
7328
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
7148
7329
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -7161,9 +7342,9 @@ async function scanAssets(root, opts = {}) {
|
|
|
7161
7342
|
skipped.push(`${file} (not a readable PNG)`);
|
|
7162
7343
|
continue;
|
|
7163
7344
|
}
|
|
7164
|
-
const rel =
|
|
7165
|
-
const category =
|
|
7166
|
-
let id = slugify(
|
|
7345
|
+
const rel = path18.relative(root, file);
|
|
7346
|
+
const category = path18.dirname(rel) === "." ? "" : path18.dirname(rel);
|
|
7347
|
+
let id = slugify(path18.basename(rel));
|
|
7167
7348
|
if (seen.has(id)) {
|
|
7168
7349
|
const n = seen.get(id) + 1;
|
|
7169
7350
|
seen.set(id, n);
|
|
@@ -7220,7 +7401,7 @@ function mode(values) {
|
|
|
7220
7401
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
7221
7402
|
}
|
|
7222
7403
|
async function writeManifestFile(target, manifest) {
|
|
7223
|
-
if (
|
|
7404
|
+
if (existsSync15(target)) {
|
|
7224
7405
|
throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
|
|
7225
7406
|
}
|
|
7226
7407
|
await writeFile6(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
@@ -7706,7 +7887,7 @@ async function currentArt(entry, spec) {
|
|
|
7706
7887
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
7707
7888
|
if (!previous) return null;
|
|
7708
7889
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
7709
|
-
if (!
|
|
7890
|
+
if (!existsSync16(file)) return null;
|
|
7710
7891
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
7711
7892
|
if (!contentType) return null;
|
|
7712
7893
|
let width = entry.width;
|
|
@@ -7877,14 +8058,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
7877
8058
|
|
|
7878
8059
|
// src/gallery/snapshot.ts
|
|
7879
8060
|
import { createHash as createHash3 } from "crypto";
|
|
7880
|
-
import { existsSync as
|
|
8061
|
+
import { existsSync as existsSync19 } from "fs";
|
|
7881
8062
|
import { readFile as readFile16, stat as stat3 } from "fs/promises";
|
|
7882
|
-
import
|
|
8063
|
+
import path23 from "path";
|
|
7883
8064
|
|
|
7884
8065
|
// src/workspace.ts
|
|
7885
8066
|
import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7886
|
-
import { existsSync as
|
|
7887
|
-
import
|
|
8067
|
+
import { existsSync as existsSync17 } from "fs";
|
|
8068
|
+
import path19 from "path";
|
|
7888
8069
|
import { z as z4 } from "zod";
|
|
7889
8070
|
var WorkspaceProjectSchema = z4.object({
|
|
7890
8071
|
id: z4.string().min(1),
|
|
@@ -7909,7 +8090,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
7909
8090
|
);
|
|
7910
8091
|
}
|
|
7911
8092
|
async function loadWorkspace(workspacePath) {
|
|
7912
|
-
if (!
|
|
8093
|
+
if (!existsSync17(workspacePath)) return { version: 1, projects: [] };
|
|
7913
8094
|
let raw;
|
|
7914
8095
|
try {
|
|
7915
8096
|
raw = JSON.parse(await readFile13(workspacePath, "utf8"));
|
|
@@ -7926,7 +8107,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7926
8107
|
version: 1,
|
|
7927
8108
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
7928
8109
|
};
|
|
7929
|
-
await mkdir5(
|
|
8110
|
+
await mkdir5(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
|
|
7930
8111
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7931
8112
|
try {
|
|
7932
8113
|
await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -7936,12 +8117,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7936
8117
|
}
|
|
7937
8118
|
}
|
|
7938
8119
|
function toPortablePath(dir, absolute) {
|
|
7939
|
-
return
|
|
8120
|
+
return path19.relative(dir, absolute).split(path19.sep).join("/");
|
|
7940
8121
|
}
|
|
7941
8122
|
function resolveProject(dir, project) {
|
|
7942
8123
|
return {
|
|
7943
|
-
manifestPath:
|
|
7944
|
-
lockPath:
|
|
8124
|
+
manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
|
|
8125
|
+
lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
|
|
7945
8126
|
};
|
|
7946
8127
|
}
|
|
7947
8128
|
function validateWorkspace(ws, dir) {
|
|
@@ -7954,21 +8135,21 @@ function validateWorkspace(ws, dir) {
|
|
|
7954
8135
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
7955
8136
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
7956
8137
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
7957
|
-
if (
|
|
8138
|
+
if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
|
|
7958
8139
|
diagnostics.push({
|
|
7959
8140
|
id: "absolute-path",
|
|
7960
8141
|
level: "warning",
|
|
7961
8142
|
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
7962
8143
|
});
|
|
7963
8144
|
}
|
|
7964
|
-
if (!
|
|
8145
|
+
if (!existsSync17(manifestPath)) {
|
|
7965
8146
|
diagnostics.push({
|
|
7966
8147
|
id: "missing-manifest",
|
|
7967
8148
|
level: "error",
|
|
7968
8149
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
7969
8150
|
});
|
|
7970
8151
|
}
|
|
7971
|
-
if (!
|
|
8152
|
+
if (!existsSync17(lockPath)) {
|
|
7972
8153
|
diagnostics.push({
|
|
7973
8154
|
id: "missing-lock",
|
|
7974
8155
|
level: "error",
|
|
@@ -8015,12 +8196,12 @@ function validateWorkspace(ws, dir) {
|
|
|
8015
8196
|
}
|
|
8016
8197
|
|
|
8017
8198
|
// src/gallery/edit.ts
|
|
8018
|
-
import
|
|
8199
|
+
import path22 from "path";
|
|
8019
8200
|
import { z as z7 } from "zod";
|
|
8020
8201
|
|
|
8021
8202
|
// src/manifest-edit.ts
|
|
8022
8203
|
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
8023
|
-
import
|
|
8204
|
+
import path20 from "path";
|
|
8024
8205
|
import { z as z5 } from "zod";
|
|
8025
8206
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
8026
8207
|
var AssetPatchSchema = z5.object({
|
|
@@ -8232,7 +8413,7 @@ function applyEdit(raw, edit) {
|
|
|
8232
8413
|
}
|
|
8233
8414
|
}
|
|
8234
8415
|
async function applyManifestEdit(manifestPath, edit) {
|
|
8235
|
-
const absolute =
|
|
8416
|
+
const absolute = path20.resolve(manifestPath);
|
|
8236
8417
|
const original = await readFile14(absolute, "utf8");
|
|
8237
8418
|
const before = sha256(original);
|
|
8238
8419
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
@@ -8247,9 +8428,9 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8247
8428
|
applyEdit(raw, edit);
|
|
8248
8429
|
const next = serializeLike(original, raw);
|
|
8249
8430
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
8250
|
-
const tmp =
|
|
8251
|
-
|
|
8252
|
-
`.${
|
|
8431
|
+
const tmp = path20.join(
|
|
8432
|
+
path20.dirname(absolute),
|
|
8433
|
+
`.${path20.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
8253
8434
|
);
|
|
8254
8435
|
try {
|
|
8255
8436
|
await writeFile8(tmp, next);
|
|
@@ -8269,18 +8450,18 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8269
8450
|
|
|
8270
8451
|
// src/pipeline/hand-edit.ts
|
|
8271
8452
|
import { spawn as spawn2 } from "child_process";
|
|
8272
|
-
import { existsSync as
|
|
8453
|
+
import { existsSync as existsSync18 } from "fs";
|
|
8273
8454
|
import { copyFile, mkdir as mkdir6, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
|
|
8274
|
-
import
|
|
8455
|
+
import path21 from "path";
|
|
8275
8456
|
import { z as z6 } from "zod";
|
|
8276
8457
|
var HAND_EDIT_DIR = "edits";
|
|
8277
8458
|
function handEditPath(loaded, spec) {
|
|
8278
8459
|
const style = loaded.manifest.styles[spec.styleId];
|
|
8279
|
-
const outDir =
|
|
8280
|
-
const relative =
|
|
8281
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
8282
|
-
const file = inside ? relative :
|
|
8283
|
-
return
|
|
8460
|
+
const outDir = path21.resolve(loaded.root, style?.outDir ?? ".");
|
|
8461
|
+
const relative = path21.relative(outDir, spec.outFile);
|
|
8462
|
+
const inside = relative && !relative.startsWith("..") && !path21.isAbsolute(relative);
|
|
8463
|
+
const file = inside ? relative : path21.basename(spec.outFile);
|
|
8464
|
+
return path21.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8284
8465
|
}
|
|
8285
8466
|
function handEditBases(spec, lock) {
|
|
8286
8467
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8293,10 +8474,10 @@ function handEditBases(spec, lock) {
|
|
|
8293
8474
|
}));
|
|
8294
8475
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
8295
8476
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
8296
|
-
if (!members.every((member) =>
|
|
8477
|
+
if (!members.every((member) => existsSync18(member.path))) return null;
|
|
8297
8478
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
8298
8479
|
}
|
|
8299
|
-
return
|
|
8480
|
+
return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
8300
8481
|
}
|
|
8301
8482
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
8302
8483
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8322,18 +8503,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
8322
8503
|
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
8323
8504
|
);
|
|
8324
8505
|
}
|
|
8325
|
-
const existing = spec.source ?
|
|
8506
|
+
const existing = spec.source ? path21.resolve(loaded.root, spec.source) : null;
|
|
8326
8507
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8327
8508
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
8328
8509
|
let created = false;
|
|
8329
8510
|
for (const member of members) {
|
|
8330
|
-
if (
|
|
8511
|
+
if (existsSync18(member.path)) continue;
|
|
8331
8512
|
if (!member.base) {
|
|
8332
8513
|
throw new ManifestEditError(
|
|
8333
8514
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8334
8515
|
);
|
|
8335
8516
|
}
|
|
8336
|
-
await mkdir6(
|
|
8517
|
+
await mkdir6(path21.dirname(member.path), { recursive: true });
|
|
8337
8518
|
await copyFile(member.base, member.path);
|
|
8338
8519
|
created = true;
|
|
8339
8520
|
}
|
|
@@ -8471,7 +8652,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8471
8652
|
editor: input.editor,
|
|
8472
8653
|
protocol: input.protocol,
|
|
8473
8654
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8474
|
-
project: projectPath ?
|
|
8655
|
+
project: projectPath ? path21.basename(projectPath) : null,
|
|
8475
8656
|
outputs: incoming.map((frame, index) => ({
|
|
8476
8657
|
role: frame.role,
|
|
8477
8658
|
basedOn: basedOnHash(entry, index),
|
|
@@ -8606,7 +8787,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8606
8787
|
}
|
|
8607
8788
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8608
8789
|
if (result.changed) {
|
|
8609
|
-
const relative =
|
|
8790
|
+
const relative = path22.relative(process.cwd(), result.manifestPath);
|
|
8610
8791
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8611
8792
|
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
|
|
8612
8793
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -8618,7 +8799,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8618
8799
|
// src/gallery/snapshot.ts
|
|
8619
8800
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
8620
8801
|
function galleryMediaId(absolutePath) {
|
|
8621
|
-
return createHash3("sha256").update(
|
|
8802
|
+
return createHash3("sha256").update(path23.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8622
8803
|
}
|
|
8623
8804
|
function galleryMediaRoute(id) {
|
|
8624
8805
|
return `/media/${id}`;
|
|
@@ -8633,6 +8814,38 @@ function matchesFilter(filter, styleId, assetId) {
|
|
|
8633
8814
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
|
|
8634
8815
|
return true;
|
|
8635
8816
|
}
|
|
8817
|
+
function describeHistory(media, entry, cacheDir) {
|
|
8818
|
+
return (entry.history ?? []).map((generation, i) => {
|
|
8819
|
+
const outputs = generation.outputs.map((output) => {
|
|
8820
|
+
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
8821
|
+
const file = path23.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
8822
|
+
let url = null;
|
|
8823
|
+
if (existsSync19(file)) {
|
|
8824
|
+
const id = galleryMediaId(file);
|
|
8825
|
+
media.set(id, { path: file, contentType: mediaType });
|
|
8826
|
+
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
8827
|
+
}
|
|
8828
|
+
return { role: output.role ?? null, sha256: output.sha256, path: output.path, url };
|
|
8829
|
+
});
|
|
8830
|
+
return {
|
|
8831
|
+
index: i + 1,
|
|
8832
|
+
objectId: generation.objectId,
|
|
8833
|
+
jobId: generation.jobId,
|
|
8834
|
+
prompt: generation.prompt,
|
|
8835
|
+
promptDiffers: generation.prompt !== entry.prompt,
|
|
8836
|
+
width: generation.width,
|
|
8837
|
+
height: generation.height,
|
|
8838
|
+
cost: generation.cost,
|
|
8839
|
+
costUnit: generation.costUnit,
|
|
8840
|
+
submittedAt: generation.submittedAt,
|
|
8841
|
+
downloadedAt: generation.downloadedAt,
|
|
8842
|
+
retiredAt: generation.retiredAt,
|
|
8843
|
+
outputs,
|
|
8844
|
+
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
8845
|
+
upstreamUrl: generation.provider === "pixellab" ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
|
|
8846
|
+
};
|
|
8847
|
+
});
|
|
8848
|
+
}
|
|
8636
8849
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
8637
8850
|
async function samePixels(a, b) {
|
|
8638
8851
|
try {
|
|
@@ -8689,10 +8902,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
8689
8902
|
async function describeQuality(media, root, inspection) {
|
|
8690
8903
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
8691
8904
|
const outputs = await Promise.all(
|
|
8692
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
8905
|
+
outputPaths.map((file) => describeOutput(media, root, path23.resolve(root, file)))
|
|
8693
8906
|
);
|
|
8694
|
-
const recordPath =
|
|
8695
|
-
const recordExists =
|
|
8907
|
+
const recordPath = path23.resolve(root, inspection.record);
|
|
8908
|
+
const recordExists = existsSync19(recordPath);
|
|
8696
8909
|
let options = null;
|
|
8697
8910
|
let check = null;
|
|
8698
8911
|
if (recordExists) {
|
|
@@ -8711,7 +8924,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
8711
8924
|
return {
|
|
8712
8925
|
state: inspection.state,
|
|
8713
8926
|
reason: inspection.reason,
|
|
8714
|
-
output: portableOutputPath(
|
|
8927
|
+
output: portableOutputPath(path23.resolve(root, inspection.output), root),
|
|
8715
8928
|
record: portableOutputPath(recordPath, root),
|
|
8716
8929
|
recordExists,
|
|
8717
8930
|
outputs,
|
|
@@ -8750,6 +8963,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8750
8963
|
const items = [];
|
|
8751
8964
|
const manifestText = await readFile16(loaded.path, "utf8");
|
|
8752
8965
|
const manifestSha256 = sha256(manifestText);
|
|
8966
|
+
const cacheDir = path23.resolve(path23.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
8753
8967
|
let rawStyles = {};
|
|
8754
8968
|
try {
|
|
8755
8969
|
const raw = JSON.parse(manifestText);
|
|
@@ -8773,12 +8987,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
8773
8987
|
)
|
|
8774
8988
|
);
|
|
8775
8989
|
if (spec.source) {
|
|
8776
|
-
const editPath =
|
|
8990
|
+
const editPath = path23.resolve(root, spec.source);
|
|
8777
8991
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
8778
8992
|
const editShas = await Promise.all(
|
|
8779
8993
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
8780
8994
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
8781
|
-
return { file, sha256:
|
|
8995
|
+
return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
|
|
8782
8996
|
})
|
|
8783
8997
|
);
|
|
8784
8998
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -8786,7 +9000,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8786
9000
|
const companion = await readHandEditCompanion(editPath);
|
|
8787
9001
|
if (companion) {
|
|
8788
9002
|
const projectPath = handEditProjectPath(editPath);
|
|
8789
|
-
const project = companion.project &&
|
|
9003
|
+
const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
|
|
8790
9004
|
let projectUrl = null;
|
|
8791
9005
|
if (project) {
|
|
8792
9006
|
const id = galleryMediaId(projectPath);
|
|
@@ -8822,8 +9036,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8822
9036
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
8823
9037
|
}
|
|
8824
9038
|
} else if (spec.source) {
|
|
8825
|
-
outputs = [await describeOutput(media, root,
|
|
8826
|
-
} else if (!entry &&
|
|
9039
|
+
outputs = [await describeOutput(media, root, path23.resolve(root, spec.source))];
|
|
9040
|
+
} else if (!entry && existsSync19(spec.outFile)) {
|
|
8827
9041
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
8828
9042
|
} else {
|
|
8829
9043
|
outputs = [];
|
|
@@ -8874,6 +9088,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8874
9088
|
editMeta,
|
|
8875
9089
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8876
9090
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9091
|
+
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
8877
9092
|
tags: spec.tags,
|
|
8878
9093
|
category: asset?.category ?? null
|
|
8879
9094
|
});
|
|
@@ -8933,6 +9148,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8933
9148
|
editMeta: null,
|
|
8934
9149
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8935
9150
|
refreshable: false,
|
|
9151
|
+
history: describeHistory(media, entry, cacheDir),
|
|
8936
9152
|
tags: [],
|
|
8937
9153
|
category: null
|
|
8938
9154
|
});
|
|
@@ -9000,11 +9216,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
9000
9216
|
id: loaded.manifest.name,
|
|
9001
9217
|
name: loaded.manifest.name,
|
|
9002
9218
|
manifest: loaded.path,
|
|
9003
|
-
lock:
|
|
9219
|
+
lock: path23.resolve(opts.lockPath),
|
|
9004
9220
|
root,
|
|
9005
9221
|
provider: loaded.manifest.provider,
|
|
9006
9222
|
account: null,
|
|
9007
9223
|
manifestSha256,
|
|
9224
|
+
historyLimit: historyLimit(loaded.manifest),
|
|
9008
9225
|
entries: Object.keys(lock.entries).length,
|
|
9009
9226
|
items: items.length,
|
|
9010
9227
|
spendByUnit: spendByUnit(lock),
|
|
@@ -9025,7 +9242,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9025
9242
|
return { snapshot, media };
|
|
9026
9243
|
}
|
|
9027
9244
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
9028
|
-
const dir =
|
|
9245
|
+
const dir = path23.dirname(path23.resolve(opts.workspacePath));
|
|
9029
9246
|
const media = /* @__PURE__ */ new Map();
|
|
9030
9247
|
const projects = [];
|
|
9031
9248
|
const items = [];
|
|
@@ -9068,11 +9285,12 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9068
9285
|
id: project.id,
|
|
9069
9286
|
name: loaded.manifest.name,
|
|
9070
9287
|
manifest: manifestPath,
|
|
9071
|
-
lock:
|
|
9288
|
+
lock: path23.resolve(lockPath),
|
|
9072
9289
|
root: loaded.root,
|
|
9073
9290
|
provider: loaded.manifest.provider,
|
|
9074
9291
|
account: project.account ?? null,
|
|
9075
9292
|
manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
|
|
9293
|
+
historyLimit: historyLimit(loaded.manifest),
|
|
9076
9294
|
entries: Object.keys(lock.entries).length,
|
|
9077
9295
|
items: projectItems.length,
|
|
9078
9296
|
spendByUnit: spendByUnit(lock),
|
|
@@ -9083,11 +9301,12 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9083
9301
|
id: project.id,
|
|
9084
9302
|
name: project.id,
|
|
9085
9303
|
manifest: manifestPath,
|
|
9086
|
-
lock:
|
|
9087
|
-
root:
|
|
9304
|
+
lock: path23.resolve(lockPath),
|
|
9305
|
+
root: path23.dirname(manifestPath),
|
|
9088
9306
|
provider: project.provider,
|
|
9089
9307
|
account: project.account ?? null,
|
|
9090
9308
|
manifestSha256: null,
|
|
9309
|
+
historyLimit: 0,
|
|
9091
9310
|
entries: 0,
|
|
9092
9311
|
items: 0,
|
|
9093
9312
|
spendByUnit: {},
|
|
@@ -9110,7 +9329,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9110
9329
|
version: 1,
|
|
9111
9330
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
9112
9331
|
project: null,
|
|
9113
|
-
workspace: { path:
|
|
9332
|
+
workspace: { path: path23.resolve(opts.workspacePath), dir, projects },
|
|
9114
9333
|
filter,
|
|
9115
9334
|
totals: {
|
|
9116
9335
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -9431,6 +9650,31 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9431
9650
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
9432
9651
|
.pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
|
|
9433
9652
|
.pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
|
|
9653
|
+
.versions { display:grid; gap:8px; margin:8px 0 4px; }
|
|
9654
|
+
.version { display:grid; grid-template-columns:auto 1fr auto; gap:6px 12px; align-items:center; border:1px solid var(--line);
|
|
9655
|
+
background:var(--panel-deep); padding:8px 10px; font-size:12.5px; }
|
|
9656
|
+
.version .thumb { width:64px; height:64px; display:grid; place-items:center; background-image:var(--checker); border:1px solid var(--line); }
|
|
9657
|
+
.version .thumb img { image-rendering:pixelated; max-width:64px; max-height:64px; }
|
|
9658
|
+
.version .thumb span { font:10.5px var(--mono); color:var(--dim); text-align:center; padding:4px; }
|
|
9659
|
+
.version .facts { display:grid; gap:2px; min-width:0; }
|
|
9660
|
+
.version .facts b { font:650 12px/1.4 var(--mono); }
|
|
9661
|
+
.version .facts .prompt { color:var(--dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
9662
|
+
.version .facts .prompt.differs { color:var(--warn); }
|
|
9663
|
+
.version .facts .meta { color:var(--dim); font-size:12px; }
|
|
9664
|
+
.version .acts { display:grid; gap:6px; justify-items:end; }
|
|
9665
|
+
.version .acts button { padding:3px 9px; font-size:12px; white-space:nowrap; }
|
|
9666
|
+
.dot.busy { background:var(--cool); animation: pulse 1.2s ease-in-out infinite; }
|
|
9667
|
+
@keyframes pulse { 0%, 100% { opacity:1; } 50% { opacity:.25; } }
|
|
9668
|
+
@media (prefers-reduced-motion: reduce) { .dot.busy { animation:none; } }
|
|
9669
|
+
.card .busy-badge { position:absolute; left:6px; bottom:6px; display:inline-flex; align-items:center; gap:5px;
|
|
9670
|
+
background:var(--panel); color:var(--cool); border:1px solid var(--cool); padding:2px 7px; font:650 10.5px/1.4 var(--mono); }
|
|
9671
|
+
.drawer .job-strip { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; align-items:baseline; margin-top:10px;
|
|
9672
|
+
padding:10px 12px; border:1px solid var(--cool); background:color-mix(in srgb, var(--cool) 8%, var(--panel-deep)); font-size:12.5px; }
|
|
9673
|
+
.drawer .job-strip .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1.4 var(--mono); color:var(--cool); white-space:nowrap; }
|
|
9674
|
+
.drawer .job-strip .last { color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
|
|
9675
|
+
.drawer .job-strip .what { grid-column:1 / -1; color:var(--text); }
|
|
9676
|
+
.drawer .job-strip .acts { grid-column:1 / -1; display:flex; gap:6px; }
|
|
9677
|
+
.drawer .job-strip .acts button { padding:3px 9px; font-size:12px; }
|
|
9434
9678
|
.card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
|
|
9435
9679
|
font:700 11px/1 var(--mono); padding:3px 5px; }
|
|
9436
9680
|
.hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
@@ -9589,6 +9833,11 @@ const splitTags = (value) => value.split(',').map((t) => t.trim()).filter(Boolea
|
|
|
9589
9833
|
|
|
9590
9834
|
const ACTIVE_PHASES = new Set(['queued', 'submitting', 'polling', 'fetching', 'review']);
|
|
9591
9835
|
const PHASE_TONE = { queued: 'cool', submitting: 'cool', polling: 'cool', fetching: 'cool', review: 'warn', done: 'ok', failed: 'bad' };
|
|
9836
|
+
// The job still working on this record, if any: what the drawer and its card
|
|
9837
|
+
// show while a regeneration, restore, or pull is in flight.
|
|
9838
|
+
const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
|
|
9839
|
+
const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring', revert: 'bringing back a previous generation of' };
|
|
9840
|
+
const PHASE_TEXT = { queued: 'queued', submitting: 'submitting to the provider', polling: 'waiting for the provider to finish', fetching: 'downloading the result', review: 'candidates are ready to review' };
|
|
9592
9841
|
const remainingBudget = (provider) => {
|
|
9593
9842
|
const keyed = GEN.budget.byProvider[provider];
|
|
9594
9843
|
const ceiling = keyed !== undefined ? keyed : GEN.budget.amount;
|
|
@@ -9618,10 +9867,34 @@ async function pollJobs() {
|
|
|
9618
9867
|
renderHeader();
|
|
9619
9868
|
let changed = false;
|
|
9620
9869
|
for (const job of GEN.jobs) {
|
|
9621
|
-
if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) {
|
|
9870
|
+
if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) {
|
|
9871
|
+
ui.settled.add(job.id); changed = true;
|
|
9872
|
+
// The open record hears how its job ended, where the strip was.
|
|
9873
|
+
const open = ui.open && snap.items.find((i) => i.id === ui.open);
|
|
9874
|
+
if (open && job.project === open.project && job.keys.includes(open.key)) {
|
|
9875
|
+
ui.notice = { id: open.id, text: job.phase === 'failed'
|
|
9876
|
+
? (JOB_VERB[job.mode] || job.mode).replace(/ing$/, 'ing') + ' this asset failed: ' + (job.error || 'see the log above')
|
|
9877
|
+
: job.mode === 'generate' ? 'Regenerated. The new result is on disk and recorded as this generation.'
|
|
9878
|
+
: job.mode === 'restore' ? 'Restored. The recorded bytes are back on disk.'
|
|
9879
|
+
: job.mode === 'revert' ? 'Brought back generation #' + job.revert + '. The one it replaced is now #1 in the list below.'
|
|
9880
|
+
: job.mode === 'refresh' ? (job.counts.downloaded ? 'Pulled the upstream change; the lockfile records the new bytes.' : 'Nothing changed upstream; the file was left alone.')
|
|
9881
|
+
: 'Done.' };
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9622
9884
|
if (job.phase === 'review' && !ui.settled.has(job.id + ':review')) { ui.settled.add(job.id + ':review'); changed = true; }
|
|
9623
9885
|
}
|
|
9624
9886
|
if (changed) refresh();
|
|
9887
|
+
else {
|
|
9888
|
+
// A job in flight: keep the open record's strip and the card badges honest without a full re-render.
|
|
9889
|
+
if (ui.open && !$('dialog-host').childNodes.length) renderDrawer();
|
|
9890
|
+
for (const c of document.querySelectorAll('.card')) {
|
|
9891
|
+
const item = snap.items.find((i) => i.id === c.dataset.key);
|
|
9892
|
+
const job = item && activeJobFor(item);
|
|
9893
|
+
const badge = c.querySelector('.busy-badge');
|
|
9894
|
+
if (job && !badge) c.replaceWith(card(item));
|
|
9895
|
+
else if (!job && badge) badge.remove();
|
|
9896
|
+
}
|
|
9897
|
+
}
|
|
9625
9898
|
const active = GEN.jobs.some((job) => ACTIVE_PHASES.has(job.phase) && job.phase !== 'review');
|
|
9626
9899
|
clearTimeout(jobsTimer);
|
|
9627
9900
|
if (active) jobsTimer = setTimeout(pollJobs, 2000);
|
|
@@ -9635,7 +9908,7 @@ function renderJobs() {
|
|
|
9635
9908
|
const row = el('div', 'job');
|
|
9636
9909
|
const ph = el('span', 'ph');
|
|
9637
9910
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
9638
|
-
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'restore' ? 'restore ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9911
|
+
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'restore' ? 'restore ' : job.mode === 'revert' ? 'bring back generation #' + job.revert + ' of ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9639
9912
|
(job.project ? ' in ' + job.project : '');
|
|
9640
9913
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
9641
9914
|
last.title = job.keys.join('\\n');
|
|
@@ -9760,15 +10033,16 @@ function budgetLine() {
|
|
|
9760
10033
|
|
|
9761
10034
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
9762
10035
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
9763
|
-
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false } = {}) {
|
|
10036
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false, restore = false, revert = null, generation = null } = {}) {
|
|
9764
10037
|
const host = $('dialog-host');
|
|
9765
10038
|
host.textContent = '';
|
|
9766
10039
|
const wrap = el('div', 'dialog');
|
|
9767
10040
|
const form = el('form');
|
|
9768
10041
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
9769
|
-
const free = resume || refresh || restore;
|
|
10042
|
+
const free = resume || refresh || restore || revert !== null;
|
|
9770
10043
|
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
9771
10044
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
10045
|
+
: revert !== null ? 'Bring back generation #' + revert + ' of ' + items[0].key
|
|
9772
10046
|
: restore ? 'Restore ' + noun
|
|
9773
10047
|
: resume ? 'Resume ' + noun
|
|
9774
10048
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
@@ -9790,6 +10064,10 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9790
10064
|
const sum = el('div', 'sum');
|
|
9791
10065
|
if (refresh) {
|
|
9792
10066
|
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.'));
|
|
10067
|
+
} else if (revert !== null) {
|
|
10068
|
+
sum.append(el('div', null, 'The current generation moves into this asset\u2019s history and generation #' + revert + ' becomes current again: its object, hashes, and cost are recorded as this generation, and its bytes are written back ' +
|
|
10069
|
+
(generation && generation.cached ? 'from the local cache.' : 'by re-downloading them from the provider.') + ' Nothing is generated; nothing upstream changes. Undo it from the same list.'));
|
|
10070
|
+
if (generation && generation.promptDiffers) sum.append(el('div', 'warn', 'That generation was made with a different prompt than the current one; the record will show it as stale until the manifest matches or it is regenerated.'));
|
|
9793
10071
|
} else if (restore) {
|
|
9794
10072
|
sum.append(el('div', null, 'Puts the recorded bytes back on disk from the local cache or the provider object. Nothing is generated or submitted.'));
|
|
9795
10073
|
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.'));
|
|
@@ -9811,7 +10089,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9811
10089
|
}
|
|
9812
10090
|
form.append(sum);
|
|
9813
10091
|
const actions = el('div', 'actions');
|
|
9814
|
-
const go = el('button', 'primary', refresh ? 'Pull changes' : restore ? (force && replacing.length ? 'Restore and discard my change' : 'Restore') : resume ? 'Resume' : force && replacing.length ? 'Generate and replace' : 'Generate');
|
|
10092
|
+
const go = el('button', 'primary', refresh ? 'Pull changes' : revert !== null ? 'Bring it back' : restore ? (force && replacing.length ? 'Restore and discard my change' : 'Restore') : resume ? 'Resume' : force && replacing.length ? 'Generate and replace' : 'Generate');
|
|
9815
10093
|
go.type = 'submit';
|
|
9816
10094
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
9817
10095
|
const msg = el('span', 'msg');
|
|
@@ -9827,6 +10105,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9827
10105
|
if (resume) body.resume = true;
|
|
9828
10106
|
if (refresh) body.refresh = true;
|
|
9829
10107
|
if (restore) body.restore = true;
|
|
10108
|
+
if (revert !== null) body.revert = revert;
|
|
9830
10109
|
const job = await postGenerate(body);
|
|
9831
10110
|
host.textContent = '';
|
|
9832
10111
|
GEN.jobs.unshift(job);
|
|
@@ -10270,6 +10549,13 @@ function card(item) {
|
|
|
10270
10549
|
if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
|
|
10271
10550
|
const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
|
|
10272
10551
|
}
|
|
10552
|
+
const job = activeJobFor(item);
|
|
10553
|
+
if (job) {
|
|
10554
|
+
const badge = el('span', 'busy-badge');
|
|
10555
|
+
badge.append(el('i', 'dot busy'), document.createTextNode(job.phase === 'review' ? 'review' : job.mode === 'generate' ? 'generating' : job.mode));
|
|
10556
|
+
badge.title = (JOB_VERB[job.mode] || job.mode) + ' this asset \u2014 ' + (PHASE_TEXT[job.phase] || job.phase);
|
|
10557
|
+
cell.append(badge);
|
|
10558
|
+
}
|
|
10273
10559
|
c.append(cell, body);
|
|
10274
10560
|
c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
|
|
10275
10561
|
return c;
|
|
@@ -10450,6 +10736,54 @@ function candidatesControl(style) {
|
|
|
10450
10736
|
return wrap;
|
|
10451
10737
|
}
|
|
10452
10738
|
|
|
10739
|
+
// The generations this one replaced: what the lockfile still remembers, with
|
|
10740
|
+
// thumbnails from the content cache while the bytes are there, and a way back.
|
|
10741
|
+
function historySection(item) {
|
|
10742
|
+
const limit = projectOf(item)?.historyLimit;
|
|
10743
|
+
if (!item.history.length && !(limit > 0 && item.status === 'downloaded')) return null;
|
|
10744
|
+
const s = el('section', 'meta');
|
|
10745
|
+
s.append(el('h3', null, item.history.length ? 'Previous generations (' + item.history.length + ')' : 'Previous generations'));
|
|
10746
|
+
if (!item.history.length) {
|
|
10747
|
+
s.append(el('div', 'state-dim', 'None yet. A regeneration keeps the generation it replaces \u2014 up to ' + limit + ' per asset (PIXELKILN_HISTORY, or the manifest\u2019s history) \u2014 so it can be brought back from here.'));
|
|
10748
|
+
return s;
|
|
10749
|
+
}
|
|
10750
|
+
const list = el('div', 'versions');
|
|
10751
|
+
const job = GENERATION ? activeJobFor(item) : null;
|
|
10752
|
+
for (const g of item.history) {
|
|
10753
|
+
const v = el('div', 'version');
|
|
10754
|
+
const thumb = el('div', 'thumb');
|
|
10755
|
+
const first = g.outputs[0];
|
|
10756
|
+
if (first && first.url) { const img = el('img'); img.src = first.url; img.alt = 'generation #' + g.index; thumb.append(img); }
|
|
10757
|
+
else thumb.append(el('span', null, g.cached ? '' : 'not cached'));
|
|
10758
|
+
const facts = el('div', 'facts');
|
|
10759
|
+
const head = el('div');
|
|
10760
|
+
head.append(el('b', null, '#' + g.index), document.createTextNode(' \xB7 ' + fmtWhen(g.downloadedAt || g.submittedAt) + ' \xB7 ' + fmtCost(g.costUnit, g.cost) +
|
|
10761
|
+
(g.outputs.length > 1 ? ' \xB7 ' + g.outputs.length + ' files' : '')));
|
|
10762
|
+
facts.append(head);
|
|
10763
|
+
const prompt = el('div', 'prompt' + (g.promptDiffers ? ' differs' : ''), (g.promptDiffers ? 'prompt: ' : 'same prompt: ') + g.prompt);
|
|
10764
|
+
prompt.title = g.prompt;
|
|
10765
|
+
facts.append(prompt);
|
|
10766
|
+
const meta = el('div', 'meta');
|
|
10767
|
+
meta.append(document.createTextNode((first ? first.sha256.slice(0, 12) + '\u2026' : '') + (g.cached ? ' \xB7 bytes cached locally' : ' \xB7 not in the local cache; a restore re-downloads it')));
|
|
10768
|
+
if (g.upstreamUrl) { const a = el('a', null, 'open in ' + item.provider + ' \u2197'); a.href = g.upstreamUrl; a.target = '_blank'; a.rel = 'noopener'; meta.append(document.createTextNode(' \xB7 '), a); }
|
|
10769
|
+
facts.append(meta);
|
|
10770
|
+
const acts = el('div', 'acts');
|
|
10771
|
+
if (GENERATION) {
|
|
10772
|
+
const b = el('button', null, 'Restore this one \xB7 no cost'); b.type = 'button';
|
|
10773
|
+
b.disabled = !!job;
|
|
10774
|
+
b.title = job ? 'Wait for the running job to finish' : 'The current generation moves into history; this one becomes current and its bytes are written back';
|
|
10775
|
+
b.onclick = () => generateDialog([item], { project: item.project, revert: String(g.index), generation: g });
|
|
10776
|
+
acts.append(b);
|
|
10777
|
+
}
|
|
10778
|
+
v.append(thumb, facts, acts);
|
|
10779
|
+
list.append(v);
|
|
10780
|
+
}
|
|
10781
|
+
s.append(list);
|
|
10782
|
+
s.append(el('div', 'state-dim', 'Keeping up to ' + limit + ' per asset; a restore moves the current generation into this list, so it can be undone the same way.' +
|
|
10783
|
+
(GENERATION ? '' : ' Restoring needs the gallery started with --budget (any amount; nothing is spent).')));
|
|
10784
|
+
return s;
|
|
10785
|
+
}
|
|
10786
|
+
|
|
10453
10787
|
function upstreamSection(item) {
|
|
10454
10788
|
if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
|
|
10455
10789
|
const s = el('section', 'meta');
|
|
@@ -10476,6 +10810,30 @@ function upstreamSection(item) {
|
|
|
10476
10810
|
return s;
|
|
10477
10811
|
}
|
|
10478
10812
|
|
|
10813
|
+
// What a job is doing to this record right now. The record's state line still
|
|
10814
|
+
// says what is on disk; this says what is about to replace it.
|
|
10815
|
+
function jobStrip(item, job) {
|
|
10816
|
+
const strip = el('div', 'job-strip');
|
|
10817
|
+
const ph = el('span', 'ph');
|
|
10818
|
+
ph.append(el('i', 'dot busy'), document.createTextNode(job.phase));
|
|
10819
|
+
const last = el('span', 'last', job.messages.length ? job.messages[job.messages.length - 1].trim() : '');
|
|
10820
|
+
last.title = job.messages.join('\\n');
|
|
10821
|
+
const what = el('span', 'what', (JOB_VERB[job.mode] || job.mode) + ' this asset \u2014 ' + (PHASE_TEXT[job.phase] || job.phase) +
|
|
10822
|
+
(job.mode === 'generate' && job.phase !== 'review' ? '. The current file stays until the new result is downloaded.' : '.'));
|
|
10823
|
+
strip.append(ph, last, what);
|
|
10824
|
+
const acts = el('div', 'acts');
|
|
10825
|
+
if (job.phase === 'review' && job.review.includes(item.key)) {
|
|
10826
|
+
const b = el('button', 'primary', 'Review candidates'); b.type = 'button'; b.onclick = () => openReview(job.id);
|
|
10827
|
+
acts.append(b);
|
|
10828
|
+
}
|
|
10829
|
+
const logBtn = el('button', null, ui.logs.has(job.id) ? 'Hide log' : 'Log'); logBtn.type = 'button';
|
|
10830
|
+
logBtn.onclick = () => { ui.logs.has(job.id) ? ui.logs.delete(job.id) : ui.logs.add(job.id); renderDrawer(); };
|
|
10831
|
+
acts.append(logBtn);
|
|
10832
|
+
strip.append(acts);
|
|
10833
|
+
if (ui.logs.has(job.id)) { const pre = el('pre', null, job.messages.join('\\n')); pre.style.gridColumn = '1 / -1'; strip.append(pre); }
|
|
10834
|
+
return strip;
|
|
10835
|
+
}
|
|
10836
|
+
|
|
10479
10837
|
function generateActions(item) {
|
|
10480
10838
|
const row = el('div', 'gen');
|
|
10481
10839
|
if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
|
|
@@ -11183,7 +11541,9 @@ function renderDrawer() {
|
|
|
11183
11541
|
body.append(previewHost);
|
|
11184
11542
|
if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
|
|
11185
11543
|
body.append(stateNode(item.state, item.reason));
|
|
11186
|
-
|
|
11544
|
+
const job = GENERATION ? activeJobFor(item) : null;
|
|
11545
|
+
if (job) body.append(jobStrip(item, job));
|
|
11546
|
+
else if (GENERATION) body.append(generateActions(item));
|
|
11187
11547
|
// Writes are opt-in flags on the command line; say so where the buttons would be.
|
|
11188
11548
|
if ((!EDITABLE || !GENERATION) && item.declared) {
|
|
11189
11549
|
const missing = [];
|
|
@@ -11248,6 +11608,8 @@ function renderDrawer() {
|
|
|
11248
11608
|
if (hand) body.append(hand);
|
|
11249
11609
|
const upstream = upstreamSection(item);
|
|
11250
11610
|
if (upstream) body.append(upstream);
|
|
11611
|
+
const versions = historySection(item);
|
|
11612
|
+
if (versions) body.append(versions);
|
|
11251
11613
|
|
|
11252
11614
|
if (item.revision || item.revisionParentKey) {
|
|
11253
11615
|
const { s, dl } = section('Lineage');
|
|
@@ -11794,10 +12156,15 @@ var GenerateRequestSchema = z8.object({
|
|
|
11794
12156
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11795
12157
|
refresh: z8.boolean().optional(),
|
|
11796
12158
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
11797
|
-
restore: z8.boolean().optional()
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
12159
|
+
restore: z8.boolean().optional(),
|
|
12160
|
+
/**
|
|
12161
|
+
* Bring a replaced generation back: 1-based position in the record's
|
|
12162
|
+
* history (newest first) or an output hash prefix. One key at a time.
|
|
12163
|
+
*/
|
|
12164
|
+
revert: z8.string().min(1).max(64).optional()
|
|
12165
|
+
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
12166
|
+
message: "resume, refresh, restore, and revert are exclusive"
|
|
12167
|
+
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
11801
12168
|
var GenerateRequestError = class extends Error {
|
|
11802
12169
|
constructor(message6, status = 400) {
|
|
11803
12170
|
super(message6);
|
|
@@ -11928,6 +12295,22 @@ function createGenerateHandlers(opts) {
|
|
|
11928
12295
|
await restore(job, ctx, specs);
|
|
11929
12296
|
return;
|
|
11930
12297
|
}
|
|
12298
|
+
if (job.mode === "revert") {
|
|
12299
|
+
job.phase = "fetching";
|
|
12300
|
+
const spec = specs[0];
|
|
12301
|
+
const entry = ctx.lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
12302
|
+
const provider = opts.providerFor(job.project ?? void 0, entry.provider);
|
|
12303
|
+
const result = await revertGeneration(provider, spec, ctx.lock, ctx.lockPath, {
|
|
12304
|
+
generation: job.revert,
|
|
12305
|
+
force: job.force,
|
|
12306
|
+
onProgress: (line) => say(job, line)
|
|
12307
|
+
});
|
|
12308
|
+
job.counts.downloaded += result.downloaded;
|
|
12309
|
+
say(job, `restored generation #${result.index}${result.restored.objectId ? ` (${result.restored.objectId})` : ""}; the replaced one is now #1 in its history`);
|
|
12310
|
+
job.phase = "done";
|
|
12311
|
+
job.finishedAt = now().toISOString();
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
11931
12314
|
if (job.mode === "generate") {
|
|
11932
12315
|
job.phase = "submitting";
|
|
11933
12316
|
for (const group of groups) {
|
|
@@ -11998,8 +12381,13 @@ function createGenerateHandlers(opts) {
|
|
|
11998
12381
|
}
|
|
11999
12382
|
}
|
|
12000
12383
|
}
|
|
12384
|
+
if (request.revert !== void 0) {
|
|
12385
|
+
const entry = ctx.lock.entries[request.keys[0]];
|
|
12386
|
+
if (!entry?.history?.length) throw new GenerateRequestError(`"${request.keys[0]}" has no previous generation recorded`);
|
|
12387
|
+
if (entry.status !== "downloaded") throw new GenerateRequestError(`"${request.keys[0]}" is ${entry.status}; finish or fetch the current generation first`);
|
|
12388
|
+
}
|
|
12001
12389
|
let groups = [];
|
|
12002
|
-
if (!request.resume && !request.refresh && !request.restore) {
|
|
12390
|
+
if (!request.resume && !request.refresh && !request.restore && request.revert === void 0) {
|
|
12003
12391
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
12004
12392
|
if (!plan.actionable.length) {
|
|
12005
12393
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -12019,8 +12407,9 @@ function createGenerateHandlers(opts) {
|
|
|
12019
12407
|
id: randomBytes2(8).toString("hex"),
|
|
12020
12408
|
project: request.project ?? null,
|
|
12021
12409
|
keys: request.keys,
|
|
12022
|
-
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : "generate",
|
|
12410
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : request.revert !== void 0 ? "revert" : "generate",
|
|
12023
12411
|
force: Boolean(request.force),
|
|
12412
|
+
revert: request.revert ?? null,
|
|
12024
12413
|
phase: "queued",
|
|
12025
12414
|
startedAt: now().toISOString(),
|
|
12026
12415
|
finishedAt: null,
|
|
@@ -12032,7 +12421,7 @@ function createGenerateHandlers(opts) {
|
|
|
12032
12421
|
};
|
|
12033
12422
|
jobs.set(job.id, job);
|
|
12034
12423
|
contexts.set(job.id, ctx);
|
|
12035
|
-
say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.restore ? `restoring ${request.keys.length} asset(s) at no cost${request.force ? " (replacing modified files)" : ""}` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
12424
|
+
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.revert !== void 0 ? `bringing back generation ${request.revert} of ${request.keys[0]} 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("; "));
|
|
12036
12425
|
void run(job, ctx, specs, groups);
|
|
12037
12426
|
return job;
|
|
12038
12427
|
},
|
|
@@ -12083,14 +12472,14 @@ function createGenerateHandlers(opts) {
|
|
|
12083
12472
|
|
|
12084
12473
|
// src/gallery/editor.ts
|
|
12085
12474
|
import { readFile as readFile19 } from "fs/promises";
|
|
12086
|
-
import
|
|
12475
|
+
import path25 from "path";
|
|
12087
12476
|
|
|
12088
12477
|
// src/editor/install.ts
|
|
12089
12478
|
import { createHash as createHash4 } from "crypto";
|
|
12090
|
-
import { existsSync as
|
|
12479
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12091
12480
|
import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
|
|
12092
12481
|
import os2 from "os";
|
|
12093
|
-
import
|
|
12482
|
+
import path24 from "path";
|
|
12094
12483
|
|
|
12095
12484
|
// tools/pixelorama-bridge/pin.json
|
|
12096
12485
|
var pin_default = {
|
|
@@ -12154,15 +12543,15 @@ var EditorInstallError = class extends Error {
|
|
|
12154
12543
|
};
|
|
12155
12544
|
function editorToolsRoot() {
|
|
12156
12545
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
12157
|
-
if (override) return
|
|
12158
|
-
if (process.platform === "darwin") return
|
|
12546
|
+
if (override) return path24.resolve(override);
|
|
12547
|
+
if (process.platform === "darwin") return path24.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
12159
12548
|
if (process.platform === "win32") {
|
|
12160
|
-
return
|
|
12549
|
+
return path24.join(process.env.LOCALAPPDATA ?? path24.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
12161
12550
|
}
|
|
12162
|
-
return
|
|
12551
|
+
return path24.join(process.env.XDG_CACHE_HOME ?? path24.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
12163
12552
|
}
|
|
12164
12553
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
12165
|
-
return
|
|
12554
|
+
return path24.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
12166
12555
|
}
|
|
12167
12556
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
12168
12557
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -12185,8 +12574,8 @@ async function editorStatus(opts = {}) {
|
|
|
12185
12574
|
let totalBytes = 0;
|
|
12186
12575
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
12187
12576
|
totalBytes += expected.bytes;
|
|
12188
|
-
const file =
|
|
12189
|
-
const info =
|
|
12577
|
+
const file = path24.join(dir, name);
|
|
12578
|
+
const info = existsSync20(file) ? await stat4(file).catch(() => null) : null;
|
|
12190
12579
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
12191
12580
|
installedBytes += expected.bytes;
|
|
12192
12581
|
} else {
|
|
@@ -12243,7 +12632,7 @@ async function installEditor(opts = {}) {
|
|
|
12243
12632
|
if (digest2 !== expected.sha256) {
|
|
12244
12633
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
12245
12634
|
}
|
|
12246
|
-
const final =
|
|
12635
|
+
const final = path24.join(dir, name);
|
|
12247
12636
|
const tmp = `${final}.${process.pid}.part`;
|
|
12248
12637
|
try {
|
|
12249
12638
|
await writeFile10(tmp, bytes);
|
|
@@ -12289,7 +12678,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
12289
12678
|
".json": "application/json; charset=utf-8"
|
|
12290
12679
|
};
|
|
12291
12680
|
function editorContentType(name) {
|
|
12292
|
-
return EDITOR_CONTENT_TYPES[
|
|
12681
|
+
return EDITOR_CONTENT_TYPES[path24.extname(name).toLowerCase()] ?? null;
|
|
12293
12682
|
}
|
|
12294
12683
|
|
|
12295
12684
|
// src/gallery/editor.ts
|
|
@@ -12365,7 +12754,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12365
12754
|
if (!status.installed) return null;
|
|
12366
12755
|
let bytes;
|
|
12367
12756
|
try {
|
|
12368
|
-
bytes = await readFile19(
|
|
12757
|
+
bytes = await readFile19(path25.join(dir, name));
|
|
12369
12758
|
} catch {
|
|
12370
12759
|
verified = null;
|
|
12371
12760
|
return null;
|
|
@@ -12381,15 +12770,15 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12381
12770
|
|
|
12382
12771
|
// src/pipeline/salvage.ts
|
|
12383
12772
|
import { readFile as readFile20 } from "fs/promises";
|
|
12384
|
-
import { existsSync as
|
|
12385
|
-
import
|
|
12773
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12774
|
+
import path26 from "path";
|
|
12386
12775
|
function providerClaimId(provider, objectId) {
|
|
12387
12776
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12388
12777
|
}
|
|
12389
12778
|
async function loadClaims(lockPaths, opts = {}) {
|
|
12390
12779
|
const claimed = /* @__PURE__ */ new Set();
|
|
12391
12780
|
for (const p of lockPaths) {
|
|
12392
|
-
if (!
|
|
12781
|
+
if (!existsSync21(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12393
12782
|
let parsed;
|
|
12394
12783
|
try {
|
|
12395
12784
|
parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
|
|
@@ -12440,20 +12829,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
12440
12829
|
return null;
|
|
12441
12830
|
}
|
|
12442
12831
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12443
|
-
const own =
|
|
12832
|
+
const own = path26.resolve(ownManifestPath);
|
|
12444
12833
|
const siblingManifestPaths = [
|
|
12445
12834
|
.../* @__PURE__ */ new Set([
|
|
12446
12835
|
...workspaceManifestPaths,
|
|
12447
|
-
...claimPaths.map((c) =>
|
|
12836
|
+
...claimPaths.map((c) => path26.join(path26.dirname(path26.resolve(c)), "pixelkiln.manifest.json"))
|
|
12448
12837
|
])
|
|
12449
12838
|
];
|
|
12450
12839
|
const siblings = [];
|
|
12451
12840
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12452
|
-
if (
|
|
12453
|
-
if (!
|
|
12841
|
+
if (path26.resolve(siblingManifestPath) === own) continue;
|
|
12842
|
+
if (!existsSync21(siblingManifestPath)) continue;
|
|
12454
12843
|
try {
|
|
12455
12844
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12456
|
-
siblings.push({ label:
|
|
12845
|
+
siblings.push({ label: path26.basename(path26.dirname(siblingManifestPath)), manifest });
|
|
12457
12846
|
} catch {
|
|
12458
12847
|
}
|
|
12459
12848
|
}
|
|
@@ -12672,18 +13061,16 @@ async function workspaceStatus(ws, dir) {
|
|
|
12672
13061
|
}
|
|
12673
13062
|
|
|
12674
13063
|
// src/pipeline/cache-health.ts
|
|
12675
|
-
import { existsSync as
|
|
13064
|
+
import { existsSync as existsSync22 } from "fs";
|
|
12676
13065
|
import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
12677
|
-
import
|
|
13066
|
+
import path27 from "path";
|
|
12678
13067
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
12679
|
-
if (options.prune && !
|
|
12680
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
13068
|
+
if (options.prune && !existsSync22(lockPath)) {
|
|
13069
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path27.resolve(lockPath)}`);
|
|
12681
13070
|
}
|
|
12682
|
-
const contentDir =
|
|
12683
|
-
const remotePath =
|
|
12684
|
-
const referenced =
|
|
12685
|
-
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
12686
|
-
);
|
|
13071
|
+
const contentDir = path27.resolve(path27.dirname(lockPath), ".pixelkiln", "cache");
|
|
13072
|
+
const remotePath = path27.resolve(cachePathFor(lockPath));
|
|
13073
|
+
const referenced = referencedHashes(lock);
|
|
12687
13074
|
let content = await inspectContentCache(contentDir, referenced);
|
|
12688
13075
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
12689
13076
|
const removed = { contentFiles: 0, remoteHashEntries: 0, resetRemoteHashCache: false };
|
|
@@ -12694,7 +13081,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12694
13081
|
]);
|
|
12695
13082
|
for (const name of names) {
|
|
12696
13083
|
try {
|
|
12697
|
-
await rm10(
|
|
13084
|
+
await rm10(path27.join(contentDir, name), { force: true });
|
|
12698
13085
|
removed.contentFiles++;
|
|
12699
13086
|
} catch {
|
|
12700
13087
|
}
|
|
@@ -12721,7 +13108,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12721
13108
|
async function inspectContentCache(contentDir, referenced) {
|
|
12722
13109
|
const report = {
|
|
12723
13110
|
path: contentDir,
|
|
12724
|
-
exists:
|
|
13111
|
+
exists: existsSync22(contentDir),
|
|
12725
13112
|
files: 0,
|
|
12726
13113
|
bytes: 0,
|
|
12727
13114
|
valid: 0,
|
|
@@ -12742,7 +13129,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12742
13129
|
continue;
|
|
12743
13130
|
}
|
|
12744
13131
|
report.files++;
|
|
12745
|
-
const file =
|
|
13132
|
+
const file = path27.join(contentDir, entry.name);
|
|
12746
13133
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
12747
13134
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
12748
13135
|
let bytes;
|
|
@@ -12785,7 +13172,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12785
13172
|
async function inspectRemoteHashCache(remotePath) {
|
|
12786
13173
|
const report = {
|
|
12787
13174
|
path: remotePath,
|
|
12788
|
-
exists:
|
|
13175
|
+
exists: existsSync22(remotePath),
|
|
12789
13176
|
entries: 0,
|
|
12790
13177
|
valid: 0,
|
|
12791
13178
|
invalidIds: [],
|
|
@@ -13069,7 +13456,7 @@ function isRecord2(value) {
|
|
|
13069
13456
|
|
|
13070
13457
|
// src/pick/salvage-server.ts
|
|
13071
13458
|
import { mkdir as mkdir8, writeFile as writeFile11, readFile as readFile22 } from "fs/promises";
|
|
13072
|
-
import
|
|
13459
|
+
import path28 from "path";
|
|
13073
13460
|
|
|
13074
13461
|
// src/pick/salvage-sheet.ts
|
|
13075
13462
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13234,7 +13621,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13234
13621
|
});
|
|
13235
13622
|
const html = renderSalvageSheet(orphans, {
|
|
13236
13623
|
styleId: ctx.styleId,
|
|
13237
|
-
importDir:
|
|
13624
|
+
importDir: path28.relative(process.cwd(), ctx.importDir) || "."
|
|
13238
13625
|
});
|
|
13239
13626
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13240
13627
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13263,9 +13650,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13263
13650
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13264
13651
|
decodePng(buf);
|
|
13265
13652
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13266
|
-
const rel =
|
|
13267
|
-
const outFile =
|
|
13268
|
-
await mkdir8(
|
|
13653
|
+
const rel = path28.join("_salvaged", `${assetId}.png`);
|
|
13654
|
+
const outFile = path28.resolve(ctx.importDir, rel);
|
|
13655
|
+
await mkdir8(path28.dirname(outFile), { recursive: true });
|
|
13269
13656
|
await writeFile11(outFile, buf);
|
|
13270
13657
|
ctx.manifest.assets[assetId] = {
|
|
13271
13658
|
prompt: orphan.prompt,
|
|
@@ -13298,7 +13685,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13298
13685
|
sourceUrl: durableSource,
|
|
13299
13686
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13300
13687
|
outputs: [{
|
|
13301
|
-
path: portableOutputPath(outFile,
|
|
13688
|
+
path: portableOutputPath(outFile, path28.dirname(ctx.manifestPath)),
|
|
13302
13689
|
sha256: sha256(buf)
|
|
13303
13690
|
}],
|
|
13304
13691
|
submittedAt: orphan.createdAt,
|
|
@@ -13332,8 +13719,8 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13332
13719
|
}
|
|
13333
13720
|
|
|
13334
13721
|
// src/recipes.ts
|
|
13335
|
-
import
|
|
13336
|
-
import { existsSync as
|
|
13722
|
+
import path29 from "path";
|
|
13723
|
+
import { existsSync as existsSync23 } from "fs";
|
|
13337
13724
|
import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
|
|
13338
13725
|
import { fileURLToPath } from "url";
|
|
13339
13726
|
import { z as z9 } from "zod";
|
|
@@ -13343,7 +13730,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
13343
13730
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
13344
13731
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
13345
13732
|
var PortablePathSchema = z9.string().min(1).refine((value) => {
|
|
13346
|
-
if (value.includes("\\") ||
|
|
13733
|
+
if (value.includes("\\") || path29.posix.isAbsolute(value)) return false;
|
|
13347
13734
|
const parts = value.split("/");
|
|
13348
13735
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
13349
13736
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -13519,7 +13906,7 @@ function message4(error) {
|
|
|
13519
13906
|
return error instanceof Error ? error.message : String(error);
|
|
13520
13907
|
}
|
|
13521
13908
|
async function readRecipeFile(recipePath, bundled) {
|
|
13522
|
-
const absolute =
|
|
13909
|
+
const absolute = path29.resolve(recipePath);
|
|
13523
13910
|
let raw;
|
|
13524
13911
|
try {
|
|
13525
13912
|
raw = JSON.parse(await readFile23(absolute, "utf8"));
|
|
@@ -13533,13 +13920,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
13533
13920
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
13534
13921
|
);
|
|
13535
13922
|
}
|
|
13536
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
13923
|
+
return { recipe: parsed.data, path: absolute, dir: path29.dirname(absolute), bundled };
|
|
13537
13924
|
}
|
|
13538
13925
|
async function walkRecipeFiles(root) {
|
|
13539
|
-
if (!
|
|
13926
|
+
if (!existsSync23(root)) return [];
|
|
13540
13927
|
const out = [];
|
|
13541
13928
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
13542
|
-
const resolved =
|
|
13929
|
+
const resolved = path29.join(root, entry.name);
|
|
13543
13930
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
13544
13931
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
13545
13932
|
}
|
|
@@ -13550,7 +13937,7 @@ function bundledRecipeRoot() {
|
|
|
13550
13937
|
}
|
|
13551
13938
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
13552
13939
|
const recipes = await Promise.all(
|
|
13553
|
-
(await walkRecipeFiles(
|
|
13940
|
+
(await walkRecipeFiles(path29.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
13554
13941
|
);
|
|
13555
13942
|
return recipes.sort(
|
|
13556
13943
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -13573,9 +13960,9 @@ function parseSelector(selector) {
|
|
|
13573
13960
|
return { id: match[1], version: match[2] };
|
|
13574
13961
|
}
|
|
13575
13962
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
13576
|
-
const local =
|
|
13577
|
-
if (
|
|
13578
|
-
const recipePath =
|
|
13963
|
+
const local = path29.resolve(target);
|
|
13964
|
+
if (existsSync23(local)) {
|
|
13965
|
+
const recipePath = path29.basename(local) === RECIPE_FILE ? local : path29.join(local, RECIPE_FILE);
|
|
13579
13966
|
return readRecipeFile(recipePath, false);
|
|
13580
13967
|
}
|
|
13581
13968
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -13589,8 +13976,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
13589
13976
|
return matches[0];
|
|
13590
13977
|
}
|
|
13591
13978
|
async function verifyFile(root, file) {
|
|
13592
|
-
const absolute =
|
|
13593
|
-
if (!
|
|
13979
|
+
const absolute = path29.resolve(root, ...file.path.split("/"));
|
|
13980
|
+
if (!existsSync23(absolute)) {
|
|
13594
13981
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
13595
13982
|
}
|
|
13596
13983
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -13605,7 +13992,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
13605
13992
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
13606
13993
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
13607
13994
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
13608
|
-
const modelRoot = options.modelRoot ?
|
|
13995
|
+
const modelRoot = options.modelRoot ? path29.resolve(options.modelRoot) : null;
|
|
13609
13996
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
13610
13997
|
...await verifyFile(modelRoot, model),
|
|
13611
13998
|
source: model.source,
|
|
@@ -13639,7 +14026,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
13639
14026
|
};
|
|
13640
14027
|
}
|
|
13641
14028
|
function renderStyle(style, destination, cwd) {
|
|
13642
|
-
const portableDir =
|
|
14029
|
+
const portableDir = path29.relative(cwd, destination).split(path29.sep).join("/") || ".";
|
|
13643
14030
|
const replace = (value) => {
|
|
13644
14031
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
13645
14032
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -13651,24 +14038,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
13651
14038
|
return replace(style);
|
|
13652
14039
|
}
|
|
13653
14040
|
async function installRecipe(target, options = {}) {
|
|
13654
|
-
const cwd =
|
|
14041
|
+
const cwd = path29.resolve(options.cwd ?? process.cwd());
|
|
13655
14042
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
13656
14043
|
const verification = await verifyRecipe(loaded.path);
|
|
13657
14044
|
if (!verification.ok) {
|
|
13658
14045
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
13659
14046
|
}
|
|
13660
|
-
const destination = options.out ?
|
|
14047
|
+
const destination = options.out ? path29.resolve(cwd, options.out) : path29.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
13661
14048
|
const sources = [
|
|
13662
14049
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
13663
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
14050
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path29.join(loaded.dir, ...file.path.split("/")) }))
|
|
13664
14051
|
];
|
|
13665
14052
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
13666
|
-
path:
|
|
14053
|
+
path: path29.join(destination, ...file.path.split("/")),
|
|
13667
14054
|
data: await readFile23(file.source)
|
|
13668
14055
|
})));
|
|
13669
14056
|
if (!options.force) {
|
|
13670
14057
|
for (const file of files) {
|
|
13671
|
-
if (!
|
|
14058
|
+
if (!existsSync23(file.path)) continue;
|
|
13672
14059
|
const current = await readFile23(file.path);
|
|
13673
14060
|
if (!current.equals(file.data)) {
|
|
13674
14061
|
throw new Error(
|
|
@@ -13692,15 +14079,15 @@ async function installRecipe(target, options = {}) {
|
|
|
13692
14079
|
}
|
|
13693
14080
|
|
|
13694
14081
|
// src/pipeline/quality-regression.ts
|
|
13695
|
-
import
|
|
13696
|
-
import { existsSync as
|
|
14082
|
+
import path30 from "path";
|
|
14083
|
+
import { existsSync as existsSync24 } from "fs";
|
|
13697
14084
|
import { readFile as readFile24 } from "fs/promises";
|
|
13698
14085
|
import { z as z10 } from "zod";
|
|
13699
14086
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
13700
14087
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
13701
14088
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
13702
14089
|
var QualityPathSchema = z10.string().min(1).refine(
|
|
13703
|
-
(value) => !
|
|
14090
|
+
(value) => !path30.posix.isAbsolute(value) && !value.includes("\\"),
|
|
13704
14091
|
"expected a portable relative path"
|
|
13705
14092
|
);
|
|
13706
14093
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -13847,7 +14234,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
13847
14234
|
};
|
|
13848
14235
|
}
|
|
13849
14236
|
async function measureImageQuality(file) {
|
|
13850
|
-
const absolute =
|
|
14237
|
+
const absolute = path30.resolve(file);
|
|
13851
14238
|
let bytes;
|
|
13852
14239
|
try {
|
|
13853
14240
|
bytes = await readFile24(absolute);
|
|
@@ -13869,11 +14256,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13869
14256
|
}
|
|
13870
14257
|
const ids = parsed.data.map((entry) => entry.id);
|
|
13871
14258
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
13872
|
-
const root =
|
|
14259
|
+
const root = path30.dirname(path30.resolve(inputsFilePath));
|
|
13873
14260
|
return parsed.data.map((entry) => ({
|
|
13874
14261
|
id: entry.id,
|
|
13875
|
-
file:
|
|
13876
|
-
...entry.record ? { record:
|
|
14262
|
+
file: path30.resolve(root, entry.path),
|
|
14263
|
+
...entry.record ? { record: path30.resolve(root, entry.record) } : {},
|
|
13877
14264
|
tolerances: QualityToleranceSchema.parse({
|
|
13878
14265
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
13879
14266
|
...entry.tolerances
|
|
@@ -13881,7 +14268,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13881
14268
|
}));
|
|
13882
14269
|
}
|
|
13883
14270
|
function portableRelative2(from, to) {
|
|
13884
|
-
return
|
|
14271
|
+
return path30.relative(from, path30.resolve(to)).split(path30.sep).join("/") || ".";
|
|
13885
14272
|
}
|
|
13886
14273
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
13887
14274
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -13896,20 +14283,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
13896
14283
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
13897
14284
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
13898
14285
|
}
|
|
13899
|
-
const outputs = record.outputs.map((output) =>
|
|
13900
|
-
if (!outputs.includes(
|
|
14286
|
+
const outputs = record.outputs.map((output) => path30.resolve(path30.dirname(recordPath), output.path));
|
|
14287
|
+
if (!outputs.includes(path30.resolve(imagePath))) {
|
|
13901
14288
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
13902
14289
|
}
|
|
13903
14290
|
}
|
|
13904
14291
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
13905
14292
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
13906
|
-
const absolute =
|
|
14293
|
+
const absolute = path30.resolve(baselinePath);
|
|
13907
14294
|
for (const input of inputs) {
|
|
13908
|
-
if (
|
|
14295
|
+
if (path30.resolve(input.file) === absolute || input.record && path30.resolve(input.record) === absolute) {
|
|
13909
14296
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
13910
14297
|
}
|
|
13911
14298
|
}
|
|
13912
|
-
const root =
|
|
14299
|
+
const root = path30.dirname(absolute);
|
|
13913
14300
|
const cases = [];
|
|
13914
14301
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
13915
14302
|
const expected = await measureImageQuality(input.file);
|
|
@@ -13936,7 +14323,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
13936
14323
|
cases
|
|
13937
14324
|
});
|
|
13938
14325
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
13939
|
-
if (
|
|
14326
|
+
if (existsSync24(absolute) && !options.force) {
|
|
13940
14327
|
const current = await readFile24(absolute);
|
|
13941
14328
|
if (!current.equals(data)) {
|
|
13942
14329
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
@@ -13946,7 +14333,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
13946
14333
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
13947
14334
|
}
|
|
13948
14335
|
async function readQualityBaseline(baselinePath) {
|
|
13949
|
-
const absolute =
|
|
14336
|
+
const absolute = path30.resolve(baselinePath);
|
|
13950
14337
|
let raw;
|
|
13951
14338
|
try {
|
|
13952
14339
|
raw = JSON.parse(await readFile24(absolute, "utf8"));
|
|
@@ -14019,12 +14406,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
14019
14406
|
return { violations, warnings };
|
|
14020
14407
|
}
|
|
14021
14408
|
async function checkQualityBaseline(baselinePath) {
|
|
14022
|
-
const absolute =
|
|
14409
|
+
const absolute = path30.resolve(baselinePath);
|
|
14023
14410
|
const baseline = await readQualityBaseline(absolute);
|
|
14024
|
-
const root =
|
|
14411
|
+
const root = path30.dirname(absolute);
|
|
14025
14412
|
const cases = [];
|
|
14026
14413
|
for (const entry of baseline.cases) {
|
|
14027
|
-
const file =
|
|
14414
|
+
const file = path30.resolve(root, entry.file);
|
|
14028
14415
|
let actual = null;
|
|
14029
14416
|
const violations = [];
|
|
14030
14417
|
const warnings = [];
|
|
@@ -14037,7 +14424,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
14037
14424
|
violations.push(message5(error));
|
|
14038
14425
|
}
|
|
14039
14426
|
if (entry.record) {
|
|
14040
|
-
const recordPath =
|
|
14427
|
+
const recordPath = path30.resolve(root, entry.record.path);
|
|
14041
14428
|
try {
|
|
14042
14429
|
const recordHash = await sha256File(recordPath);
|
|
14043
14430
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -14123,13 +14510,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
14123
14510
|
};
|
|
14124
14511
|
const loadProject = async (project) => {
|
|
14125
14512
|
const ctx = await access2.loadProject(project);
|
|
14126
|
-
const dir =
|
|
14513
|
+
const dir = path31.dirname(ctx.loaded.path);
|
|
14127
14514
|
const declared = readEnvFiles(dir);
|
|
14128
14515
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
14129
14516
|
for (const [name, value] of Object.entries(declared)) {
|
|
14130
14517
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
14131
14518
|
throw new Error(
|
|
14132
|
-
`${
|
|
14519
|
+
`${path31.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
14133
14520
|
);
|
|
14134
14521
|
}
|
|
14135
14522
|
}
|
|
@@ -14173,11 +14560,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
14173
14560
|
log("\n gallery closed");
|
|
14174
14561
|
}
|
|
14175
14562
|
async function provenanceFile(id, file) {
|
|
14176
|
-
const absolute =
|
|
14563
|
+
const absolute = path31.resolve(file);
|
|
14177
14564
|
return {
|
|
14178
14565
|
id,
|
|
14179
14566
|
path: absolute,
|
|
14180
|
-
sha256:
|
|
14567
|
+
sha256: existsSync25(absolute) ? await sha256File(absolute) : null,
|
|
14181
14568
|
included: true
|
|
14182
14569
|
};
|
|
14183
14570
|
}
|
|
@@ -14211,7 +14598,8 @@ var VALUE_FLAGS = [
|
|
|
14211
14598
|
"--min-grid-confidence",
|
|
14212
14599
|
"--reviewer",
|
|
14213
14600
|
"--note",
|
|
14214
|
-
"--model-root"
|
|
14601
|
+
"--model-root",
|
|
14602
|
+
"--generation"
|
|
14215
14603
|
];
|
|
14216
14604
|
var BOOL_FLAGS = [
|
|
14217
14605
|
"--force",
|
|
@@ -14256,6 +14644,7 @@ var COMMANDS = [
|
|
|
14256
14644
|
"gallery",
|
|
14257
14645
|
"edit",
|
|
14258
14646
|
"tools",
|
|
14647
|
+
"history",
|
|
14259
14648
|
"quality",
|
|
14260
14649
|
"refine",
|
|
14261
14650
|
"recipe",
|
|
@@ -14465,7 +14854,7 @@ function parseArgs(argv) {
|
|
|
14465
14854
|
return {
|
|
14466
14855
|
command,
|
|
14467
14856
|
manifest,
|
|
14468
|
-
lock: get("--lock") ??
|
|
14857
|
+
lock: get("--lock") ?? path31.join(path31.dirname(path31.resolve(manifest)), "pixelkiln.lock.json"),
|
|
14469
14858
|
explicitLock: get("--lock"),
|
|
14470
14859
|
styles: list("--style"),
|
|
14471
14860
|
assets: list("--only"),
|
|
@@ -14507,6 +14896,7 @@ function parseArgs(argv) {
|
|
|
14507
14896
|
reviewer: get("--reviewer"),
|
|
14508
14897
|
note: get("--note"),
|
|
14509
14898
|
modelRoot: get("--model-root"),
|
|
14899
|
+
generation: get("--generation"),
|
|
14510
14900
|
subcommand,
|
|
14511
14901
|
workspace: get("--workspace"),
|
|
14512
14902
|
target,
|
|
@@ -14566,7 +14956,9 @@ Commands
|
|
|
14566
14956
|
poll Advance in-flight jobs to their settled state.
|
|
14567
14957
|
pick Open the contact sheet to choose among candidates.
|
|
14568
14958
|
fetch Download selected objects to their manifest paths.
|
|
14569
|
-
restore Re-download missing generated files without generating new art
|
|
14959
|
+
restore Re-download missing generated files without generating new art;
|
|
14960
|
+
--generation <n|hash> brings a previous generation back instead.
|
|
14961
|
+
history List the generations each asset has replaced and can restore.
|
|
14570
14962
|
adopt Match existing account objects to files already in the repo.
|
|
14571
14963
|
accept Keep existing art after a style reword \u2014 re-baseline, do not regenerate.
|
|
14572
14964
|
salvage Triage account objects no lockfile claims. Recovers usable art.
|
|
@@ -14620,6 +15012,7 @@ Options
|
|
|
14620
15012
|
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
14621
15013
|
--note <text> refine approve: optional review note
|
|
14622
15014
|
--model-root <dir> recipe verify: also hash required local model files
|
|
15015
|
+
--generation <n|hash> restore: the previous generation to bring back (1 = newest)
|
|
14623
15016
|
--prune cache: remove invalid/unreferenced local cache data
|
|
14624
15017
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
14625
15018
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
@@ -14817,10 +15210,10 @@ async function confirm(question, auto) {
|
|
|
14817
15210
|
return answer === "y" || answer === "yes";
|
|
14818
15211
|
}
|
|
14819
15212
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
14820
|
-
if (!
|
|
15213
|
+
if (!existsSync25(workspacePath)) {
|
|
14821
15214
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14822
15215
|
}
|
|
14823
|
-
const dir =
|
|
15216
|
+
const dir = path31.dirname(path31.resolve(workspacePath));
|
|
14824
15217
|
const ws = await loadWorkspace(workspacePath);
|
|
14825
15218
|
const diagnostics = validateWorkspace(ws, dir);
|
|
14826
15219
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -14856,15 +15249,15 @@ async function main() {
|
|
|
14856
15249
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
14857
15250
|
let raw;
|
|
14858
15251
|
try {
|
|
14859
|
-
raw = JSON.parse(await readFile25(
|
|
15252
|
+
raw = JSON.parse(await readFile25(path31.resolve(args.inputs), "utf8"));
|
|
14860
15253
|
} catch (error) {
|
|
14861
15254
|
throw new Error(
|
|
14862
|
-
`Could not read quality inputs ${
|
|
15255
|
+
`Could not read quality inputs ${path31.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
14863
15256
|
{ cause: error }
|
|
14864
15257
|
);
|
|
14865
15258
|
}
|
|
14866
15259
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
14867
|
-
if (
|
|
15260
|
+
if (path31.resolve(args.inputs) === path31.resolve(args.out)) {
|
|
14868
15261
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
14869
15262
|
}
|
|
14870
15263
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -14877,7 +15270,7 @@ async function main() {
|
|
|
14877
15270
|
}, null, 2));
|
|
14878
15271
|
} else {
|
|
14879
15272
|
log(
|
|
14880
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
15273
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path31.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
14881
15274
|
);
|
|
14882
15275
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
14883
15276
|
}
|
|
@@ -14972,14 +15365,14 @@ async function main() {
|
|
|
14972
15365
|
}, null, 2));
|
|
14973
15366
|
} else {
|
|
14974
15367
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
14975
|
-
log(` ${
|
|
15368
|
+
log(` ${path31.relative(process.cwd(), result.destination) || "."}`);
|
|
14976
15369
|
log(`
|
|
14977
15370
|
Add this entry under your manifest's styles object:`);
|
|
14978
15371
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
14979
15372
|
if (result.recipe.models.length) {
|
|
14980
15373
|
log(`
|
|
14981
15374
|
Models are not downloaded automatically. Verify them with:`);
|
|
14982
|
-
log(` pixelkiln recipe verify ${
|
|
15375
|
+
log(` pixelkiln recipe verify ${path31.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
14983
15376
|
}
|
|
14984
15377
|
}
|
|
14985
15378
|
return;
|
|
@@ -15013,12 +15406,12 @@ async function main() {
|
|
|
15013
15406
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
15014
15407
|
);
|
|
15015
15408
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
15016
|
-
log(` wrote ${
|
|
15017
|
-
log(` quality record: ${
|
|
15409
|
+
log(` wrote ${path31.relative(process.cwd(), result2.output)}`);
|
|
15410
|
+
log(` quality record: ${path31.relative(process.cwd(), result2.record)}`);
|
|
15018
15411
|
log(`
|
|
15019
15412
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
15020
15413
|
log(
|
|
15021
|
-
` pixelkiln refine approve --from ${
|
|
15414
|
+
` pixelkiln refine approve --from ${path31.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
15022
15415
|
);
|
|
15023
15416
|
}
|
|
15024
15417
|
return;
|
|
@@ -15046,7 +15439,7 @@ async function main() {
|
|
|
15046
15439
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
15047
15440
|
else {
|
|
15048
15441
|
log(
|
|
15049
|
-
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${
|
|
15442
|
+
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path31.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path31.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
|
|
15050
15443
|
);
|
|
15051
15444
|
}
|
|
15052
15445
|
return;
|
|
@@ -15055,14 +15448,14 @@ async function main() {
|
|
|
15055
15448
|
if (args.json) {
|
|
15056
15449
|
log(JSON.stringify(result, null, 2));
|
|
15057
15450
|
} else {
|
|
15058
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
15451
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path31.relative(process.cwd(), result.output || result.record)}`);
|
|
15059
15452
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
15060
15453
|
}
|
|
15061
15454
|
if (!result.safe) process.exitCode = 1;
|
|
15062
15455
|
return;
|
|
15063
15456
|
}
|
|
15064
15457
|
if (args.command === "balance") {
|
|
15065
|
-
loadEnvFiles(
|
|
15458
|
+
loadEnvFiles(path31.dirname(path31.resolve(args.manifest)));
|
|
15066
15459
|
loadEnvFiles(process.cwd());
|
|
15067
15460
|
const loaded2 = await loadManifest(args.manifest);
|
|
15068
15461
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -15075,31 +15468,31 @@ async function main() {
|
|
|
15075
15468
|
}
|
|
15076
15469
|
if (args.command === "init") {
|
|
15077
15470
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
15078
|
-
const root =
|
|
15079
|
-
if (!
|
|
15471
|
+
const root = path31.resolve(args.from);
|
|
15472
|
+
if (!existsSync25(root)) throw new Error(`No directory at ${root}`);
|
|
15080
15473
|
const generator = args.generator ?? "map";
|
|
15081
15474
|
if (generator !== "1dir" && generator !== "map") {
|
|
15082
15475
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
15083
15476
|
}
|
|
15084
|
-
const target =
|
|
15477
|
+
const target = path31.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
15085
15478
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
15086
15479
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
15087
15480
|
const manifest = buildManifest(
|
|
15088
|
-
args.name ??
|
|
15481
|
+
args.name ?? path31.basename(path31.dirname(target)),
|
|
15089
15482
|
args.styles[0] ?? "base",
|
|
15090
15483
|
generator,
|
|
15091
|
-
|
|
15484
|
+
path31.relative(path31.dirname(target), root) || ".",
|
|
15092
15485
|
assets
|
|
15093
15486
|
);
|
|
15094
15487
|
await writeManifestFile(target, manifest);
|
|
15095
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
15488
|
+
log(` scanned ${assets.length} PNG(s) under ${path31.relative(process.cwd(), root)}`);
|
|
15096
15489
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
15097
|
-
log(` wrote ${
|
|
15490
|
+
log(` wrote ${path31.relative(process.cwd(), target)}`);
|
|
15098
15491
|
log(`
|
|
15099
15492
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
15100
15493
|
log(` PixelLab account instead of inventing them:`);
|
|
15101
15494
|
log(`
|
|
15102
|
-
pixelkiln adopt --manifest ${
|
|
15495
|
+
pixelkiln adopt --manifest ${path31.relative(process.cwd(), target)} --write-prompts
|
|
15103
15496
|
`);
|
|
15104
15497
|
return;
|
|
15105
15498
|
}
|
|
@@ -15147,10 +15540,10 @@ async function main() {
|
|
|
15147
15540
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
15148
15541
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
15149
15542
|
}
|
|
15150
|
-
const raw = JSON.parse(await readFile25(
|
|
15543
|
+
const raw = JSON.parse(await readFile25(path31.resolve(args.inputs), "utf8"));
|
|
15151
15544
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
15152
15545
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
15153
|
-
const base =
|
|
15546
|
+
const base = path31.resolve(args.out.replace(/\.png$/, ""));
|
|
15154
15547
|
const outputs = [
|
|
15155
15548
|
{ path: `${base}.png`, data: png },
|
|
15156
15549
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -15163,17 +15556,17 @@ async function main() {
|
|
|
15163
15556
|
log(
|
|
15164
15557
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
15165
15558
|
);
|
|
15166
|
-
log(` ${
|
|
15559
|
+
log(` ${path31.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
15167
15560
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15168
15561
|
return;
|
|
15169
15562
|
}
|
|
15170
15563
|
if (args.command === "workspace") {
|
|
15171
|
-
const workspacePath =
|
|
15172
|
-
const dir =
|
|
15564
|
+
const workspacePath = path31.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
15565
|
+
const dir = path31.dirname(workspacePath);
|
|
15173
15566
|
if (args.subcommand === "add") {
|
|
15174
|
-
const manifestPath =
|
|
15567
|
+
const manifestPath = path31.resolve(args.target);
|
|
15175
15568
|
const loadedTarget = await loadManifest(manifestPath);
|
|
15176
|
-
const lockPath = args.explicitLock ?
|
|
15569
|
+
const lockPath = args.explicitLock ? path31.resolve(args.explicitLock) : path31.join(path31.dirname(manifestPath), "pixelkiln.lock.json");
|
|
15177
15570
|
const ws = await loadWorkspace(workspacePath);
|
|
15178
15571
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
15179
15572
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -15193,10 +15586,10 @@ async function main() {
|
|
|
15193
15586
|
...args.account ? { account: args.account } : {}
|
|
15194
15587
|
};
|
|
15195
15588
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
15196
|
-
log(` registered "${id}" in ${
|
|
15589
|
+
log(` registered "${id}" in ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15197
15590
|
log(` manifest: ${project.manifest}`);
|
|
15198
15591
|
log(` lock: ${project.lock}`);
|
|
15199
|
-
if (!
|
|
15592
|
+
if (!existsSync25(lockPath)) {
|
|
15200
15593
|
log(
|
|
15201
15594
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
15202
15595
|
);
|
|
@@ -15205,7 +15598,7 @@ async function main() {
|
|
|
15205
15598
|
}
|
|
15206
15599
|
if (args.subcommand === "remove") {
|
|
15207
15600
|
const ws = await loadWorkspace(workspacePath);
|
|
15208
|
-
const resolvedTarget =
|
|
15601
|
+
const resolvedTarget = path31.resolve(args.target);
|
|
15209
15602
|
const match = ws.projects.find(
|
|
15210
15603
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
15211
15604
|
);
|
|
@@ -15216,11 +15609,11 @@ async function main() {
|
|
|
15216
15609
|
version: 1,
|
|
15217
15610
|
projects: ws.projects.filter((p) => p !== match)
|
|
15218
15611
|
});
|
|
15219
|
-
log(` removed "${match.id}" from ${
|
|
15612
|
+
log(` removed "${match.id}" from ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15220
15613
|
return;
|
|
15221
15614
|
}
|
|
15222
15615
|
if (args.subcommand === "list") {
|
|
15223
|
-
if (!
|
|
15616
|
+
if (!existsSync25(workspacePath)) {
|
|
15224
15617
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15225
15618
|
}
|
|
15226
15619
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -15228,9 +15621,9 @@ async function main() {
|
|
|
15228
15621
|
if (args.json) {
|
|
15229
15622
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
15230
15623
|
} else if (!ws.projects.length) {
|
|
15231
|
-
log(` no projects registered in ${
|
|
15624
|
+
log(` no projects registered in ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15232
15625
|
} else {
|
|
15233
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
15626
|
+
log(` ${ws.projects.length} project(s) in ${path31.relative(process.cwd(), workspacePath)}:`);
|
|
15234
15627
|
for (const p of ws.projects) {
|
|
15235
15628
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
15236
15629
|
}
|
|
@@ -15240,7 +15633,7 @@ async function main() {
|
|
|
15240
15633
|
return;
|
|
15241
15634
|
}
|
|
15242
15635
|
if (args.subcommand === "status") {
|
|
15243
|
-
if (!
|
|
15636
|
+
if (!existsSync25(workspacePath)) {
|
|
15244
15637
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15245
15638
|
}
|
|
15246
15639
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -15248,7 +15641,7 @@ async function main() {
|
|
|
15248
15641
|
if (args.json) {
|
|
15249
15642
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
15250
15643
|
} else {
|
|
15251
|
-
log(` workspace: ${
|
|
15644
|
+
log(` workspace: ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15252
15645
|
for (const p of report.projects) {
|
|
15253
15646
|
if (p.error) {
|
|
15254
15647
|
log(`
|
|
@@ -15295,8 +15688,8 @@ async function main() {
|
|
|
15295
15688
|
}
|
|
15296
15689
|
}
|
|
15297
15690
|
if (args.command === "gallery" && args.workspace) {
|
|
15298
|
-
const workspacePath =
|
|
15299
|
-
if (!
|
|
15691
|
+
const workspacePath = path31.resolve(args.workspace);
|
|
15692
|
+
if (!existsSync25(workspacePath)) {
|
|
15300
15693
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15301
15694
|
}
|
|
15302
15695
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -15316,7 +15709,7 @@ async function main() {
|
|
|
15316
15709
|
const registered = async (projectId) => {
|
|
15317
15710
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
15318
15711
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
15319
|
-
return resolveProject(
|
|
15712
|
+
return resolveProject(path31.dirname(workspacePath), project);
|
|
15320
15713
|
};
|
|
15321
15714
|
await runGallery(initial, build, args, {
|
|
15322
15715
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -15331,14 +15724,14 @@ async function main() {
|
|
|
15331
15724
|
});
|
|
15332
15725
|
return;
|
|
15333
15726
|
}
|
|
15334
|
-
if (!
|
|
15727
|
+
if (!existsSync25(path31.resolve(args.manifest))) {
|
|
15335
15728
|
throw new Error(
|
|
15336
|
-
`No manifest at ${
|
|
15729
|
+
`No manifest at ${path31.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
15337
15730
|
);
|
|
15338
15731
|
}
|
|
15339
|
-
const manifestDir =
|
|
15732
|
+
const manifestDir = path31.dirname(path31.resolve(args.manifest));
|
|
15340
15733
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
15341
|
-
if (
|
|
15734
|
+
if (path31.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
15342
15735
|
const loaded = await loadManifest(args.manifest);
|
|
15343
15736
|
let specs = await resolveSpecs(loaded, {
|
|
15344
15737
|
styles: args.styles,
|
|
@@ -15370,6 +15763,40 @@ async function main() {
|
|
|
15370
15763
|
}
|
|
15371
15764
|
const lock = await loadLock(args.lock);
|
|
15372
15765
|
normalizeLockOutputPaths(lock, specs);
|
|
15766
|
+
if (args.command === "history") {
|
|
15767
|
+
const limit = historyLimit(loaded.manifest);
|
|
15768
|
+
const selected = specs.map((spec) => ({ spec, entry: lock.entries[lockKey(spec.styleId, spec.assetId)] }));
|
|
15769
|
+
if (args.json) {
|
|
15770
|
+
log(JSON.stringify({
|
|
15771
|
+
version: 1,
|
|
15772
|
+
limit,
|
|
15773
|
+
assets: selected.map(({ spec, entry }) => ({
|
|
15774
|
+
key: lockKey(spec.styleId, spec.assetId),
|
|
15775
|
+
current: entry ? { objectId: entry.objectId, outputs: entry.outputs, downloadedAt: entry.downloadedAt, cost: entry.cost, costUnit: entry.costUnit } : null,
|
|
15776
|
+
history: entry?.history ?? []
|
|
15777
|
+
}))
|
|
15778
|
+
}, null, 2));
|
|
15779
|
+
return;
|
|
15780
|
+
}
|
|
15781
|
+
log(` keeping up to ${limit} replaced generation${limit === 1 ? "" : "s"} per asset` + (loaded.manifest.history !== void 0 ? " (manifest history)" : ` (${process.env.PIXELKILN_HISTORY ? "PIXELKILN_HISTORY" : "default; set PIXELKILN_HISTORY or manifest history"})`));
|
|
15782
|
+
let shown = 0;
|
|
15783
|
+
for (const { spec, entry } of selected) {
|
|
15784
|
+
if (!entry?.history?.length) continue;
|
|
15785
|
+
shown++;
|
|
15786
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
15787
|
+
log(`
|
|
15788
|
+
${key}`);
|
|
15789
|
+
log(` current ${entry.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${entry.downloadedAt ?? ""} ${entry.objectId ?? ""}`);
|
|
15790
|
+
for (const [i, generation] of entry.history.entries()) {
|
|
15791
|
+
const changed = generation.prompt !== entry.prompt ? " (different prompt)" : "";
|
|
15792
|
+
log(` #${String(i + 1).padEnd(2)} ${generation.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${generation.downloadedAt ?? ""} ${generation.objectId ?? ""}${changed}`);
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
if (!shown) log(` no asset has a previous generation recorded${selected.length ? "" : " (nothing selected)"}`);
|
|
15796
|
+
else log(`
|
|
15797
|
+
bring one back: pixelkiln restore --only <asset> --style <style> --generation <n|hash>`);
|
|
15798
|
+
return;
|
|
15799
|
+
}
|
|
15373
15800
|
if (args.command === "status") {
|
|
15374
15801
|
const byStatus = {};
|
|
15375
15802
|
for (const e of Object.values(lock.entries)) byStatus[e.status] = (byStatus[e.status] ?? 0) + 1;
|
|
@@ -15411,7 +15838,7 @@ async function main() {
|
|
|
15411
15838
|
});
|
|
15412
15839
|
};
|
|
15413
15840
|
await runGallery(await build(), reload, args, {
|
|
15414
|
-
manifestFor: () =>
|
|
15841
|
+
manifestFor: () => path31.resolve(args.manifest),
|
|
15415
15842
|
loadProject: async () => {
|
|
15416
15843
|
const freshLoaded = await loadManifest(args.manifest);
|
|
15417
15844
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -15439,7 +15866,7 @@ async function main() {
|
|
|
15439
15866
|
const set = started.members.length > 1;
|
|
15440
15867
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
15441
15868
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
15442
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
15869
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path31.relative(process.cwd(), member.path)}`);
|
|
15443
15870
|
if (args.noOpen) {
|
|
15444
15871
|
log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
|
|
15445
15872
|
} else {
|
|
@@ -15489,7 +15916,7 @@ async function main() {
|
|
|
15489
15916
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
15490
15917
|
if (item.state === "needs-approval") {
|
|
15491
15918
|
log(
|
|
15492
|
-
` pixelkiln refine approve --from ${
|
|
15919
|
+
` pixelkiln refine approve --from ${path31.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
15493
15920
|
);
|
|
15494
15921
|
}
|
|
15495
15922
|
}
|
|
@@ -15580,7 +16007,7 @@ async function main() {
|
|
|
15580
16007
|
let intact = true;
|
|
15581
16008
|
for (const output of entry.outputs) {
|
|
15582
16009
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
15583
|
-
if (!
|
|
16010
|
+
if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
|
|
15584
16011
|
intact = false;
|
|
15585
16012
|
break;
|
|
15586
16013
|
}
|
|
@@ -15638,7 +16065,7 @@ async function main() {
|
|
|
15638
16065
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
15639
16066
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
15640
16067
|
}
|
|
15641
|
-
const manifestDir2 =
|
|
16068
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15642
16069
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
15643
16070
|
for (const styleId of styleIds) {
|
|
15644
16071
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -15651,7 +16078,7 @@ async function main() {
|
|
|
15651
16078
|
sources: manifestSources(loaded.manifest, styleId)
|
|
15652
16079
|
});
|
|
15653
16080
|
const style = loaded.manifest.styles[styleId];
|
|
15654
|
-
const base = args.out ?
|
|
16081
|
+
const base = args.out ? path31.resolve(args.out.replace(/\.png$/, "")) : path31.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
15655
16082
|
const outputs = [
|
|
15656
16083
|
{ path: `${base}.png`, data: png },
|
|
15657
16084
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -15681,13 +16108,13 @@ async function main() {
|
|
|
15681
16108
|
log(
|
|
15682
16109
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
15683
16110
|
);
|
|
15684
|
-
log(` ${
|
|
16111
|
+
log(` ${path31.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
15685
16112
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15686
16113
|
}
|
|
15687
16114
|
return;
|
|
15688
16115
|
}
|
|
15689
16116
|
if (args.command === "mount") {
|
|
15690
|
-
const manifestDir2 =
|
|
16117
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15691
16118
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
15692
16119
|
for (const styleId of styleIds) {
|
|
15693
16120
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -15726,7 +16153,7 @@ async function main() {
|
|
|
15726
16153
|
sources,
|
|
15727
16154
|
outputRoles
|
|
15728
16155
|
);
|
|
15729
|
-
const out =
|
|
16156
|
+
const out = path31.resolve(manifestDir2, style.mount.out);
|
|
15730
16157
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
15731
16158
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
15732
16159
|
const outputs = [
|
|
@@ -15746,7 +16173,7 @@ async function main() {
|
|
|
15746
16173
|
await provenanceFile("$lock", args.lock),
|
|
15747
16174
|
...qualityRecords,
|
|
15748
16175
|
...artifactSources.filter(
|
|
15749
|
-
(source) => source.id !== "$base" ||
|
|
16176
|
+
(source) => source.id !== "$base" || path31.resolve(source.path) !== out
|
|
15750
16177
|
)
|
|
15751
16178
|
],
|
|
15752
16179
|
options: {
|
|
@@ -15759,7 +16186,7 @@ async function main() {
|
|
|
15759
16186
|
log(
|
|
15760
16187
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
15761
16188
|
);
|
|
15762
|
-
log(` ${
|
|
16189
|
+
log(` ${path31.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
15763
16190
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15764
16191
|
}
|
|
15765
16192
|
return;
|
|
@@ -15815,7 +16242,7 @@ async function main() {
|
|
|
15815
16242
|
}
|
|
15816
16243
|
if (args.command === "export") {
|
|
15817
16244
|
const format = args.format ?? "generic";
|
|
15818
|
-
const manifestDir2 =
|
|
16245
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15819
16246
|
const selected = specs.filter((spec) => {
|
|
15820
16247
|
if (spec.generator !== "tiles") return false;
|
|
15821
16248
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -15831,12 +16258,12 @@ async function main() {
|
|
|
15831
16258
|
for (const spec of selected) {
|
|
15832
16259
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
15833
16260
|
const style = loaded.manifest.styles[spec.styleId];
|
|
15834
|
-
const defaultBase =
|
|
15835
|
-
const base = args.out ?
|
|
16261
|
+
const defaultBase = path31.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
16262
|
+
const base = args.out ? path31.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
15836
16263
|
const result = exportTileset(entry, spec, {
|
|
15837
16264
|
format,
|
|
15838
16265
|
manifestDir: manifestDir2,
|
|
15839
|
-
imageName:
|
|
16266
|
+
imageName: path31.basename(`${base}.png`),
|
|
15840
16267
|
columns: args.columns
|
|
15841
16268
|
});
|
|
15842
16269
|
const outputs = [
|
|
@@ -15854,7 +16281,7 @@ async function main() {
|
|
|
15854
16281
|
asset: spec.assetId,
|
|
15855
16282
|
columns: args.columns ?? null,
|
|
15856
16283
|
format,
|
|
15857
|
-
image:
|
|
16284
|
+
image: path31.basename(`${base}.png`),
|
|
15858
16285
|
providerRules: result.generic.providerRules,
|
|
15859
16286
|
style: spec.styleId,
|
|
15860
16287
|
tileType: spec.tileType ?? null
|
|
@@ -15864,12 +16291,32 @@ async function main() {
|
|
|
15864
16291
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
15865
16292
|
);
|
|
15866
16293
|
log(
|
|
15867
|
-
` ${
|
|
16294
|
+
` ${path31.relative(process.cwd(), base)}.png + ${path31.basename(base)}${result.extension} + .pixelkiln.json`
|
|
15868
16295
|
);
|
|
15869
16296
|
}
|
|
15870
16297
|
return;
|
|
15871
16298
|
}
|
|
15872
16299
|
const providerMode = args.command === "restore" || args.command === "fetch" && !args.tag ? "downloads" : "online";
|
|
16300
|
+
if (args.command === "restore" && args.generation !== void 0) {
|
|
16301
|
+
if (specs.length !== 1) {
|
|
16302
|
+
throw new Error(
|
|
16303
|
+
specs.length ? `restore --generation works on one asset; --only/--style select ${specs.length}. Add --style or narrow --only.` : "restore --generation matched no asset; pass --only <asset id> (and --style when it is in several styles)."
|
|
16304
|
+
);
|
|
16305
|
+
}
|
|
16306
|
+
const spec = specs[0];
|
|
16307
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
16308
|
+
if (!entry) throw new Error(`${spec.styleId}/${spec.assetId} is not in the lockfile`);
|
|
16309
|
+
const provider = createProvider(entry.provider, "downloads");
|
|
16310
|
+
const result = await revertGeneration(provider, spec, lock, args.lock, {
|
|
16311
|
+
generation: args.generation,
|
|
16312
|
+
force: args.force,
|
|
16313
|
+
onProgress: log
|
|
16314
|
+
});
|
|
16315
|
+
log(`
|
|
16316
|
+
restored generation #${result.index} of ${spec.styleId}/${spec.assetId}${result.restored.objectId ? ` (${result.restored.objectId})` : ""}; the replaced one is now #1 in its history`);
|
|
16317
|
+
log(` lockfile written: ${args.lock}`);
|
|
16318
|
+
return;
|
|
16319
|
+
}
|
|
15873
16320
|
const providers = /* @__PURE__ */ new Map();
|
|
15874
16321
|
const providerFor = (id) => {
|
|
15875
16322
|
let resolved = providers.get(id);
|
|
@@ -15914,12 +16361,12 @@ async function main() {
|
|
|
15914
16361
|
tagged ${n} object(s) upstream`);
|
|
15915
16362
|
}
|
|
15916
16363
|
if (args.writePrompts) {
|
|
15917
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
16364
|
+
const { filled, stillEmpty } = await writePromptsBack(path31.resolve(args.manifest), lock, {
|
|
15918
16365
|
onProgress: log,
|
|
15919
16366
|
provider: provider.id,
|
|
15920
16367
|
assetIds: specs.map((spec) => spec.assetId)
|
|
15921
16368
|
});
|
|
15922
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
16369
|
+
log(` recovered ${filled} prompt(s) into ${path31.relative(process.cwd(), args.manifest)}`);
|
|
15923
16370
|
const reloaded = await loadManifest(args.manifest);
|
|
15924
16371
|
const rebased = (await resolveSpecs(reloaded, {
|
|
15925
16372
|
styles: args.styles,
|
|
@@ -15948,12 +16395,12 @@ async function main() {
|
|
|
15948
16395
|
const provider = providerFor(selectedAccountProvider);
|
|
15949
16396
|
const jsonMode = args.dryRun && args.json;
|
|
15950
16397
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
15951
|
-
const ownLock =
|
|
16398
|
+
const ownLock = path31.resolve(args.lock);
|
|
15952
16399
|
let workspaceProjects = [];
|
|
15953
16400
|
let workspaceDir = "";
|
|
15954
16401
|
if (args.workspace) {
|
|
15955
|
-
const workspacePath =
|
|
15956
|
-
workspaceDir =
|
|
16402
|
+
const workspacePath = path31.resolve(args.workspace);
|
|
16403
|
+
workspaceDir = path31.dirname(workspacePath);
|
|
15957
16404
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
15958
16405
|
workspaceProjects = complete.ws.projects;
|
|
15959
16406
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -15962,12 +16409,12 @@ async function main() {
|
|
|
15962
16409
|
const lockPaths = [
|
|
15963
16410
|
.../* @__PURE__ */ new Set([
|
|
15964
16411
|
...workspaceLockPaths,
|
|
15965
|
-
...
|
|
15966
|
-
...args.claims.map((c) =>
|
|
16412
|
+
...existsSync25(ownLock) ? [ownLock] : [],
|
|
16413
|
+
...args.claims.map((c) => path31.resolve(c))
|
|
15967
16414
|
])
|
|
15968
16415
|
];
|
|
15969
16416
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
15970
|
-
for (const p of lockPaths) diag(` ${
|
|
16417
|
+
for (const p of lockPaths) diag(` ${path31.relative(process.cwd(), p)}`);
|
|
15971
16418
|
if (!args.claims.length && !args.workspace) {
|
|
15972
16419
|
diag(
|
|
15973
16420
|
`
|
|
@@ -15975,7 +16422,7 @@ async function main() {
|
|
|
15975
16422
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
15976
16423
|
register every project in a workspace catalog and pass --workspace.`
|
|
15977
16424
|
);
|
|
15978
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
16425
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path31.resolve(args.manifest))) {
|
|
15979
16426
|
diag(
|
|
15980
16427
|
`
|
|
15981
16428
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -16059,7 +16506,7 @@ async function main() {
|
|
|
16059
16506
|
manifestPath: loaded.path,
|
|
16060
16507
|
manifest: accountManifest,
|
|
16061
16508
|
styleId,
|
|
16062
|
-
importDir:
|
|
16509
|
+
importDir: path31.resolve(loaded.root, style.outDir),
|
|
16063
16510
|
lock,
|
|
16064
16511
|
lockPath: args.lock
|
|
16065
16512
|
},
|