pixelkiln 0.33.0 → 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 +995 -622
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +783 -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 +777 -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),
|
|
@@ -2629,9 +2669,9 @@ var RetroDiffusionClient = class {
|
|
|
2629
2669
|
}
|
|
2630
2670
|
return balance;
|
|
2631
2671
|
}
|
|
2632
|
-
async call(
|
|
2672
|
+
async call(path32, init = {}) {
|
|
2633
2673
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2634
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2674
|
+
const response = await this.request(`${this.baseUrl}${path32}`, {
|
|
2635
2675
|
...init,
|
|
2636
2676
|
headers: {
|
|
2637
2677
|
"Content-Type": "application/json",
|
|
@@ -6175,313 +6215,54 @@ function summarize(plan) {
|
|
|
6175
6215
|
return counts;
|
|
6176
6216
|
}
|
|
6177
6217
|
|
|
6178
|
-
// src/pipeline/
|
|
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
|
-
}
|
|
6206
|
-
const styleImages = /* @__PURE__ */ new Map();
|
|
6207
|
-
for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
|
|
6208
|
-
styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
|
|
6209
|
-
}
|
|
6210
|
-
let submitted = 0;
|
|
6211
|
-
let failed = 0;
|
|
6212
|
-
let spent = 0;
|
|
6213
|
-
let lastSubmitAt = 0;
|
|
6214
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
6215
|
-
let lastSlotError = null;
|
|
6216
|
-
async function pruneInFlight() {
|
|
6217
|
-
for (const [id, spec] of [...inFlight]) {
|
|
6218
|
-
try {
|
|
6219
|
-
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
6220
|
-
if (state.status !== "processing") inFlight.delete(id);
|
|
6221
|
-
lastSlotError = null;
|
|
6222
|
-
} catch (err) {
|
|
6223
|
-
lastSlotError = err instanceof Error ? err.message : String(err);
|
|
6224
|
-
}
|
|
6225
|
-
}
|
|
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;
|
|
6226
6245
|
}
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
throw new Error(
|
|
6232
|
-
`Timed out waiting for a generation slot` + (lastSlotError ? `; last poll error: ${lastSlotError}` : "")
|
|
6233
|
-
);
|
|
6234
|
-
}
|
|
6235
|
-
await sleep2(slotPollMs);
|
|
6236
|
-
await pruneInFlight();
|
|
6237
|
-
}
|
|
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;
|
|
6238
6250
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
6248
|
-
);
|
|
6249
|
-
const previousJobId = resumesCheckpoint ? previousEntry.jobId : void 0;
|
|
6250
|
-
const previousMetadata = resumesCheckpoint ? previousEntry.providerMetadata[provider.id] : void 0;
|
|
6251
|
-
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
6252
|
-
upsert(lock, key, {
|
|
6253
|
-
styleId: spec.styleId,
|
|
6254
|
-
assetId: spec.assetId,
|
|
6255
|
-
specHash: spec.specHash,
|
|
6256
|
-
generator: spec.generator,
|
|
6257
|
-
tileFeature: spec.tileFeature ?? null,
|
|
6258
|
-
prompt: spec.prompt,
|
|
6259
|
-
width: spec.width,
|
|
6260
|
-
height: spec.height,
|
|
6261
|
-
revision: spec.revision ? {
|
|
6262
|
-
mode: spec.revision.mode,
|
|
6263
|
-
sourceAssetId: spec.revision.sourceAssetId,
|
|
6264
|
-
sourceSha256: spec.revision.sourceSha256,
|
|
6265
|
-
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
|
|
6266
|
-
...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
|
|
6267
|
-
} : null,
|
|
6268
|
-
status: "pending",
|
|
6269
|
-
jobId: previousJobId ?? null,
|
|
6270
|
-
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
6271
|
-
objectId: null,
|
|
6272
|
-
reviewObjectId: null,
|
|
6273
|
-
candidateIndex: null,
|
|
6274
|
-
error: null,
|
|
6275
|
-
outputs: [],
|
|
6276
|
-
// Keep the old ownership proof while new bytes are pending. Fetch may
|
|
6277
|
-
// replace that file only while its hash still matches this record.
|
|
6278
|
-
supersededOutputs,
|
|
6279
|
-
providerMetadata: resumesCheckpoint ? previousEntry.providerMetadata : {},
|
|
6280
|
-
sourceUrl: null,
|
|
6281
|
-
sourceUrls: [],
|
|
6282
|
-
submittedAt: resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
6283
|
-
cost: estimate.amount,
|
|
6284
|
-
costUnit: estimate.unit,
|
|
6285
|
-
// Persist routing before the request starts. If the process stops while
|
|
6286
|
-
// the provider is accepting work, a resumed run must not fall back to
|
|
6287
|
-
// PixelLab (the legacy lock default) for another provider's entry.
|
|
6288
|
-
provider: provider.id,
|
|
6289
|
-
downloadedAt: null
|
|
6290
|
-
});
|
|
6291
|
-
await saveLock(lockPath, lock);
|
|
6292
|
-
lastSubmitAt = Date.now();
|
|
6293
|
-
try {
|
|
6294
|
-
const refs = styleImages.get(spec.styleId) ?? [];
|
|
6295
|
-
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
6296
|
-
...previousJobId ? { previousJobId } : {},
|
|
6297
|
-
...previousMetadata ? { previousMetadata } : {},
|
|
6298
|
-
checkpoint: async (checkpoint) => {
|
|
6299
|
-
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
6300
|
-
const entry = lock.entries[key];
|
|
6301
|
-
upsert(lock, key, {
|
|
6302
|
-
jobId: checkpoint.jobId,
|
|
6303
|
-
submissionComplete: checkpoint.complete,
|
|
6304
|
-
reviewObjectId: checkpoint.complete && (spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature) ? checkpoint.jobId : null,
|
|
6305
|
-
status: checkpoint.complete ? "processing" : "pending",
|
|
6306
|
-
error: null,
|
|
6307
|
-
providerMetadata: checkpoint.metadata ? {
|
|
6308
|
-
...entry.providerMetadata,
|
|
6309
|
-
[provider.id]: {
|
|
6310
|
-
...entry.providerMetadata[provider.id],
|
|
6311
|
-
...checkpoint.metadata
|
|
6312
|
-
}
|
|
6313
|
-
} : entry.providerMetadata
|
|
6314
|
-
});
|
|
6315
|
-
await saveLock(lockPath, lock);
|
|
6316
|
-
}
|
|
6317
|
-
});
|
|
6318
|
-
upsert(lock, key, {
|
|
6319
|
-
jobId,
|
|
6320
|
-
submissionComplete: true,
|
|
6321
|
-
// A multi-candidate generator routes through review; record the parent
|
|
6322
|
-
// so `pick` knows where to look.
|
|
6323
|
-
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6324
|
-
status: "processing",
|
|
6325
|
-
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
6326
|
-
});
|
|
6327
|
-
inFlight.set(jobId, spec);
|
|
6328
|
-
submitted++;
|
|
6329
|
-
spent += estimate.amount;
|
|
6330
|
-
log2(
|
|
6331
|
-
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
6332
|
-
);
|
|
6333
|
-
} catch (err) {
|
|
6334
|
-
const message6 = err instanceof Error ? err.message : String(err);
|
|
6335
|
-
const entry = lock.entries[key];
|
|
6336
|
-
if (entry.jobId && entry.submissionComplete === true) {
|
|
6337
|
-
upsert(lock, key, { status: "processing", error: null });
|
|
6338
|
-
inFlight.set(entry.jobId, spec);
|
|
6339
|
-
submitted++;
|
|
6340
|
-
spent += estimate.amount;
|
|
6341
|
-
log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
6342
|
-
} else {
|
|
6343
|
-
failed++;
|
|
6344
|
-
upsert(lock, key, {
|
|
6345
|
-
status: "failed",
|
|
6346
|
-
error: message6,
|
|
6347
|
-
cost: entry.jobId ? estimate.amount : 0
|
|
6348
|
-
});
|
|
6349
|
-
log2(` FAILED ${key}: ${message6}`);
|
|
6350
|
-
}
|
|
6351
|
-
}
|
|
6352
|
-
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;
|
|
6353
6259
|
}
|
|
6354
|
-
|
|
6260
|
+
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
6261
|
+
return !isSensitiveSourceUrl(value);
|
|
6355
6262
|
}
|
|
6356
6263
|
|
|
6357
|
-
// src/pipeline/
|
|
6358
|
-
|
|
6359
|
-
async function poll(provider, lock, lockPath, opts = {}) {
|
|
6360
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
6361
|
-
const timeout = opts.timeoutMs ?? 15 * 60 * 1e3;
|
|
6362
|
-
const log2 = opts.onProgress ?? (() => {
|
|
6363
|
-
});
|
|
6364
|
-
const started = Date.now();
|
|
6365
|
-
const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6366
|
-
const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
|
|
6367
|
-
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
6368
|
-
const incompleteSubmissions = Object.entries(lock.entries).filter(
|
|
6369
|
-
([key, entry]) => entry.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && entry.jobId && entry.submissionComplete === false && (entry.status === "pending" || entry.status === "processing")
|
|
6370
|
-
);
|
|
6371
|
-
for (const [key] of incompleteSubmissions) {
|
|
6372
|
-
log2(` incomplete submission ${key}; rerun \`pixelkiln submit\` to resume`);
|
|
6373
|
-
}
|
|
6374
|
-
result.stillRunning = incompleteSubmissions.length;
|
|
6375
|
-
const pending = () => Object.entries(lock.entries).filter(
|
|
6376
|
-
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && e.submissionComplete !== false && (e.status === "pending" || e.status === "processing")
|
|
6377
|
-
);
|
|
6378
|
-
while (pending().length > 0) {
|
|
6379
|
-
if (Date.now() - started > timeout) {
|
|
6380
|
-
result.stillRunning += pending().length;
|
|
6381
|
-
log2(` timed out with ${result.stillRunning} job(s) still running \u2014 re-run \`poll\` to resume`);
|
|
6382
|
-
break;
|
|
6383
|
-
}
|
|
6384
|
-
for (const [key, entry] of pending()) {
|
|
6385
|
-
try {
|
|
6386
|
-
const currentSpec = specByKey.get(key);
|
|
6387
|
-
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
6388
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
6389
|
-
spec: currentSpec
|
|
6390
|
-
});
|
|
6391
|
-
if (state.status === "review" || state.status === "review-set") {
|
|
6392
|
-
upsert(lock, key, {
|
|
6393
|
-
status: "review",
|
|
6394
|
-
reviewObjectId: entry.jobId,
|
|
6395
|
-
providerMetadata: state.metadata ? {
|
|
6396
|
-
...entry.providerMetadata,
|
|
6397
|
-
[provider.id]: {
|
|
6398
|
-
...entry.providerMetadata[provider.id],
|
|
6399
|
-
...state.metadata
|
|
6400
|
-
}
|
|
6401
|
-
} : entry.providerMetadata
|
|
6402
|
-
});
|
|
6403
|
-
result.review++;
|
|
6404
|
-
log2(
|
|
6405
|
-
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6406
|
-
);
|
|
6407
|
-
} else if (state.status === "ready") {
|
|
6408
|
-
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
6409
|
-
upsert(lock, key, {
|
|
6410
|
-
status: "selected",
|
|
6411
|
-
objectId: state.objectId,
|
|
6412
|
-
sourceUrl: sourceUrls[0]?.url ?? null,
|
|
6413
|
-
sourceUrls,
|
|
6414
|
-
providerMetadata: state.metadata ? {
|
|
6415
|
-
...entry.providerMetadata,
|
|
6416
|
-
[provider.id]: {
|
|
6417
|
-
...entry.providerMetadata[provider.id],
|
|
6418
|
-
...state.metadata
|
|
6419
|
-
}
|
|
6420
|
-
} : entry.providerMetadata,
|
|
6421
|
-
error: null
|
|
6422
|
-
});
|
|
6423
|
-
result.completed++;
|
|
6424
|
-
log2(` ready ${key}`);
|
|
6425
|
-
} else if (state.status === "failed") {
|
|
6426
|
-
upsert(lock, key, { status: "failed", error: state.error });
|
|
6427
|
-
result.failed++;
|
|
6428
|
-
log2(` FAILED ${key}: ${state.error}`);
|
|
6429
|
-
}
|
|
6430
|
-
} catch (err) {
|
|
6431
|
-
log2(` error polling ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6432
|
-
}
|
|
6433
|
-
}
|
|
6434
|
-
await saveLock(lockPath, lock);
|
|
6435
|
-
if (pending().length > 0) await sleep3(interval);
|
|
6436
|
-
}
|
|
6437
|
-
await saveLock(lockPath, lock);
|
|
6438
|
-
return result;
|
|
6439
|
-
}
|
|
6440
|
-
|
|
6441
|
-
// src/pipeline/fetch.ts
|
|
6442
|
-
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
6443
|
-
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
6444
|
-
import path14 from "path";
|
|
6445
|
-
|
|
6446
|
-
// src/source-url.ts
|
|
6447
|
-
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
6448
|
-
"access_token",
|
|
6449
|
-
"apikey",
|
|
6450
|
-
"api_key",
|
|
6451
|
-
"auth",
|
|
6452
|
-
"authorization",
|
|
6453
|
-
"awsaccesskeyid",
|
|
6454
|
-
"googleaccessid",
|
|
6455
|
-
"key",
|
|
6456
|
-
"sig"
|
|
6457
|
-
]);
|
|
6458
|
-
function isSensitiveSourceUrl(value) {
|
|
6459
|
-
let parsed;
|
|
6460
|
-
try {
|
|
6461
|
-
parsed = new URL(value);
|
|
6462
|
-
} catch {
|
|
6463
|
-
return false;
|
|
6464
|
-
}
|
|
6465
|
-
if (parsed.username || parsed.password) return true;
|
|
6466
|
-
for (const key of parsed.searchParams.keys()) {
|
|
6467
|
-
const normalized = key.toLowerCase();
|
|
6468
|
-
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
6469
|
-
}
|
|
6470
|
-
return false;
|
|
6471
|
-
}
|
|
6472
|
-
function shouldPersistSourceUrl(value) {
|
|
6473
|
-
let parsed;
|
|
6474
|
-
try {
|
|
6475
|
-
parsed = new URL(value);
|
|
6476
|
-
} catch {
|
|
6477
|
-
return true;
|
|
6478
|
-
}
|
|
6479
|
-
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
6480
|
-
return !isSensitiveSourceUrl(value);
|
|
6481
|
-
}
|
|
6482
|
-
|
|
6483
|
-
// src/pipeline/fetch.ts
|
|
6484
|
-
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
6264
|
+
// src/pipeline/fetch.ts
|
|
6265
|
+
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
6485
6266
|
const log2 = opts.onProgress ?? (() => {
|
|
6486
6267
|
});
|
|
6487
6268
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
@@ -6499,7 +6280,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6499
6280
|
return e.outputs.some((output) => {
|
|
6500
6281
|
const file = resolveOutputPath(output.path, spec.root);
|
|
6501
6282
|
if (!existsSync10(file)) return true;
|
|
6502
|
-
|
|
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);
|
|
6503
6286
|
});
|
|
6504
6287
|
});
|
|
6505
6288
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
@@ -6569,7 +6352,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6569
6352
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
6570
6353
|
);
|
|
6571
6354
|
if (!opts.force) {
|
|
6572
|
-
if (
|
|
6355
|
+
if (superseded && currentHash === superseded.sha256) {
|
|
6573
6356
|
} else if (recorded || superseded) {
|
|
6574
6357
|
throw new Error(
|
|
6575
6358
|
`refusing to overwrite modified output ${target}; pass --force to replace it`
|
|
@@ -6643,79 +6426,476 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6643
6426
|
} catch (err) {
|
|
6644
6427
|
const message6 = err instanceof Error ? err.message : String(err);
|
|
6645
6428
|
upsert(lock, key, {
|
|
6646
|
-
status: "download-failed",
|
|
6647
|
-
outputs: mergeOutputs(entry.outputs, outputs),
|
|
6648
|
-
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
|
|
6649
6802
|
});
|
|
6650
|
-
|
|
6651
|
-
log2(` FAILED ${key}: ${message6}`);
|
|
6803
|
+
log2(` FAILED ${key}: ${message6}`);
|
|
6652
6804
|
}
|
|
6653
|
-
await saveLock(lockPath, lock);
|
|
6654
6805
|
}
|
|
6806
|
+
await saveLock(lockPath, lock);
|
|
6655
6807
|
}
|
|
6656
|
-
|
|
6657
|
-
await saveLock(lockPath, lock);
|
|
6658
|
-
return result;
|
|
6659
|
-
}
|
|
6660
|
-
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
6661
|
-
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6662
|
-
if (!existsSync10(file)) return null;
|
|
6663
|
-
try {
|
|
6664
|
-
const buf = await readFile7(file);
|
|
6665
|
-
if (sha256(buf) !== hash) return null;
|
|
6666
|
-
validateMedia(buf, mediaType);
|
|
6667
|
-
return buf;
|
|
6668
|
-
} catch {
|
|
6669
|
-
return null;
|
|
6670
|
-
}
|
|
6671
|
-
}
|
|
6672
|
-
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
6673
|
-
const hash = knownHash ?? sha256(buf);
|
|
6674
|
-
validateMedia(buf, mediaType);
|
|
6675
|
-
const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
|
|
6676
|
-
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
6677
|
-
await mkdir3(cacheDir, { recursive: true });
|
|
6678
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6679
|
-
await writeFile3(tmp, buf);
|
|
6680
|
-
try {
|
|
6681
|
-
await rename3(tmp, file);
|
|
6682
|
-
} catch (err) {
|
|
6683
|
-
if (!existsSync10(file)) throw err;
|
|
6684
|
-
} finally {
|
|
6685
|
-
await rm4(tmp, { force: true });
|
|
6686
|
-
}
|
|
6687
|
-
}
|
|
6688
|
-
function mergeOutputs(previous, current) {
|
|
6689
|
-
const merged = /* @__PURE__ */ new Map();
|
|
6690
|
-
for (const output of [...previous, ...current]) {
|
|
6691
|
-
merged.set(output.role ? `role:${output.role}` : `path:${output.path}`, output);
|
|
6692
|
-
}
|
|
6693
|
-
return [...merged.values()];
|
|
6808
|
+
return { submitted, failed, spent, unit };
|
|
6694
6809
|
}
|
|
6695
|
-
|
|
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;
|
|
6696
6816
|
const log2 = opts.onProgress ?? (() => {
|
|
6697
6817
|
});
|
|
6698
|
-
const
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
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
|
+
}
|
|
6710
6887
|
}
|
|
6888
|
+
await saveLock(lockPath, lock);
|
|
6889
|
+
if (pending().length > 0) await sleep3(interval);
|
|
6711
6890
|
}
|
|
6712
|
-
|
|
6891
|
+
await saveLock(lockPath, lock);
|
|
6892
|
+
return result;
|
|
6713
6893
|
}
|
|
6714
6894
|
|
|
6715
6895
|
// src/pipeline/doctor.ts
|
|
6716
|
-
import { constants, existsSync as
|
|
6896
|
+
import { constants, existsSync as existsSync12 } from "fs";
|
|
6717
6897
|
import { access, stat as stat2 } from "fs/promises";
|
|
6718
|
-
import
|
|
6898
|
+
import path16 from "path";
|
|
6719
6899
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
6720
6900
|
const checks = [];
|
|
6721
6901
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -6726,8 +6906,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6726
6906
|
);
|
|
6727
6907
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
6728
6908
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
6729
|
-
|
|
6730
|
-
...spec.quality ? [
|
|
6909
|
+
path16.dirname(spec.outFile),
|
|
6910
|
+
...spec.quality ? [path16.dirname(spec.quality.outFile)] : []
|
|
6731
6911
|
]))];
|
|
6732
6912
|
const unwritable = [];
|
|
6733
6913
|
for (const dir of dirs) {
|
|
@@ -6750,7 +6930,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6750
6930
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
6751
6931
|
const output = entry.outputs[index];
|
|
6752
6932
|
const spec = specByKey.get(key);
|
|
6753
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
6933
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path16.dirname(lockPath));
|
|
6754
6934
|
const owner = outputOwners.get(absolute);
|
|
6755
6935
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
6756
6936
|
outputOwners.set(absolute, key);
|
|
@@ -6765,7 +6945,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6765
6945
|
} else {
|
|
6766
6946
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
6767
6947
|
}
|
|
6768
|
-
const cacheDir =
|
|
6948
|
+
const cacheDir = path16.join(path16.dirname(lockPath), ".pixelkiln", "cache");
|
|
6769
6949
|
const stranded = [];
|
|
6770
6950
|
const invalidState = [];
|
|
6771
6951
|
const staleMapUrls = [];
|
|
@@ -6781,11 +6961,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
6781
6961
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
6782
6962
|
let hasCache = false;
|
|
6783
6963
|
for (const output of entry.outputs) {
|
|
6784
|
-
const cached =
|
|
6964
|
+
const cached = path16.join(
|
|
6785
6965
|
cacheDir,
|
|
6786
6966
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
6787
6967
|
);
|
|
6788
|
-
if (
|
|
6968
|
+
if (existsSync12(cached) && await sha256File(cached) === output.sha256) {
|
|
6789
6969
|
hasCache = true;
|
|
6790
6970
|
break;
|
|
6791
6971
|
}
|
|
@@ -6881,20 +7061,20 @@ async function nearestExistingDirectory(start) {
|
|
|
6881
7061
|
if ((await stat2(current)).isDirectory()) return current;
|
|
6882
7062
|
} catch {
|
|
6883
7063
|
}
|
|
6884
|
-
const parent =
|
|
7064
|
+
const parent = path16.dirname(current);
|
|
6885
7065
|
if (parent === current) return current;
|
|
6886
7066
|
current = parent;
|
|
6887
7067
|
}
|
|
6888
7068
|
}
|
|
6889
7069
|
|
|
6890
7070
|
// src/pipeline/adopt.ts
|
|
6891
|
-
import { existsSync as
|
|
7071
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6892
7072
|
import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
6893
|
-
import
|
|
7073
|
+
import path17 from "path";
|
|
6894
7074
|
|
|
6895
7075
|
// src/cache.ts
|
|
6896
7076
|
import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
6897
|
-
import { existsSync as
|
|
7077
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6898
7078
|
import pathModule from "path";
|
|
6899
7079
|
import { z as z3 } from "zod";
|
|
6900
7080
|
var HashCacheSchema = z3.object({
|
|
@@ -6909,10 +7089,10 @@ function isSha256Hash(value) {
|
|
|
6909
7089
|
function parseCache(value) {
|
|
6910
7090
|
return HashCacheSchema.parse(value);
|
|
6911
7091
|
}
|
|
6912
|
-
async function loadCache(
|
|
6913
|
-
if (!
|
|
7092
|
+
async function loadCache(path32) {
|
|
7093
|
+
if (!existsSync13(path32)) return { version: 1, hashes: {} };
|
|
6914
7094
|
try {
|
|
6915
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
7095
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path32, "utf8")));
|
|
6916
7096
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6917
7097
|
return {
|
|
6918
7098
|
version: 1,
|
|
@@ -6924,18 +7104,18 @@ async function loadCache(path31) {
|
|
|
6924
7104
|
return { version: 1, hashes: {} };
|
|
6925
7105
|
}
|
|
6926
7106
|
}
|
|
6927
|
-
async function saveCache(
|
|
7107
|
+
async function saveCache(path32, cache) {
|
|
6928
7108
|
const sorted = {};
|
|
6929
7109
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6930
7110
|
const hash = cache.hashes[key];
|
|
6931
7111
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6932
7112
|
sorted[key] = hash;
|
|
6933
7113
|
}
|
|
6934
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6935
|
-
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`;
|
|
6936
7116
|
try {
|
|
6937
7117
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6938
|
-
await rename4(tmp,
|
|
7118
|
+
await rename4(tmp, path32);
|
|
6939
7119
|
} finally {
|
|
6940
7120
|
await rm5(tmp, { force: true });
|
|
6941
7121
|
}
|
|
@@ -6966,7 +7146,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6966
7146
|
const localByHash = /* @__PURE__ */ new Map();
|
|
6967
7147
|
const unmatchedLocal = [];
|
|
6968
7148
|
for (const spec of specs) {
|
|
6969
|
-
if (!
|
|
7149
|
+
if (!existsSync14(spec.outFile)) {
|
|
6970
7150
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
6971
7151
|
continue;
|
|
6972
7152
|
}
|
|
@@ -7123,13 +7303,13 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
7123
7303
|
}
|
|
7124
7304
|
|
|
7125
7305
|
// src/pick/server.ts
|
|
7126
|
-
import { existsSync as
|
|
7306
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7127
7307
|
import { readFile as readFile12 } from "fs/promises";
|
|
7128
7308
|
|
|
7129
7309
|
// src/pipeline/init.ts
|
|
7130
7310
|
import { readdir, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
7131
|
-
import { existsSync as
|
|
7132
|
-
import
|
|
7311
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7312
|
+
import path18 from "path";
|
|
7133
7313
|
function pngSize(buf) {
|
|
7134
7314
|
if (buf.length < 24) return null;
|
|
7135
7315
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -7143,7 +7323,7 @@ function slugify(value) {
|
|
|
7143
7323
|
async function walk(dir, exclude) {
|
|
7144
7324
|
const out = [];
|
|
7145
7325
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
7146
|
-
const full =
|
|
7326
|
+
const full = path18.join(dir, entry.name);
|
|
7147
7327
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
7148
7328
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
7149
7329
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -7162,9 +7342,9 @@ async function scanAssets(root, opts = {}) {
|
|
|
7162
7342
|
skipped.push(`${file} (not a readable PNG)`);
|
|
7163
7343
|
continue;
|
|
7164
7344
|
}
|
|
7165
|
-
const rel =
|
|
7166
|
-
const category =
|
|
7167
|
-
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));
|
|
7168
7348
|
if (seen.has(id)) {
|
|
7169
7349
|
const n = seen.get(id) + 1;
|
|
7170
7350
|
seen.set(id, n);
|
|
@@ -7221,7 +7401,7 @@ function mode(values) {
|
|
|
7221
7401
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
7222
7402
|
}
|
|
7223
7403
|
async function writeManifestFile(target, manifest) {
|
|
7224
|
-
if (
|
|
7404
|
+
if (existsSync15(target)) {
|
|
7225
7405
|
throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
|
|
7226
7406
|
}
|
|
7227
7407
|
await writeFile6(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
@@ -7707,7 +7887,7 @@ async function currentArt(entry, spec) {
|
|
|
7707
7887
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
7708
7888
|
if (!previous) return null;
|
|
7709
7889
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
7710
|
-
if (!
|
|
7890
|
+
if (!existsSync16(file)) return null;
|
|
7711
7891
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
7712
7892
|
if (!contentType) return null;
|
|
7713
7893
|
let width = entry.width;
|
|
@@ -7878,14 +8058,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
7878
8058
|
|
|
7879
8059
|
// src/gallery/snapshot.ts
|
|
7880
8060
|
import { createHash as createHash3 } from "crypto";
|
|
7881
|
-
import { existsSync as
|
|
8061
|
+
import { existsSync as existsSync19 } from "fs";
|
|
7882
8062
|
import { readFile as readFile16, stat as stat3 } from "fs/promises";
|
|
7883
|
-
import
|
|
8063
|
+
import path23 from "path";
|
|
7884
8064
|
|
|
7885
8065
|
// src/workspace.ts
|
|
7886
8066
|
import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7887
|
-
import { existsSync as
|
|
7888
|
-
import
|
|
8067
|
+
import { existsSync as existsSync17 } from "fs";
|
|
8068
|
+
import path19 from "path";
|
|
7889
8069
|
import { z as z4 } from "zod";
|
|
7890
8070
|
var WorkspaceProjectSchema = z4.object({
|
|
7891
8071
|
id: z4.string().min(1),
|
|
@@ -7910,7 +8090,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
7910
8090
|
);
|
|
7911
8091
|
}
|
|
7912
8092
|
async function loadWorkspace(workspacePath) {
|
|
7913
|
-
if (!
|
|
8093
|
+
if (!existsSync17(workspacePath)) return { version: 1, projects: [] };
|
|
7914
8094
|
let raw;
|
|
7915
8095
|
try {
|
|
7916
8096
|
raw = JSON.parse(await readFile13(workspacePath, "utf8"));
|
|
@@ -7927,7 +8107,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7927
8107
|
version: 1,
|
|
7928
8108
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
7929
8109
|
};
|
|
7930
|
-
await mkdir5(
|
|
8110
|
+
await mkdir5(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
|
|
7931
8111
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7932
8112
|
try {
|
|
7933
8113
|
await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -7937,12 +8117,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
7937
8117
|
}
|
|
7938
8118
|
}
|
|
7939
8119
|
function toPortablePath(dir, absolute) {
|
|
7940
|
-
return
|
|
8120
|
+
return path19.relative(dir, absolute).split(path19.sep).join("/");
|
|
7941
8121
|
}
|
|
7942
8122
|
function resolveProject(dir, project) {
|
|
7943
8123
|
return {
|
|
7944
|
-
manifestPath:
|
|
7945
|
-
lockPath:
|
|
8124
|
+
manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
|
|
8125
|
+
lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
|
|
7946
8126
|
};
|
|
7947
8127
|
}
|
|
7948
8128
|
function validateWorkspace(ws, dir) {
|
|
@@ -7955,21 +8135,21 @@ function validateWorkspace(ws, dir) {
|
|
|
7955
8135
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
7956
8136
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
7957
8137
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
7958
|
-
if (
|
|
8138
|
+
if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
|
|
7959
8139
|
diagnostics.push({
|
|
7960
8140
|
id: "absolute-path",
|
|
7961
8141
|
level: "warning",
|
|
7962
8142
|
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
7963
8143
|
});
|
|
7964
8144
|
}
|
|
7965
|
-
if (!
|
|
8145
|
+
if (!existsSync17(manifestPath)) {
|
|
7966
8146
|
diagnostics.push({
|
|
7967
8147
|
id: "missing-manifest",
|
|
7968
8148
|
level: "error",
|
|
7969
8149
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
7970
8150
|
});
|
|
7971
8151
|
}
|
|
7972
|
-
if (!
|
|
8152
|
+
if (!existsSync17(lockPath)) {
|
|
7973
8153
|
diagnostics.push({
|
|
7974
8154
|
id: "missing-lock",
|
|
7975
8155
|
level: "error",
|
|
@@ -8016,12 +8196,12 @@ function validateWorkspace(ws, dir) {
|
|
|
8016
8196
|
}
|
|
8017
8197
|
|
|
8018
8198
|
// src/gallery/edit.ts
|
|
8019
|
-
import
|
|
8199
|
+
import path22 from "path";
|
|
8020
8200
|
import { z as z7 } from "zod";
|
|
8021
8201
|
|
|
8022
8202
|
// src/manifest-edit.ts
|
|
8023
8203
|
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
8024
|
-
import
|
|
8204
|
+
import path20 from "path";
|
|
8025
8205
|
import { z as z5 } from "zod";
|
|
8026
8206
|
var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
8027
8207
|
var AssetPatchSchema = z5.object({
|
|
@@ -8233,7 +8413,7 @@ function applyEdit(raw, edit) {
|
|
|
8233
8413
|
}
|
|
8234
8414
|
}
|
|
8235
8415
|
async function applyManifestEdit(manifestPath, edit) {
|
|
8236
|
-
const absolute =
|
|
8416
|
+
const absolute = path20.resolve(manifestPath);
|
|
8237
8417
|
const original = await readFile14(absolute, "utf8");
|
|
8238
8418
|
const before = sha256(original);
|
|
8239
8419
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
@@ -8248,9 +8428,9 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8248
8428
|
applyEdit(raw, edit);
|
|
8249
8429
|
const next = serializeLike(original, raw);
|
|
8250
8430
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
8251
|
-
const tmp =
|
|
8252
|
-
|
|
8253
|
-
`.${
|
|
8431
|
+
const tmp = path20.join(
|
|
8432
|
+
path20.dirname(absolute),
|
|
8433
|
+
`.${path20.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
8254
8434
|
);
|
|
8255
8435
|
try {
|
|
8256
8436
|
await writeFile8(tmp, next);
|
|
@@ -8270,18 +8450,18 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8270
8450
|
|
|
8271
8451
|
// src/pipeline/hand-edit.ts
|
|
8272
8452
|
import { spawn as spawn2 } from "child_process";
|
|
8273
|
-
import { existsSync as
|
|
8453
|
+
import { existsSync as existsSync18 } from "fs";
|
|
8274
8454
|
import { copyFile, mkdir as mkdir6, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
|
|
8275
|
-
import
|
|
8455
|
+
import path21 from "path";
|
|
8276
8456
|
import { z as z6 } from "zod";
|
|
8277
8457
|
var HAND_EDIT_DIR = "edits";
|
|
8278
8458
|
function handEditPath(loaded, spec) {
|
|
8279
8459
|
const style = loaded.manifest.styles[spec.styleId];
|
|
8280
|
-
const outDir =
|
|
8281
|
-
const relative =
|
|
8282
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
8283
|
-
const file = inside ? relative :
|
|
8284
|
-
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"));
|
|
8285
8465
|
}
|
|
8286
8466
|
function handEditBases(spec, lock) {
|
|
8287
8467
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8294,10 +8474,10 @@ function handEditBases(spec, lock) {
|
|
|
8294
8474
|
}));
|
|
8295
8475
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
8296
8476
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
8297
|
-
if (!members.every((member) =>
|
|
8477
|
+
if (!members.every((member) => existsSync18(member.path))) return null;
|
|
8298
8478
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
8299
8479
|
}
|
|
8300
|
-
return
|
|
8480
|
+
return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
8301
8481
|
}
|
|
8302
8482
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
8303
8483
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -8323,18 +8503,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
8323
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`
|
|
8324
8504
|
);
|
|
8325
8505
|
}
|
|
8326
|
-
const existing = spec.source ?
|
|
8506
|
+
const existing = spec.source ? path21.resolve(loaded.root, spec.source) : null;
|
|
8327
8507
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8328
8508
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
8329
8509
|
let created = false;
|
|
8330
8510
|
for (const member of members) {
|
|
8331
|
-
if (
|
|
8511
|
+
if (existsSync18(member.path)) continue;
|
|
8332
8512
|
if (!member.base) {
|
|
8333
8513
|
throw new ManifestEditError(
|
|
8334
8514
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8335
8515
|
);
|
|
8336
8516
|
}
|
|
8337
|
-
await mkdir6(
|
|
8517
|
+
await mkdir6(path21.dirname(member.path), { recursive: true });
|
|
8338
8518
|
await copyFile(member.base, member.path);
|
|
8339
8519
|
created = true;
|
|
8340
8520
|
}
|
|
@@ -8472,7 +8652,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
8472
8652
|
editor: input.editor,
|
|
8473
8653
|
protocol: input.protocol,
|
|
8474
8654
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
8475
|
-
project: projectPath ?
|
|
8655
|
+
project: projectPath ? path21.basename(projectPath) : null,
|
|
8476
8656
|
outputs: incoming.map((frame, index) => ({
|
|
8477
8657
|
role: frame.role,
|
|
8478
8658
|
basedOn: basedOnHash(entry, index),
|
|
@@ -8607,7 +8787,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8607
8787
|
}
|
|
8608
8788
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8609
8789
|
if (result.changed) {
|
|
8610
|
-
const relative =
|
|
8790
|
+
const relative = path22.relative(process.cwd(), result.manifestPath);
|
|
8611
8791
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8612
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;
|
|
8613
8793
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -8619,7 +8799,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8619
8799
|
// src/gallery/snapshot.ts
|
|
8620
8800
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
8621
8801
|
function galleryMediaId(absolutePath) {
|
|
8622
|
-
return createHash3("sha256").update(
|
|
8802
|
+
return createHash3("sha256").update(path23.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8623
8803
|
}
|
|
8624
8804
|
function galleryMediaRoute(id) {
|
|
8625
8805
|
return `/media/${id}`;
|
|
@@ -8634,6 +8814,38 @@ function matchesFilter(filter, styleId, assetId) {
|
|
|
8634
8814
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
|
|
8635
8815
|
return true;
|
|
8636
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
|
+
}
|
|
8637
8849
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
8638
8850
|
async function samePixels(a, b) {
|
|
8639
8851
|
try {
|
|
@@ -8690,10 +8902,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
8690
8902
|
async function describeQuality(media, root, inspection) {
|
|
8691
8903
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
8692
8904
|
const outputs = await Promise.all(
|
|
8693
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
8905
|
+
outputPaths.map((file) => describeOutput(media, root, path23.resolve(root, file)))
|
|
8694
8906
|
);
|
|
8695
|
-
const recordPath =
|
|
8696
|
-
const recordExists =
|
|
8907
|
+
const recordPath = path23.resolve(root, inspection.record);
|
|
8908
|
+
const recordExists = existsSync19(recordPath);
|
|
8697
8909
|
let options = null;
|
|
8698
8910
|
let check = null;
|
|
8699
8911
|
if (recordExists) {
|
|
@@ -8712,7 +8924,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
8712
8924
|
return {
|
|
8713
8925
|
state: inspection.state,
|
|
8714
8926
|
reason: inspection.reason,
|
|
8715
|
-
output: portableOutputPath(
|
|
8927
|
+
output: portableOutputPath(path23.resolve(root, inspection.output), root),
|
|
8716
8928
|
record: portableOutputPath(recordPath, root),
|
|
8717
8929
|
recordExists,
|
|
8718
8930
|
outputs,
|
|
@@ -8751,6 +8963,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8751
8963
|
const items = [];
|
|
8752
8964
|
const manifestText = await readFile16(loaded.path, "utf8");
|
|
8753
8965
|
const manifestSha256 = sha256(manifestText);
|
|
8966
|
+
const cacheDir = path23.resolve(path23.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
8754
8967
|
let rawStyles = {};
|
|
8755
8968
|
try {
|
|
8756
8969
|
const raw = JSON.parse(manifestText);
|
|
@@ -8774,12 +8987,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
8774
8987
|
)
|
|
8775
8988
|
);
|
|
8776
8989
|
if (spec.source) {
|
|
8777
|
-
const editPath =
|
|
8990
|
+
const editPath = path23.resolve(root, spec.source);
|
|
8778
8991
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
8779
8992
|
const editShas = await Promise.all(
|
|
8780
8993
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
8781
8994
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
8782
|
-
return { file, sha256:
|
|
8995
|
+
return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
|
|
8783
8996
|
})
|
|
8784
8997
|
);
|
|
8785
8998
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -8787,7 +9000,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8787
9000
|
const companion = await readHandEditCompanion(editPath);
|
|
8788
9001
|
if (companion) {
|
|
8789
9002
|
const projectPath = handEditProjectPath(editPath);
|
|
8790
|
-
const project = companion.project &&
|
|
9003
|
+
const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
|
|
8791
9004
|
let projectUrl = null;
|
|
8792
9005
|
if (project) {
|
|
8793
9006
|
const id = galleryMediaId(projectPath);
|
|
@@ -8823,8 +9036,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
8823
9036
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
8824
9037
|
}
|
|
8825
9038
|
} else if (spec.source) {
|
|
8826
|
-
outputs = [await describeOutput(media, root,
|
|
8827
|
-
} else if (!entry &&
|
|
9039
|
+
outputs = [await describeOutput(media, root, path23.resolve(root, spec.source))];
|
|
9040
|
+
} else if (!entry && existsSync19(spec.outFile)) {
|
|
8828
9041
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
8829
9042
|
} else {
|
|
8830
9043
|
outputs = [];
|
|
@@ -8875,6 +9088,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8875
9088
|
editMeta,
|
|
8876
9089
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8877
9090
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
9091
|
+
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
8878
9092
|
tags: spec.tags,
|
|
8879
9093
|
category: asset?.category ?? null
|
|
8880
9094
|
});
|
|
@@ -8934,6 +9148,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8934
9148
|
editMeta: null,
|
|
8935
9149
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8936
9150
|
refreshable: false,
|
|
9151
|
+
history: describeHistory(media, entry, cacheDir),
|
|
8937
9152
|
tags: [],
|
|
8938
9153
|
category: null
|
|
8939
9154
|
});
|
|
@@ -9001,11 +9216,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
9001
9216
|
id: loaded.manifest.name,
|
|
9002
9217
|
name: loaded.manifest.name,
|
|
9003
9218
|
manifest: loaded.path,
|
|
9004
|
-
lock:
|
|
9219
|
+
lock: path23.resolve(opts.lockPath),
|
|
9005
9220
|
root,
|
|
9006
9221
|
provider: loaded.manifest.provider,
|
|
9007
9222
|
account: null,
|
|
9008
9223
|
manifestSha256,
|
|
9224
|
+
historyLimit: historyLimit(loaded.manifest),
|
|
9009
9225
|
entries: Object.keys(lock.entries).length,
|
|
9010
9226
|
items: items.length,
|
|
9011
9227
|
spendByUnit: spendByUnit(lock),
|
|
@@ -9026,7 +9242,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
9026
9242
|
return { snapshot, media };
|
|
9027
9243
|
}
|
|
9028
9244
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
9029
|
-
const dir =
|
|
9245
|
+
const dir = path23.dirname(path23.resolve(opts.workspacePath));
|
|
9030
9246
|
const media = /* @__PURE__ */ new Map();
|
|
9031
9247
|
const projects = [];
|
|
9032
9248
|
const items = [];
|
|
@@ -9069,11 +9285,12 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9069
9285
|
id: project.id,
|
|
9070
9286
|
name: loaded.manifest.name,
|
|
9071
9287
|
manifest: manifestPath,
|
|
9072
|
-
lock:
|
|
9288
|
+
lock: path23.resolve(lockPath),
|
|
9073
9289
|
root: loaded.root,
|
|
9074
9290
|
provider: loaded.manifest.provider,
|
|
9075
9291
|
account: project.account ?? null,
|
|
9076
9292
|
manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
|
|
9293
|
+
historyLimit: historyLimit(loaded.manifest),
|
|
9077
9294
|
entries: Object.keys(lock.entries).length,
|
|
9078
9295
|
items: projectItems.length,
|
|
9079
9296
|
spendByUnit: spendByUnit(lock),
|
|
@@ -9084,11 +9301,12 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9084
9301
|
id: project.id,
|
|
9085
9302
|
name: project.id,
|
|
9086
9303
|
manifest: manifestPath,
|
|
9087
|
-
lock:
|
|
9088
|
-
root:
|
|
9304
|
+
lock: path23.resolve(lockPath),
|
|
9305
|
+
root: path23.dirname(manifestPath),
|
|
9089
9306
|
provider: project.provider,
|
|
9090
9307
|
account: project.account ?? null,
|
|
9091
9308
|
manifestSha256: null,
|
|
9309
|
+
historyLimit: 0,
|
|
9092
9310
|
entries: 0,
|
|
9093
9311
|
items: 0,
|
|
9094
9312
|
spendByUnit: {},
|
|
@@ -9111,7 +9329,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
9111
9329
|
version: 1,
|
|
9112
9330
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
9113
9331
|
project: null,
|
|
9114
|
-
workspace: { path:
|
|
9332
|
+
workspace: { path: path23.resolve(opts.workspacePath), dir, projects },
|
|
9115
9333
|
filter,
|
|
9116
9334
|
totals: {
|
|
9117
9335
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -9432,6 +9650,19 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
9432
9650
|
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
9433
9651
|
.pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
|
|
9434
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; }
|
|
9435
9666
|
.dot.busy { background:var(--cool); animation: pulse 1.2s ease-in-out infinite; }
|
|
9436
9667
|
@keyframes pulse { 0%, 100% { opacity:1; } 50% { opacity:.25; } }
|
|
9437
9668
|
@media (prefers-reduced-motion: reduce) { .dot.busy { animation:none; } }
|
|
@@ -9605,7 +9836,7 @@ const PHASE_TONE = { queued: 'cool', submitting: 'cool', polling: 'cool', fetchi
|
|
|
9605
9836
|
// The job still working on this record, if any: what the drawer and its card
|
|
9606
9837
|
// show while a regeneration, restore, or pull is in flight.
|
|
9607
9838
|
const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
|
|
9608
|
-
const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring' };
|
|
9839
|
+
const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring', revert: 'bringing back a previous generation of' };
|
|
9609
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' };
|
|
9610
9841
|
const remainingBudget = (provider) => {
|
|
9611
9842
|
const keyed = GEN.budget.byProvider[provider];
|
|
@@ -9645,6 +9876,7 @@ async function pollJobs() {
|
|
|
9645
9876
|
? (JOB_VERB[job.mode] || job.mode).replace(/ing$/, 'ing') + ' this asset failed: ' + (job.error || 'see the log above')
|
|
9646
9877
|
: job.mode === 'generate' ? 'Regenerated. The new result is on disk and recorded as this generation.'
|
|
9647
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.'
|
|
9648
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.')
|
|
9649
9881
|
: 'Done.' };
|
|
9650
9882
|
}
|
|
@@ -9676,7 +9908,7 @@ function renderJobs() {
|
|
|
9676
9908
|
const row = el('div', 'job');
|
|
9677
9909
|
const ph = el('span', 'ph');
|
|
9678
9910
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
9679
|
-
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') +
|
|
9680
9912
|
(job.project ? ' in ' + job.project : '');
|
|
9681
9913
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
9682
9914
|
last.title = job.keys.join('\\n');
|
|
@@ -9801,15 +10033,16 @@ function budgetLine() {
|
|
|
9801
10033
|
|
|
9802
10034
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
9803
10035
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
9804
|
-
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 } = {}) {
|
|
9805
10037
|
const host = $('dialog-host');
|
|
9806
10038
|
host.textContent = '';
|
|
9807
10039
|
const wrap = el('div', 'dialog');
|
|
9808
10040
|
const form = el('form');
|
|
9809
10041
|
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
9810
|
-
const free = resume || refresh || restore;
|
|
10042
|
+
const free = resume || refresh || restore || revert !== null;
|
|
9811
10043
|
const replacing = items.filter((i) => i.state === 'orphaned' && i.outputs.every((o) => o.exists) || i.state === 'untracked');
|
|
9812
10044
|
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
10045
|
+
: revert !== null ? 'Bring back generation #' + revert + ' of ' + items[0].key
|
|
9813
10046
|
: restore ? 'Restore ' + noun
|
|
9814
10047
|
: resume ? 'Resume ' + noun
|
|
9815
10048
|
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
@@ -9831,6 +10064,10 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9831
10064
|
const sum = el('div', 'sum');
|
|
9832
10065
|
if (refresh) {
|
|
9833
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.'));
|
|
9834
10071
|
} else if (restore) {
|
|
9835
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.'));
|
|
9836
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.'));
|
|
@@ -9852,7 +10089,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9852
10089
|
}
|
|
9853
10090
|
form.append(sum);
|
|
9854
10091
|
const actions = el('div', 'actions');
|
|
9855
|
-
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');
|
|
9856
10093
|
go.type = 'submit';
|
|
9857
10094
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
9858
10095
|
const msg = el('span', 'msg');
|
|
@@ -9868,6 +10105,7 @@ function generateDialog(items, { project = null, force = false, resume = false,
|
|
|
9868
10105
|
if (resume) body.resume = true;
|
|
9869
10106
|
if (refresh) body.refresh = true;
|
|
9870
10107
|
if (restore) body.restore = true;
|
|
10108
|
+
if (revert !== null) body.revert = revert;
|
|
9871
10109
|
const job = await postGenerate(body);
|
|
9872
10110
|
host.textContent = '';
|
|
9873
10111
|
GEN.jobs.unshift(job);
|
|
@@ -10498,6 +10736,54 @@ function candidatesControl(style) {
|
|
|
10498
10736
|
return wrap;
|
|
10499
10737
|
}
|
|
10500
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
|
+
|
|
10501
10787
|
function upstreamSection(item) {
|
|
10502
10788
|
if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
|
|
10503
10789
|
const s = el('section', 'meta');
|
|
@@ -11322,6 +11608,8 @@ function renderDrawer() {
|
|
|
11322
11608
|
if (hand) body.append(hand);
|
|
11323
11609
|
const upstream = upstreamSection(item);
|
|
11324
11610
|
if (upstream) body.append(upstream);
|
|
11611
|
+
const versions = historySection(item);
|
|
11612
|
+
if (versions) body.append(versions);
|
|
11325
11613
|
|
|
11326
11614
|
if (item.revision || item.revisionParentKey) {
|
|
11327
11615
|
const { s, dl } = section('Lineage');
|
|
@@ -11868,10 +12156,15 @@ var GenerateRequestSchema = z8.object({
|
|
|
11868
12156
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11869
12157
|
refresh: z8.boolean().optional(),
|
|
11870
12158
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
11871
|
-
restore: z8.boolean().optional()
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
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" });
|
|
11875
12168
|
var GenerateRequestError = class extends Error {
|
|
11876
12169
|
constructor(message6, status = 400) {
|
|
11877
12170
|
super(message6);
|
|
@@ -12002,6 +12295,22 @@ function createGenerateHandlers(opts) {
|
|
|
12002
12295
|
await restore(job, ctx, specs);
|
|
12003
12296
|
return;
|
|
12004
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
|
+
}
|
|
12005
12314
|
if (job.mode === "generate") {
|
|
12006
12315
|
job.phase = "submitting";
|
|
12007
12316
|
for (const group of groups) {
|
|
@@ -12072,8 +12381,13 @@ function createGenerateHandlers(opts) {
|
|
|
12072
12381
|
}
|
|
12073
12382
|
}
|
|
12074
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
|
+
}
|
|
12075
12389
|
let groups = [];
|
|
12076
|
-
if (!request.resume && !request.refresh && !request.restore) {
|
|
12390
|
+
if (!request.resume && !request.refresh && !request.restore && request.revert === void 0) {
|
|
12077
12391
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
12078
12392
|
if (!plan.actionable.length) {
|
|
12079
12393
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -12093,8 +12407,9 @@ function createGenerateHandlers(opts) {
|
|
|
12093
12407
|
id: randomBytes2(8).toString("hex"),
|
|
12094
12408
|
project: request.project ?? null,
|
|
12095
12409
|
keys: request.keys,
|
|
12096
|
-
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",
|
|
12097
12411
|
force: Boolean(request.force),
|
|
12412
|
+
revert: request.revert ?? null,
|
|
12098
12413
|
phase: "queued",
|
|
12099
12414
|
startedAt: now().toISOString(),
|
|
12100
12415
|
finishedAt: null,
|
|
@@ -12106,7 +12421,7 @@ function createGenerateHandlers(opts) {
|
|
|
12106
12421
|
};
|
|
12107
12422
|
jobs.set(job.id, job);
|
|
12108
12423
|
contexts.set(job.id, ctx);
|
|
12109
|
-
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("; "));
|
|
12110
12425
|
void run(job, ctx, specs, groups);
|
|
12111
12426
|
return job;
|
|
12112
12427
|
},
|
|
@@ -12157,14 +12472,14 @@ function createGenerateHandlers(opts) {
|
|
|
12157
12472
|
|
|
12158
12473
|
// src/gallery/editor.ts
|
|
12159
12474
|
import { readFile as readFile19 } from "fs/promises";
|
|
12160
|
-
import
|
|
12475
|
+
import path25 from "path";
|
|
12161
12476
|
|
|
12162
12477
|
// src/editor/install.ts
|
|
12163
12478
|
import { createHash as createHash4 } from "crypto";
|
|
12164
|
-
import { existsSync as
|
|
12479
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12165
12480
|
import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
|
|
12166
12481
|
import os2 from "os";
|
|
12167
|
-
import
|
|
12482
|
+
import path24 from "path";
|
|
12168
12483
|
|
|
12169
12484
|
// tools/pixelorama-bridge/pin.json
|
|
12170
12485
|
var pin_default = {
|
|
@@ -12228,15 +12543,15 @@ var EditorInstallError = class extends Error {
|
|
|
12228
12543
|
};
|
|
12229
12544
|
function editorToolsRoot() {
|
|
12230
12545
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
12231
|
-
if (override) return
|
|
12232
|
-
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");
|
|
12233
12548
|
if (process.platform === "win32") {
|
|
12234
|
-
return
|
|
12549
|
+
return path24.join(process.env.LOCALAPPDATA ?? path24.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
12235
12550
|
}
|
|
12236
|
-
return
|
|
12551
|
+
return path24.join(process.env.XDG_CACHE_HOME ?? path24.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
12237
12552
|
}
|
|
12238
12553
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
12239
|
-
return
|
|
12554
|
+
return path24.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
12240
12555
|
}
|
|
12241
12556
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
12242
12557
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -12259,8 +12574,8 @@ async function editorStatus(opts = {}) {
|
|
|
12259
12574
|
let totalBytes = 0;
|
|
12260
12575
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
12261
12576
|
totalBytes += expected.bytes;
|
|
12262
|
-
const file =
|
|
12263
|
-
const info =
|
|
12577
|
+
const file = path24.join(dir, name);
|
|
12578
|
+
const info = existsSync20(file) ? await stat4(file).catch(() => null) : null;
|
|
12264
12579
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
12265
12580
|
installedBytes += expected.bytes;
|
|
12266
12581
|
} else {
|
|
@@ -12317,7 +12632,7 @@ async function installEditor(opts = {}) {
|
|
|
12317
12632
|
if (digest2 !== expected.sha256) {
|
|
12318
12633
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
12319
12634
|
}
|
|
12320
|
-
const final =
|
|
12635
|
+
const final = path24.join(dir, name);
|
|
12321
12636
|
const tmp = `${final}.${process.pid}.part`;
|
|
12322
12637
|
try {
|
|
12323
12638
|
await writeFile10(tmp, bytes);
|
|
@@ -12363,7 +12678,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
12363
12678
|
".json": "application/json; charset=utf-8"
|
|
12364
12679
|
};
|
|
12365
12680
|
function editorContentType(name) {
|
|
12366
|
-
return EDITOR_CONTENT_TYPES[
|
|
12681
|
+
return EDITOR_CONTENT_TYPES[path24.extname(name).toLowerCase()] ?? null;
|
|
12367
12682
|
}
|
|
12368
12683
|
|
|
12369
12684
|
// src/gallery/editor.ts
|
|
@@ -12439,7 +12754,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12439
12754
|
if (!status.installed) return null;
|
|
12440
12755
|
let bytes;
|
|
12441
12756
|
try {
|
|
12442
|
-
bytes = await readFile19(
|
|
12757
|
+
bytes = await readFile19(path25.join(dir, name));
|
|
12443
12758
|
} catch {
|
|
12444
12759
|
verified = null;
|
|
12445
12760
|
return null;
|
|
@@ -12455,15 +12770,15 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12455
12770
|
|
|
12456
12771
|
// src/pipeline/salvage.ts
|
|
12457
12772
|
import { readFile as readFile20 } from "fs/promises";
|
|
12458
|
-
import { existsSync as
|
|
12459
|
-
import
|
|
12773
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12774
|
+
import path26 from "path";
|
|
12460
12775
|
function providerClaimId(provider, objectId) {
|
|
12461
12776
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12462
12777
|
}
|
|
12463
12778
|
async function loadClaims(lockPaths, opts = {}) {
|
|
12464
12779
|
const claimed = /* @__PURE__ */ new Set();
|
|
12465
12780
|
for (const p of lockPaths) {
|
|
12466
|
-
if (!
|
|
12781
|
+
if (!existsSync21(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12467
12782
|
let parsed;
|
|
12468
12783
|
try {
|
|
12469
12784
|
parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
|
|
@@ -12514,20 +12829,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
12514
12829
|
return null;
|
|
12515
12830
|
}
|
|
12516
12831
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12517
|
-
const own =
|
|
12832
|
+
const own = path26.resolve(ownManifestPath);
|
|
12518
12833
|
const siblingManifestPaths = [
|
|
12519
12834
|
.../* @__PURE__ */ new Set([
|
|
12520
12835
|
...workspaceManifestPaths,
|
|
12521
|
-
...claimPaths.map((c) =>
|
|
12836
|
+
...claimPaths.map((c) => path26.join(path26.dirname(path26.resolve(c)), "pixelkiln.manifest.json"))
|
|
12522
12837
|
])
|
|
12523
12838
|
];
|
|
12524
12839
|
const siblings = [];
|
|
12525
12840
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12526
|
-
if (
|
|
12527
|
-
if (!
|
|
12841
|
+
if (path26.resolve(siblingManifestPath) === own) continue;
|
|
12842
|
+
if (!existsSync21(siblingManifestPath)) continue;
|
|
12528
12843
|
try {
|
|
12529
12844
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12530
|
-
siblings.push({ label:
|
|
12845
|
+
siblings.push({ label: path26.basename(path26.dirname(siblingManifestPath)), manifest });
|
|
12531
12846
|
} catch {
|
|
12532
12847
|
}
|
|
12533
12848
|
}
|
|
@@ -12746,18 +13061,16 @@ async function workspaceStatus(ws, dir) {
|
|
|
12746
13061
|
}
|
|
12747
13062
|
|
|
12748
13063
|
// src/pipeline/cache-health.ts
|
|
12749
|
-
import { existsSync as
|
|
13064
|
+
import { existsSync as existsSync22 } from "fs";
|
|
12750
13065
|
import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
|
|
12751
|
-
import
|
|
13066
|
+
import path27 from "path";
|
|
12752
13067
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
12753
|
-
if (options.prune && !
|
|
12754
|
-
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)}`);
|
|
12755
13070
|
}
|
|
12756
|
-
const contentDir =
|
|
12757
|
-
const remotePath =
|
|
12758
|
-
const referenced =
|
|
12759
|
-
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
12760
|
-
);
|
|
13071
|
+
const contentDir = path27.resolve(path27.dirname(lockPath), ".pixelkiln", "cache");
|
|
13072
|
+
const remotePath = path27.resolve(cachePathFor(lockPath));
|
|
13073
|
+
const referenced = referencedHashes(lock);
|
|
12761
13074
|
let content = await inspectContentCache(contentDir, referenced);
|
|
12762
13075
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
12763
13076
|
const removed = { contentFiles: 0, remoteHashEntries: 0, resetRemoteHashCache: false };
|
|
@@ -12768,7 +13081,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12768
13081
|
]);
|
|
12769
13082
|
for (const name of names) {
|
|
12770
13083
|
try {
|
|
12771
|
-
await rm10(
|
|
13084
|
+
await rm10(path27.join(contentDir, name), { force: true });
|
|
12772
13085
|
removed.contentFiles++;
|
|
12773
13086
|
} catch {
|
|
12774
13087
|
}
|
|
@@ -12795,7 +13108,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12795
13108
|
async function inspectContentCache(contentDir, referenced) {
|
|
12796
13109
|
const report = {
|
|
12797
13110
|
path: contentDir,
|
|
12798
|
-
exists:
|
|
13111
|
+
exists: existsSync22(contentDir),
|
|
12799
13112
|
files: 0,
|
|
12800
13113
|
bytes: 0,
|
|
12801
13114
|
valid: 0,
|
|
@@ -12816,7 +13129,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12816
13129
|
continue;
|
|
12817
13130
|
}
|
|
12818
13131
|
report.files++;
|
|
12819
|
-
const file =
|
|
13132
|
+
const file = path27.join(contentDir, entry.name);
|
|
12820
13133
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
12821
13134
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
12822
13135
|
let bytes;
|
|
@@ -12859,7 +13172,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12859
13172
|
async function inspectRemoteHashCache(remotePath) {
|
|
12860
13173
|
const report = {
|
|
12861
13174
|
path: remotePath,
|
|
12862
|
-
exists:
|
|
13175
|
+
exists: existsSync22(remotePath),
|
|
12863
13176
|
entries: 0,
|
|
12864
13177
|
valid: 0,
|
|
12865
13178
|
invalidIds: [],
|
|
@@ -13143,7 +13456,7 @@ function isRecord2(value) {
|
|
|
13143
13456
|
|
|
13144
13457
|
// src/pick/salvage-server.ts
|
|
13145
13458
|
import { mkdir as mkdir8, writeFile as writeFile11, readFile as readFile22 } from "fs/promises";
|
|
13146
|
-
import
|
|
13459
|
+
import path28 from "path";
|
|
13147
13460
|
|
|
13148
13461
|
// src/pick/salvage-sheet.ts
|
|
13149
13462
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13308,7 +13621,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13308
13621
|
});
|
|
13309
13622
|
const html = renderSalvageSheet(orphans, {
|
|
13310
13623
|
styleId: ctx.styleId,
|
|
13311
|
-
importDir:
|
|
13624
|
+
importDir: path28.relative(process.cwd(), ctx.importDir) || "."
|
|
13312
13625
|
});
|
|
13313
13626
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13314
13627
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13337,9 +13650,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13337
13650
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13338
13651
|
decodePng(buf);
|
|
13339
13652
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13340
|
-
const rel =
|
|
13341
|
-
const outFile =
|
|
13342
|
-
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 });
|
|
13343
13656
|
await writeFile11(outFile, buf);
|
|
13344
13657
|
ctx.manifest.assets[assetId] = {
|
|
13345
13658
|
prompt: orphan.prompt,
|
|
@@ -13372,7 +13685,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13372
13685
|
sourceUrl: durableSource,
|
|
13373
13686
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13374
13687
|
outputs: [{
|
|
13375
|
-
path: portableOutputPath(outFile,
|
|
13688
|
+
path: portableOutputPath(outFile, path28.dirname(ctx.manifestPath)),
|
|
13376
13689
|
sha256: sha256(buf)
|
|
13377
13690
|
}],
|
|
13378
13691
|
submittedAt: orphan.createdAt,
|
|
@@ -13406,8 +13719,8 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13406
13719
|
}
|
|
13407
13720
|
|
|
13408
13721
|
// src/recipes.ts
|
|
13409
|
-
import
|
|
13410
|
-
import { existsSync as
|
|
13722
|
+
import path29 from "path";
|
|
13723
|
+
import { existsSync as existsSync23 } from "fs";
|
|
13411
13724
|
import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
|
|
13412
13725
|
import { fileURLToPath } from "url";
|
|
13413
13726
|
import { z as z9 } from "zod";
|
|
@@ -13417,7 +13730,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
13417
13730
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
13418
13731
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
13419
13732
|
var PortablePathSchema = z9.string().min(1).refine((value) => {
|
|
13420
|
-
if (value.includes("\\") ||
|
|
13733
|
+
if (value.includes("\\") || path29.posix.isAbsolute(value)) return false;
|
|
13421
13734
|
const parts = value.split("/");
|
|
13422
13735
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
13423
13736
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -13593,7 +13906,7 @@ function message4(error) {
|
|
|
13593
13906
|
return error instanceof Error ? error.message : String(error);
|
|
13594
13907
|
}
|
|
13595
13908
|
async function readRecipeFile(recipePath, bundled) {
|
|
13596
|
-
const absolute =
|
|
13909
|
+
const absolute = path29.resolve(recipePath);
|
|
13597
13910
|
let raw;
|
|
13598
13911
|
try {
|
|
13599
13912
|
raw = JSON.parse(await readFile23(absolute, "utf8"));
|
|
@@ -13607,13 +13920,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
13607
13920
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
13608
13921
|
);
|
|
13609
13922
|
}
|
|
13610
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
13923
|
+
return { recipe: parsed.data, path: absolute, dir: path29.dirname(absolute), bundled };
|
|
13611
13924
|
}
|
|
13612
13925
|
async function walkRecipeFiles(root) {
|
|
13613
|
-
if (!
|
|
13926
|
+
if (!existsSync23(root)) return [];
|
|
13614
13927
|
const out = [];
|
|
13615
13928
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
13616
|
-
const resolved =
|
|
13929
|
+
const resolved = path29.join(root, entry.name);
|
|
13617
13930
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
13618
13931
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
13619
13932
|
}
|
|
@@ -13624,7 +13937,7 @@ function bundledRecipeRoot() {
|
|
|
13624
13937
|
}
|
|
13625
13938
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
13626
13939
|
const recipes = await Promise.all(
|
|
13627
|
-
(await walkRecipeFiles(
|
|
13940
|
+
(await walkRecipeFiles(path29.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
13628
13941
|
);
|
|
13629
13942
|
return recipes.sort(
|
|
13630
13943
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -13647,9 +13960,9 @@ function parseSelector(selector) {
|
|
|
13647
13960
|
return { id: match[1], version: match[2] };
|
|
13648
13961
|
}
|
|
13649
13962
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
13650
|
-
const local =
|
|
13651
|
-
if (
|
|
13652
|
-
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);
|
|
13653
13966
|
return readRecipeFile(recipePath, false);
|
|
13654
13967
|
}
|
|
13655
13968
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -13663,8 +13976,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
13663
13976
|
return matches[0];
|
|
13664
13977
|
}
|
|
13665
13978
|
async function verifyFile(root, file) {
|
|
13666
|
-
const absolute =
|
|
13667
|
-
if (!
|
|
13979
|
+
const absolute = path29.resolve(root, ...file.path.split("/"));
|
|
13980
|
+
if (!existsSync23(absolute)) {
|
|
13668
13981
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
13669
13982
|
}
|
|
13670
13983
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -13679,7 +13992,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
13679
13992
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
13680
13993
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
13681
13994
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
13682
|
-
const modelRoot = options.modelRoot ?
|
|
13995
|
+
const modelRoot = options.modelRoot ? path29.resolve(options.modelRoot) : null;
|
|
13683
13996
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
13684
13997
|
...await verifyFile(modelRoot, model),
|
|
13685
13998
|
source: model.source,
|
|
@@ -13713,7 +14026,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
13713
14026
|
};
|
|
13714
14027
|
}
|
|
13715
14028
|
function renderStyle(style, destination, cwd) {
|
|
13716
|
-
const portableDir =
|
|
14029
|
+
const portableDir = path29.relative(cwd, destination).split(path29.sep).join("/") || ".";
|
|
13717
14030
|
const replace = (value) => {
|
|
13718
14031
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
13719
14032
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -13725,24 +14038,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
13725
14038
|
return replace(style);
|
|
13726
14039
|
}
|
|
13727
14040
|
async function installRecipe(target, options = {}) {
|
|
13728
|
-
const cwd =
|
|
14041
|
+
const cwd = path29.resolve(options.cwd ?? process.cwd());
|
|
13729
14042
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
13730
14043
|
const verification = await verifyRecipe(loaded.path);
|
|
13731
14044
|
if (!verification.ok) {
|
|
13732
14045
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
13733
14046
|
}
|
|
13734
|
-
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);
|
|
13735
14048
|
const sources = [
|
|
13736
14049
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
13737
|
-
...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("/")) }))
|
|
13738
14051
|
];
|
|
13739
14052
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
13740
|
-
path:
|
|
14053
|
+
path: path29.join(destination, ...file.path.split("/")),
|
|
13741
14054
|
data: await readFile23(file.source)
|
|
13742
14055
|
})));
|
|
13743
14056
|
if (!options.force) {
|
|
13744
14057
|
for (const file of files) {
|
|
13745
|
-
if (!
|
|
14058
|
+
if (!existsSync23(file.path)) continue;
|
|
13746
14059
|
const current = await readFile23(file.path);
|
|
13747
14060
|
if (!current.equals(file.data)) {
|
|
13748
14061
|
throw new Error(
|
|
@@ -13766,15 +14079,15 @@ async function installRecipe(target, options = {}) {
|
|
|
13766
14079
|
}
|
|
13767
14080
|
|
|
13768
14081
|
// src/pipeline/quality-regression.ts
|
|
13769
|
-
import
|
|
13770
|
-
import { existsSync as
|
|
14082
|
+
import path30 from "path";
|
|
14083
|
+
import { existsSync as existsSync24 } from "fs";
|
|
13771
14084
|
import { readFile as readFile24 } from "fs/promises";
|
|
13772
14085
|
import { z as z10 } from "zod";
|
|
13773
14086
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
13774
14087
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
13775
14088
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
13776
14089
|
var QualityPathSchema = z10.string().min(1).refine(
|
|
13777
|
-
(value) => !
|
|
14090
|
+
(value) => !path30.posix.isAbsolute(value) && !value.includes("\\"),
|
|
13778
14091
|
"expected a portable relative path"
|
|
13779
14092
|
);
|
|
13780
14093
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -13921,7 +14234,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
13921
14234
|
};
|
|
13922
14235
|
}
|
|
13923
14236
|
async function measureImageQuality(file) {
|
|
13924
|
-
const absolute =
|
|
14237
|
+
const absolute = path30.resolve(file);
|
|
13925
14238
|
let bytes;
|
|
13926
14239
|
try {
|
|
13927
14240
|
bytes = await readFile24(absolute);
|
|
@@ -13943,11 +14256,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13943
14256
|
}
|
|
13944
14257
|
const ids = parsed.data.map((entry) => entry.id);
|
|
13945
14258
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
13946
|
-
const root =
|
|
14259
|
+
const root = path30.dirname(path30.resolve(inputsFilePath));
|
|
13947
14260
|
return parsed.data.map((entry) => ({
|
|
13948
14261
|
id: entry.id,
|
|
13949
|
-
file:
|
|
13950
|
-
...entry.record ? { record:
|
|
14262
|
+
file: path30.resolve(root, entry.path),
|
|
14263
|
+
...entry.record ? { record: path30.resolve(root, entry.record) } : {},
|
|
13951
14264
|
tolerances: QualityToleranceSchema.parse({
|
|
13952
14265
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
13953
14266
|
...entry.tolerances
|
|
@@ -13955,7 +14268,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
13955
14268
|
}));
|
|
13956
14269
|
}
|
|
13957
14270
|
function portableRelative2(from, to) {
|
|
13958
|
-
return
|
|
14271
|
+
return path30.relative(from, path30.resolve(to)).split(path30.sep).join("/") || ".";
|
|
13959
14272
|
}
|
|
13960
14273
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
13961
14274
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -13970,20 +14283,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
13970
14283
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
13971
14284
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
13972
14285
|
}
|
|
13973
|
-
const outputs = record.outputs.map((output) =>
|
|
13974
|
-
if (!outputs.includes(
|
|
14286
|
+
const outputs = record.outputs.map((output) => path30.resolve(path30.dirname(recordPath), output.path));
|
|
14287
|
+
if (!outputs.includes(path30.resolve(imagePath))) {
|
|
13975
14288
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
13976
14289
|
}
|
|
13977
14290
|
}
|
|
13978
14291
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
13979
14292
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
13980
|
-
const absolute =
|
|
14293
|
+
const absolute = path30.resolve(baselinePath);
|
|
13981
14294
|
for (const input of inputs) {
|
|
13982
|
-
if (
|
|
14295
|
+
if (path30.resolve(input.file) === absolute || input.record && path30.resolve(input.record) === absolute) {
|
|
13983
14296
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
13984
14297
|
}
|
|
13985
14298
|
}
|
|
13986
|
-
const root =
|
|
14299
|
+
const root = path30.dirname(absolute);
|
|
13987
14300
|
const cases = [];
|
|
13988
14301
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
13989
14302
|
const expected = await measureImageQuality(input.file);
|
|
@@ -14010,7 +14323,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
14010
14323
|
cases
|
|
14011
14324
|
});
|
|
14012
14325
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
14013
|
-
if (
|
|
14326
|
+
if (existsSync24(absolute) && !options.force) {
|
|
14014
14327
|
const current = await readFile24(absolute);
|
|
14015
14328
|
if (!current.equals(data)) {
|
|
14016
14329
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
@@ -14020,7 +14333,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
14020
14333
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
14021
14334
|
}
|
|
14022
14335
|
async function readQualityBaseline(baselinePath) {
|
|
14023
|
-
const absolute =
|
|
14336
|
+
const absolute = path30.resolve(baselinePath);
|
|
14024
14337
|
let raw;
|
|
14025
14338
|
try {
|
|
14026
14339
|
raw = JSON.parse(await readFile24(absolute, "utf8"));
|
|
@@ -14093,12 +14406,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
14093
14406
|
return { violations, warnings };
|
|
14094
14407
|
}
|
|
14095
14408
|
async function checkQualityBaseline(baselinePath) {
|
|
14096
|
-
const absolute =
|
|
14409
|
+
const absolute = path30.resolve(baselinePath);
|
|
14097
14410
|
const baseline = await readQualityBaseline(absolute);
|
|
14098
|
-
const root =
|
|
14411
|
+
const root = path30.dirname(absolute);
|
|
14099
14412
|
const cases = [];
|
|
14100
14413
|
for (const entry of baseline.cases) {
|
|
14101
|
-
const file =
|
|
14414
|
+
const file = path30.resolve(root, entry.file);
|
|
14102
14415
|
let actual = null;
|
|
14103
14416
|
const violations = [];
|
|
14104
14417
|
const warnings = [];
|
|
@@ -14111,7 +14424,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
14111
14424
|
violations.push(message5(error));
|
|
14112
14425
|
}
|
|
14113
14426
|
if (entry.record) {
|
|
14114
|
-
const recordPath =
|
|
14427
|
+
const recordPath = path30.resolve(root, entry.record.path);
|
|
14115
14428
|
try {
|
|
14116
14429
|
const recordHash = await sha256File(recordPath);
|
|
14117
14430
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -14197,13 +14510,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
14197
14510
|
};
|
|
14198
14511
|
const loadProject = async (project) => {
|
|
14199
14512
|
const ctx = await access2.loadProject(project);
|
|
14200
|
-
const dir =
|
|
14513
|
+
const dir = path31.dirname(ctx.loaded.path);
|
|
14201
14514
|
const declared = readEnvFiles(dir);
|
|
14202
14515
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
14203
14516
|
for (const [name, value] of Object.entries(declared)) {
|
|
14204
14517
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
14205
14518
|
throw new Error(
|
|
14206
|
-
`${
|
|
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.`
|
|
14207
14520
|
);
|
|
14208
14521
|
}
|
|
14209
14522
|
}
|
|
@@ -14247,11 +14560,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
14247
14560
|
log("\n gallery closed");
|
|
14248
14561
|
}
|
|
14249
14562
|
async function provenanceFile(id, file) {
|
|
14250
|
-
const absolute =
|
|
14563
|
+
const absolute = path31.resolve(file);
|
|
14251
14564
|
return {
|
|
14252
14565
|
id,
|
|
14253
14566
|
path: absolute,
|
|
14254
|
-
sha256:
|
|
14567
|
+
sha256: existsSync25(absolute) ? await sha256File(absolute) : null,
|
|
14255
14568
|
included: true
|
|
14256
14569
|
};
|
|
14257
14570
|
}
|
|
@@ -14285,7 +14598,8 @@ var VALUE_FLAGS = [
|
|
|
14285
14598
|
"--min-grid-confidence",
|
|
14286
14599
|
"--reviewer",
|
|
14287
14600
|
"--note",
|
|
14288
|
-
"--model-root"
|
|
14601
|
+
"--model-root",
|
|
14602
|
+
"--generation"
|
|
14289
14603
|
];
|
|
14290
14604
|
var BOOL_FLAGS = [
|
|
14291
14605
|
"--force",
|
|
@@ -14330,6 +14644,7 @@ var COMMANDS = [
|
|
|
14330
14644
|
"gallery",
|
|
14331
14645
|
"edit",
|
|
14332
14646
|
"tools",
|
|
14647
|
+
"history",
|
|
14333
14648
|
"quality",
|
|
14334
14649
|
"refine",
|
|
14335
14650
|
"recipe",
|
|
@@ -14539,7 +14854,7 @@ function parseArgs(argv) {
|
|
|
14539
14854
|
return {
|
|
14540
14855
|
command,
|
|
14541
14856
|
manifest,
|
|
14542
|
-
lock: get("--lock") ??
|
|
14857
|
+
lock: get("--lock") ?? path31.join(path31.dirname(path31.resolve(manifest)), "pixelkiln.lock.json"),
|
|
14543
14858
|
explicitLock: get("--lock"),
|
|
14544
14859
|
styles: list("--style"),
|
|
14545
14860
|
assets: list("--only"),
|
|
@@ -14581,6 +14896,7 @@ function parseArgs(argv) {
|
|
|
14581
14896
|
reviewer: get("--reviewer"),
|
|
14582
14897
|
note: get("--note"),
|
|
14583
14898
|
modelRoot: get("--model-root"),
|
|
14899
|
+
generation: get("--generation"),
|
|
14584
14900
|
subcommand,
|
|
14585
14901
|
workspace: get("--workspace"),
|
|
14586
14902
|
target,
|
|
@@ -14640,7 +14956,9 @@ Commands
|
|
|
14640
14956
|
poll Advance in-flight jobs to their settled state.
|
|
14641
14957
|
pick Open the contact sheet to choose among candidates.
|
|
14642
14958
|
fetch Download selected objects to their manifest paths.
|
|
14643
|
-
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.
|
|
14644
14962
|
adopt Match existing account objects to files already in the repo.
|
|
14645
14963
|
accept Keep existing art after a style reword \u2014 re-baseline, do not regenerate.
|
|
14646
14964
|
salvage Triage account objects no lockfile claims. Recovers usable art.
|
|
@@ -14694,6 +15012,7 @@ Options
|
|
|
14694
15012
|
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
14695
15013
|
--note <text> refine approve: optional review note
|
|
14696
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)
|
|
14697
15016
|
--prune cache: remove invalid/unreferenced local cache data
|
|
14698
15017
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
14699
15018
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
@@ -14891,10 +15210,10 @@ async function confirm(question, auto) {
|
|
|
14891
15210
|
return answer === "y" || answer === "yes";
|
|
14892
15211
|
}
|
|
14893
15212
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
14894
|
-
if (!
|
|
15213
|
+
if (!existsSync25(workspacePath)) {
|
|
14895
15214
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
14896
15215
|
}
|
|
14897
|
-
const dir =
|
|
15216
|
+
const dir = path31.dirname(path31.resolve(workspacePath));
|
|
14898
15217
|
const ws = await loadWorkspace(workspacePath);
|
|
14899
15218
|
const diagnostics = validateWorkspace(ws, dir);
|
|
14900
15219
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -14930,15 +15249,15 @@ async function main() {
|
|
|
14930
15249
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
14931
15250
|
let raw;
|
|
14932
15251
|
try {
|
|
14933
|
-
raw = JSON.parse(await readFile25(
|
|
15252
|
+
raw = JSON.parse(await readFile25(path31.resolve(args.inputs), "utf8"));
|
|
14934
15253
|
} catch (error) {
|
|
14935
15254
|
throw new Error(
|
|
14936
|
-
`Could not read quality inputs ${
|
|
15255
|
+
`Could not read quality inputs ${path31.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
14937
15256
|
{ cause: error }
|
|
14938
15257
|
);
|
|
14939
15258
|
}
|
|
14940
15259
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
14941
|
-
if (
|
|
15260
|
+
if (path31.resolve(args.inputs) === path31.resolve(args.out)) {
|
|
14942
15261
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
14943
15262
|
}
|
|
14944
15263
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -14951,7 +15270,7 @@ async function main() {
|
|
|
14951
15270
|
}, null, 2));
|
|
14952
15271
|
} else {
|
|
14953
15272
|
log(
|
|
14954
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
15273
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path31.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
14955
15274
|
);
|
|
14956
15275
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
14957
15276
|
}
|
|
@@ -15046,14 +15365,14 @@ async function main() {
|
|
|
15046
15365
|
}, null, 2));
|
|
15047
15366
|
} else {
|
|
15048
15367
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
15049
|
-
log(` ${
|
|
15368
|
+
log(` ${path31.relative(process.cwd(), result.destination) || "."}`);
|
|
15050
15369
|
log(`
|
|
15051
15370
|
Add this entry under your manifest's styles object:`);
|
|
15052
15371
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
15053
15372
|
if (result.recipe.models.length) {
|
|
15054
15373
|
log(`
|
|
15055
15374
|
Models are not downloaded automatically. Verify them with:`);
|
|
15056
|
-
log(` pixelkiln recipe verify ${
|
|
15375
|
+
log(` pixelkiln recipe verify ${path31.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
15057
15376
|
}
|
|
15058
15377
|
}
|
|
15059
15378
|
return;
|
|
@@ -15087,12 +15406,12 @@ async function main() {
|
|
|
15087
15406
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
15088
15407
|
);
|
|
15089
15408
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
15090
|
-
log(` wrote ${
|
|
15091
|
-
log(` quality record: ${
|
|
15409
|
+
log(` wrote ${path31.relative(process.cwd(), result2.output)}`);
|
|
15410
|
+
log(` quality record: ${path31.relative(process.cwd(), result2.record)}`);
|
|
15092
15411
|
log(`
|
|
15093
15412
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
15094
15413
|
log(
|
|
15095
|
-
` pixelkiln refine approve --from ${
|
|
15414
|
+
` pixelkiln refine approve --from ${path31.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
15096
15415
|
);
|
|
15097
15416
|
}
|
|
15098
15417
|
return;
|
|
@@ -15120,7 +15439,7 @@ async function main() {
|
|
|
15120
15439
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
15121
15440
|
else {
|
|
15122
15441
|
log(
|
|
15123
|
-
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()}`
|
|
15124
15443
|
);
|
|
15125
15444
|
}
|
|
15126
15445
|
return;
|
|
@@ -15129,14 +15448,14 @@ async function main() {
|
|
|
15129
15448
|
if (args.json) {
|
|
15130
15449
|
log(JSON.stringify(result, null, 2));
|
|
15131
15450
|
} else {
|
|
15132
|
-
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)}`);
|
|
15133
15452
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
15134
15453
|
}
|
|
15135
15454
|
if (!result.safe) process.exitCode = 1;
|
|
15136
15455
|
return;
|
|
15137
15456
|
}
|
|
15138
15457
|
if (args.command === "balance") {
|
|
15139
|
-
loadEnvFiles(
|
|
15458
|
+
loadEnvFiles(path31.dirname(path31.resolve(args.manifest)));
|
|
15140
15459
|
loadEnvFiles(process.cwd());
|
|
15141
15460
|
const loaded2 = await loadManifest(args.manifest);
|
|
15142
15461
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -15149,31 +15468,31 @@ async function main() {
|
|
|
15149
15468
|
}
|
|
15150
15469
|
if (args.command === "init") {
|
|
15151
15470
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
15152
|
-
const root =
|
|
15153
|
-
if (!
|
|
15471
|
+
const root = path31.resolve(args.from);
|
|
15472
|
+
if (!existsSync25(root)) throw new Error(`No directory at ${root}`);
|
|
15154
15473
|
const generator = args.generator ?? "map";
|
|
15155
15474
|
if (generator !== "1dir" && generator !== "map") {
|
|
15156
15475
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
15157
15476
|
}
|
|
15158
|
-
const target =
|
|
15477
|
+
const target = path31.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
15159
15478
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
15160
15479
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
15161
15480
|
const manifest = buildManifest(
|
|
15162
|
-
args.name ??
|
|
15481
|
+
args.name ?? path31.basename(path31.dirname(target)),
|
|
15163
15482
|
args.styles[0] ?? "base",
|
|
15164
15483
|
generator,
|
|
15165
|
-
|
|
15484
|
+
path31.relative(path31.dirname(target), root) || ".",
|
|
15166
15485
|
assets
|
|
15167
15486
|
);
|
|
15168
15487
|
await writeManifestFile(target, manifest);
|
|
15169
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
15488
|
+
log(` scanned ${assets.length} PNG(s) under ${path31.relative(process.cwd(), root)}`);
|
|
15170
15489
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
15171
|
-
log(` wrote ${
|
|
15490
|
+
log(` wrote ${path31.relative(process.cwd(), target)}`);
|
|
15172
15491
|
log(`
|
|
15173
15492
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
15174
15493
|
log(` PixelLab account instead of inventing them:`);
|
|
15175
15494
|
log(`
|
|
15176
|
-
pixelkiln adopt --manifest ${
|
|
15495
|
+
pixelkiln adopt --manifest ${path31.relative(process.cwd(), target)} --write-prompts
|
|
15177
15496
|
`);
|
|
15178
15497
|
return;
|
|
15179
15498
|
}
|
|
@@ -15221,10 +15540,10 @@ async function main() {
|
|
|
15221
15540
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
15222
15541
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
15223
15542
|
}
|
|
15224
|
-
const raw = JSON.parse(await readFile25(
|
|
15543
|
+
const raw = JSON.parse(await readFile25(path31.resolve(args.inputs), "utf8"));
|
|
15225
15544
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
15226
15545
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
15227
|
-
const base =
|
|
15546
|
+
const base = path31.resolve(args.out.replace(/\.png$/, ""));
|
|
15228
15547
|
const outputs = [
|
|
15229
15548
|
{ path: `${base}.png`, data: png },
|
|
15230
15549
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -15237,17 +15556,17 @@ async function main() {
|
|
|
15237
15556
|
log(
|
|
15238
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`
|
|
15239
15558
|
);
|
|
15240
|
-
log(` ${
|
|
15559
|
+
log(` ${path31.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
15241
15560
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15242
15561
|
return;
|
|
15243
15562
|
}
|
|
15244
15563
|
if (args.command === "workspace") {
|
|
15245
|
-
const workspacePath =
|
|
15246
|
-
const dir =
|
|
15564
|
+
const workspacePath = path31.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
15565
|
+
const dir = path31.dirname(workspacePath);
|
|
15247
15566
|
if (args.subcommand === "add") {
|
|
15248
|
-
const manifestPath =
|
|
15567
|
+
const manifestPath = path31.resolve(args.target);
|
|
15249
15568
|
const loadedTarget = await loadManifest(manifestPath);
|
|
15250
|
-
const lockPath = args.explicitLock ?
|
|
15569
|
+
const lockPath = args.explicitLock ? path31.resolve(args.explicitLock) : path31.join(path31.dirname(manifestPath), "pixelkiln.lock.json");
|
|
15251
15570
|
const ws = await loadWorkspace(workspacePath);
|
|
15252
15571
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
15253
15572
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -15267,10 +15586,10 @@ async function main() {
|
|
|
15267
15586
|
...args.account ? { account: args.account } : {}
|
|
15268
15587
|
};
|
|
15269
15588
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
15270
|
-
log(` registered "${id}" in ${
|
|
15589
|
+
log(` registered "${id}" in ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15271
15590
|
log(` manifest: ${project.manifest}`);
|
|
15272
15591
|
log(` lock: ${project.lock}`);
|
|
15273
|
-
if (!
|
|
15592
|
+
if (!existsSync25(lockPath)) {
|
|
15274
15593
|
log(
|
|
15275
15594
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
15276
15595
|
);
|
|
@@ -15279,7 +15598,7 @@ async function main() {
|
|
|
15279
15598
|
}
|
|
15280
15599
|
if (args.subcommand === "remove") {
|
|
15281
15600
|
const ws = await loadWorkspace(workspacePath);
|
|
15282
|
-
const resolvedTarget =
|
|
15601
|
+
const resolvedTarget = path31.resolve(args.target);
|
|
15283
15602
|
const match = ws.projects.find(
|
|
15284
15603
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
15285
15604
|
);
|
|
@@ -15290,11 +15609,11 @@ async function main() {
|
|
|
15290
15609
|
version: 1,
|
|
15291
15610
|
projects: ws.projects.filter((p) => p !== match)
|
|
15292
15611
|
});
|
|
15293
|
-
log(` removed "${match.id}" from ${
|
|
15612
|
+
log(` removed "${match.id}" from ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15294
15613
|
return;
|
|
15295
15614
|
}
|
|
15296
15615
|
if (args.subcommand === "list") {
|
|
15297
|
-
if (!
|
|
15616
|
+
if (!existsSync25(workspacePath)) {
|
|
15298
15617
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15299
15618
|
}
|
|
15300
15619
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -15302,9 +15621,9 @@ async function main() {
|
|
|
15302
15621
|
if (args.json) {
|
|
15303
15622
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
15304
15623
|
} else if (!ws.projects.length) {
|
|
15305
|
-
log(` no projects registered in ${
|
|
15624
|
+
log(` no projects registered in ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15306
15625
|
} else {
|
|
15307
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
15626
|
+
log(` ${ws.projects.length} project(s) in ${path31.relative(process.cwd(), workspacePath)}:`);
|
|
15308
15627
|
for (const p of ws.projects) {
|
|
15309
15628
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
15310
15629
|
}
|
|
@@ -15314,7 +15633,7 @@ async function main() {
|
|
|
15314
15633
|
return;
|
|
15315
15634
|
}
|
|
15316
15635
|
if (args.subcommand === "status") {
|
|
15317
|
-
if (!
|
|
15636
|
+
if (!existsSync25(workspacePath)) {
|
|
15318
15637
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15319
15638
|
}
|
|
15320
15639
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -15322,7 +15641,7 @@ async function main() {
|
|
|
15322
15641
|
if (args.json) {
|
|
15323
15642
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
15324
15643
|
} else {
|
|
15325
|
-
log(` workspace: ${
|
|
15644
|
+
log(` workspace: ${path31.relative(process.cwd(), workspacePath)}`);
|
|
15326
15645
|
for (const p of report.projects) {
|
|
15327
15646
|
if (p.error) {
|
|
15328
15647
|
log(`
|
|
@@ -15369,8 +15688,8 @@ async function main() {
|
|
|
15369
15688
|
}
|
|
15370
15689
|
}
|
|
15371
15690
|
if (args.command === "gallery" && args.workspace) {
|
|
15372
|
-
const workspacePath =
|
|
15373
|
-
if (!
|
|
15691
|
+
const workspacePath = path31.resolve(args.workspace);
|
|
15692
|
+
if (!existsSync25(workspacePath)) {
|
|
15374
15693
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
15375
15694
|
}
|
|
15376
15695
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -15390,7 +15709,7 @@ async function main() {
|
|
|
15390
15709
|
const registered = async (projectId) => {
|
|
15391
15710
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
15392
15711
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
15393
|
-
return resolveProject(
|
|
15712
|
+
return resolveProject(path31.dirname(workspacePath), project);
|
|
15394
15713
|
};
|
|
15395
15714
|
await runGallery(initial, build, args, {
|
|
15396
15715
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -15405,14 +15724,14 @@ async function main() {
|
|
|
15405
15724
|
});
|
|
15406
15725
|
return;
|
|
15407
15726
|
}
|
|
15408
|
-
if (!
|
|
15727
|
+
if (!existsSync25(path31.resolve(args.manifest))) {
|
|
15409
15728
|
throw new Error(
|
|
15410
|
-
`No manifest at ${
|
|
15729
|
+
`No manifest at ${path31.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
15411
15730
|
);
|
|
15412
15731
|
}
|
|
15413
|
-
const manifestDir =
|
|
15732
|
+
const manifestDir = path31.dirname(path31.resolve(args.manifest));
|
|
15414
15733
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
15415
|
-
if (
|
|
15734
|
+
if (path31.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
15416
15735
|
const loaded = await loadManifest(args.manifest);
|
|
15417
15736
|
let specs = await resolveSpecs(loaded, {
|
|
15418
15737
|
styles: args.styles,
|
|
@@ -15444,6 +15763,40 @@ async function main() {
|
|
|
15444
15763
|
}
|
|
15445
15764
|
const lock = await loadLock(args.lock);
|
|
15446
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
|
+
}
|
|
15447
15800
|
if (args.command === "status") {
|
|
15448
15801
|
const byStatus = {};
|
|
15449
15802
|
for (const e of Object.values(lock.entries)) byStatus[e.status] = (byStatus[e.status] ?? 0) + 1;
|
|
@@ -15485,7 +15838,7 @@ async function main() {
|
|
|
15485
15838
|
});
|
|
15486
15839
|
};
|
|
15487
15840
|
await runGallery(await build(), reload, args, {
|
|
15488
|
-
manifestFor: () =>
|
|
15841
|
+
manifestFor: () => path31.resolve(args.manifest),
|
|
15489
15842
|
loadProject: async () => {
|
|
15490
15843
|
const freshLoaded = await loadManifest(args.manifest);
|
|
15491
15844
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -15513,7 +15866,7 @@ async function main() {
|
|
|
15513
15866
|
const set = started.members.length > 1;
|
|
15514
15867
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
15515
15868
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
15516
|
-
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)}`);
|
|
15517
15870
|
if (args.noOpen) {
|
|
15518
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`);
|
|
15519
15872
|
} else {
|
|
@@ -15563,7 +15916,7 @@ async function main() {
|
|
|
15563
15916
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
15564
15917
|
if (item.state === "needs-approval") {
|
|
15565
15918
|
log(
|
|
15566
|
-
` pixelkiln refine approve --from ${
|
|
15919
|
+
` pixelkiln refine approve --from ${path31.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
15567
15920
|
);
|
|
15568
15921
|
}
|
|
15569
15922
|
}
|
|
@@ -15654,7 +16007,7 @@ async function main() {
|
|
|
15654
16007
|
let intact = true;
|
|
15655
16008
|
for (const output of entry.outputs) {
|
|
15656
16009
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
15657
|
-
if (!
|
|
16010
|
+
if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
|
|
15658
16011
|
intact = false;
|
|
15659
16012
|
break;
|
|
15660
16013
|
}
|
|
@@ -15712,7 +16065,7 @@ async function main() {
|
|
|
15712
16065
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
15713
16066
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
15714
16067
|
}
|
|
15715
|
-
const manifestDir2 =
|
|
16068
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15716
16069
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
15717
16070
|
for (const styleId of styleIds) {
|
|
15718
16071
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -15725,7 +16078,7 @@ async function main() {
|
|
|
15725
16078
|
sources: manifestSources(loaded.manifest, styleId)
|
|
15726
16079
|
});
|
|
15727
16080
|
const style = loaded.manifest.styles[styleId];
|
|
15728
|
-
const base = args.out ?
|
|
16081
|
+
const base = args.out ? path31.resolve(args.out.replace(/\.png$/, "")) : path31.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
15729
16082
|
const outputs = [
|
|
15730
16083
|
{ path: `${base}.png`, data: png },
|
|
15731
16084
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -15755,13 +16108,13 @@ async function main() {
|
|
|
15755
16108
|
log(
|
|
15756
16109
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
15757
16110
|
);
|
|
15758
|
-
log(` ${
|
|
16111
|
+
log(` ${path31.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
15759
16112
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15760
16113
|
}
|
|
15761
16114
|
return;
|
|
15762
16115
|
}
|
|
15763
16116
|
if (args.command === "mount") {
|
|
15764
|
-
const manifestDir2 =
|
|
16117
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15765
16118
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
15766
16119
|
for (const styleId of styleIds) {
|
|
15767
16120
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -15800,7 +16153,7 @@ async function main() {
|
|
|
15800
16153
|
sources,
|
|
15801
16154
|
outputRoles
|
|
15802
16155
|
);
|
|
15803
|
-
const out =
|
|
16156
|
+
const out = path31.resolve(manifestDir2, style.mount.out);
|
|
15804
16157
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
15805
16158
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
15806
16159
|
const outputs = [
|
|
@@ -15820,7 +16173,7 @@ async function main() {
|
|
|
15820
16173
|
await provenanceFile("$lock", args.lock),
|
|
15821
16174
|
...qualityRecords,
|
|
15822
16175
|
...artifactSources.filter(
|
|
15823
|
-
(source) => source.id !== "$base" ||
|
|
16176
|
+
(source) => source.id !== "$base" || path31.resolve(source.path) !== out
|
|
15824
16177
|
)
|
|
15825
16178
|
],
|
|
15826
16179
|
options: {
|
|
@@ -15833,7 +16186,7 @@ async function main() {
|
|
|
15833
16186
|
log(
|
|
15834
16187
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
15835
16188
|
);
|
|
15836
|
-
log(` ${
|
|
16189
|
+
log(` ${path31.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
15837
16190
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
15838
16191
|
}
|
|
15839
16192
|
return;
|
|
@@ -15889,7 +16242,7 @@ async function main() {
|
|
|
15889
16242
|
}
|
|
15890
16243
|
if (args.command === "export") {
|
|
15891
16244
|
const format = args.format ?? "generic";
|
|
15892
|
-
const manifestDir2 =
|
|
16245
|
+
const manifestDir2 = path31.dirname(path31.resolve(args.manifest));
|
|
15893
16246
|
const selected = specs.filter((spec) => {
|
|
15894
16247
|
if (spec.generator !== "tiles") return false;
|
|
15895
16248
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -15905,12 +16258,12 @@ async function main() {
|
|
|
15905
16258
|
for (const spec of selected) {
|
|
15906
16259
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
15907
16260
|
const style = loaded.manifest.styles[spec.styleId];
|
|
15908
|
-
const defaultBase =
|
|
15909
|
-
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;
|
|
15910
16263
|
const result = exportTileset(entry, spec, {
|
|
15911
16264
|
format,
|
|
15912
16265
|
manifestDir: manifestDir2,
|
|
15913
|
-
imageName:
|
|
16266
|
+
imageName: path31.basename(`${base}.png`),
|
|
15914
16267
|
columns: args.columns
|
|
15915
16268
|
});
|
|
15916
16269
|
const outputs = [
|
|
@@ -15928,7 +16281,7 @@ async function main() {
|
|
|
15928
16281
|
asset: spec.assetId,
|
|
15929
16282
|
columns: args.columns ?? null,
|
|
15930
16283
|
format,
|
|
15931
|
-
image:
|
|
16284
|
+
image: path31.basename(`${base}.png`),
|
|
15932
16285
|
providerRules: result.generic.providerRules,
|
|
15933
16286
|
style: spec.styleId,
|
|
15934
16287
|
tileType: spec.tileType ?? null
|
|
@@ -15938,12 +16291,32 @@ async function main() {
|
|
|
15938
16291
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
15939
16292
|
);
|
|
15940
16293
|
log(
|
|
15941
|
-
` ${
|
|
16294
|
+
` ${path31.relative(process.cwd(), base)}.png + ${path31.basename(base)}${result.extension} + .pixelkiln.json`
|
|
15942
16295
|
);
|
|
15943
16296
|
}
|
|
15944
16297
|
return;
|
|
15945
16298
|
}
|
|
15946
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
|
+
}
|
|
15947
16320
|
const providers = /* @__PURE__ */ new Map();
|
|
15948
16321
|
const providerFor = (id) => {
|
|
15949
16322
|
let resolved = providers.get(id);
|
|
@@ -15988,12 +16361,12 @@ async function main() {
|
|
|
15988
16361
|
tagged ${n} object(s) upstream`);
|
|
15989
16362
|
}
|
|
15990
16363
|
if (args.writePrompts) {
|
|
15991
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
16364
|
+
const { filled, stillEmpty } = await writePromptsBack(path31.resolve(args.manifest), lock, {
|
|
15992
16365
|
onProgress: log,
|
|
15993
16366
|
provider: provider.id,
|
|
15994
16367
|
assetIds: specs.map((spec) => spec.assetId)
|
|
15995
16368
|
});
|
|
15996
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
16369
|
+
log(` recovered ${filled} prompt(s) into ${path31.relative(process.cwd(), args.manifest)}`);
|
|
15997
16370
|
const reloaded = await loadManifest(args.manifest);
|
|
15998
16371
|
const rebased = (await resolveSpecs(reloaded, {
|
|
15999
16372
|
styles: args.styles,
|
|
@@ -16022,12 +16395,12 @@ async function main() {
|
|
|
16022
16395
|
const provider = providerFor(selectedAccountProvider);
|
|
16023
16396
|
const jsonMode = args.dryRun && args.json;
|
|
16024
16397
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
16025
|
-
const ownLock =
|
|
16398
|
+
const ownLock = path31.resolve(args.lock);
|
|
16026
16399
|
let workspaceProjects = [];
|
|
16027
16400
|
let workspaceDir = "";
|
|
16028
16401
|
if (args.workspace) {
|
|
16029
|
-
const workspacePath =
|
|
16030
|
-
workspaceDir =
|
|
16402
|
+
const workspacePath = path31.resolve(args.workspace);
|
|
16403
|
+
workspaceDir = path31.dirname(workspacePath);
|
|
16031
16404
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
16032
16405
|
workspaceProjects = complete.ws.projects;
|
|
16033
16406
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -16036,12 +16409,12 @@ async function main() {
|
|
|
16036
16409
|
const lockPaths = [
|
|
16037
16410
|
.../* @__PURE__ */ new Set([
|
|
16038
16411
|
...workspaceLockPaths,
|
|
16039
|
-
...
|
|
16040
|
-
...args.claims.map((c) =>
|
|
16412
|
+
...existsSync25(ownLock) ? [ownLock] : [],
|
|
16413
|
+
...args.claims.map((c) => path31.resolve(c))
|
|
16041
16414
|
])
|
|
16042
16415
|
];
|
|
16043
16416
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
16044
|
-
for (const p of lockPaths) diag(` ${
|
|
16417
|
+
for (const p of lockPaths) diag(` ${path31.relative(process.cwd(), p)}`);
|
|
16045
16418
|
if (!args.claims.length && !args.workspace) {
|
|
16046
16419
|
diag(
|
|
16047
16420
|
`
|
|
@@ -16049,7 +16422,7 @@ async function main() {
|
|
|
16049
16422
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
16050
16423
|
register every project in a workspace catalog and pass --workspace.`
|
|
16051
16424
|
);
|
|
16052
|
-
} 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))) {
|
|
16053
16426
|
diag(
|
|
16054
16427
|
`
|
|
16055
16428
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -16133,7 +16506,7 @@ async function main() {
|
|
|
16133
16506
|
manifestPath: loaded.path,
|
|
16134
16507
|
manifest: accountManifest,
|
|
16135
16508
|
styleId,
|
|
16136
|
-
importDir:
|
|
16509
|
+
importDir: path31.resolve(loaded.root, style.outDir),
|
|
16137
16510
|
lock,
|
|
16138
16511
|
lockPath: args.lock
|
|
16139
16512
|
},
|