pixelkiln 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +776 -332
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +511 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +247 -113
- package/dist/index.d.ts +247 -113
- package/dist/index.js +499 -108
- package/dist/index.js.map +1 -1
- package/docs/ARTIFACTS.md +10 -5
- package/docs/CLI.md +54 -1
- package/docs/MANIFEST.md +5 -2
- package/docs/PIXELLAB.md +12 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +8 -0
- package/skills/pixelkiln/SKILL.md +4 -1
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 path28 from "path";
|
|
5
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6
6
|
import { readFile as readFile22 } 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(path29) {
|
|
138
138
|
return new Promise((resolve, reject) => {
|
|
139
139
|
const hash = createHash("sha256");
|
|
140
|
-
const stream = createReadStream(
|
|
140
|
+
const stream = createReadStream(path29);
|
|
141
141
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
142
142
|
stream.on("error", reject);
|
|
143
143
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -1617,11 +1617,11 @@ var PixelLabClient = class {
|
|
|
1617
1617
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1618
1618
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1619
1619
|
*/
|
|
1620
|
-
async request(
|
|
1620
|
+
async request(path29, init, attempt = 0) {
|
|
1621
1621
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1622
1622
|
let res;
|
|
1623
1623
|
try {
|
|
1624
|
-
res = await fetch(`${BASE}${
|
|
1624
|
+
res = await fetch(`${BASE}${path29}`, {
|
|
1625
1625
|
...init,
|
|
1626
1626
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1627
1627
|
headers: {
|
|
@@ -1633,24 +1633,24 @@ var PixelLabClient = class {
|
|
|
1633
1633
|
} catch (err) {
|
|
1634
1634
|
if (attempt < MAX_RETRIES) {
|
|
1635
1635
|
await sleep(backoffMs(attempt));
|
|
1636
|
-
return this.request(
|
|
1636
|
+
return this.request(path29, init, attempt + 1);
|
|
1637
1637
|
}
|
|
1638
1638
|
throw err;
|
|
1639
1639
|
}
|
|
1640
1640
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1641
1641
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1642
1642
|
await sleep(waitMs);
|
|
1643
|
-
return this.request(
|
|
1643
|
+
return this.request(path29, init, attempt + 1);
|
|
1644
1644
|
}
|
|
1645
1645
|
const text = await res.text();
|
|
1646
1646
|
if (!res.ok) {
|
|
1647
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1647
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path29} \u2192 ${res.status}`, res.status, text);
|
|
1648
1648
|
}
|
|
1649
1649
|
if (!text) return {};
|
|
1650
1650
|
try {
|
|
1651
1651
|
return JSON.parse(text);
|
|
1652
1652
|
} catch {
|
|
1653
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1653
|
+
throw new Error(`${init?.method ?? "GET"} ${path29} returned invalid JSON`);
|
|
1654
1654
|
}
|
|
1655
1655
|
}
|
|
1656
1656
|
async balance() {
|
|
@@ -2112,6 +2112,15 @@ var AssetSchema = z2.object({
|
|
|
2112
2112
|
* that wasn't. `prompt` still records what was asked for.
|
|
2113
2113
|
*/
|
|
2114
2114
|
source: z2.string().optional(),
|
|
2115
|
+
/**
|
|
2116
|
+
* Per-style replacement for `source`, keyed by style id.
|
|
2117
|
+
*
|
|
2118
|
+
* A hand edit belongs to one generation: the same asset generated in two
|
|
2119
|
+
* styles is two different pictures, so the touch-up of one must not be
|
|
2120
|
+
* placed for the other. `pixelkiln edit` writes here when an asset is in
|
|
2121
|
+
* more than one style, and to `source` when it is in exactly one.
|
|
2122
|
+
*/
|
|
2123
|
+
sourceByStyle: z2.record(z2.string().min(1)).default({}),
|
|
2115
2124
|
/** Controlled generation derived from another asset in the same style. */
|
|
2116
2125
|
revision: RevisionSchema.optional(),
|
|
2117
2126
|
/**
|
|
@@ -2146,7 +2155,7 @@ var AssetSchema = z2.object({
|
|
|
2146
2155
|
z2.array(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).min(2).max(64)
|
|
2147
2156
|
])).default({})
|
|
2148
2157
|
}).strict().superRefine((asset, context) => {
|
|
2149
|
-
if (asset.source && asset.revision) {
|
|
2158
|
+
if ((asset.source || Object.keys(asset.sourceByStyle).length) && asset.revision) {
|
|
2150
2159
|
context.addIssue({
|
|
2151
2160
|
code: z2.ZodIssueCode.custom,
|
|
2152
2161
|
message: "source and revision are mutually exclusive",
|
|
@@ -2280,6 +2289,10 @@ function lockKey(styleId, assetId) {
|
|
|
2280
2289
|
}
|
|
2281
2290
|
|
|
2282
2291
|
// src/providers/pixellab.ts
|
|
2292
|
+
function pixelLabObjectUrl(generator, objectId) {
|
|
2293
|
+
if (!objectId || generator !== "map" && generator !== "1dir") return null;
|
|
2294
|
+
return `https://www.pixellab.ai/objects/${encodeURIComponent(objectId)}`;
|
|
2295
|
+
}
|
|
2283
2296
|
var PixelLabProvider = class _PixelLabProvider {
|
|
2284
2297
|
constructor(client) {
|
|
2285
2298
|
this.client = client;
|
|
@@ -2613,9 +2626,9 @@ var RetroDiffusionClient = class {
|
|
|
2613
2626
|
}
|
|
2614
2627
|
return balance;
|
|
2615
2628
|
}
|
|
2616
|
-
async call(
|
|
2629
|
+
async call(path29, init = {}) {
|
|
2617
2630
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2618
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2631
|
+
const response = await this.request(`${this.baseUrl}${path29}`, {
|
|
2619
2632
|
...init,
|
|
2620
2633
|
headers: {
|
|
2621
2634
|
"Content-Type": "application/json",
|
|
@@ -3784,6 +3797,11 @@ ${issues}`);
|
|
|
3784
3797
|
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3785
3798
|
}
|
|
3786
3799
|
}
|
|
3800
|
+
for (const styleId of Object.keys(asset.sourceByStyle)) {
|
|
3801
|
+
if (!styleIds.has(styleId)) {
|
|
3802
|
+
unknownReferences.push(`assets.${assetId}.sourceByStyle: unknown style "${styleId}"`);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3787
3805
|
if (asset.revision) {
|
|
3788
3806
|
if (!parsed.data.assets[asset.revision.from]) {
|
|
3789
3807
|
unknownReferences.push(
|
|
@@ -4048,7 +4066,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4048
4066
|
}
|
|
4049
4067
|
} : {},
|
|
4050
4068
|
tags,
|
|
4051
|
-
source: asset.source,
|
|
4069
|
+
source: asset.sourceByStyle[styleId] ?? asset.source,
|
|
4052
4070
|
// Revision identity is attached after every dependency in this style
|
|
4053
4071
|
// has a concrete output target. Finalization below computes the hash.
|
|
4054
4072
|
specHash: ""
|
|
@@ -4306,16 +4324,24 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
4306
4324
|
}
|
|
4307
4325
|
const prefix = `${styleId}/`;
|
|
4308
4326
|
const entries = Object.entries(lock.entries).filter(([key]) => key.startsWith(prefix));
|
|
4309
|
-
|
|
4327
|
+
const sources = options.sources ?? {};
|
|
4328
|
+
if (!entries.length && !Object.keys(sources).length) {
|
|
4310
4329
|
throw new Error(
|
|
4311
4330
|
`No locked assets for style "${styleId}". Run \`pixelkiln gen --style ${styleId}\` first.`
|
|
4312
4331
|
);
|
|
4313
4332
|
}
|
|
4314
4333
|
const inputs = [];
|
|
4315
4334
|
const noOutput = [];
|
|
4335
|
+
const locked = /* @__PURE__ */ new Set();
|
|
4316
4336
|
for (const [key, entry] of entries) {
|
|
4317
4337
|
const id = key.slice(prefix.length);
|
|
4338
|
+
locked.add(id);
|
|
4318
4339
|
const outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
4340
|
+
const source = sources[id];
|
|
4341
|
+
if (source && outputs.length <= 1) {
|
|
4342
|
+
inputs.push({ id, path: path7.resolve(manifestDir, source) });
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4319
4345
|
let selected = outputs;
|
|
4320
4346
|
if (options.outputRoles?.length) {
|
|
4321
4347
|
selected = outputs.filter(
|
|
@@ -4332,6 +4358,9 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
4332
4358
|
}
|
|
4333
4359
|
for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
|
|
4334
4360
|
}
|
|
4361
|
+
for (const [id, source] of Object.entries(sources)) {
|
|
4362
|
+
if (!locked.has(id)) inputs.push({ id, path: path7.resolve(manifestDir, source) });
|
|
4363
|
+
}
|
|
4335
4364
|
if (!inputs.length) {
|
|
4336
4365
|
throw new Error(`No readable sprites for style "${styleId}" \u2014 ${noOutput.length} skipped.`);
|
|
4337
4366
|
}
|
|
@@ -6423,14 +6452,16 @@ function shouldPersistSourceUrl(value) {
|
|
|
6423
6452
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
6424
6453
|
const log2 = opts.onProgress ?? (() => {
|
|
6425
6454
|
});
|
|
6426
|
-
const result = { downloaded: 0, skipped: 0, failed: 0 };
|
|
6455
|
+
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
6427
6456
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
6428
6457
|
normalizeLockOutputPaths(lock, specs);
|
|
6429
6458
|
const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
|
|
6430
6459
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
6431
6460
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
6432
6461
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
6433
|
-
if (
|
|
6462
|
+
if (e.status !== "downloaded") return false;
|
|
6463
|
+
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
6464
|
+
if (!opts.repair) return false;
|
|
6434
6465
|
const spec = specByKey.get(key);
|
|
6435
6466
|
return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync9(resolveOutputPath(output.path, spec.root)));
|
|
6436
6467
|
});
|
|
@@ -6462,6 +6493,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6462
6493
|
continue;
|
|
6463
6494
|
}
|
|
6464
6495
|
const outputs = [];
|
|
6496
|
+
let wrote = false;
|
|
6465
6497
|
try {
|
|
6466
6498
|
for (let index = 0; index < sources.length; index++) {
|
|
6467
6499
|
const source = sources[index];
|
|
@@ -6469,41 +6501,55 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6469
6501
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
6470
6502
|
);
|
|
6471
6503
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
6504
|
+
let fresh = null;
|
|
6505
|
+
if (opts.refresh) {
|
|
6506
|
+
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
6507
|
+
fresh = await provider.download(source.url);
|
|
6508
|
+
if (recorded && sha256(fresh) === recorded.sha256) {
|
|
6509
|
+
log2(` current ${path14.relative(process.cwd(), target)}`);
|
|
6510
|
+
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
6511
|
+
continue;
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6472
6514
|
if (existsSync9(target)) {
|
|
6473
6515
|
const currentHash = await sha256File(target);
|
|
6474
6516
|
if (recorded && currentHash === recorded.sha256) {
|
|
6475
|
-
if (
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6517
|
+
if (!fresh) {
|
|
6518
|
+
if (cacheDir) {
|
|
6519
|
+
await cacheMedia(
|
|
6520
|
+
cacheDir,
|
|
6521
|
+
await readFile7(target),
|
|
6522
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
6523
|
+
recorded.sha256
|
|
6524
|
+
);
|
|
6525
|
+
}
|
|
6526
|
+
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
6527
|
+
continue;
|
|
6482
6528
|
}
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6529
|
+
log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
|
|
6530
|
+
} else {
|
|
6531
|
+
const superseded = (entry.supersededOutputs ?? []).find(
|
|
6532
|
+
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
6533
|
+
);
|
|
6534
|
+
if (!opts.force) {
|
|
6535
|
+
if (!recorded && superseded && currentHash === superseded.sha256) {
|
|
6536
|
+
} else if (recorded || superseded) {
|
|
6537
|
+
throw new Error(
|
|
6538
|
+
`refusing to overwrite modified output ${target}; pass --force to replace it`
|
|
6539
|
+
);
|
|
6540
|
+
} else {
|
|
6541
|
+
throw new Error(
|
|
6542
|
+
`refusing to overwrite untracked output ${target}; pass --force to replace it`
|
|
6543
|
+
);
|
|
6544
|
+
}
|
|
6499
6545
|
}
|
|
6546
|
+
log2(
|
|
6547
|
+
` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
6548
|
+
);
|
|
6500
6549
|
}
|
|
6501
|
-
log2(
|
|
6502
|
-
` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
6503
|
-
);
|
|
6504
6550
|
}
|
|
6505
6551
|
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
6506
|
-
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
6552
|
+
let buf = fresh ?? (recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null);
|
|
6507
6553
|
if (buf) {
|
|
6508
6554
|
log2(` cached ${path14.relative(process.cwd(), target)}`);
|
|
6509
6555
|
} else {
|
|
@@ -6535,8 +6581,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
6535
6581
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
6536
6582
|
await saveLock(lockPath, lock);
|
|
6537
6583
|
await rename3(tmp, target);
|
|
6584
|
+
wrote = true;
|
|
6538
6585
|
log2(` wrote ${path14.relative(process.cwd(), target)}`);
|
|
6539
6586
|
}
|
|
6587
|
+
if (opts.refresh && !wrote) {
|
|
6588
|
+
result.unchanged = (result.unchanged ?? 0) + 1;
|
|
6589
|
+
continue;
|
|
6590
|
+
}
|
|
6540
6591
|
const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
|
|
6541
6592
|
upsert(lock, key, {
|
|
6542
6593
|
status: "downloaded",
|
|
@@ -6821,10 +6872,10 @@ function isSha256Hash(value) {
|
|
|
6821
6872
|
function parseCache(value) {
|
|
6822
6873
|
return HashCacheSchema.parse(value);
|
|
6823
6874
|
}
|
|
6824
|
-
async function loadCache(
|
|
6825
|
-
if (!existsSync11(
|
|
6875
|
+
async function loadCache(path29) {
|
|
6876
|
+
if (!existsSync11(path29)) return { version: 1, hashes: {} };
|
|
6826
6877
|
try {
|
|
6827
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
6878
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path29, "utf8")));
|
|
6828
6879
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6829
6880
|
return {
|
|
6830
6881
|
version: 1,
|
|
@@ -6836,18 +6887,18 @@ async function loadCache(path27) {
|
|
|
6836
6887
|
return { version: 1, hashes: {} };
|
|
6837
6888
|
}
|
|
6838
6889
|
}
|
|
6839
|
-
async function saveCache(
|
|
6890
|
+
async function saveCache(path29, cache) {
|
|
6840
6891
|
const sorted = {};
|
|
6841
6892
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
6842
6893
|
const hash = cache.hashes[key];
|
|
6843
6894
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
6844
6895
|
sorted[key] = hash;
|
|
6845
6896
|
}
|
|
6846
|
-
await mkdir4(pathModule.dirname(pathModule.resolve(
|
|
6847
|
-
const tmp = `${
|
|
6897
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path29)), { recursive: true });
|
|
6898
|
+
const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
6848
6899
|
try {
|
|
6849
6900
|
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
6850
|
-
await rename4(tmp,
|
|
6901
|
+
await rename4(tmp, path29);
|
|
6851
6902
|
} finally {
|
|
6852
6903
|
await rm5(tmp, { force: true });
|
|
6853
6904
|
}
|
|
@@ -7096,6 +7147,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
7096
7147
|
assets[asset.id] = {
|
|
7097
7148
|
prompt: "",
|
|
7098
7149
|
promptByStyle: {},
|
|
7150
|
+
sourceByStyle: {},
|
|
7099
7151
|
category: asset.category || void 0,
|
|
7100
7152
|
file: asset.file,
|
|
7101
7153
|
tags: [],
|
|
@@ -7789,9 +7841,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
7789
7841
|
|
|
7790
7842
|
// src/gallery/snapshot.ts
|
|
7791
7843
|
import { createHash as createHash3 } from "crypto";
|
|
7792
|
-
import { existsSync as
|
|
7844
|
+
import { existsSync as existsSync17 } from "fs";
|
|
7793
7845
|
import { readFile as readFile15, stat as stat3 } from "fs/promises";
|
|
7794
|
-
import
|
|
7846
|
+
import path22 from "path";
|
|
7795
7847
|
|
|
7796
7848
|
// src/workspace.ts
|
|
7797
7849
|
import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -7927,6 +7979,10 @@ function validateWorkspace(ws, dir) {
|
|
|
7927
7979
|
}
|
|
7928
7980
|
|
|
7929
7981
|
// src/gallery/edit.ts
|
|
7982
|
+
import path21 from "path";
|
|
7983
|
+
import { z as z6 } from "zod";
|
|
7984
|
+
|
|
7985
|
+
// src/manifest-edit.ts
|
|
7930
7986
|
import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
7931
7987
|
import path19 from "path";
|
|
7932
7988
|
import { z as z5 } from "zod";
|
|
@@ -7973,6 +8029,22 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
|
|
|
7973
8029
|
expectedSha256: HexSha,
|
|
7974
8030
|
asset: NewAssetSchema
|
|
7975
8031
|
}).strict(),
|
|
8032
|
+
z5.object({
|
|
8033
|
+
action: z5.literal("set-source"),
|
|
8034
|
+
project: z5.string().min(1).optional(),
|
|
8035
|
+
assetId: z5.string().min(1),
|
|
8036
|
+
styleId: z5.string().min(1),
|
|
8037
|
+
/** Manifest-relative path of the art that stands in for this asset in this style. */
|
|
8038
|
+
source: z5.string().min(1),
|
|
8039
|
+
expectedSha256: HexSha
|
|
8040
|
+
}).strict(),
|
|
8041
|
+
z5.object({
|
|
8042
|
+
action: z5.literal("clear-source"),
|
|
8043
|
+
project: z5.string().min(1).optional(),
|
|
8044
|
+
assetId: z5.string().min(1),
|
|
8045
|
+
styleId: z5.string().min(1),
|
|
8046
|
+
expectedSha256: HexSha
|
|
8047
|
+
}).strict(),
|
|
7976
8048
|
z5.object({
|
|
7977
8049
|
action: z5.literal("patch-style"),
|
|
7978
8050
|
project: z5.string().min(1).optional(),
|
|
@@ -8023,8 +8095,38 @@ function setOrDelete(target, key, value) {
|
|
|
8023
8095
|
if (value === null || value === void 0) delete target[key];
|
|
8024
8096
|
else target[key] = value;
|
|
8025
8097
|
}
|
|
8098
|
+
function assetStyles(raw, asset) {
|
|
8099
|
+
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
8100
|
+
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
8101
|
+
}
|
|
8026
8102
|
function applyEdit(raw, edit) {
|
|
8027
8103
|
raw.assets ??= {};
|
|
8104
|
+
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
8105
|
+
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
8106
|
+
if (!asset2) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
|
|
8107
|
+
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
8108
|
+
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
8109
|
+
}
|
|
8110
|
+
if (!assetStyles(raw, asset2).includes(edit.styleId)) {
|
|
8111
|
+
throw new ManifestEditError(`asset "${edit.assetId}" is not in style "${edit.styleId}"`);
|
|
8112
|
+
}
|
|
8113
|
+
const single = assetStyles(raw, asset2).length === 1;
|
|
8114
|
+
const byStyle = { ...asset2.sourceByStyle ?? {} };
|
|
8115
|
+
if (edit.action === "set-source") {
|
|
8116
|
+
if (single) {
|
|
8117
|
+
asset2.source = edit.source;
|
|
8118
|
+
delete byStyle[edit.styleId];
|
|
8119
|
+
} else {
|
|
8120
|
+
byStyle[edit.styleId] = edit.source;
|
|
8121
|
+
}
|
|
8122
|
+
} else {
|
|
8123
|
+
delete byStyle[edit.styleId];
|
|
8124
|
+
if (single) delete asset2.source;
|
|
8125
|
+
}
|
|
8126
|
+
if (Object.keys(byStyle).length) asset2.sourceByStyle = byStyle;
|
|
8127
|
+
else delete asset2.sourceByStyle;
|
|
8128
|
+
return;
|
|
8129
|
+
}
|
|
8028
8130
|
if (edit.action === "patch-style") {
|
|
8029
8131
|
const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
|
|
8030
8132
|
if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
|
|
@@ -8128,10 +8230,134 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
8128
8230
|
}
|
|
8129
8231
|
return { manifestPath: absolute, sha256: sha256(next), changed: true };
|
|
8130
8232
|
}
|
|
8233
|
+
|
|
8234
|
+
// src/pipeline/hand-edit.ts
|
|
8235
|
+
import { spawn as spawn2 } from "child_process";
|
|
8236
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8237
|
+
import { copyFile, mkdir as mkdir6 } from "fs/promises";
|
|
8238
|
+
import path20 from "path";
|
|
8239
|
+
var HAND_EDIT_DIR = "edits";
|
|
8240
|
+
function handEditPath(loaded, spec) {
|
|
8241
|
+
const style = loaded.manifest.styles[spec.styleId];
|
|
8242
|
+
const outDir = path20.resolve(loaded.root, style?.outDir ?? ".");
|
|
8243
|
+
const relative = path20.relative(outDir, spec.outFile);
|
|
8244
|
+
const inside = relative && !relative.startsWith("..") && !path20.isAbsolute(relative);
|
|
8245
|
+
const file = inside ? relative : path20.basename(spec.outFile);
|
|
8246
|
+
return path20.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
8247
|
+
}
|
|
8248
|
+
function handEditBase(spec, lock) {
|
|
8249
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8250
|
+
if (entry) {
|
|
8251
|
+
if (entry.outputs.length !== 1) return null;
|
|
8252
|
+
const primary = primaryOutput(entry);
|
|
8253
|
+
if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
|
|
8254
|
+
const file = currentEntryOutputPath(entry, spec, 0);
|
|
8255
|
+
return existsSync16(file) ? file : null;
|
|
8256
|
+
}
|
|
8257
|
+
return existsSync16(spec.outFile) ? spec.outFile : null;
|
|
8258
|
+
}
|
|
8259
|
+
async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
8260
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
8261
|
+
if (entry && entry.outputs.length > 1) {
|
|
8262
|
+
throw new ManifestEditError(
|
|
8263
|
+
`${spec.styleId}/${spec.assetId} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
|
|
8264
|
+
);
|
|
8265
|
+
}
|
|
8266
|
+
const existing = spec.source ? path20.resolve(loaded.root, spec.source) : null;
|
|
8267
|
+
const editPath = existing ?? handEditPath(loaded, spec);
|
|
8268
|
+
let created = false;
|
|
8269
|
+
if (!existsSync16(editPath)) {
|
|
8270
|
+
const base = handEditBase(spec, lock);
|
|
8271
|
+
if (!base) {
|
|
8272
|
+
throw new ManifestEditError(
|
|
8273
|
+
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
8274
|
+
);
|
|
8275
|
+
}
|
|
8276
|
+
await mkdir6(path20.dirname(editPath), { recursive: true });
|
|
8277
|
+
await copyFile(base, editPath);
|
|
8278
|
+
created = true;
|
|
8279
|
+
}
|
|
8280
|
+
const source = portableOutputPath(editPath, loaded.root);
|
|
8281
|
+
let declared = false;
|
|
8282
|
+
let manifestSha256 = opts.expectedSha256 ?? await sha256File(loaded.path);
|
|
8283
|
+
if (spec.source !== source) {
|
|
8284
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
8285
|
+
action: "set-source",
|
|
8286
|
+
assetId: spec.assetId,
|
|
8287
|
+
styleId: spec.styleId,
|
|
8288
|
+
source,
|
|
8289
|
+
expectedSha256: manifestSha256
|
|
8290
|
+
});
|
|
8291
|
+
declared = result.changed;
|
|
8292
|
+
manifestSha256 = result.sha256;
|
|
8293
|
+
}
|
|
8294
|
+
return { editPath, source, created, declared, manifestSha256 };
|
|
8295
|
+
}
|
|
8296
|
+
async function detachHandEdit(loaded, spec, opts = {}) {
|
|
8297
|
+
if (!spec.source) {
|
|
8298
|
+
return { manifestSha256: opts.expectedSha256 ?? await sha256File(loaded.path), changed: false };
|
|
8299
|
+
}
|
|
8300
|
+
const result = await applyManifestEdit(loaded.path, {
|
|
8301
|
+
action: "clear-source",
|
|
8302
|
+
assetId: spec.assetId,
|
|
8303
|
+
styleId: spec.styleId,
|
|
8304
|
+
expectedSha256: opts.expectedSha256 ?? await sha256File(loaded.path)
|
|
8305
|
+
});
|
|
8306
|
+
return { manifestSha256: result.sha256, changed: result.changed };
|
|
8307
|
+
}
|
|
8308
|
+
function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
8309
|
+
const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
|
|
8310
|
+
const [program, ...args] = command;
|
|
8311
|
+
const child = spawn2(program, [...args, file], { stdio: "ignore", detached: true });
|
|
8312
|
+
child.on("error", () => {
|
|
8313
|
+
});
|
|
8314
|
+
child.unref();
|
|
8315
|
+
return command.join(" ");
|
|
8316
|
+
}
|
|
8317
|
+
|
|
8318
|
+
// src/gallery/edit.ts
|
|
8319
|
+
var HandEditRequestSchema = z6.discriminatedUnion("action", [
|
|
8320
|
+
z6.object({
|
|
8321
|
+
action: z6.literal("start-edit"),
|
|
8322
|
+
project: z6.string().min(1).optional(),
|
|
8323
|
+
assetId: z6.string().min(1),
|
|
8324
|
+
styleId: z6.string().min(1),
|
|
8325
|
+
expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
8326
|
+
/** Also launch the author's editor on the file. */
|
|
8327
|
+
open: z6.boolean().optional()
|
|
8328
|
+
}).strict(),
|
|
8329
|
+
z6.object({
|
|
8330
|
+
action: z6.literal("detach-edit"),
|
|
8331
|
+
project: z6.string().min(1).optional(),
|
|
8332
|
+
assetId: z6.string().min(1),
|
|
8333
|
+
styleId: z6.string().min(1),
|
|
8334
|
+
expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/)
|
|
8335
|
+
}).strict()
|
|
8336
|
+
]);
|
|
8131
8337
|
function createGalleryEditHandler(opts) {
|
|
8132
8338
|
const log2 = opts.onProgress ?? (() => {
|
|
8133
8339
|
});
|
|
8134
8340
|
return async (body) => {
|
|
8341
|
+
const handEdit = HandEditRequestSchema.safeParse(body);
|
|
8342
|
+
if (handEdit.success) {
|
|
8343
|
+
if (!opts.loadProject) throw new ManifestEditError("hand edits are not available in this gallery");
|
|
8344
|
+
const request = handEdit.data;
|
|
8345
|
+
const ctx = await opts.loadProject(request.project);
|
|
8346
|
+
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === lockKey(request.styleId, request.assetId));
|
|
8347
|
+
if (!spec) throw new ManifestEditError(`"${request.styleId}/${request.assetId}" is not declared by the manifest`);
|
|
8348
|
+
if (request.action === "start-edit") {
|
|
8349
|
+
const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
|
|
8350
|
+
log2(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
|
|
8351
|
+
if (request.open) {
|
|
8352
|
+
const command = (opts.openEditor ?? openInEditor)(started.editPath);
|
|
8353
|
+
log2(` opened with: ${command}`);
|
|
8354
|
+
}
|
|
8355
|
+
} else {
|
|
8356
|
+
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
8357
|
+
if (result2.changed) log2(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
8358
|
+
}
|
|
8359
|
+
return opts.reload();
|
|
8360
|
+
}
|
|
8135
8361
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
8136
8362
|
if (!parsed.success) {
|
|
8137
8363
|
throw new ManifestEditError(
|
|
@@ -8146,9 +8372,9 @@ function createGalleryEditHandler(opts) {
|
|
|
8146
8372
|
}
|
|
8147
8373
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
8148
8374
|
if (result.changed) {
|
|
8149
|
-
const relative =
|
|
8375
|
+
const relative = path21.relative(process.cwd(), result.manifestPath);
|
|
8150
8376
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
8151
|
-
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.assetId;
|
|
8377
|
+
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;
|
|
8152
8378
|
log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
8153
8379
|
}
|
|
8154
8380
|
return opts.reload();
|
|
@@ -8157,7 +8383,7 @@ function createGalleryEditHandler(opts) {
|
|
|
8157
8383
|
|
|
8158
8384
|
// src/gallery/snapshot.ts
|
|
8159
8385
|
function galleryMediaId(absolutePath) {
|
|
8160
|
-
return createHash3("sha256").update(
|
|
8386
|
+
return createHash3("sha256").update(path22.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
8161
8387
|
}
|
|
8162
8388
|
function galleryMediaRoute(id) {
|
|
8163
8389
|
return `/media/${id}`;
|
|
@@ -8182,23 +8408,24 @@ function metadataFps(entry) {
|
|
|
8182
8408
|
}
|
|
8183
8409
|
return null;
|
|
8184
8410
|
}
|
|
8185
|
-
async function
|
|
8411
|
+
async function fileInfo(absolutePath) {
|
|
8186
8412
|
try {
|
|
8187
8413
|
const info = await stat3(absolutePath);
|
|
8188
|
-
return info.isFile() ? info.size : null;
|
|
8414
|
+
return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
|
|
8189
8415
|
} catch {
|
|
8190
8416
|
return null;
|
|
8191
8417
|
}
|
|
8192
8418
|
}
|
|
8193
8419
|
async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
8194
|
-
const
|
|
8195
|
-
const exists =
|
|
8420
|
+
const info = await fileInfo(absolutePath);
|
|
8421
|
+
const exists = info !== null;
|
|
8196
8422
|
const mediaType = recorded.mediaType ?? mediaTypeFromExtension(absolutePath);
|
|
8197
8423
|
let url = null;
|
|
8198
8424
|
if (exists && mediaType) {
|
|
8199
8425
|
const id = galleryMediaId(absolutePath);
|
|
8200
8426
|
media.set(id, { path: absolutePath, contentType: mediaType });
|
|
8201
|
-
|
|
8427
|
+
const version = recorded.sha256 ?? `${info.bytes}-${Date.parse(info.modifiedAt).toString(36)}`;
|
|
8428
|
+
url = `${galleryMediaRoute(id)}?v=${version.slice(0, 24)}`;
|
|
8202
8429
|
}
|
|
8203
8430
|
return {
|
|
8204
8431
|
path: portableOutputPath(absolutePath, root),
|
|
@@ -8207,17 +8434,18 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
8207
8434
|
role: recorded.role ?? null,
|
|
8208
8435
|
mediaType,
|
|
8209
8436
|
exists,
|
|
8210
|
-
bytes,
|
|
8437
|
+
bytes: info?.bytes ?? null,
|
|
8438
|
+
modifiedAt: info?.modifiedAt ?? null,
|
|
8211
8439
|
url
|
|
8212
8440
|
};
|
|
8213
8441
|
}
|
|
8214
8442
|
async function describeQuality(media, root, inspection) {
|
|
8215
8443
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
8216
8444
|
const outputs = await Promise.all(
|
|
8217
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
8445
|
+
outputPaths.map((file) => describeOutput(media, root, path22.resolve(root, file)))
|
|
8218
8446
|
);
|
|
8219
|
-
const recordPath =
|
|
8220
|
-
const recordExists =
|
|
8447
|
+
const recordPath = path22.resolve(root, inspection.record);
|
|
8448
|
+
const recordExists = existsSync17(recordPath);
|
|
8221
8449
|
let options = null;
|
|
8222
8450
|
let check = null;
|
|
8223
8451
|
if (recordExists) {
|
|
@@ -8236,7 +8464,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
8236
8464
|
return {
|
|
8237
8465
|
state: inspection.state,
|
|
8238
8466
|
reason: inspection.reason,
|
|
8239
|
-
output: portableOutputPath(
|
|
8467
|
+
output: portableOutputPath(path22.resolve(root, inspection.output), root),
|
|
8240
8468
|
record: portableOutputPath(recordPath, root),
|
|
8241
8469
|
recordExists,
|
|
8242
8470
|
outputs,
|
|
@@ -8286,15 +8514,24 @@ async function buildGallerySnapshot(opts) {
|
|
|
8286
8514
|
const entry = lock.entries[key];
|
|
8287
8515
|
const asset = loaded.manifest.assets[spec.assetId] ?? null;
|
|
8288
8516
|
let outputs;
|
|
8517
|
+
let edit = null;
|
|
8518
|
+
let editStatus = null;
|
|
8289
8519
|
if (entry && entry.outputs.length) {
|
|
8290
8520
|
outputs = await Promise.all(
|
|
8291
8521
|
entry.outputs.map(
|
|
8292
8522
|
(output, index) => describeOutput(media, root, currentEntryOutputPath(entry, spec, index), output)
|
|
8293
8523
|
)
|
|
8294
8524
|
);
|
|
8525
|
+
if (spec.source) {
|
|
8526
|
+
const editPath = path22.resolve(root, spec.source);
|
|
8527
|
+
const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
|
|
8528
|
+
edit = await describeOutput(media, root, editPath, { sha256: editSha });
|
|
8529
|
+
const generated = outputs[0];
|
|
8530
|
+
editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists ? "regenerated-since" : "edited";
|
|
8531
|
+
}
|
|
8295
8532
|
} else if (spec.source) {
|
|
8296
|
-
outputs = [await describeOutput(media, root,
|
|
8297
|
-
} else if (!entry &&
|
|
8533
|
+
outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
|
|
8534
|
+
} else if (!entry && existsSync17(spec.outFile)) {
|
|
8298
8535
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
8299
8536
|
} else {
|
|
8300
8537
|
outputs = [];
|
|
@@ -8338,6 +8575,10 @@ async function buildGallerySnapshot(opts) {
|
|
|
8338
8575
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
8339
8576
|
asset,
|
|
8340
8577
|
source: spec.source ?? null,
|
|
8578
|
+
edit,
|
|
8579
|
+
editStatus,
|
|
8580
|
+
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8581
|
+
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
8341
8582
|
tags: spec.tags,
|
|
8342
8583
|
category: asset?.category ?? null
|
|
8343
8584
|
});
|
|
@@ -8390,6 +8631,10 @@ async function buildGallerySnapshot(opts) {
|
|
|
8390
8631
|
providerMetadata: entry.providerMetadata ?? {},
|
|
8391
8632
|
asset: null,
|
|
8392
8633
|
source: null,
|
|
8634
|
+
edit: null,
|
|
8635
|
+
editStatus: null,
|
|
8636
|
+
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
8637
|
+
refreshable: false,
|
|
8393
8638
|
tags: [],
|
|
8394
8639
|
category: null
|
|
8395
8640
|
});
|
|
@@ -8457,7 +8702,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8457
8702
|
id: loaded.manifest.name,
|
|
8458
8703
|
name: loaded.manifest.name,
|
|
8459
8704
|
manifest: loaded.path,
|
|
8460
|
-
lock:
|
|
8705
|
+
lock: path22.resolve(opts.lockPath),
|
|
8461
8706
|
root,
|
|
8462
8707
|
provider: loaded.manifest.provider,
|
|
8463
8708
|
account: null,
|
|
@@ -8482,7 +8727,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
8482
8727
|
return { snapshot, media };
|
|
8483
8728
|
}
|
|
8484
8729
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
8485
|
-
const dir =
|
|
8730
|
+
const dir = path22.dirname(path22.resolve(opts.workspacePath));
|
|
8486
8731
|
const media = /* @__PURE__ */ new Map();
|
|
8487
8732
|
const projects = [];
|
|
8488
8733
|
const items = [];
|
|
@@ -8525,7 +8770,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8525
8770
|
id: project.id,
|
|
8526
8771
|
name: loaded.manifest.name,
|
|
8527
8772
|
manifest: manifestPath,
|
|
8528
|
-
lock:
|
|
8773
|
+
lock: path22.resolve(lockPath),
|
|
8529
8774
|
root: loaded.root,
|
|
8530
8775
|
provider: loaded.manifest.provider,
|
|
8531
8776
|
account: project.account ?? null,
|
|
@@ -8540,8 +8785,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8540
8785
|
id: project.id,
|
|
8541
8786
|
name: project.id,
|
|
8542
8787
|
manifest: manifestPath,
|
|
8543
|
-
lock:
|
|
8544
|
-
root:
|
|
8788
|
+
lock: path22.resolve(lockPath),
|
|
8789
|
+
root: path22.dirname(manifestPath),
|
|
8545
8790
|
provider: project.provider,
|
|
8546
8791
|
account: project.account ?? null,
|
|
8547
8792
|
manifestSha256: null,
|
|
@@ -8567,7 +8812,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8567
8812
|
version: 1,
|
|
8568
8813
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
8569
8814
|
project: null,
|
|
8570
|
-
workspace: { path:
|
|
8815
|
+
workspace: { path: path22.resolve(opts.workspacePath), dir, projects },
|
|
8571
8816
|
filter,
|
|
8572
8817
|
totals: {
|
|
8573
8818
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -8584,7 +8829,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
8584
8829
|
}
|
|
8585
8830
|
|
|
8586
8831
|
// src/gallery/server.ts
|
|
8587
|
-
import { spawn as
|
|
8832
|
+
import { spawn as spawn3 } from "child_process";
|
|
8588
8833
|
import { randomBytes } from "crypto";
|
|
8589
8834
|
import { createServer as createServer2 } from "http";
|
|
8590
8835
|
import { readFile as readFile16 } from "fs/promises";
|
|
@@ -8860,6 +9105,14 @@ function renderGallery(snapshot, opts = {}) {
|
|
|
8860
9105
|
.tray > button { padding:5px 11px; font-size:12.5px; }
|
|
8861
9106
|
@media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
|
|
8862
9107
|
.drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
|
9108
|
+
.pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
|
|
9109
|
+
.pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
|
|
9110
|
+
background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
|
|
9111
|
+
.pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
|
|
9112
|
+
.pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
|
|
9113
|
+
.card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
|
|
9114
|
+
font:700 11px/1 var(--mono); padding:3px 5px; }
|
|
9115
|
+
.hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
8863
9116
|
.drawer .gen button.primary { padding:7px 14px; }
|
|
8864
9117
|
@media (max-width: 720px) {
|
|
8865
9118
|
.bar, .totals, .chips, .style, footer { padding-inline:14px; }
|
|
@@ -9059,7 +9312,7 @@ function renderJobs() {
|
|
|
9059
9312
|
const row = el('div', 'job');
|
|
9060
9313
|
const ph = el('span', 'ph');
|
|
9061
9314
|
ph.append(el('i', 'dot ' + PHASE_TONE[job.phase]), document.createTextNode(job.phase));
|
|
9062
|
-
const what = (job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9315
|
+
const what = (job.mode === 'refresh' ? 'pull upstream for ' : job.mode === 'resume' ? 'resume ' : 'generate ') + job.keys.length + (job.keys.length === 1 ? ' asset' : ' assets') +
|
|
9063
9316
|
(job.project ? ' in ' + job.project : '');
|
|
9064
9317
|
const last = el('span', 'last', what + (job.messages.length ? ' \u2014 ' + job.messages[job.messages.length - 1].trim() : ''));
|
|
9065
9318
|
last.title = job.keys.join('\\n');
|
|
@@ -9098,13 +9351,15 @@ function budgetLine() {
|
|
|
9098
9351
|
|
|
9099
9352
|
// The confirm step: what will be sent, what it is estimated to cost, and what
|
|
9100
9353
|
// this session may still spend. Mirrors gen's "Spend \u2026 on N asset(s)?" prompt.
|
|
9101
|
-
function generateDialog(items, { project = null, force = false, resume = false } = {}) {
|
|
9354
|
+
function generateDialog(items, { project = null, force = false, resume = false, refresh = false } = {}) {
|
|
9102
9355
|
const host = $('dialog-host');
|
|
9103
9356
|
host.textContent = '';
|
|
9104
9357
|
const wrap = el('div', 'dialog');
|
|
9105
9358
|
const form = el('form');
|
|
9106
|
-
|
|
9107
|
-
|
|
9359
|
+
const noun = items.length + (items.length === 1 ? ' asset' : ' assets');
|
|
9360
|
+
form.append(el('h3', null, refresh ? 'Pull upstream changes for ' + noun
|
|
9361
|
+
: resume ? 'Resume ' + noun
|
|
9362
|
+
: (force ? 'Regenerate ' : 'Generate ') + noun));
|
|
9108
9363
|
const table = el('table');
|
|
9109
9364
|
const thead = el('tr'); thead.append(el('th', null, 'asset'), el('th', null, 'state'), el('th', 'n', 'candidates'), el('th', 'n', 'estimate'));
|
|
9110
9365
|
table.append(thead);
|
|
@@ -9112,16 +9367,18 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
9112
9367
|
for (const item of items) {
|
|
9113
9368
|
const tr = el('tr');
|
|
9114
9369
|
tr.append(el('td', 'mono', item.key), el('td', null, item.state), el('td', 'n', item.candidates ?? '\u2014'),
|
|
9115
|
-
el('td', 'n', resume ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
9370
|
+
el('td', 'n', resume || refresh ? 'no cost' : fmtCost(item.costUnit, item.estimatedCost ?? 0)));
|
|
9116
9371
|
table.append(tr);
|
|
9117
|
-
if (!resume) {
|
|
9372
|
+
if (!resume && !refresh) {
|
|
9118
9373
|
const g = byProvider.get(item.provider) || { cost: 0, unit: item.costUnit };
|
|
9119
9374
|
g.cost += item.estimatedCost ?? 0; byProvider.set(item.provider, g);
|
|
9120
9375
|
}
|
|
9121
9376
|
}
|
|
9122
9377
|
form.append(table);
|
|
9123
9378
|
const sum = el('div', 'sum');
|
|
9124
|
-
if (
|
|
9379
|
+
if (refresh) {
|
|
9380
|
+
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.'));
|
|
9381
|
+
} else if (resume) {
|
|
9125
9382
|
sum.append(el('div', null, 'Polls, reviews, and downloads existing provider work. Nothing is submitted.'));
|
|
9126
9383
|
} else {
|
|
9127
9384
|
for (const [provider, g] of byProvider) {
|
|
@@ -9138,7 +9395,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
9138
9395
|
}
|
|
9139
9396
|
form.append(sum);
|
|
9140
9397
|
const actions = el('div', 'actions');
|
|
9141
|
-
const go = el('button', 'primary', resume ? 'Resume' : 'Generate');
|
|
9398
|
+
const go = el('button', 'primary', refresh ? 'Pull changes' : resume ? 'Resume' : 'Generate');
|
|
9142
9399
|
go.type = 'submit';
|
|
9143
9400
|
const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { host.textContent = ''; };
|
|
9144
9401
|
const msg = el('span', 'msg');
|
|
@@ -9152,6 +9409,7 @@ function generateDialog(items, { project = null, force = false, resume = false }
|
|
|
9152
9409
|
if (project) body.project = project;
|
|
9153
9410
|
if (force) body.force = true;
|
|
9154
9411
|
if (resume) body.resume = true;
|
|
9412
|
+
if (refresh) body.refresh = true;
|
|
9155
9413
|
const job = await postGenerate(body);
|
|
9156
9414
|
host.textContent = '';
|
|
9157
9415
|
GEN.jobs.unshift(job);
|
|
@@ -9306,7 +9564,8 @@ function clearFilters() {
|
|
|
9306
9564
|
|
|
9307
9565
|
function thumb(item) {
|
|
9308
9566
|
const cell = el('div', 'cell');
|
|
9309
|
-
|
|
9567
|
+
// A hand edit is what ships, so it is what the card shows.
|
|
9568
|
+
const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
|
|
9310
9569
|
if (!shown.length) {
|
|
9311
9570
|
cell.classList.add('none');
|
|
9312
9571
|
cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
|
|
@@ -9357,6 +9616,9 @@ function card(item) {
|
|
|
9357
9616
|
body.append(meta);
|
|
9358
9617
|
const cell = thumb(item);
|
|
9359
9618
|
if (slot >= 0) cell.append(el('span', 'slot', String(slot + 1)));
|
|
9619
|
+
if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
|
|
9620
|
+
const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
|
|
9621
|
+
}
|
|
9360
9622
|
c.append(cell, body);
|
|
9361
9623
|
c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
|
|
9362
9624
|
return c;
|
|
@@ -9437,6 +9699,13 @@ function renderMain(items) {
|
|
|
9437
9699
|
g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
|
|
9438
9700
|
tools.append(g);
|
|
9439
9701
|
}
|
|
9702
|
+
const refreshable = snap.items.filter((i) => i.project === s.project && i.styleId === s.id && i.refreshable && i.upstreamUrl);
|
|
9703
|
+
if (GENERATION && refreshable.length) {
|
|
9704
|
+
const r = el('button', null, 'Pull upstream ' + refreshable.length); r.type = 'button';
|
|
9705
|
+
r.title = 'Re-download objects edited in the provider\u2019s own editor; no cost';
|
|
9706
|
+
r.onclick = () => generateDialog(refreshable, { project: s.project, refresh: true });
|
|
9707
|
+
tools.append(r);
|
|
9708
|
+
}
|
|
9440
9709
|
const styleKey = 'style:' + (s.project || '') + ':' + s.id;
|
|
9441
9710
|
if (EDITABLE && s.outDir) {
|
|
9442
9711
|
const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
|
|
@@ -9530,6 +9799,30 @@ function candidatesControl(style) {
|
|
|
9530
9799
|
return wrap;
|
|
9531
9800
|
}
|
|
9532
9801
|
|
|
9802
|
+
function upstreamSection(item) {
|
|
9803
|
+
if (!item.upstreamUrl && !(GENERATION && item.refreshable)) return null;
|
|
9804
|
+
const s = el('section', 'meta');
|
|
9805
|
+
s.append(el('h3', null, 'Upstream'));
|
|
9806
|
+
const dl = el('dl');
|
|
9807
|
+
if (item.upstreamUrl) {
|
|
9808
|
+
const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
|
|
9809
|
+
a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
|
|
9810
|
+
row(dl, 'object', a);
|
|
9811
|
+
row(dl, 'note', item.provider === 'pixellab'
|
|
9812
|
+
? 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
|
|
9813
|
+
: 'The provider\u2019s page for this object.');
|
|
9814
|
+
}
|
|
9815
|
+
s.append(dl);
|
|
9816
|
+
if (GENERATION && item.refreshable) {
|
|
9817
|
+
const acts = el('div', 'hand-actions');
|
|
9818
|
+
const b = el('button', null, 'Pull upstream changes \xB7 no cost'); b.type = 'button';
|
|
9819
|
+
b.onclick = () => generateDialog([item], { project: item.project, refresh: true });
|
|
9820
|
+
acts.append(b);
|
|
9821
|
+
s.append(acts);
|
|
9822
|
+
}
|
|
9823
|
+
return s;
|
|
9824
|
+
}
|
|
9825
|
+
|
|
9533
9826
|
function generateActions(item) {
|
|
9534
9827
|
const row = el('div', 'gen');
|
|
9535
9828
|
if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
|
|
@@ -9683,6 +9976,78 @@ function openCompare() {
|
|
|
9683
9976
|
close.focus({ preventScroll: true });
|
|
9684
9977
|
}
|
|
9685
9978
|
|
|
9979
|
+
// ---- hand edits -------------------------------------------------------------
|
|
9980
|
+
|
|
9981
|
+
const EDIT_STATUS_TEXT = {
|
|
9982
|
+
same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
|
|
9983
|
+
edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
|
|
9984
|
+
'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
|
|
9985
|
+
missing: 'The declared file is not on disk.',
|
|
9986
|
+
};
|
|
9987
|
+
async function postHandEdit(item, action, extra) {
|
|
9988
|
+
const pr = projectOf(item);
|
|
9989
|
+
const body = { action, assetId: item.assetId, styleId: item.styleId, expectedSha256: pr.manifestSha256, ...extra };
|
|
9990
|
+
if (item.project) body.project = item.project;
|
|
9991
|
+
snap = await postEdit(body);
|
|
9992
|
+
}
|
|
9993
|
+
function handEditSection(item) {
|
|
9994
|
+
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
|
|
9995
|
+
item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
|
|
9996
|
+
if (!item.edit && !canStart) return null;
|
|
9997
|
+
const s = el('section', 'meta');
|
|
9998
|
+
s.append(el('h3', null, 'Hand edit'));
|
|
9999
|
+
if (item.edit) {
|
|
10000
|
+
const pair = el('div', 'pair');
|
|
10001
|
+
const shared = displayScale(item.width, item.height, 200, 160);
|
|
10002
|
+
const fig = (out, label) => {
|
|
10003
|
+
const f = el('figure');
|
|
10004
|
+
if (out && out.url) {
|
|
10005
|
+
const img = el('img'); img.src = out.url; img.alt = label;
|
|
10006
|
+
if (shared >= 1) { img.width = item.width * shared; img.height = item.height * shared; }
|
|
10007
|
+
f.append(img);
|
|
10008
|
+
} else f.append(el('span', 'state-dim', 'not on disk'));
|
|
10009
|
+
f.append(el('figcaption', null, label));
|
|
10010
|
+
return f;
|
|
10011
|
+
};
|
|
10012
|
+
pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
|
|
10013
|
+
s.append(pair);
|
|
10014
|
+
const dl = el('dl');
|
|
10015
|
+
const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
|
|
10016
|
+
const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
|
|
10017
|
+
row(dl, 'status', st);
|
|
10018
|
+
row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
|
|
10019
|
+
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
10020
|
+
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
10021
|
+
row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
|
|
10022
|
+
s.append(dl);
|
|
10023
|
+
} else {
|
|
10024
|
+
s.append(el('div', 'state-dim', 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
|
|
10025
|
+
}
|
|
10026
|
+
if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
|
|
10027
|
+
const acts = el('div', 'hand-actions');
|
|
10028
|
+
const msg = el('span', 'msg');
|
|
10029
|
+
const go = async (label, action, extra, done) => {
|
|
10030
|
+
const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
|
|
10031
|
+
b.onclick = async () => {
|
|
10032
|
+
b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
|
|
10033
|
+
try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
|
|
10034
|
+
catch (err) { b.disabled = false; msg.className = 'msg bad'; msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : ''); }
|
|
10035
|
+
};
|
|
10036
|
+
acts.append(b);
|
|
10037
|
+
};
|
|
10038
|
+
if (item.edit) {
|
|
10039
|
+
go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
10040
|
+
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
|
|
10041
|
+
} else if (canStart) {
|
|
10042
|
+
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
|
|
10043
|
+
go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
|
|
10044
|
+
}
|
|
10045
|
+
acts.append(msg);
|
|
10046
|
+
s.append(acts);
|
|
10047
|
+
}
|
|
10048
|
+
return s;
|
|
10049
|
+
}
|
|
10050
|
+
|
|
9686
10051
|
// ---- editing (only when the server minted a session) ---------------------
|
|
9687
10052
|
|
|
9688
10053
|
// Styles a parent edit reaches: every descendant (via extends) that does not
|
|
@@ -10174,6 +10539,11 @@ function renderDrawer() {
|
|
|
10174
10539
|
body.append(s);
|
|
10175
10540
|
}
|
|
10176
10541
|
|
|
10542
|
+
const hand = handEditSection(item);
|
|
10543
|
+
if (hand) body.append(hand);
|
|
10544
|
+
const upstream = upstreamSection(item);
|
|
10545
|
+
if (upstream) body.append(upstream);
|
|
10546
|
+
|
|
10177
10547
|
if (item.revision || item.revisionParentKey) {
|
|
10178
10548
|
const { s, dl } = section('Lineage');
|
|
10179
10549
|
row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
|
|
@@ -10641,7 +11011,7 @@ async function serveGallery(opts) {
|
|
|
10641
11011
|
});
|
|
10642
11012
|
opts.onReady?.(url);
|
|
10643
11013
|
if (opts.open !== false && process.platform === "darwin") {
|
|
10644
|
-
|
|
11014
|
+
spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
10645
11015
|
}
|
|
10646
11016
|
return {
|
|
10647
11017
|
url,
|
|
@@ -10655,15 +11025,17 @@ async function serveGallery(opts) {
|
|
|
10655
11025
|
|
|
10656
11026
|
// src/gallery/generate.ts
|
|
10657
11027
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
10658
|
-
import { z as
|
|
10659
|
-
var GenerateRequestSchema =
|
|
10660
|
-
project:
|
|
10661
|
-
keys:
|
|
11028
|
+
import { z as z7 } from "zod";
|
|
11029
|
+
var GenerateRequestSchema = z7.object({
|
|
11030
|
+
project: z7.string().min(1).optional(),
|
|
11031
|
+
keys: z7.array(z7.string().min(1)).min(1).max(500),
|
|
10662
11032
|
/** Regenerate work that is up to date, as `gen --force` does. */
|
|
10663
|
-
force:
|
|
11033
|
+
force: z7.boolean().optional(),
|
|
10664
11034
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
10665
|
-
resume:
|
|
10666
|
-
|
|
11035
|
+
resume: z7.boolean().optional(),
|
|
11036
|
+
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
11037
|
+
refresh: z7.boolean().optional()
|
|
11038
|
+
}).strict().refine((request) => !(request.resume && request.refresh), { message: "resume and refresh are exclusive" });
|
|
10667
11039
|
var GenerateRequestError = class extends Error {
|
|
10668
11040
|
constructor(message6, status = 400) {
|
|
10669
11041
|
super(message6);
|
|
@@ -10721,6 +11093,20 @@ function createGenerateHandlers(opts) {
|
|
|
10721
11093
|
}
|
|
10722
11094
|
return groups;
|
|
10723
11095
|
};
|
|
11096
|
+
async function refresh(job, ctx, specs) {
|
|
11097
|
+
job.phase = "fetching";
|
|
11098
|
+
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
11099
|
+
const provider = opts.providerFor(job.project ?? void 0, providerId);
|
|
11100
|
+
const res = await fetchAssets(provider, providerSpecs, ctx.lock, ctx.lockPath, {
|
|
11101
|
+
onProgress: (line) => say(job, line),
|
|
11102
|
+
refresh: true
|
|
11103
|
+
});
|
|
11104
|
+
job.counts.downloaded += res.downloaded;
|
|
11105
|
+
say(job, `${providerId}: ${res.downloaded} changed upstream and replaced, ${res.unchanged ?? 0} unchanged, ${res.skipped} skipped, ${res.failed} failed`);
|
|
11106
|
+
}
|
|
11107
|
+
job.phase = "done";
|
|
11108
|
+
job.finishedAt = now().toISOString();
|
|
11109
|
+
}
|
|
10724
11110
|
async function settle(job, ctx, specs) {
|
|
10725
11111
|
job.phase = "polling";
|
|
10726
11112
|
for (const [providerId, providerSpecs] of groupByRecordedProvider(specs, ctx.lock)) {
|
|
@@ -10754,6 +11140,10 @@ function createGenerateHandlers(opts) {
|
|
|
10754
11140
|
}
|
|
10755
11141
|
async function run(job, ctx, specs, groups) {
|
|
10756
11142
|
try {
|
|
11143
|
+
if (job.mode === "refresh") {
|
|
11144
|
+
await refresh(job, ctx, specs);
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
10757
11147
|
if (job.mode === "generate") {
|
|
10758
11148
|
job.phase = "submitting";
|
|
10759
11149
|
for (const group of groups) {
|
|
@@ -10817,7 +11207,7 @@ function createGenerateHandlers(opts) {
|
|
|
10817
11207
|
if (holder) throw new GenerateRequestError(`"${key}" is already being worked on by job ${holder}`, 409);
|
|
10818
11208
|
}
|
|
10819
11209
|
let groups = [];
|
|
10820
|
-
if (!request.resume) {
|
|
11210
|
+
if (!request.resume && !request.refresh) {
|
|
10821
11211
|
const plan = await buildPlan(specs, ctx.lock, { force: request.force });
|
|
10822
11212
|
if (!plan.actionable.length) {
|
|
10823
11213
|
const reasons = plan.items.map((item) => `${item.key}: ${item.state} \u2014 ${item.reason}`).slice(0, 5);
|
|
@@ -10837,7 +11227,7 @@ function createGenerateHandlers(opts) {
|
|
|
10837
11227
|
id: randomBytes2(8).toString("hex"),
|
|
10838
11228
|
project: request.project ?? null,
|
|
10839
11229
|
keys: request.keys,
|
|
10840
|
-
mode: request.resume ? "resume" : "generate",
|
|
11230
|
+
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
10841
11231
|
phase: "queued",
|
|
10842
11232
|
startedAt: now().toISOString(),
|
|
10843
11233
|
finishedAt: null,
|
|
@@ -10849,7 +11239,7 @@ function createGenerateHandlers(opts) {
|
|
|
10849
11239
|
};
|
|
10850
11240
|
jobs.set(job.id, job);
|
|
10851
11241
|
contexts.set(job.id, ctx);
|
|
10852
|
-
say(job, request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
11242
|
+
say(job, request.refresh ? `checking ${request.keys.length} asset(s) upstream at no cost` : request.resume ? `resuming ${request.keys.length} asset(s) at no cost` : `generating ${groups.reduce((n, g) => n + g.actionable.length, 0)} asset(s): ` + groups.map((g) => `${g.provider} ${formatCost(g.costUnit, g.cost)}`).join("; "));
|
|
10853
11243
|
void run(job, ctx, specs, groups);
|
|
10854
11244
|
return job;
|
|
10855
11245
|
},
|
|
@@ -10900,15 +11290,15 @@ function createGenerateHandlers(opts) {
|
|
|
10900
11290
|
|
|
10901
11291
|
// src/pipeline/salvage.ts
|
|
10902
11292
|
import { readFile as readFile17 } from "fs/promises";
|
|
10903
|
-
import { existsSync as
|
|
10904
|
-
import
|
|
11293
|
+
import { existsSync as existsSync18 } from "fs";
|
|
11294
|
+
import path23 from "path";
|
|
10905
11295
|
function providerClaimId(provider, objectId) {
|
|
10906
11296
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
10907
11297
|
}
|
|
10908
11298
|
async function loadClaims(lockPaths, opts = {}) {
|
|
10909
11299
|
const claimed = /* @__PURE__ */ new Set();
|
|
10910
11300
|
for (const p of lockPaths) {
|
|
10911
|
-
if (!
|
|
11301
|
+
if (!existsSync18(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
10912
11302
|
let parsed;
|
|
10913
11303
|
try {
|
|
10914
11304
|
parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
|
|
@@ -10959,20 +11349,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
10959
11349
|
return null;
|
|
10960
11350
|
}
|
|
10961
11351
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
10962
|
-
const own =
|
|
11352
|
+
const own = path23.resolve(ownManifestPath);
|
|
10963
11353
|
const siblingManifestPaths = [
|
|
10964
11354
|
.../* @__PURE__ */ new Set([
|
|
10965
11355
|
...workspaceManifestPaths,
|
|
10966
|
-
...claimPaths.map((c) =>
|
|
11356
|
+
...claimPaths.map((c) => path23.join(path23.dirname(path23.resolve(c)), "pixelkiln.manifest.json"))
|
|
10967
11357
|
])
|
|
10968
11358
|
];
|
|
10969
11359
|
const siblings = [];
|
|
10970
11360
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
10971
|
-
if (
|
|
10972
|
-
if (!
|
|
11361
|
+
if (path23.resolve(siblingManifestPath) === own) continue;
|
|
11362
|
+
if (!existsSync18(siblingManifestPath)) continue;
|
|
10973
11363
|
try {
|
|
10974
11364
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
10975
|
-
siblings.push({ label:
|
|
11365
|
+
siblings.push({ label: path23.basename(path23.dirname(siblingManifestPath)), manifest });
|
|
10976
11366
|
} catch {
|
|
10977
11367
|
}
|
|
10978
11368
|
}
|
|
@@ -11191,15 +11581,15 @@ async function workspaceStatus(ws, dir) {
|
|
|
11191
11581
|
}
|
|
11192
11582
|
|
|
11193
11583
|
// src/pipeline/cache-health.ts
|
|
11194
|
-
import { existsSync as
|
|
11584
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11195
11585
|
import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
|
|
11196
|
-
import
|
|
11586
|
+
import path24 from "path";
|
|
11197
11587
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11198
|
-
if (options.prune && !
|
|
11199
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
11588
|
+
if (options.prune && !existsSync19(lockPath)) {
|
|
11589
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path24.resolve(lockPath)}`);
|
|
11200
11590
|
}
|
|
11201
|
-
const contentDir =
|
|
11202
|
-
const remotePath =
|
|
11591
|
+
const contentDir = path24.resolve(path24.dirname(lockPath), ".pixelkiln", "cache");
|
|
11592
|
+
const remotePath = path24.resolve(cachePathFor(lockPath));
|
|
11203
11593
|
const referenced = new Set(
|
|
11204
11594
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
11205
11595
|
);
|
|
@@ -11213,7 +11603,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11213
11603
|
]);
|
|
11214
11604
|
for (const name of names) {
|
|
11215
11605
|
try {
|
|
11216
|
-
await rm8(
|
|
11606
|
+
await rm8(path24.join(contentDir, name), { force: true });
|
|
11217
11607
|
removed.contentFiles++;
|
|
11218
11608
|
} catch {
|
|
11219
11609
|
}
|
|
@@ -11240,7 +11630,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11240
11630
|
async function inspectContentCache(contentDir, referenced) {
|
|
11241
11631
|
const report = {
|
|
11242
11632
|
path: contentDir,
|
|
11243
|
-
exists:
|
|
11633
|
+
exists: existsSync19(contentDir),
|
|
11244
11634
|
files: 0,
|
|
11245
11635
|
bytes: 0,
|
|
11246
11636
|
valid: 0,
|
|
@@ -11261,7 +11651,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11261
11651
|
continue;
|
|
11262
11652
|
}
|
|
11263
11653
|
report.files++;
|
|
11264
|
-
const file =
|
|
11654
|
+
const file = path24.join(contentDir, entry.name);
|
|
11265
11655
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11266
11656
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11267
11657
|
let bytes;
|
|
@@ -11304,7 +11694,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11304
11694
|
async function inspectRemoteHashCache(remotePath) {
|
|
11305
11695
|
const report = {
|
|
11306
11696
|
path: remotePath,
|
|
11307
|
-
exists:
|
|
11697
|
+
exists: existsSync19(remotePath),
|
|
11308
11698
|
entries: 0,
|
|
11309
11699
|
valid: 0,
|
|
11310
11700
|
invalidIds: [],
|
|
@@ -11584,8 +11974,8 @@ function isRecord2(value) {
|
|
|
11584
11974
|
}
|
|
11585
11975
|
|
|
11586
11976
|
// src/pick/salvage-server.ts
|
|
11587
|
-
import { mkdir as
|
|
11588
|
-
import
|
|
11977
|
+
import { mkdir as mkdir7, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
|
|
11978
|
+
import path25 from "path";
|
|
11589
11979
|
|
|
11590
11980
|
// src/pick/salvage-sheet.ts
|
|
11591
11981
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -11750,7 +12140,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11750
12140
|
});
|
|
11751
12141
|
const html = renderSalvageSheet(orphans, {
|
|
11752
12142
|
styleId: ctx.styleId,
|
|
11753
|
-
importDir:
|
|
12143
|
+
importDir: path25.relative(process.cwd(), ctx.importDir) || "."
|
|
11754
12144
|
});
|
|
11755
12145
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
11756
12146
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -11779,13 +12169,14 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11779
12169
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
11780
12170
|
decodePng(buf);
|
|
11781
12171
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
11782
|
-
const rel =
|
|
11783
|
-
const outFile =
|
|
11784
|
-
await
|
|
12172
|
+
const rel = path25.join("_salvaged", `${assetId}.png`);
|
|
12173
|
+
const outFile = path25.resolve(ctx.importDir, rel);
|
|
12174
|
+
await mkdir7(path25.dirname(outFile), { recursive: true });
|
|
11785
12175
|
await writeFile9(outFile, buf);
|
|
11786
12176
|
ctx.manifest.assets[assetId] = {
|
|
11787
12177
|
prompt: orphan.prompt,
|
|
11788
12178
|
promptByStyle: {},
|
|
12179
|
+
sourceByStyle: {},
|
|
11789
12180
|
category: "_salvaged",
|
|
11790
12181
|
file: rel,
|
|
11791
12182
|
tags: ["salvaged"],
|
|
@@ -11813,7 +12204,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11813
12204
|
sourceUrl: durableSource,
|
|
11814
12205
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
11815
12206
|
outputs: [{
|
|
11816
|
-
path: portableOutputPath(outFile,
|
|
12207
|
+
path: portableOutputPath(outFile, path25.dirname(ctx.manifestPath)),
|
|
11817
12208
|
sha256: sha256(buf)
|
|
11818
12209
|
}],
|
|
11819
12210
|
submittedAt: orphan.createdAt,
|
|
@@ -11847,51 +12238,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
11847
12238
|
}
|
|
11848
12239
|
|
|
11849
12240
|
// src/recipes.ts
|
|
11850
|
-
import
|
|
11851
|
-
import { existsSync as
|
|
12241
|
+
import path26 from "path";
|
|
12242
|
+
import { existsSync as existsSync20 } from "fs";
|
|
11852
12243
|
import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
|
|
11853
12244
|
import { fileURLToPath } from "url";
|
|
11854
|
-
import { z as
|
|
12245
|
+
import { z as z8 } from "zod";
|
|
11855
12246
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
11856
12247
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
11857
12248
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
11858
12249
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
11859
12250
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
11860
|
-
var PortablePathSchema =
|
|
11861
|
-
if (value.includes("\\") ||
|
|
12251
|
+
var PortablePathSchema = z8.string().min(1).refine((value) => {
|
|
12252
|
+
if (value.includes("\\") || path26.posix.isAbsolute(value)) return false;
|
|
11862
12253
|
const parts = value.split("/");
|
|
11863
12254
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
11864
12255
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
11865
|
-
var RecipeBindingSchema =
|
|
11866
|
-
nodeId:
|
|
11867
|
-
input:
|
|
12256
|
+
var RecipeBindingSchema = z8.object({
|
|
12257
|
+
nodeId: z8.string().min(1),
|
|
12258
|
+
input: z8.string().min(1)
|
|
11868
12259
|
}).strict();
|
|
11869
|
-
var RecipeSchema =
|
|
11870
|
-
$schema:
|
|
11871
|
-
format:
|
|
11872
|
-
schemaVersion:
|
|
11873
|
-
id:
|
|
11874
|
-
version:
|
|
11875
|
-
provider:
|
|
11876
|
-
summary:
|
|
11877
|
-
files:
|
|
12260
|
+
var RecipeSchema = z8.object({
|
|
12261
|
+
$schema: z8.string().url().optional(),
|
|
12262
|
+
format: z8.literal("pixelkiln-recipe"),
|
|
12263
|
+
schemaVersion: z8.literal(1),
|
|
12264
|
+
id: z8.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
12265
|
+
version: z8.string().regex(VERSION_RE, "expected x.y.z"),
|
|
12266
|
+
provider: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
12267
|
+
summary: z8.string().min(1).max(240),
|
|
12268
|
+
files: z8.array(z8.object({
|
|
11878
12269
|
path: PortablePathSchema,
|
|
11879
|
-
role:
|
|
11880
|
-
sha256:
|
|
12270
|
+
role: z8.enum(["workflow", "reference"]),
|
|
12271
|
+
sha256: z8.string().regex(SHA256_RE)
|
|
11881
12272
|
}).strict()).default([]),
|
|
11882
|
-
models:
|
|
12273
|
+
models: z8.array(z8.object({
|
|
11883
12274
|
path: PortablePathSchema,
|
|
11884
|
-
sha256:
|
|
11885
|
-
source:
|
|
11886
|
-
license:
|
|
12275
|
+
sha256: z8.string().regex(SHA256_RE),
|
|
12276
|
+
source: z8.string().url(),
|
|
12277
|
+
license: z8.string().min(1)
|
|
11887
12278
|
}).strict()).default([]),
|
|
11888
|
-
styleId:
|
|
12279
|
+
styleId: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
11889
12280
|
style: StyleSchema,
|
|
11890
|
-
workflow:
|
|
12281
|
+
workflow: z8.object({
|
|
11891
12282
|
path: PortablePathSchema,
|
|
11892
|
-
outputNodeId:
|
|
11893
|
-
numImages:
|
|
11894
|
-
bindings:
|
|
12283
|
+
outputNodeId: z8.string().min(1),
|
|
12284
|
+
numImages: z8.number().int().min(1).max(16),
|
|
12285
|
+
bindings: z8.object({
|
|
11895
12286
|
prompt: RecipeBindingSchema,
|
|
11896
12287
|
width: RecipeBindingSchema.optional(),
|
|
11897
12288
|
height: RecipeBindingSchema.optional(),
|
|
@@ -11902,67 +12293,67 @@ var RecipeSchema = z7.object({
|
|
|
11902
12293
|
strength: RecipeBindingSchema.optional()
|
|
11903
12294
|
}).strict()
|
|
11904
12295
|
}).strict().optional(),
|
|
11905
|
-
quality:
|
|
11906
|
-
stage:
|
|
11907
|
-
workingCanvas:
|
|
11908
|
-
width:
|
|
11909
|
-
height:
|
|
12296
|
+
quality: z8.object({
|
|
12297
|
+
stage: z8.enum(["composition-source", "production-candidate"]),
|
|
12298
|
+
workingCanvas: z8.object({
|
|
12299
|
+
width: z8.number().int().min(16),
|
|
12300
|
+
height: z8.number().int().min(16)
|
|
11910
12301
|
}).strict(),
|
|
11911
|
-
recommendedNativeSize:
|
|
11912
|
-
min:
|
|
11913
|
-
max:
|
|
12302
|
+
recommendedNativeSize: z8.object({
|
|
12303
|
+
min: z8.number().int().min(1),
|
|
12304
|
+
max: z8.number().int().min(1)
|
|
11914
12305
|
}).strict(),
|
|
11915
|
-
paletteColors:
|
|
11916
|
-
min:
|
|
11917
|
-
max:
|
|
12306
|
+
paletteColors: z8.object({
|
|
12307
|
+
min: z8.number().int().min(1),
|
|
12308
|
+
max: z8.number().int().min(1)
|
|
11918
12309
|
}).strict(),
|
|
11919
|
-
background:
|
|
11920
|
-
checks:
|
|
12310
|
+
background: z8.enum(["opaque", "transparent", "full-bleed"]),
|
|
12311
|
+
checks: z8.array(z8.enum([
|
|
11921
12312
|
"prompt-coverage",
|
|
11922
12313
|
"native-grid",
|
|
11923
12314
|
"final-palette",
|
|
11924
12315
|
"alpha-edge",
|
|
11925
12316
|
"human-1x"
|
|
11926
12317
|
])).min(1),
|
|
11927
|
-
notes:
|
|
12318
|
+
notes: z8.array(z8.string().min(1)).default([])
|
|
11928
12319
|
}).strict(),
|
|
11929
|
-
integrity:
|
|
11930
|
-
algorithm:
|
|
11931
|
-
digest:
|
|
12320
|
+
integrity: z8.object({
|
|
12321
|
+
algorithm: z8.literal("sha256"),
|
|
12322
|
+
digest: z8.string().regex(SHA256_RE)
|
|
11932
12323
|
}).strict()
|
|
11933
12324
|
}).strict().superRefine((recipe, context) => {
|
|
11934
12325
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
11935
12326
|
context.addIssue({
|
|
11936
|
-
code:
|
|
12327
|
+
code: z8.ZodIssueCode.custom,
|
|
11937
12328
|
path: ["style", "provider"],
|
|
11938
12329
|
message: "must match the recipe provider"
|
|
11939
12330
|
});
|
|
11940
12331
|
}
|
|
11941
12332
|
const filePaths = recipe.files.map((file) => file.path);
|
|
11942
12333
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
11943
|
-
context.addIssue({ code:
|
|
12334
|
+
context.addIssue({ code: z8.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
11944
12335
|
}
|
|
11945
12336
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
11946
12337
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
11947
|
-
context.addIssue({ code:
|
|
12338
|
+
context.addIssue({ code: z8.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
11948
12339
|
}
|
|
11949
12340
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
11950
12341
|
context.addIssue({
|
|
11951
|
-
code:
|
|
12342
|
+
code: z8.ZodIssueCode.custom,
|
|
11952
12343
|
path: ["files"],
|
|
11953
12344
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
11954
12345
|
});
|
|
11955
12346
|
}
|
|
11956
12347
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
11957
12348
|
context.addIssue({
|
|
11958
|
-
code:
|
|
12349
|
+
code: z8.ZodIssueCode.custom,
|
|
11959
12350
|
path: ["quality", "recommendedNativeSize"],
|
|
11960
12351
|
message: "min cannot exceed max"
|
|
11961
12352
|
});
|
|
11962
12353
|
}
|
|
11963
12354
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
11964
12355
|
context.addIssue({
|
|
11965
|
-
code:
|
|
12356
|
+
code: z8.ZodIssueCode.custom,
|
|
11966
12357
|
path: ["quality", "paletteColors"],
|
|
11967
12358
|
message: "min cannot exceed max"
|
|
11968
12359
|
});
|
|
@@ -11971,7 +12362,7 @@ var RecipeSchema = z7.object({
|
|
|
11971
12362
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
11972
12363
|
)) {
|
|
11973
12364
|
context.addIssue({
|
|
11974
|
-
code:
|
|
12365
|
+
code: z8.ZodIssueCode.custom,
|
|
11975
12366
|
path: ["workflow", "path"],
|
|
11976
12367
|
message: "must name a file whose role is workflow"
|
|
11977
12368
|
});
|
|
@@ -11979,7 +12370,7 @@ var RecipeSchema = z7.object({
|
|
|
11979
12370
|
if (recipe.provider === "comfyui") {
|
|
11980
12371
|
if (!recipe.workflow) {
|
|
11981
12372
|
context.addIssue({
|
|
11982
|
-
code:
|
|
12373
|
+
code: z8.ZodIssueCode.custom,
|
|
11983
12374
|
path: ["workflow"],
|
|
11984
12375
|
message: "ComfyUI recipes require workflow metadata"
|
|
11985
12376
|
});
|
|
@@ -11988,7 +12379,7 @@ var RecipeSchema = z7.object({
|
|
|
11988
12379
|
const options = recipe.style.providerOptions.comfyui;
|
|
11989
12380
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
11990
12381
|
context.addIssue({
|
|
11991
|
-
code:
|
|
12382
|
+
code: z8.ZodIssueCode.custom,
|
|
11992
12383
|
path: ["style", "providerOptions", "comfyui"],
|
|
11993
12384
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
11994
12385
|
});
|
|
@@ -12002,7 +12393,7 @@ var RecipeSchema = z7.object({
|
|
|
12002
12393
|
};
|
|
12003
12394
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
12004
12395
|
context.addIssue({
|
|
12005
|
-
code:
|
|
12396
|
+
code: z8.ZodIssueCode.custom,
|
|
12006
12397
|
path: ["style", "providerOptions", "comfyui"],
|
|
12007
12398
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
12008
12399
|
});
|
|
@@ -12034,7 +12425,7 @@ function message4(error) {
|
|
|
12034
12425
|
return error instanceof Error ? error.message : String(error);
|
|
12035
12426
|
}
|
|
12036
12427
|
async function readRecipeFile(recipePath, bundled) {
|
|
12037
|
-
const absolute =
|
|
12428
|
+
const absolute = path26.resolve(recipePath);
|
|
12038
12429
|
let raw;
|
|
12039
12430
|
try {
|
|
12040
12431
|
raw = JSON.parse(await readFile20(absolute, "utf8"));
|
|
@@ -12048,13 +12439,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12048
12439
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12049
12440
|
);
|
|
12050
12441
|
}
|
|
12051
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
12442
|
+
return { recipe: parsed.data, path: absolute, dir: path26.dirname(absolute), bundled };
|
|
12052
12443
|
}
|
|
12053
12444
|
async function walkRecipeFiles(root) {
|
|
12054
|
-
if (!
|
|
12445
|
+
if (!existsSync20(root)) return [];
|
|
12055
12446
|
const out = [];
|
|
12056
12447
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
12057
|
-
const resolved =
|
|
12448
|
+
const resolved = path26.join(root, entry.name);
|
|
12058
12449
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12059
12450
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12060
12451
|
}
|
|
@@ -12065,7 +12456,7 @@ function bundledRecipeRoot() {
|
|
|
12065
12456
|
}
|
|
12066
12457
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12067
12458
|
const recipes = await Promise.all(
|
|
12068
|
-
(await walkRecipeFiles(
|
|
12459
|
+
(await walkRecipeFiles(path26.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12069
12460
|
);
|
|
12070
12461
|
return recipes.sort(
|
|
12071
12462
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12088,9 +12479,9 @@ function parseSelector(selector) {
|
|
|
12088
12479
|
return { id: match[1], version: match[2] };
|
|
12089
12480
|
}
|
|
12090
12481
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12091
|
-
const local =
|
|
12092
|
-
if (
|
|
12093
|
-
const recipePath =
|
|
12482
|
+
const local = path26.resolve(target);
|
|
12483
|
+
if (existsSync20(local)) {
|
|
12484
|
+
const recipePath = path26.basename(local) === RECIPE_FILE ? local : path26.join(local, RECIPE_FILE);
|
|
12094
12485
|
return readRecipeFile(recipePath, false);
|
|
12095
12486
|
}
|
|
12096
12487
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12104,8 +12495,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12104
12495
|
return matches[0];
|
|
12105
12496
|
}
|
|
12106
12497
|
async function verifyFile(root, file) {
|
|
12107
|
-
const absolute =
|
|
12108
|
-
if (!
|
|
12498
|
+
const absolute = path26.resolve(root, ...file.path.split("/"));
|
|
12499
|
+
if (!existsSync20(absolute)) {
|
|
12109
12500
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12110
12501
|
}
|
|
12111
12502
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12120,7 +12511,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12120
12511
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12121
12512
|
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
12122
12513
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12123
|
-
const modelRoot = options.modelRoot ?
|
|
12514
|
+
const modelRoot = options.modelRoot ? path26.resolve(options.modelRoot) : null;
|
|
12124
12515
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12125
12516
|
...await verifyFile(modelRoot, model),
|
|
12126
12517
|
source: model.source,
|
|
@@ -12154,7 +12545,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12154
12545
|
};
|
|
12155
12546
|
}
|
|
12156
12547
|
function renderStyle(style, destination, cwd) {
|
|
12157
|
-
const portableDir =
|
|
12548
|
+
const portableDir = path26.relative(cwd, destination).split(path26.sep).join("/") || ".";
|
|
12158
12549
|
const replace = (value) => {
|
|
12159
12550
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12160
12551
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12166,24 +12557,24 @@ function renderStyle(style, destination, cwd) {
|
|
|
12166
12557
|
return replace(style);
|
|
12167
12558
|
}
|
|
12168
12559
|
async function installRecipe(target, options = {}) {
|
|
12169
|
-
const cwd =
|
|
12560
|
+
const cwd = path26.resolve(options.cwd ?? process.cwd());
|
|
12170
12561
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12171
12562
|
const verification = await verifyRecipe(loaded.path);
|
|
12172
12563
|
if (!verification.ok) {
|
|
12173
12564
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12174
12565
|
}
|
|
12175
|
-
const destination = options.out ?
|
|
12566
|
+
const destination = options.out ? path26.resolve(cwd, options.out) : path26.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12176
12567
|
const sources = [
|
|
12177
12568
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12178
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
12569
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path26.join(loaded.dir, ...file.path.split("/")) }))
|
|
12179
12570
|
];
|
|
12180
12571
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12181
|
-
path:
|
|
12572
|
+
path: path26.join(destination, ...file.path.split("/")),
|
|
12182
12573
|
data: await readFile20(file.source)
|
|
12183
12574
|
})));
|
|
12184
12575
|
if (!options.force) {
|
|
12185
12576
|
for (const file of files) {
|
|
12186
|
-
if (!
|
|
12577
|
+
if (!existsSync20(file.path)) continue;
|
|
12187
12578
|
const current = await readFile20(file.path);
|
|
12188
12579
|
if (!current.equals(file.data)) {
|
|
12189
12580
|
throw new Error(
|
|
@@ -12207,15 +12598,15 @@ async function installRecipe(target, options = {}) {
|
|
|
12207
12598
|
}
|
|
12208
12599
|
|
|
12209
12600
|
// src/pipeline/quality-regression.ts
|
|
12210
|
-
import
|
|
12211
|
-
import { existsSync as
|
|
12601
|
+
import path27 from "path";
|
|
12602
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12212
12603
|
import { readFile as readFile21 } from "fs/promises";
|
|
12213
|
-
import { z as
|
|
12604
|
+
import { z as z9 } from "zod";
|
|
12214
12605
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
12215
12606
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
12216
12607
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
12217
|
-
var QualityPathSchema =
|
|
12218
|
-
(value) => !
|
|
12608
|
+
var QualityPathSchema = z9.string().min(1).refine(
|
|
12609
|
+
(value) => !path27.posix.isAbsolute(value) && !value.includes("\\"),
|
|
12219
12610
|
"expected a portable relative path"
|
|
12220
12611
|
);
|
|
12221
12612
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -12228,53 +12619,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
12228
12619
|
maxEdgeContrastDrop: 0.03,
|
|
12229
12620
|
maxIsolatedPixelIncrease: 5e-3
|
|
12230
12621
|
};
|
|
12231
|
-
var QualityToleranceSchema =
|
|
12232
|
-
requireExactHash:
|
|
12233
|
-
maxColorCountIncrease:
|
|
12234
|
-
maxNewColors:
|
|
12235
|
-
maxTransparencyDelta:
|
|
12236
|
-
maxPartialAlphaIncrease:
|
|
12237
|
-
maxEdgeDensityDelta:
|
|
12238
|
-
maxEdgeContrastDrop:
|
|
12239
|
-
maxIsolatedPixelIncrease:
|
|
12622
|
+
var QualityToleranceSchema = z9.object({
|
|
12623
|
+
requireExactHash: z9.boolean(),
|
|
12624
|
+
maxColorCountIncrease: z9.number().int().min(0),
|
|
12625
|
+
maxNewColors: z9.number().int().min(0),
|
|
12626
|
+
maxTransparencyDelta: z9.number().min(0).max(1),
|
|
12627
|
+
maxPartialAlphaIncrease: z9.number().min(0).max(1),
|
|
12628
|
+
maxEdgeDensityDelta: z9.number().min(0).max(1),
|
|
12629
|
+
maxEdgeContrastDrop: z9.number().min(0).max(1),
|
|
12630
|
+
maxIsolatedPixelIncrease: z9.number().min(0).max(1)
|
|
12240
12631
|
}).strict();
|
|
12241
|
-
var ImageQualityMetricsSchema =
|
|
12242
|
-
sha256:
|
|
12243
|
-
width:
|
|
12244
|
-
height:
|
|
12245
|
-
colorCount:
|
|
12246
|
-
colors:
|
|
12247
|
-
transparency:
|
|
12248
|
-
partialAlpha:
|
|
12249
|
-
edgeDensity:
|
|
12250
|
-
meanEdgeContrast:
|
|
12251
|
-
isolatedPixelRatio:
|
|
12632
|
+
var ImageQualityMetricsSchema = z9.object({
|
|
12633
|
+
sha256: z9.string().regex(SHA256_RE2),
|
|
12634
|
+
width: z9.number().int().min(1),
|
|
12635
|
+
height: z9.number().int().min(1),
|
|
12636
|
+
colorCount: z9.number().int().min(0),
|
|
12637
|
+
colors: z9.array(z9.string().regex(HEX_RE)),
|
|
12638
|
+
transparency: z9.number().min(0).max(1),
|
|
12639
|
+
partialAlpha: z9.number().min(0).max(1),
|
|
12640
|
+
edgeDensity: z9.number().min(0).max(1),
|
|
12641
|
+
meanEdgeContrast: z9.number().min(0).max(1),
|
|
12642
|
+
isolatedPixelRatio: z9.number().min(0).max(1)
|
|
12252
12643
|
}).strict();
|
|
12253
|
-
var QualityBaselineCaseSchema =
|
|
12254
|
-
id:
|
|
12644
|
+
var QualityBaselineCaseSchema = z9.object({
|
|
12645
|
+
id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12255
12646
|
file: QualityPathSchema,
|
|
12256
|
-
record:
|
|
12647
|
+
record: z9.object({
|
|
12257
12648
|
path: QualityPathSchema,
|
|
12258
|
-
sha256:
|
|
12649
|
+
sha256: z9.string().regex(SHA256_RE2)
|
|
12259
12650
|
}).strict().optional(),
|
|
12260
12651
|
expected: ImageQualityMetricsSchema,
|
|
12261
12652
|
tolerances: QualityToleranceSchema
|
|
12262
12653
|
}).strict();
|
|
12263
|
-
var QualityBaselineSchema =
|
|
12264
|
-
$schema:
|
|
12265
|
-
format:
|
|
12266
|
-
schemaVersion:
|
|
12267
|
-
cases:
|
|
12654
|
+
var QualityBaselineSchema = z9.object({
|
|
12655
|
+
$schema: z9.string().url().optional(),
|
|
12656
|
+
format: z9.literal("pixelkiln-quality-baseline"),
|
|
12657
|
+
schemaVersion: z9.literal(1),
|
|
12658
|
+
cases: z9.array(QualityBaselineCaseSchema).min(1)
|
|
12268
12659
|
}).strict().superRefine((baseline, context) => {
|
|
12269
12660
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
12270
12661
|
if (new Set(ids).size !== ids.length) {
|
|
12271
|
-
context.addIssue({ code:
|
|
12662
|
+
context.addIssue({ code: z9.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
12272
12663
|
}
|
|
12273
12664
|
});
|
|
12274
|
-
var QualityInputSchema =
|
|
12275
|
-
id:
|
|
12276
|
-
path:
|
|
12277
|
-
record:
|
|
12665
|
+
var QualityInputSchema = z9.object({
|
|
12666
|
+
id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
12667
|
+
path: z9.string().min(1),
|
|
12668
|
+
record: z9.string().min(1).optional(),
|
|
12278
12669
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
12279
12670
|
}).strict();
|
|
12280
12671
|
function message5(error) {
|
|
@@ -12362,7 +12753,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
12362
12753
|
};
|
|
12363
12754
|
}
|
|
12364
12755
|
async function measureImageQuality(file) {
|
|
12365
|
-
const absolute =
|
|
12756
|
+
const absolute = path27.resolve(file);
|
|
12366
12757
|
let bytes;
|
|
12367
12758
|
try {
|
|
12368
12759
|
bytes = await readFile21(absolute);
|
|
@@ -12376,7 +12767,7 @@ async function measureImageQuality(file) {
|
|
|
12376
12767
|
}
|
|
12377
12768
|
}
|
|
12378
12769
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
12379
|
-
const parsed =
|
|
12770
|
+
const parsed = z9.array(QualityInputSchema).min(1).safeParse(raw);
|
|
12380
12771
|
if (!parsed.success) {
|
|
12381
12772
|
throw new Error(
|
|
12382
12773
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -12384,11 +12775,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12384
12775
|
}
|
|
12385
12776
|
const ids = parsed.data.map((entry) => entry.id);
|
|
12386
12777
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
12387
|
-
const root =
|
|
12778
|
+
const root = path27.dirname(path27.resolve(inputsFilePath));
|
|
12388
12779
|
return parsed.data.map((entry) => ({
|
|
12389
12780
|
id: entry.id,
|
|
12390
|
-
file:
|
|
12391
|
-
...entry.record ? { record:
|
|
12781
|
+
file: path27.resolve(root, entry.path),
|
|
12782
|
+
...entry.record ? { record: path27.resolve(root, entry.record) } : {},
|
|
12392
12783
|
tolerances: QualityToleranceSchema.parse({
|
|
12393
12784
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
12394
12785
|
...entry.tolerances
|
|
@@ -12396,7 +12787,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
12396
12787
|
}));
|
|
12397
12788
|
}
|
|
12398
12789
|
function portableRelative2(from, to) {
|
|
12399
|
-
return
|
|
12790
|
+
return path27.relative(from, path27.resolve(to)).split(path27.sep).join("/") || ".";
|
|
12400
12791
|
}
|
|
12401
12792
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
12402
12793
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -12411,20 +12802,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
12411
12802
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
12412
12803
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
12413
12804
|
}
|
|
12414
|
-
const outputs = record.outputs.map((output) =>
|
|
12415
|
-
if (!outputs.includes(
|
|
12805
|
+
const outputs = record.outputs.map((output) => path27.resolve(path27.dirname(recordPath), output.path));
|
|
12806
|
+
if (!outputs.includes(path27.resolve(imagePath))) {
|
|
12416
12807
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
12417
12808
|
}
|
|
12418
12809
|
}
|
|
12419
12810
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
12420
12811
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
12421
|
-
const absolute =
|
|
12812
|
+
const absolute = path27.resolve(baselinePath);
|
|
12422
12813
|
for (const input of inputs) {
|
|
12423
|
-
if (
|
|
12814
|
+
if (path27.resolve(input.file) === absolute || input.record && path27.resolve(input.record) === absolute) {
|
|
12424
12815
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
12425
12816
|
}
|
|
12426
12817
|
}
|
|
12427
|
-
const root =
|
|
12818
|
+
const root = path27.dirname(absolute);
|
|
12428
12819
|
const cases = [];
|
|
12429
12820
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
12430
12821
|
const expected = await measureImageQuality(input.file);
|
|
@@ -12451,7 +12842,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12451
12842
|
cases
|
|
12452
12843
|
});
|
|
12453
12844
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
12454
|
-
if (
|
|
12845
|
+
if (existsSync21(absolute) && !options.force) {
|
|
12455
12846
|
const current = await readFile21(absolute);
|
|
12456
12847
|
if (!current.equals(data)) {
|
|
12457
12848
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
@@ -12461,7 +12852,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
12461
12852
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
12462
12853
|
}
|
|
12463
12854
|
async function readQualityBaseline(baselinePath) {
|
|
12464
|
-
const absolute =
|
|
12855
|
+
const absolute = path27.resolve(baselinePath);
|
|
12465
12856
|
let raw;
|
|
12466
12857
|
try {
|
|
12467
12858
|
raw = JSON.parse(await readFile21(absolute, "utf8"));
|
|
@@ -12534,12 +12925,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
12534
12925
|
return { violations, warnings };
|
|
12535
12926
|
}
|
|
12536
12927
|
async function checkQualityBaseline(baselinePath) {
|
|
12537
|
-
const absolute =
|
|
12928
|
+
const absolute = path27.resolve(baselinePath);
|
|
12538
12929
|
const baseline = await readQualityBaseline(absolute);
|
|
12539
|
-
const root =
|
|
12930
|
+
const root = path27.dirname(absolute);
|
|
12540
12931
|
const cases = [];
|
|
12541
12932
|
for (const entry of baseline.cases) {
|
|
12542
|
-
const file =
|
|
12933
|
+
const file = path27.resolve(root, entry.file);
|
|
12543
12934
|
let actual = null;
|
|
12544
12935
|
const violations = [];
|
|
12545
12936
|
const warnings = [];
|
|
@@ -12552,7 +12943,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12552
12943
|
violations.push(message5(error));
|
|
12553
12944
|
}
|
|
12554
12945
|
if (entry.record) {
|
|
12555
|
-
const recordPath =
|
|
12946
|
+
const recordPath = path27.resolve(root, entry.record.path);
|
|
12556
12947
|
try {
|
|
12557
12948
|
const recordHash = await sha256File(recordPath);
|
|
12558
12949
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -12638,13 +13029,13 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12638
13029
|
};
|
|
12639
13030
|
const loadProject = async (project) => {
|
|
12640
13031
|
const ctx = await access2.loadProject(project);
|
|
12641
|
-
const dir =
|
|
13032
|
+
const dir = path28.dirname(ctx.loaded.path);
|
|
12642
13033
|
const declared = readEnvFiles(dir);
|
|
12643
13034
|
const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
|
|
12644
13035
|
for (const [name, value] of Object.entries(declared)) {
|
|
12645
13036
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
12646
13037
|
throw new Error(
|
|
12647
|
-
`${
|
|
13038
|
+
`${path28.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.`
|
|
12648
13039
|
);
|
|
12649
13040
|
}
|
|
12650
13041
|
}
|
|
@@ -12670,7 +13061,7 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12670
13061
|
args.edit,
|
|
12671
13062
|
budget ? describeBudget(budget) : null
|
|
12672
13063
|
),
|
|
12673
|
-
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, reload, onProgress: log }) } : {},
|
|
13064
|
+
...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
|
|
12674
13065
|
...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
|
|
12675
13066
|
});
|
|
12676
13067
|
await new Promise((resolve) => {
|
|
@@ -12686,11 +13077,11 @@ async function runGallery(initial, reload, args, access2) {
|
|
|
12686
13077
|
log("\n gallery closed");
|
|
12687
13078
|
}
|
|
12688
13079
|
async function provenanceFile(id, file) {
|
|
12689
|
-
const absolute =
|
|
13080
|
+
const absolute = path28.resolve(file);
|
|
12690
13081
|
return {
|
|
12691
13082
|
id,
|
|
12692
13083
|
path: absolute,
|
|
12693
|
-
sha256:
|
|
13084
|
+
sha256: existsSync22(absolute) ? await sha256File(absolute) : null,
|
|
12694
13085
|
included: true
|
|
12695
13086
|
};
|
|
12696
13087
|
}
|
|
@@ -12739,7 +13130,8 @@ var BOOL_FLAGS = [
|
|
|
12739
13130
|
"--write-prompts",
|
|
12740
13131
|
"--primary-only",
|
|
12741
13132
|
"--prune",
|
|
12742
|
-
"--edit"
|
|
13133
|
+
"--edit",
|
|
13134
|
+
"--refresh"
|
|
12743
13135
|
];
|
|
12744
13136
|
var COMMANDS = [
|
|
12745
13137
|
"init",
|
|
@@ -12765,6 +13157,7 @@ var COMMANDS = [
|
|
|
12765
13157
|
"balance",
|
|
12766
13158
|
"status",
|
|
12767
13159
|
"gallery",
|
|
13160
|
+
"edit",
|
|
12768
13161
|
"quality",
|
|
12769
13162
|
"refine",
|
|
12770
13163
|
"recipe",
|
|
@@ -12779,6 +13172,7 @@ var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
|
12779
13172
|
var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
12780
13173
|
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
12781
13174
|
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
13175
|
+
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
12782
13176
|
function parseArgs(argv) {
|
|
12783
13177
|
const [command = "help"] = argv;
|
|
12784
13178
|
if (!COMMANDS.includes(command)) {
|
|
@@ -12836,6 +13230,12 @@ function parseArgs(argv) {
|
|
|
12836
13230
|
}
|
|
12837
13231
|
rest = rest.slice(1);
|
|
12838
13232
|
}
|
|
13233
|
+
} else if (command === "edit") {
|
|
13234
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "start" : rest[0];
|
|
13235
|
+
if (!EDIT_SUBCOMMANDS.includes(subcommand)) {
|
|
13236
|
+
throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
|
|
13237
|
+
}
|
|
13238
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
12839
13239
|
} else if (command === "refine") {
|
|
12840
13240
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
12841
13241
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -12949,7 +13349,7 @@ function parseArgs(argv) {
|
|
|
12949
13349
|
return {
|
|
12950
13350
|
command,
|
|
12951
13351
|
manifest,
|
|
12952
|
-
lock: get("--lock") ??
|
|
13352
|
+
lock: get("--lock") ?? path28.join(path28.dirname(path28.resolve(manifest)), "pixelkiln.lock.json"),
|
|
12953
13353
|
explicitLock: get("--lock"),
|
|
12954
13354
|
styles: list("--style"),
|
|
12955
13355
|
assets: list("--only"),
|
|
@@ -12964,6 +13364,7 @@ function parseArgs(argv) {
|
|
|
12964
13364
|
noOpen: rest.includes("--no-open"),
|
|
12965
13365
|
tag: rest.includes("--tag"),
|
|
12966
13366
|
edit: rest.includes("--edit"),
|
|
13367
|
+
refresh: rest.includes("--refresh"),
|
|
12967
13368
|
from: get("--from"),
|
|
12968
13369
|
out: get("--out"),
|
|
12969
13370
|
generator: get("--generator"),
|
|
@@ -13030,6 +13431,9 @@ Commands
|
|
|
13030
13431
|
tag Push manifest tags to the objects upstream (free).
|
|
13031
13432
|
balance Show the provider's remaining balance.
|
|
13032
13433
|
status Summarise the lockfile.
|
|
13434
|
+
edit Hand-edit an asset in your own editor: copies the generated PNG to
|
|
13435
|
+
<outDir>/edits/, declares it as the asset's source, and opens it.
|
|
13436
|
+
edit detach places the generated art again (the file is kept).
|
|
13033
13437
|
gallery Open a local read-only gallery of every generation and its
|
|
13034
13438
|
provenance: prompt, provider, cost, outputs, lineage, quality.
|
|
13035
13439
|
--workspace <catalog> shows every registered project at once;
|
|
@@ -13072,9 +13476,11 @@ Options
|
|
|
13072
13476
|
and the gallery snapshot (no server)
|
|
13073
13477
|
--check plan/audit/cache: exit nonzero when the selected state is unsafe
|
|
13074
13478
|
--yes, -y Skip the confirmation prompt
|
|
13075
|
-
--no-open Do not auto-open the browser
|
|
13479
|
+
--no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
|
|
13076
13480
|
--edit gallery: allow manifest edits from the page (never spends)
|
|
13077
13481
|
--tag Also push tags upstream after fetch
|
|
13482
|
+
--refresh fetch: re-download and replace files whose object changed
|
|
13483
|
+
upstream (e.g. edited in PixelLab's editor); no generation
|
|
13078
13484
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
13079
13485
|
--workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
|
|
13080
13486
|
derives salvage's claim set instead of repeated --claims,
|
|
@@ -13089,6 +13495,8 @@ Examples
|
|
|
13089
13495
|
pixelkiln gallery --style heybud-premium
|
|
13090
13496
|
pixelkiln gallery --workspace pixelkiln.workspace.json
|
|
13091
13497
|
pixelkiln gallery --edit --budget 80
|
|
13498
|
+
pixelkiln edit --style base --only anvil
|
|
13499
|
+
PIXELKILN_EDITOR="open -a Aseprite" pixelkiln edit --only anvil --style base
|
|
13092
13500
|
pixelkiln adopt --tag
|
|
13093
13501
|
pixelkiln pack --style heybud-premium
|
|
13094
13502
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
@@ -13165,6 +13573,15 @@ function printResumeActions(specs, lock) {
|
|
|
13165
13573
|
}
|
|
13166
13574
|
return actions.length;
|
|
13167
13575
|
}
|
|
13576
|
+
function manifestSources(manifest, styleId) {
|
|
13577
|
+
const sources = {};
|
|
13578
|
+
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
13579
|
+
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
13580
|
+
const source = asset.sourceByStyle[styleId] ?? asset.source;
|
|
13581
|
+
if (source) sources[assetId] = source;
|
|
13582
|
+
}
|
|
13583
|
+
return sources;
|
|
13584
|
+
}
|
|
13168
13585
|
function manifestProviderIds(manifest) {
|
|
13169
13586
|
return [...new Set(
|
|
13170
13587
|
Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
|
|
@@ -13238,10 +13655,10 @@ async function confirm(question, auto) {
|
|
|
13238
13655
|
return answer === "y" || answer === "yes";
|
|
13239
13656
|
}
|
|
13240
13657
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13241
|
-
if (!
|
|
13658
|
+
if (!existsSync22(workspacePath)) {
|
|
13242
13659
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13243
13660
|
}
|
|
13244
|
-
const dir =
|
|
13661
|
+
const dir = path28.dirname(path28.resolve(workspacePath));
|
|
13245
13662
|
const ws = await loadWorkspace(workspacePath);
|
|
13246
13663
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13247
13664
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13273,15 +13690,15 @@ async function main() {
|
|
|
13273
13690
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
13274
13691
|
let raw;
|
|
13275
13692
|
try {
|
|
13276
|
-
raw = JSON.parse(await readFile22(
|
|
13693
|
+
raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
|
|
13277
13694
|
} catch (error) {
|
|
13278
13695
|
throw new Error(
|
|
13279
|
-
`Could not read quality inputs ${
|
|
13696
|
+
`Could not read quality inputs ${path28.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13280
13697
|
{ cause: error }
|
|
13281
13698
|
);
|
|
13282
13699
|
}
|
|
13283
13700
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
13284
|
-
if (
|
|
13701
|
+
if (path28.resolve(args.inputs) === path28.resolve(args.out)) {
|
|
13285
13702
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
13286
13703
|
}
|
|
13287
13704
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -13294,7 +13711,7 @@ async function main() {
|
|
|
13294
13711
|
}, null, 2));
|
|
13295
13712
|
} else {
|
|
13296
13713
|
log(
|
|
13297
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
13714
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path28.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
13298
13715
|
);
|
|
13299
13716
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
13300
13717
|
}
|
|
@@ -13389,14 +13806,14 @@ async function main() {
|
|
|
13389
13806
|
}, null, 2));
|
|
13390
13807
|
} else {
|
|
13391
13808
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
13392
|
-
log(` ${
|
|
13809
|
+
log(` ${path28.relative(process.cwd(), result.destination) || "."}`);
|
|
13393
13810
|
log(`
|
|
13394
13811
|
Add this entry under your manifest's styles object:`);
|
|
13395
13812
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
13396
13813
|
if (result.recipe.models.length) {
|
|
13397
13814
|
log(`
|
|
13398
13815
|
Models are not downloaded automatically. Verify them with:`);
|
|
13399
|
-
log(` pixelkiln recipe verify ${
|
|
13816
|
+
log(` pixelkiln recipe verify ${path28.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
13400
13817
|
}
|
|
13401
13818
|
}
|
|
13402
13819
|
return;
|
|
@@ -13430,12 +13847,12 @@ async function main() {
|
|
|
13430
13847
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
13431
13848
|
);
|
|
13432
13849
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
13433
|
-
log(` wrote ${
|
|
13434
|
-
log(` quality record: ${
|
|
13850
|
+
log(` wrote ${path28.relative(process.cwd(), result2.output)}`);
|
|
13851
|
+
log(` quality record: ${path28.relative(process.cwd(), result2.record)}`);
|
|
13435
13852
|
log(`
|
|
13436
13853
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
13437
13854
|
log(
|
|
13438
|
-
` pixelkiln refine approve --from ${
|
|
13855
|
+
` pixelkiln refine approve --from ${path28.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
13439
13856
|
);
|
|
13440
13857
|
}
|
|
13441
13858
|
return;
|
|
@@ -13463,7 +13880,7 @@ async function main() {
|
|
|
13463
13880
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
13464
13881
|
else {
|
|
13465
13882
|
log(
|
|
13466
|
-
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${
|
|
13883
|
+
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path28.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path28.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
|
|
13467
13884
|
);
|
|
13468
13885
|
}
|
|
13469
13886
|
return;
|
|
@@ -13472,14 +13889,14 @@ async function main() {
|
|
|
13472
13889
|
if (args.json) {
|
|
13473
13890
|
log(JSON.stringify(result, null, 2));
|
|
13474
13891
|
} else {
|
|
13475
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
13892
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path28.relative(process.cwd(), result.output || result.record)}`);
|
|
13476
13893
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
13477
13894
|
}
|
|
13478
13895
|
if (!result.safe) process.exitCode = 1;
|
|
13479
13896
|
return;
|
|
13480
13897
|
}
|
|
13481
13898
|
if (args.command === "balance") {
|
|
13482
|
-
loadEnvFiles(
|
|
13899
|
+
loadEnvFiles(path28.dirname(path28.resolve(args.manifest)));
|
|
13483
13900
|
loadEnvFiles(process.cwd());
|
|
13484
13901
|
const loaded2 = await loadManifest(args.manifest);
|
|
13485
13902
|
const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
|
|
@@ -13492,31 +13909,31 @@ async function main() {
|
|
|
13492
13909
|
}
|
|
13493
13910
|
if (args.command === "init") {
|
|
13494
13911
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
13495
|
-
const root =
|
|
13496
|
-
if (!
|
|
13912
|
+
const root = path28.resolve(args.from);
|
|
13913
|
+
if (!existsSync22(root)) throw new Error(`No directory at ${root}`);
|
|
13497
13914
|
const generator = args.generator ?? "map";
|
|
13498
13915
|
if (generator !== "1dir" && generator !== "map") {
|
|
13499
13916
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
13500
13917
|
}
|
|
13501
|
-
const target =
|
|
13918
|
+
const target = path28.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
13502
13919
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
13503
13920
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
13504
13921
|
const manifest = buildManifest(
|
|
13505
|
-
args.name ??
|
|
13922
|
+
args.name ?? path28.basename(path28.dirname(target)),
|
|
13506
13923
|
args.styles[0] ?? "base",
|
|
13507
13924
|
generator,
|
|
13508
|
-
|
|
13925
|
+
path28.relative(path28.dirname(target), root) || ".",
|
|
13509
13926
|
assets
|
|
13510
13927
|
);
|
|
13511
13928
|
await writeManifestFile(target, manifest);
|
|
13512
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
13929
|
+
log(` scanned ${assets.length} PNG(s) under ${path28.relative(process.cwd(), root)}`);
|
|
13513
13930
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
13514
|
-
log(` wrote ${
|
|
13931
|
+
log(` wrote ${path28.relative(process.cwd(), target)}`);
|
|
13515
13932
|
log(`
|
|
13516
13933
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
13517
13934
|
log(` PixelLab account instead of inventing them:`);
|
|
13518
13935
|
log(`
|
|
13519
|
-
pixelkiln adopt --manifest ${
|
|
13936
|
+
pixelkiln adopt --manifest ${path28.relative(process.cwd(), target)} --write-prompts
|
|
13520
13937
|
`);
|
|
13521
13938
|
return;
|
|
13522
13939
|
}
|
|
@@ -13564,10 +13981,10 @@ async function main() {
|
|
|
13564
13981
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
13565
13982
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
13566
13983
|
}
|
|
13567
|
-
const raw = JSON.parse(await readFile22(
|
|
13984
|
+
const raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
|
|
13568
13985
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
13569
13986
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
13570
|
-
const base =
|
|
13987
|
+
const base = path28.resolve(args.out.replace(/\.png$/, ""));
|
|
13571
13988
|
const outputs = [
|
|
13572
13989
|
{ path: `${base}.png`, data: png },
|
|
13573
13990
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -13580,17 +13997,17 @@ async function main() {
|
|
|
13580
13997
|
log(
|
|
13581
13998
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
13582
13999
|
);
|
|
13583
|
-
log(` ${
|
|
14000
|
+
log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
13584
14001
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
13585
14002
|
return;
|
|
13586
14003
|
}
|
|
13587
14004
|
if (args.command === "workspace") {
|
|
13588
|
-
const workspacePath =
|
|
13589
|
-
const dir =
|
|
14005
|
+
const workspacePath = path28.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
14006
|
+
const dir = path28.dirname(workspacePath);
|
|
13590
14007
|
if (args.subcommand === "add") {
|
|
13591
|
-
const manifestPath =
|
|
14008
|
+
const manifestPath = path28.resolve(args.target);
|
|
13592
14009
|
const loadedTarget = await loadManifest(manifestPath);
|
|
13593
|
-
const lockPath = args.explicitLock ?
|
|
14010
|
+
const lockPath = args.explicitLock ? path28.resolve(args.explicitLock) : path28.join(path28.dirname(manifestPath), "pixelkiln.lock.json");
|
|
13594
14011
|
const ws = await loadWorkspace(workspacePath);
|
|
13595
14012
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
13596
14013
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -13610,10 +14027,10 @@ async function main() {
|
|
|
13610
14027
|
...args.account ? { account: args.account } : {}
|
|
13611
14028
|
};
|
|
13612
14029
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
13613
|
-
log(` registered "${id}" in ${
|
|
14030
|
+
log(` registered "${id}" in ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13614
14031
|
log(` manifest: ${project.manifest}`);
|
|
13615
14032
|
log(` lock: ${project.lock}`);
|
|
13616
|
-
if (!
|
|
14033
|
+
if (!existsSync22(lockPath)) {
|
|
13617
14034
|
log(
|
|
13618
14035
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
13619
14036
|
);
|
|
@@ -13622,7 +14039,7 @@ async function main() {
|
|
|
13622
14039
|
}
|
|
13623
14040
|
if (args.subcommand === "remove") {
|
|
13624
14041
|
const ws = await loadWorkspace(workspacePath);
|
|
13625
|
-
const resolvedTarget =
|
|
14042
|
+
const resolvedTarget = path28.resolve(args.target);
|
|
13626
14043
|
const match = ws.projects.find(
|
|
13627
14044
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
13628
14045
|
);
|
|
@@ -13633,11 +14050,11 @@ async function main() {
|
|
|
13633
14050
|
version: 1,
|
|
13634
14051
|
projects: ws.projects.filter((p) => p !== match)
|
|
13635
14052
|
});
|
|
13636
|
-
log(` removed "${match.id}" from ${
|
|
14053
|
+
log(` removed "${match.id}" from ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13637
14054
|
return;
|
|
13638
14055
|
}
|
|
13639
14056
|
if (args.subcommand === "list") {
|
|
13640
|
-
if (!
|
|
14057
|
+
if (!existsSync22(workspacePath)) {
|
|
13641
14058
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13642
14059
|
}
|
|
13643
14060
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -13645,9 +14062,9 @@ async function main() {
|
|
|
13645
14062
|
if (args.json) {
|
|
13646
14063
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
13647
14064
|
} else if (!ws.projects.length) {
|
|
13648
|
-
log(` no projects registered in ${
|
|
14065
|
+
log(` no projects registered in ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13649
14066
|
} else {
|
|
13650
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
14067
|
+
log(` ${ws.projects.length} project(s) in ${path28.relative(process.cwd(), workspacePath)}:`);
|
|
13651
14068
|
for (const p of ws.projects) {
|
|
13652
14069
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
13653
14070
|
}
|
|
@@ -13657,7 +14074,7 @@ async function main() {
|
|
|
13657
14074
|
return;
|
|
13658
14075
|
}
|
|
13659
14076
|
if (args.subcommand === "status") {
|
|
13660
|
-
if (!
|
|
14077
|
+
if (!existsSync22(workspacePath)) {
|
|
13661
14078
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13662
14079
|
}
|
|
13663
14080
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -13665,7 +14082,7 @@ async function main() {
|
|
|
13665
14082
|
if (args.json) {
|
|
13666
14083
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
13667
14084
|
} else {
|
|
13668
|
-
log(` workspace: ${
|
|
14085
|
+
log(` workspace: ${path28.relative(process.cwd(), workspacePath)}`);
|
|
13669
14086
|
for (const p of report.projects) {
|
|
13670
14087
|
if (p.error) {
|
|
13671
14088
|
log(`
|
|
@@ -13712,8 +14129,8 @@ async function main() {
|
|
|
13712
14129
|
}
|
|
13713
14130
|
}
|
|
13714
14131
|
if (args.command === "gallery" && args.workspace) {
|
|
13715
|
-
const workspacePath =
|
|
13716
|
-
if (!
|
|
14132
|
+
const workspacePath = path28.resolve(args.workspace);
|
|
14133
|
+
if (!existsSync22(workspacePath)) {
|
|
13717
14134
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
13718
14135
|
}
|
|
13719
14136
|
const filter = { styles: args.styles, assets: args.assets };
|
|
@@ -13733,7 +14150,7 @@ async function main() {
|
|
|
13733
14150
|
const registered = async (projectId) => {
|
|
13734
14151
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
13735
14152
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
13736
|
-
return resolveProject(
|
|
14153
|
+
return resolveProject(path28.dirname(workspacePath), project);
|
|
13737
14154
|
};
|
|
13738
14155
|
await runGallery(initial, build, args, {
|
|
13739
14156
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -13748,14 +14165,14 @@ async function main() {
|
|
|
13748
14165
|
});
|
|
13749
14166
|
return;
|
|
13750
14167
|
}
|
|
13751
|
-
if (!
|
|
14168
|
+
if (!existsSync22(path28.resolve(args.manifest))) {
|
|
13752
14169
|
throw new Error(
|
|
13753
|
-
`No manifest at ${
|
|
14170
|
+
`No manifest at ${path28.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
13754
14171
|
);
|
|
13755
14172
|
}
|
|
13756
|
-
const manifestDir =
|
|
14173
|
+
const manifestDir = path28.dirname(path28.resolve(args.manifest));
|
|
13757
14174
|
const envFiles2 = [...loadEnvFiles(manifestDir)];
|
|
13758
|
-
if (
|
|
14175
|
+
if (path28.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
|
|
13759
14176
|
const loaded = await loadManifest(args.manifest);
|
|
13760
14177
|
let specs = await resolveSpecs(loaded, {
|
|
13761
14178
|
styles: args.styles,
|
|
@@ -13828,7 +14245,7 @@ async function main() {
|
|
|
13828
14245
|
});
|
|
13829
14246
|
};
|
|
13830
14247
|
await runGallery(await build(), reload, args, {
|
|
13831
|
-
manifestFor: () =>
|
|
14248
|
+
manifestFor: () => path28.resolve(args.manifest),
|
|
13832
14249
|
loadProject: async () => {
|
|
13833
14250
|
const freshLoaded = await loadManifest(args.manifest);
|
|
13834
14251
|
const freshSpecs = await resolveSpecs(freshLoaded);
|
|
@@ -13839,6 +14256,29 @@ async function main() {
|
|
|
13839
14256
|
});
|
|
13840
14257
|
return;
|
|
13841
14258
|
}
|
|
14259
|
+
if (args.command === "edit") {
|
|
14260
|
+
if (!args.assets.length) throw new Error("edit needs --only <asset id> (and --style when the asset is in several styles).");
|
|
14261
|
+
if (specs.length !== 1) {
|
|
14262
|
+
throw new Error(
|
|
14263
|
+
specs.length ? `edit works on one asset in one style; --only ${args.assets.join(",")} resolves to ${specs.length}: ` + specs.map((spec2) => `${spec2.styleId}/${spec2.assetId}`).join(", ") + ". Add --style." : "edit matched no asset."
|
|
14264
|
+
);
|
|
14265
|
+
}
|
|
14266
|
+
const spec = specs[0];
|
|
14267
|
+
if (args.subcommand === "detach") {
|
|
14268
|
+
const result = await detachHandEdit(loaded, spec);
|
|
14269
|
+
log(result.changed ? ` detached the hand edit for ${spec.styleId}/${spec.assetId}; the generated art is placed again (the edit file is kept)` : ` ${spec.styleId}/${spec.assetId} has no hand edit`);
|
|
14270
|
+
return;
|
|
14271
|
+
}
|
|
14272
|
+
const started = await startHandEdit(loaded, lock, spec);
|
|
14273
|
+
log(` ${started.created ? "created" : "found"} ${started.source}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
14274
|
+
if (args.noOpen) {
|
|
14275
|
+
log(` edit it with your own tool, then run pixelkiln plan; mount and pack place it in place of the generated art`);
|
|
14276
|
+
} else {
|
|
14277
|
+
const command = openInEditor(started.editPath);
|
|
14278
|
+
log(` opened with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
|
|
14279
|
+
}
|
|
14280
|
+
return;
|
|
14281
|
+
}
|
|
13842
14282
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
13843
14283
|
if (args.command === "refine") {
|
|
13844
14284
|
const pathOwnedFlags = [
|
|
@@ -13880,7 +14320,7 @@ async function main() {
|
|
|
13880
14320
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
13881
14321
|
if (item.state === "needs-approval") {
|
|
13882
14322
|
log(
|
|
13883
|
-
` pixelkiln refine approve --from ${
|
|
14323
|
+
` pixelkiln refine approve --from ${path28.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
13884
14324
|
);
|
|
13885
14325
|
}
|
|
13886
14326
|
}
|
|
@@ -13971,7 +14411,7 @@ async function main() {
|
|
|
13971
14411
|
let intact = true;
|
|
13972
14412
|
for (const output of entry.outputs) {
|
|
13973
14413
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
13974
|
-
if (!
|
|
14414
|
+
if (!existsSync22(file) || await sha256File(file) !== output.sha256) {
|
|
13975
14415
|
intact = false;
|
|
13976
14416
|
break;
|
|
13977
14417
|
}
|
|
@@ -14029,7 +14469,7 @@ async function main() {
|
|
|
14029
14469
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
14030
14470
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
14031
14471
|
}
|
|
14032
|
-
const manifestDir2 =
|
|
14472
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14033
14473
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14034
14474
|
for (const styleId of styleIds) {
|
|
14035
14475
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14038,10 +14478,11 @@ async function main() {
|
|
|
14038
14478
|
columns: args.columns,
|
|
14039
14479
|
outputRoles: args.outputRoles,
|
|
14040
14480
|
primaryOnly: args.primaryOnly,
|
|
14041
|
-
sourceOverrides: qualitySources
|
|
14481
|
+
sourceOverrides: qualitySources,
|
|
14482
|
+
sources: manifestSources(loaded.manifest, styleId)
|
|
14042
14483
|
});
|
|
14043
14484
|
const style = loaded.manifest.styles[styleId];
|
|
14044
|
-
const base = args.out ?
|
|
14485
|
+
const base = args.out ? path28.resolve(args.out.replace(/\.png$/, "")) : path28.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
14045
14486
|
const outputs = [
|
|
14046
14487
|
{ path: `${base}.png`, data: png },
|
|
14047
14488
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -14071,13 +14512,13 @@ async function main() {
|
|
|
14071
14512
|
log(
|
|
14072
14513
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
14073
14514
|
);
|
|
14074
|
-
log(` ${
|
|
14515
|
+
log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
14075
14516
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14076
14517
|
}
|
|
14077
14518
|
return;
|
|
14078
14519
|
}
|
|
14079
14520
|
if (args.command === "mount") {
|
|
14080
|
-
const manifestDir2 =
|
|
14521
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14081
14522
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
14082
14523
|
for (const styleId of styleIds) {
|
|
14083
14524
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -14092,10 +14533,11 @@ async function main() {
|
|
|
14092
14533
|
const cells = {};
|
|
14093
14534
|
const sources = {};
|
|
14094
14535
|
const outputRoles = {};
|
|
14536
|
+
const styleSources = manifestSources(loaded.manifest, styleId);
|
|
14095
14537
|
for (const [assetId, asset] of Object.entries(loaded.manifest.assets)) {
|
|
14096
14538
|
if (!asset.cell) continue;
|
|
14097
14539
|
cells[assetId] = asset.cell;
|
|
14098
|
-
if (
|
|
14540
|
+
if (styleSources[assetId]) sources[assetId] = styleSources[assetId];
|
|
14099
14541
|
if (asset.outputRole) outputRoles[assetId] = asset.outputRole;
|
|
14100
14542
|
}
|
|
14101
14543
|
const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
|
|
@@ -14115,7 +14557,7 @@ async function main() {
|
|
|
14115
14557
|
sources,
|
|
14116
14558
|
outputRoles
|
|
14117
14559
|
);
|
|
14118
|
-
const out =
|
|
14560
|
+
const out = path28.resolve(manifestDir2, style.mount.out);
|
|
14119
14561
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
14120
14562
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
14121
14563
|
const outputs = [
|
|
@@ -14135,7 +14577,7 @@ async function main() {
|
|
|
14135
14577
|
await provenanceFile("$lock", args.lock),
|
|
14136
14578
|
...qualityRecords,
|
|
14137
14579
|
...artifactSources.filter(
|
|
14138
|
-
(source) => source.id !== "$base" ||
|
|
14580
|
+
(source) => source.id !== "$base" || path28.resolve(source.path) !== out
|
|
14139
14581
|
)
|
|
14140
14582
|
],
|
|
14141
14583
|
options: {
|
|
@@ -14148,7 +14590,7 @@ async function main() {
|
|
|
14148
14590
|
log(
|
|
14149
14591
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
14150
14592
|
);
|
|
14151
|
-
log(` ${
|
|
14593
|
+
log(` ${path28.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
14152
14594
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
14153
14595
|
}
|
|
14154
14596
|
return;
|
|
@@ -14204,7 +14646,7 @@ async function main() {
|
|
|
14204
14646
|
}
|
|
14205
14647
|
if (args.command === "export") {
|
|
14206
14648
|
const format = args.format ?? "generic";
|
|
14207
|
-
const manifestDir2 =
|
|
14649
|
+
const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
|
|
14208
14650
|
const selected = specs.filter((spec) => {
|
|
14209
14651
|
if (spec.generator !== "tiles") return false;
|
|
14210
14652
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -14220,12 +14662,12 @@ async function main() {
|
|
|
14220
14662
|
for (const spec of selected) {
|
|
14221
14663
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
14222
14664
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14223
|
-
const defaultBase =
|
|
14224
|
-
const base = args.out ?
|
|
14665
|
+
const defaultBase = path28.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
14666
|
+
const base = args.out ? path28.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
14225
14667
|
const result = exportTileset(entry, spec, {
|
|
14226
14668
|
format,
|
|
14227
14669
|
manifestDir: manifestDir2,
|
|
14228
|
-
imageName:
|
|
14670
|
+
imageName: path28.basename(`${base}.png`),
|
|
14229
14671
|
columns: args.columns
|
|
14230
14672
|
});
|
|
14231
14673
|
const outputs = [
|
|
@@ -14243,7 +14685,7 @@ async function main() {
|
|
|
14243
14685
|
asset: spec.assetId,
|
|
14244
14686
|
columns: args.columns ?? null,
|
|
14245
14687
|
format,
|
|
14246
|
-
image:
|
|
14688
|
+
image: path28.basename(`${base}.png`),
|
|
14247
14689
|
providerRules: result.generic.providerRules,
|
|
14248
14690
|
style: spec.styleId,
|
|
14249
14691
|
tileType: spec.tileType ?? null
|
|
@@ -14253,7 +14695,7 @@ async function main() {
|
|
|
14253
14695
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
14254
14696
|
);
|
|
14255
14697
|
log(
|
|
14256
|
-
` ${
|
|
14698
|
+
` ${path28.relative(process.cwd(), base)}.png + ${path28.basename(base)}${result.extension} + .pixelkiln.json`
|
|
14257
14699
|
);
|
|
14258
14700
|
}
|
|
14259
14701
|
return;
|
|
@@ -14303,12 +14745,12 @@ async function main() {
|
|
|
14303
14745
|
tagged ${n} object(s) upstream`);
|
|
14304
14746
|
}
|
|
14305
14747
|
if (args.writePrompts) {
|
|
14306
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
14748
|
+
const { filled, stillEmpty } = await writePromptsBack(path28.resolve(args.manifest), lock, {
|
|
14307
14749
|
onProgress: log,
|
|
14308
14750
|
provider: provider.id,
|
|
14309
14751
|
assetIds: specs.map((spec) => spec.assetId)
|
|
14310
14752
|
});
|
|
14311
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
14753
|
+
log(` recovered ${filled} prompt(s) into ${path28.relative(process.cwd(), args.manifest)}`);
|
|
14312
14754
|
const reloaded = await loadManifest(args.manifest);
|
|
14313
14755
|
const rebased = (await resolveSpecs(reloaded, {
|
|
14314
14756
|
styles: args.styles,
|
|
@@ -14337,12 +14779,12 @@ async function main() {
|
|
|
14337
14779
|
const provider = providerFor(selectedAccountProvider);
|
|
14338
14780
|
const jsonMode = args.dryRun && args.json;
|
|
14339
14781
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
14340
|
-
const ownLock =
|
|
14782
|
+
const ownLock = path28.resolve(args.lock);
|
|
14341
14783
|
let workspaceProjects = [];
|
|
14342
14784
|
let workspaceDir = "";
|
|
14343
14785
|
if (args.workspace) {
|
|
14344
|
-
const workspacePath =
|
|
14345
|
-
workspaceDir =
|
|
14786
|
+
const workspacePath = path28.resolve(args.workspace);
|
|
14787
|
+
workspaceDir = path28.dirname(workspacePath);
|
|
14346
14788
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
14347
14789
|
workspaceProjects = complete.ws.projects;
|
|
14348
14790
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -14351,12 +14793,12 @@ async function main() {
|
|
|
14351
14793
|
const lockPaths = [
|
|
14352
14794
|
.../* @__PURE__ */ new Set([
|
|
14353
14795
|
...workspaceLockPaths,
|
|
14354
|
-
...
|
|
14355
|
-
...args.claims.map((c) =>
|
|
14796
|
+
...existsSync22(ownLock) ? [ownLock] : [],
|
|
14797
|
+
...args.claims.map((c) => path28.resolve(c))
|
|
14356
14798
|
])
|
|
14357
14799
|
];
|
|
14358
14800
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
14359
|
-
for (const p of lockPaths) diag(` ${
|
|
14801
|
+
for (const p of lockPaths) diag(` ${path28.relative(process.cwd(), p)}`);
|
|
14360
14802
|
if (!args.claims.length && !args.workspace) {
|
|
14361
14803
|
diag(
|
|
14362
14804
|
`
|
|
@@ -14364,7 +14806,7 @@ async function main() {
|
|
|
14364
14806
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
14365
14807
|
register every project in a workspace catalog and pass --workspace.`
|
|
14366
14808
|
);
|
|
14367
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
14809
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path28.resolve(args.manifest))) {
|
|
14368
14810
|
diag(
|
|
14369
14811
|
`
|
|
14370
14812
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14448,7 +14890,7 @@ async function main() {
|
|
|
14448
14890
|
manifestPath: loaded.path,
|
|
14449
14891
|
manifest: accountManifest,
|
|
14450
14892
|
styleId,
|
|
14451
|
-
importDir:
|
|
14893
|
+
importDir: path28.resolve(loaded.root, style.outDir),
|
|
14452
14894
|
lock,
|
|
14453
14895
|
lockPath: args.lock
|
|
14454
14896
|
},
|
|
@@ -14683,17 +15125,19 @@ async function main() {
|
|
|
14683
15125
|
if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
|
|
14684
15126
|
log(`
|
|
14685
15127
|
downloading\u2026`);
|
|
14686
|
-
const total = { downloaded: 0, skipped: 0, failed: 0, tagged: 0 };
|
|
15128
|
+
const total = { downloaded: 0, skipped: 0, failed: 0, tagged: 0, unchanged: 0 };
|
|
14687
15129
|
for (const [providerId, providerSpecsForRun] of specsByRecordedProvider(specs, lock)) {
|
|
14688
15130
|
const groupProvider = providerFor(providerId);
|
|
14689
15131
|
const res = await fetchAssets(groupProvider, providerSpecsForRun, lock, args.lock, {
|
|
14690
15132
|
onProgress: log,
|
|
14691
15133
|
repair: args.command === "restore",
|
|
14692
|
-
force: args.force
|
|
15134
|
+
force: args.force,
|
|
15135
|
+
refresh: args.command === "fetch" && args.refresh
|
|
14693
15136
|
});
|
|
14694
15137
|
total.downloaded += res.downloaded;
|
|
14695
15138
|
total.skipped += res.skipped;
|
|
14696
15139
|
total.failed += res.failed;
|
|
15140
|
+
total.unchanged += res.unchanged ?? 0;
|
|
14697
15141
|
if (args.tag) {
|
|
14698
15142
|
total.tagged += await pushTags(groupProvider, providerSpecsForRun, lock, {
|
|
14699
15143
|
onProgress: log
|
|
@@ -14702,7 +15146,7 @@ async function main() {
|
|
|
14702
15146
|
}
|
|
14703
15147
|
log(
|
|
14704
15148
|
`
|
|
14705
|
-
downloaded ${total.downloaded}, skipped ${total.skipped}, failed ${total.failed}`
|
|
15149
|
+
downloaded ${total.downloaded}, skipped ${total.skipped}, failed ${total.failed}` + (args.refresh ? `, unchanged upstream ${total.unchanged}` : "")
|
|
14706
15150
|
);
|
|
14707
15151
|
if (total.failed) process.exitCode = 1;
|
|
14708
15152
|
if (args.tag) log(` tagged ${total.tagged} object(s) upstream`);
|