pixelkiln 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -44
- package/dist/cli.js +398 -185
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +776 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +439 -73
- package/dist/index.d.ts +439 -73
- package/dist/index.js +738 -198
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +5 -0
- package/docs/ARCHITECTURE.md +14 -0
- package/docs/ARTIFACTS.md +19 -13
- package/docs/CLI.md +18 -9
- package/docs/GETTING_STARTED.md +45 -3
- package/docs/LIBRARY.md +48 -2
- package/docs/MANIFEST.md +2 -2
- package/docs/README.md +3 -3
- package/docs/TILES.md +3 -1
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +6 -2
package/dist/index.cjs
CHANGED
|
@@ -34,17 +34,23 @@ __export(index_exports, {
|
|
|
34
34
|
ComfyUIProvider: () => ComfyUIProvider,
|
|
35
35
|
DEFAULT_QUALITY_TOLERANCES: () => DEFAULT_QUALITY_TOLERANCES,
|
|
36
36
|
DEFAULT_RATE_LIMIT: () => DEFAULT_RATE_LIMIT,
|
|
37
|
+
EDITOR_PIN: () => EDITOR_PIN,
|
|
38
|
+
EDITOR_RELEASE_BASE: () => EDITOR_RELEASE_BASE,
|
|
39
|
+
EditorInstallError: () => EditorInstallError,
|
|
37
40
|
FAKE_PNG: () => FAKE_PNG,
|
|
38
41
|
FakeProvider: () => FakeProvider,
|
|
39
42
|
GenerateRequestError: () => GenerateRequestError,
|
|
40
43
|
GenerateRequestSchema: () => GenerateRequestSchema,
|
|
41
44
|
GeneratorSchema: () => GeneratorSchema,
|
|
42
45
|
GridConfidenceSchema: () => GridConfidenceSchema,
|
|
46
|
+
HAND_EDIT_DIR: () => HAND_EDIT_DIR,
|
|
47
|
+
HandEditCompanionSchema: () => HandEditCompanionSchema,
|
|
43
48
|
ImageQualityMetricsSchema: () => ImageQualityMetricsSchema,
|
|
44
49
|
InheritedStyleSchema: () => InheritedStyleSchema,
|
|
45
50
|
LockEntrySchema: () => LockEntrySchema,
|
|
46
51
|
LockSchema: () => LockSchema,
|
|
47
52
|
MAX_DOWNLOAD_BYTES: () => MAX_DOWNLOAD_BYTES,
|
|
53
|
+
MAX_HAND_EDIT_BYTES: () => MAX_HAND_EDIT_BYTES,
|
|
48
54
|
MAX_RETRIES: () => MAX_RETRIES,
|
|
49
55
|
ManifestDriftError: () => ManifestDriftError,
|
|
50
56
|
ManifestEditError: () => ManifestEditError,
|
|
@@ -91,12 +97,19 @@ __export(index_exports, {
|
|
|
91
97
|
countNumberedDescriptions: () => countNumberedDescriptions,
|
|
92
98
|
createArtifactBundleManifest: () => createArtifactBundleManifest,
|
|
93
99
|
createGalleryEditHandler: () => createGalleryEditHandler,
|
|
100
|
+
createGalleryEditorHandlers: () => createGalleryEditorHandlers,
|
|
94
101
|
createGenerateHandlers: () => createGenerateHandlers,
|
|
95
102
|
createProvider: () => createProvider,
|
|
96
103
|
currentEntryOutputPath: () => currentEntryOutputPath,
|
|
97
104
|
currentOutputPath: () => currentOutputPath,
|
|
105
|
+
detachHandEdit: () => detachHandEdit,
|
|
98
106
|
detectMediaType: () => detectMediaType,
|
|
99
107
|
doctor: () => doctor,
|
|
108
|
+
editorBaseUrl: () => editorBaseUrl,
|
|
109
|
+
editorDir: () => editorDir,
|
|
110
|
+
editorRoute: () => editorRoute,
|
|
111
|
+
editorStatus: () => editorStatus,
|
|
112
|
+
editorToolsRoot: () => editorToolsRoot,
|
|
100
113
|
evaluateAudit: () => evaluateAudit,
|
|
101
114
|
expectedOutputPath: () => expectedOutputPath,
|
|
102
115
|
exportTileset: () => exportTileset,
|
|
@@ -108,13 +121,22 @@ __export(index_exports, {
|
|
|
108
121
|
galleryMediaRoute: () => galleryMediaRoute,
|
|
109
122
|
generationCost: () => generationCost,
|
|
110
123
|
groupOrphansByStyle: () => groupOrphansByStyle,
|
|
124
|
+
handEditBase: () => handEditBase,
|
|
125
|
+
handEditBases: () => handEditBases,
|
|
126
|
+
handEditCompanionPath: () => handEditCompanionPath,
|
|
127
|
+
handEditMembers: () => handEditMembers,
|
|
128
|
+
handEditPath: () => handEditPath,
|
|
129
|
+
handEditProjectPath: () => handEditProjectPath,
|
|
111
130
|
hex: () => hex,
|
|
112
131
|
idFromPrompt: () => idFromPrompt,
|
|
113
132
|
imageMetadata: () => imageMetadata,
|
|
114
133
|
inspectCaches: () => inspectCaches,
|
|
115
134
|
inspectQualityProfile: () => inspectQualityProfile,
|
|
116
135
|
inspectRevisionReadiness: () => inspectRevisionReadiness,
|
|
136
|
+
installEditor: () => installEditor,
|
|
117
137
|
installRecipe: () => installRecipe,
|
|
138
|
+
isFrameSetEntry: () => isFrameSetEntry,
|
|
139
|
+
isMemberSetEntry: () => isMemberSetEntry,
|
|
118
140
|
isSensitiveSourceUrl: () => isSensitiveSourceUrl,
|
|
119
141
|
listBundledRecipes: () => listBundledRecipes,
|
|
120
142
|
loadClaims: () => loadClaims,
|
|
@@ -129,12 +151,14 @@ __export(index_exports, {
|
|
|
129
151
|
measureImageQuality: () => measureImageQuality,
|
|
130
152
|
mediaExtension: () => mediaExtension,
|
|
131
153
|
mediaTypeFromExtension: () => mediaTypeFromExtension,
|
|
154
|
+
memberPath: () => memberPath,
|
|
132
155
|
mergePalettes: () => mergePalettes,
|
|
133
156
|
mountSprites: () => mountSprites,
|
|
134
157
|
mountStyle: () => mountStyle,
|
|
135
158
|
normalizeLockOutputPaths: () => normalizeLockOutputPaths,
|
|
136
159
|
normalizeRefinementPalette: () => normalizeRefinementPalette,
|
|
137
160
|
normalizeTileRules: () => normalizeTileRules,
|
|
161
|
+
openInEditor: () => openInEditor,
|
|
138
162
|
outliers: () => outliers,
|
|
139
163
|
outputId: () => outputId,
|
|
140
164
|
packSprites: () => packSprites,
|
|
@@ -154,6 +178,7 @@ __export(index_exports, {
|
|
|
154
178
|
qualityRecordPath: () => qualityRecordPath,
|
|
155
179
|
quantizeToPalette: () => quantizeToPalette,
|
|
156
180
|
readArtifactBundleManifest: () => readArtifactBundleManifest,
|
|
181
|
+
readHandEditCompanion: () => readHandEditCompanion,
|
|
157
182
|
readQualityBaseline: () => readQualityBaseline,
|
|
158
183
|
recipeDigest: () => recipeDigest,
|
|
159
184
|
refineAsset: () => refineAsset,
|
|
@@ -186,6 +211,7 @@ __export(index_exports, {
|
|
|
186
211
|
retryAfterMs: () => retryAfterMs,
|
|
187
212
|
runPicker: () => runPicker,
|
|
188
213
|
runSalvage: () => runSalvage,
|
|
214
|
+
saveHandEdit: () => saveHandEdit,
|
|
189
215
|
saveLock: () => saveLock,
|
|
190
216
|
saveWorkspace: () => saveWorkspace,
|
|
191
217
|
scanAssets: () => scanAssets,
|
|
@@ -197,8 +223,11 @@ __export(index_exports, {
|
|
|
197
223
|
shouldRetry: () => shouldRetry,
|
|
198
224
|
slugify: () => slugify,
|
|
199
225
|
snapshotQualityBaseline: () => snapshotQualityBaseline,
|
|
226
|
+
sourceIsStem: () => sourceIsStem,
|
|
227
|
+
sourceOutputPath: () => sourceOutputPath,
|
|
200
228
|
specHash: () => specHash,
|
|
201
229
|
spendByUnit: () => spendByUnit,
|
|
230
|
+
startHandEdit: () => startHandEdit,
|
|
202
231
|
styleImagesBase64: () => styleImagesBase64,
|
|
203
232
|
submit: () => submit,
|
|
204
233
|
summarize: () => summarize,
|
|
@@ -768,11 +797,11 @@ var PixelLabClient = class {
|
|
|
768
797
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
769
798
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
770
799
|
*/
|
|
771
|
-
async request(
|
|
800
|
+
async request(path29, init, attempt = 0) {
|
|
772
801
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
773
802
|
let res;
|
|
774
803
|
try {
|
|
775
|
-
res = await fetch(`${BASE}${
|
|
804
|
+
res = await fetch(`${BASE}${path29}`, {
|
|
776
805
|
...init,
|
|
777
806
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
778
807
|
headers: {
|
|
@@ -784,24 +813,24 @@ var PixelLabClient = class {
|
|
|
784
813
|
} catch (err) {
|
|
785
814
|
if (attempt < MAX_RETRIES) {
|
|
786
815
|
await sleep(backoffMs(attempt));
|
|
787
|
-
return this.request(
|
|
816
|
+
return this.request(path29, init, attempt + 1);
|
|
788
817
|
}
|
|
789
818
|
throw err;
|
|
790
819
|
}
|
|
791
820
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
792
821
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
793
822
|
await sleep(waitMs);
|
|
794
|
-
return this.request(
|
|
823
|
+
return this.request(path29, init, attempt + 1);
|
|
795
824
|
}
|
|
796
825
|
const text = await res.text();
|
|
797
826
|
if (!res.ok) {
|
|
798
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
827
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path29} \u2192 ${res.status}`, res.status, text);
|
|
799
828
|
}
|
|
800
829
|
if (!text) return {};
|
|
801
830
|
try {
|
|
802
831
|
return JSON.parse(text);
|
|
803
832
|
} catch {
|
|
804
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
833
|
+
throw new Error(`${init?.method ?? "GET"} ${path29} returned invalid JSON`);
|
|
805
834
|
}
|
|
806
835
|
}
|
|
807
836
|
async balance() {
|
|
@@ -1852,9 +1881,9 @@ var RetroDiffusionClient = class {
|
|
|
1852
1881
|
}
|
|
1853
1882
|
return balance;
|
|
1854
1883
|
}
|
|
1855
|
-
async call(
|
|
1884
|
+
async call(path29, init = {}) {
|
|
1856
1885
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1857
|
-
const response = await this.request(`${this.baseUrl}${
|
|
1886
|
+
const response = await this.request(`${this.baseUrl}${path29}`, {
|
|
1858
1887
|
...init,
|
|
1859
1888
|
headers: {
|
|
1860
1889
|
"Content-Type": "application/json",
|
|
@@ -2657,10 +2686,10 @@ var import_node_fs2 = require("fs");
|
|
|
2657
2686
|
function sha256(data) {
|
|
2658
2687
|
return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
|
|
2659
2688
|
}
|
|
2660
|
-
async function sha256File(
|
|
2689
|
+
async function sha256File(path29) {
|
|
2661
2690
|
return new Promise((resolve, reject) => {
|
|
2662
2691
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
2663
|
-
const stream = (0, import_node_fs2.createReadStream)(
|
|
2692
|
+
const stream = (0, import_node_fs2.createReadStream)(path29);
|
|
2664
2693
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
2665
2694
|
stream.on("error", reject);
|
|
2666
2695
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -2953,12 +2982,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
2953
2982
|
}
|
|
2954
2983
|
validate(spec, styleImages) {
|
|
2955
2984
|
const options = resolvedOptions(spec);
|
|
2956
|
-
if (spec.source && !spec.revision)
|
|
2957
|
-
if (spec.generator === "frames") {
|
|
2958
|
-
throw new Error("ComfyUI frame sets require generated provider outputs, not asset.source");
|
|
2959
|
-
}
|
|
2960
|
-
return;
|
|
2961
|
-
}
|
|
2985
|
+
if (spec.source && !spec.revision && spec.generator !== "frames") return;
|
|
2962
2986
|
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
2963
2987
|
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
2964
2988
|
}
|
|
@@ -3792,10 +3816,11 @@ registerProvider({
|
|
|
3792
3816
|
|
|
3793
3817
|
// src/manifest.ts
|
|
3794
3818
|
var import_promises3 = require("fs/promises");
|
|
3795
|
-
var
|
|
3819
|
+
var import_node_fs5 = require("fs");
|
|
3796
3820
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
3797
3821
|
|
|
3798
3822
|
// src/outputs.ts
|
|
3823
|
+
var import_node_fs4 = require("fs");
|
|
3799
3824
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
3800
3825
|
|
|
3801
3826
|
// src/lock.ts
|
|
@@ -3985,13 +4010,31 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
3985
4010
|
return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
|
|
3986
4011
|
}
|
|
3987
4012
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3988
|
-
|
|
4013
|
+
return memberPath(spec.outFile, role, index, total, mediaType);
|
|
4014
|
+
}
|
|
4015
|
+
function memberPath(file, role, index, total, mediaType) {
|
|
4016
|
+
const originalExt = import_node_path4.default.extname(file);
|
|
3989
4017
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3990
|
-
const stem = originalExt ?
|
|
4018
|
+
const stem = originalExt ? file.slice(0, -originalExt.length) : file;
|
|
3991
4019
|
if (total === 1) return `${stem}${ext}`;
|
|
3992
4020
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3993
4021
|
return `${stem}-${safeRole}${ext}`;
|
|
3994
4022
|
}
|
|
4023
|
+
function isMemberSetEntry(entry) {
|
|
4024
|
+
return entry.outputs.length > 1 && entry.outputs.every((output) => !output.mediaType || output.mediaType === "image/png");
|
|
4025
|
+
}
|
|
4026
|
+
function isFrameSetEntry(entry) {
|
|
4027
|
+
return entry.generator === "frames" && isMemberSetEntry(entry);
|
|
4028
|
+
}
|
|
4029
|
+
function sourceIsStem(source, entry, manifestDir) {
|
|
4030
|
+
return isMemberSetEntry(entry) && !(0, import_node_fs4.existsSync)(import_node_path4.default.resolve(manifestDir, source));
|
|
4031
|
+
}
|
|
4032
|
+
function sourceOutputPath(source, entry, index, manifestDir) {
|
|
4033
|
+
const absolute = import_node_path4.default.resolve(manifestDir, source);
|
|
4034
|
+
if (!sourceIsStem(source, entry, manifestDir)) return absolute;
|
|
4035
|
+
const output = entry.outputs[index];
|
|
4036
|
+
return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
|
|
4037
|
+
}
|
|
3995
4038
|
function currentOutputPath(output, spec, index, total) {
|
|
3996
4039
|
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3997
4040
|
}
|
|
@@ -4082,7 +4125,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
4082
4125
|
// src/manifest.ts
|
|
4083
4126
|
async function loadManifest(manifestPath) {
|
|
4084
4127
|
const abs = import_node_path5.default.resolve(manifestPath);
|
|
4085
|
-
if (!(0,
|
|
4128
|
+
if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`No manifest at ${abs}`);
|
|
4086
4129
|
const input = ManifestInputSchema.safeParse(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
|
|
4087
4130
|
if (!input.success) {
|
|
4088
4131
|
const issues = formatManifestIssues(input.error.issues);
|
|
@@ -4254,7 +4297,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4254
4297
|
const abs = import_node_path5.default.resolve(root, rel);
|
|
4255
4298
|
let hit = styleImageCache.get(abs);
|
|
4256
4299
|
if (!hit) {
|
|
4257
|
-
if (!(0,
|
|
4300
|
+
if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
4258
4301
|
const buf = await (0, import_promises3.readFile)(abs);
|
|
4259
4302
|
const metadata = imageMetadata(buf);
|
|
4260
4303
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
@@ -4495,7 +4538,7 @@ function pngPath(file) {
|
|
|
4495
4538
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
4496
4539
|
}
|
|
4497
4540
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
4498
|
-
if (!(0,
|
|
4541
|
+
if (!(0, import_node_fs5.existsSync)(file)) return null;
|
|
4499
4542
|
const bytes = await (0, import_promises3.readFile)(file);
|
|
4500
4543
|
const metadata = imageMetadata(bytes);
|
|
4501
4544
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
@@ -5047,7 +5090,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5047
5090
|
|
|
5048
5091
|
// src/recipes.ts
|
|
5049
5092
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
5050
|
-
var
|
|
5093
|
+
var import_node_fs6 = require("fs");
|
|
5051
5094
|
var import_promises5 = require("fs/promises");
|
|
5052
5095
|
var import_node_url = require("url");
|
|
5053
5096
|
var import_zod3 = require("zod");
|
|
@@ -5251,7 +5294,7 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
5251
5294
|
return { recipe: parsed.data, path: absolute, dir: import_node_path7.default.dirname(absolute), bundled };
|
|
5252
5295
|
}
|
|
5253
5296
|
async function walkRecipeFiles(root) {
|
|
5254
|
-
if (!(0,
|
|
5297
|
+
if (!(0, import_node_fs6.existsSync)(root)) return [];
|
|
5255
5298
|
const out = [];
|
|
5256
5299
|
for (const entry of await (0, import_promises5.readdir)(root, { withFileTypes: true })) {
|
|
5257
5300
|
const resolved = import_node_path7.default.join(root, entry.name);
|
|
@@ -5289,7 +5332,7 @@ function parseSelector(selector) {
|
|
|
5289
5332
|
}
|
|
5290
5333
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
5291
5334
|
const local = import_node_path7.default.resolve(target);
|
|
5292
|
-
if ((0,
|
|
5335
|
+
if ((0, import_node_fs6.existsSync)(local)) {
|
|
5293
5336
|
const recipePath = import_node_path7.default.basename(local) === RECIPE_FILE ? local : import_node_path7.default.join(local, RECIPE_FILE);
|
|
5294
5337
|
return readRecipeFile(recipePath, false);
|
|
5295
5338
|
}
|
|
@@ -5305,7 +5348,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
5305
5348
|
}
|
|
5306
5349
|
async function verifyFile(root, file) {
|
|
5307
5350
|
const absolute = import_node_path7.default.resolve(root, ...file.path.split("/"));
|
|
5308
|
-
if (!(0,
|
|
5351
|
+
if (!(0, import_node_fs6.existsSync)(absolute)) {
|
|
5309
5352
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
5310
5353
|
}
|
|
5311
5354
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -5383,7 +5426,7 @@ async function installRecipe(target, options = {}) {
|
|
|
5383
5426
|
})));
|
|
5384
5427
|
if (!options.force) {
|
|
5385
5428
|
for (const file of files) {
|
|
5386
|
-
if (!(0,
|
|
5429
|
+
if (!(0, import_node_fs6.existsSync)(file.path)) continue;
|
|
5387
5430
|
const current = await (0, import_promises5.readFile)(file.path);
|
|
5388
5431
|
if (!current.equals(file.data)) {
|
|
5389
5432
|
throw new Error(
|
|
@@ -5407,11 +5450,11 @@ async function installRecipe(target, options = {}) {
|
|
|
5407
5450
|
}
|
|
5408
5451
|
|
|
5409
5452
|
// src/pipeline/plan.ts
|
|
5410
|
-
var
|
|
5453
|
+
var import_node_fs10 = require("fs");
|
|
5411
5454
|
var import_node_path12 = __toESM(require("path"), 1);
|
|
5412
5455
|
|
|
5413
5456
|
// src/pipeline/quality-profile.ts
|
|
5414
|
-
var
|
|
5457
|
+
var import_node_fs8 = require("fs");
|
|
5415
5458
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
5416
5459
|
|
|
5417
5460
|
// src/pipeline/refine.ts
|
|
@@ -5423,7 +5466,7 @@ var import_node_util = require("util");
|
|
|
5423
5466
|
|
|
5424
5467
|
// src/pipeline/audit.ts
|
|
5425
5468
|
var import_promises6 = require("fs/promises");
|
|
5426
|
-
var
|
|
5469
|
+
var import_node_fs7 = require("fs");
|
|
5427
5470
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
5428
5471
|
function colorDistance(a, b) {
|
|
5429
5472
|
const rmean = (a.r + b.r) / 2;
|
|
@@ -5469,7 +5512,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
5469
5512
|
const unreadable = [];
|
|
5470
5513
|
for (const spec of mine) {
|
|
5471
5514
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
5472
|
-
if (!(0,
|
|
5515
|
+
if (!(0, import_node_fs7.existsSync)(output.absolutePath)) {
|
|
5473
5516
|
missing.push(output.id);
|
|
5474
5517
|
continue;
|
|
5475
5518
|
}
|
|
@@ -5499,7 +5542,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
5499
5542
|
const refPalettes = [];
|
|
5500
5543
|
for (const img of style.styleImages) {
|
|
5501
5544
|
const abs = import_node_path8.default.resolve(loaded.root, img.path);
|
|
5502
|
-
if (!(0,
|
|
5545
|
+
if (!(0, import_node_fs7.existsSync)(abs)) continue;
|
|
5503
5546
|
try {
|
|
5504
5547
|
refPalettes.push(extractPalette(decodePng(await (0, import_promises6.readFile)(abs)), 12));
|
|
5505
5548
|
} catch {
|
|
@@ -6128,7 +6171,7 @@ async function qualitySource(spec, lock) {
|
|
|
6128
6171
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
6129
6172
|
}
|
|
6130
6173
|
const source = import_node_path10.default.resolve(spec.root, spec.source);
|
|
6131
|
-
return (0,
|
|
6174
|
+
return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
6132
6175
|
}
|
|
6133
6176
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
6134
6177
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -6157,7 +6200,7 @@ async function qualitySource(spec, lock) {
|
|
|
6157
6200
|
}
|
|
6158
6201
|
if (role) roles.add(role);
|
|
6159
6202
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
6160
|
-
if (!(0,
|
|
6203
|
+
if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
6161
6204
|
if (await sha256File(source) !== output.sha256) {
|
|
6162
6205
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
6163
6206
|
}
|
|
@@ -6176,7 +6219,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
6176
6219
|
}
|
|
6177
6220
|
const sourcePaths = source.items.map((item) => item.path);
|
|
6178
6221
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
6179
|
-
if (!(0,
|
|
6222
|
+
if (!(0, import_node_fs8.existsSync)(record)) {
|
|
6180
6223
|
return {
|
|
6181
6224
|
key,
|
|
6182
6225
|
state: "needs-refinement",
|
|
@@ -6357,7 +6400,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
6357
6400
|
}
|
|
6358
6401
|
|
|
6359
6402
|
// src/pipeline/revision.ts
|
|
6360
|
-
var
|
|
6403
|
+
var import_node_fs9 = require("fs");
|
|
6361
6404
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
6362
6405
|
async function inspectRevisionReadiness(spec, lock) {
|
|
6363
6406
|
if (!spec.revision) return null;
|
|
@@ -6373,14 +6416,14 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
6373
6416
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
6374
6417
|
};
|
|
6375
6418
|
}
|
|
6376
|
-
if (!revision.sourceSha256 || !(0,
|
|
6419
|
+
if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
|
|
6377
6420
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
6378
6421
|
}
|
|
6379
6422
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
6380
6423
|
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
6381
6424
|
}
|
|
6382
6425
|
if (revision.maskFile) {
|
|
6383
|
-
if (!revision.maskSha256 || !(0,
|
|
6426
|
+
if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
|
|
6384
6427
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
6385
6428
|
}
|
|
6386
6429
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -6411,7 +6454,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6411
6454
|
}
|
|
6412
6455
|
if (spec.source) {
|
|
6413
6456
|
const source = import_node_path11.default.resolve(spec.root, spec.source);
|
|
6414
|
-
return (0,
|
|
6457
|
+
return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
6415
6458
|
}
|
|
6416
6459
|
const entry = lock.entries[key];
|
|
6417
6460
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -6425,7 +6468,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6425
6468
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
6426
6469
|
const output = entry.outputs[index];
|
|
6427
6470
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
6428
|
-
if (!(0,
|
|
6471
|
+
if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
6429
6472
|
if (await sha256File(file) !== output.sha256) {
|
|
6430
6473
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
6431
6474
|
}
|
|
@@ -6479,8 +6522,8 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6479
6522
|
} else if (opts.force) {
|
|
6480
6523
|
state = "missing";
|
|
6481
6524
|
reason = "--force";
|
|
6482
|
-
} else if (!entry && spec.source) {
|
|
6483
|
-
if ((0,
|
|
6525
|
+
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6526
|
+
if ((0, import_node_fs10.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
|
|
6484
6527
|
state = "ok";
|
|
6485
6528
|
reason = `placed from ${spec.source}; not generated`;
|
|
6486
6529
|
} else {
|
|
@@ -6488,7 +6531,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6488
6531
|
reason = `declared source is missing: ${spec.source}`;
|
|
6489
6532
|
}
|
|
6490
6533
|
} else if (!entry) {
|
|
6491
|
-
if ((0,
|
|
6534
|
+
if ((0, import_node_fs10.existsSync)(spec.outFile)) {
|
|
6492
6535
|
state = "untracked";
|
|
6493
6536
|
reason = "file on disk, no provenance recorded";
|
|
6494
6537
|
} else {
|
|
@@ -6520,9 +6563,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6520
6563
|
} else if (entry.status !== "downloaded") {
|
|
6521
6564
|
state = "in-flight";
|
|
6522
6565
|
reason = `awaiting ${entry.status}`;
|
|
6523
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0,
|
|
6566
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
|
|
6524
6567
|
state = "orphaned";
|
|
6525
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0,
|
|
6568
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
6526
6569
|
} else if (await anyOutputModified(entry, spec)) {
|
|
6527
6570
|
state = "orphaned";
|
|
6528
6571
|
reason = "output modified since download";
|
|
@@ -6588,7 +6631,7 @@ function summarize(plan) {
|
|
|
6588
6631
|
|
|
6589
6632
|
// src/pipeline/quality-regression.ts
|
|
6590
6633
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
6591
|
-
var
|
|
6634
|
+
var import_node_fs11 = require("fs");
|
|
6592
6635
|
var import_promises8 = require("fs/promises");
|
|
6593
6636
|
var import_zod4 = require("zod");
|
|
6594
6637
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -6831,7 +6874,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
6831
6874
|
cases
|
|
6832
6875
|
});
|
|
6833
6876
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
6834
|
-
if ((0,
|
|
6877
|
+
if ((0, import_node_fs11.existsSync)(absolute) && !options.force) {
|
|
6835
6878
|
const current = await (0, import_promises8.readFile)(absolute);
|
|
6836
6879
|
if (!current.equals(data)) {
|
|
6837
6880
|
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
@@ -6968,13 +7011,13 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
6968
7011
|
}
|
|
6969
7012
|
|
|
6970
7013
|
// src/pipeline/cache-health.ts
|
|
6971
|
-
var
|
|
7014
|
+
var import_node_fs13 = require("fs");
|
|
6972
7015
|
var import_promises10 = require("fs/promises");
|
|
6973
7016
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
6974
7017
|
|
|
6975
7018
|
// src/cache.ts
|
|
6976
7019
|
var import_promises9 = require("fs/promises");
|
|
6977
|
-
var
|
|
7020
|
+
var import_node_fs12 = require("fs");
|
|
6978
7021
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
6979
7022
|
var import_zod5 = require("zod");
|
|
6980
7023
|
var HashCacheSchema = import_zod5.z.object({
|
|
@@ -6989,10 +7032,10 @@ function isSha256Hash(value) {
|
|
|
6989
7032
|
function parseCache(value) {
|
|
6990
7033
|
return HashCacheSchema.parse(value);
|
|
6991
7034
|
}
|
|
6992
|
-
async function loadCache(
|
|
6993
|
-
if (!(0,
|
|
7035
|
+
async function loadCache(path29) {
|
|
7036
|
+
if (!(0, import_node_fs12.existsSync)(path29)) return { version: 1, hashes: {} };
|
|
6994
7037
|
try {
|
|
6995
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises9.readFile)(
|
|
7038
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises9.readFile)(path29, "utf8")));
|
|
6996
7039
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
6997
7040
|
return {
|
|
6998
7041
|
version: 1,
|
|
@@ -7004,18 +7047,18 @@ async function loadCache(path27) {
|
|
|
7004
7047
|
return { version: 1, hashes: {} };
|
|
7005
7048
|
}
|
|
7006
7049
|
}
|
|
7007
|
-
async function saveCache(
|
|
7050
|
+
async function saveCache(path29, cache) {
|
|
7008
7051
|
const sorted = {};
|
|
7009
7052
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
7010
7053
|
const hash = cache.hashes[key];
|
|
7011
7054
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
7012
7055
|
sorted[key] = hash;
|
|
7013
7056
|
}
|
|
7014
|
-
await (0, import_promises9.mkdir)(import_node_path14.default.dirname(import_node_path14.default.resolve(
|
|
7015
|
-
const tmp = `${
|
|
7057
|
+
await (0, import_promises9.mkdir)(import_node_path14.default.dirname(import_node_path14.default.resolve(path29)), { recursive: true });
|
|
7058
|
+
const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7016
7059
|
try {
|
|
7017
7060
|
await (0, import_promises9.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
7018
|
-
await (0, import_promises9.rename)(tmp,
|
|
7061
|
+
await (0, import_promises9.rename)(tmp, path29);
|
|
7019
7062
|
} finally {
|
|
7020
7063
|
await (0, import_promises9.rm)(tmp, { force: true });
|
|
7021
7064
|
}
|
|
@@ -7037,7 +7080,7 @@ function cachePathFor(lockPath) {
|
|
|
7037
7080
|
|
|
7038
7081
|
// src/pipeline/cache-health.ts
|
|
7039
7082
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
7040
|
-
if (options.prune && !(0,
|
|
7083
|
+
if (options.prune && !(0, import_node_fs13.existsSync)(lockPath)) {
|
|
7041
7084
|
throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path15.default.resolve(lockPath)}`);
|
|
7042
7085
|
}
|
|
7043
7086
|
const contentDir = import_node_path15.default.resolve(import_node_path15.default.dirname(lockPath), ".pixelkiln", "cache");
|
|
@@ -7082,7 +7125,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
7082
7125
|
async function inspectContentCache(contentDir, referenced) {
|
|
7083
7126
|
const report = {
|
|
7084
7127
|
path: contentDir,
|
|
7085
|
-
exists: (0,
|
|
7128
|
+
exists: (0, import_node_fs13.existsSync)(contentDir),
|
|
7086
7129
|
files: 0,
|
|
7087
7130
|
bytes: 0,
|
|
7088
7131
|
valid: 0,
|
|
@@ -7146,7 +7189,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
7146
7189
|
async function inspectRemoteHashCache(remotePath) {
|
|
7147
7190
|
const report = {
|
|
7148
7191
|
path: remotePath,
|
|
7149
|
-
exists: (0,
|
|
7192
|
+
exists: (0, import_node_fs13.existsSync)(remotePath),
|
|
7150
7193
|
entries: 0,
|
|
7151
7194
|
valid: 0,
|
|
7152
7195
|
invalidIds: [],
|
|
@@ -7170,7 +7213,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
7170
7213
|
}
|
|
7171
7214
|
|
|
7172
7215
|
// src/pipeline/pack.ts
|
|
7173
|
-
var
|
|
7216
|
+
var import_node_fs14 = require("fs");
|
|
7174
7217
|
var import_node_path16 = __toESM(require("path"), 1);
|
|
7175
7218
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
7176
7219
|
if (!Array.isArray(raw) || !raw.length) {
|
|
@@ -7203,7 +7246,7 @@ function packSprites(inputs, options = {}) {
|
|
|
7203
7246
|
for (const input of ordered) {
|
|
7204
7247
|
let sourceHash = null;
|
|
7205
7248
|
try {
|
|
7206
|
-
const bytes = (0,
|
|
7249
|
+
const bytes = (0, import_node_fs14.readFileSync)(input.path);
|
|
7207
7250
|
sourceHash = sha256(bytes);
|
|
7208
7251
|
const png = decodePng(bytes);
|
|
7209
7252
|
sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
|
|
@@ -7274,12 +7317,15 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
7274
7317
|
for (const [key, entry] of entries) {
|
|
7275
7318
|
const id = key.slice(prefix.length);
|
|
7276
7319
|
locked.add(id);
|
|
7277
|
-
|
|
7320
|
+
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
7278
7321
|
const source = sources[id];
|
|
7279
7322
|
if (source && outputs.length <= 1) {
|
|
7280
7323
|
inputs.push({ id, path: import_node_path16.default.resolve(manifestDir, source) });
|
|
7281
7324
|
continue;
|
|
7282
7325
|
}
|
|
7326
|
+
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
7327
|
+
outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(source, entry, output.index, manifestDir) }));
|
|
7328
|
+
}
|
|
7283
7329
|
let selected = outputs;
|
|
7284
7330
|
if (options.outputRoles?.length) {
|
|
7285
7331
|
selected = outputs.filter(
|
|
@@ -7332,7 +7378,7 @@ function mountSprites(placements, options) {
|
|
|
7332
7378
|
for (const p of placements) {
|
|
7333
7379
|
let sourceHash = null;
|
|
7334
7380
|
try {
|
|
7335
|
-
const bytes = (0,
|
|
7381
|
+
const bytes = (0, import_node_fs14.readFileSync)(p.path);
|
|
7336
7382
|
sourceHash = sha256(bytes);
|
|
7337
7383
|
const png = decodePng(bytes);
|
|
7338
7384
|
if (png.width > cellWidth || png.height > cellHeight) {
|
|
@@ -7401,11 +7447,21 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
7401
7447
|
for (const id of ids) {
|
|
7402
7448
|
const cell = cells[id];
|
|
7403
7449
|
const source = sources[id];
|
|
7404
|
-
|
|
7450
|
+
const entry = lock.entries[`${styleId}/${id}`];
|
|
7451
|
+
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
7405
7452
|
placements.push({ id, path: import_node_path16.default.resolve(manifestDir, source), cell });
|
|
7406
7453
|
continue;
|
|
7407
7454
|
}
|
|
7408
|
-
|
|
7455
|
+
if (source && entry) {
|
|
7456
|
+
const selection2 = selectEntryOutput(entry, outputRoles[id]);
|
|
7457
|
+
if (!selection2.ok) {
|
|
7458
|
+
skipped.push({ id, reason: selection2.reason });
|
|
7459
|
+
continue;
|
|
7460
|
+
}
|
|
7461
|
+
const index = entry.outputs.findIndex((output) => output.path === selection2.output.path);
|
|
7462
|
+
placements.push({ id, path: sourceOutputPath(source, entry, Math.max(index, 0), manifestDir), cell });
|
|
7463
|
+
continue;
|
|
7464
|
+
}
|
|
7409
7465
|
if (!entry) {
|
|
7410
7466
|
skipped.push({
|
|
7411
7467
|
id,
|
|
@@ -7426,7 +7482,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
7426
7482
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
7427
7483
|
);
|
|
7428
7484
|
}
|
|
7429
|
-
const basePng = mount.base ? (0,
|
|
7485
|
+
const basePng = mount.base ? (0, import_node_fs14.readFileSync)(import_node_path16.default.resolve(manifestDir, mount.base)) : void 0;
|
|
7430
7486
|
const mounted = mountSprites(placements, {
|
|
7431
7487
|
cellWidth: mount.cellWidth,
|
|
7432
7488
|
cellHeight: mount.cellHeight,
|
|
@@ -7472,8 +7528,11 @@ function normalizeTileRules(raw) {
|
|
|
7472
7528
|
};
|
|
7473
7529
|
}
|
|
7474
7530
|
function exportTileset(entry, spec, options) {
|
|
7475
|
-
|
|
7531
|
+
let outputs = resolveSpecEntryOutputs(entry, spec);
|
|
7476
7532
|
if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
|
|
7533
|
+
if (spec.source && sourceIsStem(spec.source, entry, options.manifestDir)) {
|
|
7534
|
+
outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(spec.source, entry, output.index, options.manifestDir) }));
|
|
7535
|
+
}
|
|
7477
7536
|
const packed = packSprites(
|
|
7478
7537
|
outputs.map((output) => ({
|
|
7479
7538
|
id: output.role ?? fallbackOutputRole(output.index),
|
|
@@ -7965,7 +8024,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
7965
8024
|
}
|
|
7966
8025
|
|
|
7967
8026
|
// src/pipeline/fetch.ts
|
|
7968
|
-
var
|
|
8027
|
+
var import_node_fs15 = require("fs");
|
|
7969
8028
|
var import_promises11 = require("fs/promises");
|
|
7970
8029
|
var import_node_path17 = __toESM(require("path"), 1);
|
|
7971
8030
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
@@ -7982,7 +8041,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7982
8041
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7983
8042
|
if (!opts.repair) return false;
|
|
7984
8043
|
const spec = specByKey.get(key);
|
|
7985
|
-
return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0,
|
|
8044
|
+
return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0, import_node_fs15.existsSync)(resolveOutputPath(output.path, spec.root)));
|
|
7986
8045
|
});
|
|
7987
8046
|
const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
|
|
7988
8047
|
let cursor = 0;
|
|
@@ -8030,7 +8089,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8030
8089
|
continue;
|
|
8031
8090
|
}
|
|
8032
8091
|
}
|
|
8033
|
-
if ((0,
|
|
8092
|
+
if ((0, import_node_fs15.existsSync)(target)) {
|
|
8034
8093
|
const currentHash = await sha256File(target);
|
|
8035
8094
|
if (recorded && currentHash === recorded.sha256) {
|
|
8036
8095
|
if (!fresh) {
|
|
@@ -8141,7 +8200,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8141
8200
|
}
|
|
8142
8201
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
8143
8202
|
const file = import_node_path17.default.join(cacheDir, cacheFileName(hash, mediaType));
|
|
8144
|
-
if (!(0,
|
|
8203
|
+
if (!(0, import_node_fs15.existsSync)(file)) return null;
|
|
8145
8204
|
try {
|
|
8146
8205
|
const buf = await (0, import_promises11.readFile)(file);
|
|
8147
8206
|
if (sha256(buf) !== hash) return null;
|
|
@@ -8162,7 +8221,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
|
8162
8221
|
try {
|
|
8163
8222
|
await (0, import_promises11.rename)(tmp, file);
|
|
8164
8223
|
} catch (err) {
|
|
8165
|
-
if (!(0,
|
|
8224
|
+
if (!(0, import_node_fs15.existsSync)(file)) throw err;
|
|
8166
8225
|
} finally {
|
|
8167
8226
|
await (0, import_promises11.rm)(tmp, { force: true });
|
|
8168
8227
|
}
|
|
@@ -8195,7 +8254,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
8195
8254
|
}
|
|
8196
8255
|
|
|
8197
8256
|
// src/pipeline/doctor.ts
|
|
8198
|
-
var
|
|
8257
|
+
var import_node_fs16 = require("fs");
|
|
8199
8258
|
var import_promises12 = require("fs/promises");
|
|
8200
8259
|
var import_node_path18 = __toESM(require("path"), 1);
|
|
8201
8260
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
@@ -8215,7 +8274,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
8215
8274
|
for (const dir of dirs) {
|
|
8216
8275
|
const ancestor = await nearestExistingDirectory(dir);
|
|
8217
8276
|
try {
|
|
8218
|
-
await (0, import_promises12.access)(ancestor,
|
|
8277
|
+
await (0, import_promises12.access)(ancestor, import_node_fs16.constants.W_OK);
|
|
8219
8278
|
} catch {
|
|
8220
8279
|
unwritable.push(dir);
|
|
8221
8280
|
}
|
|
@@ -8267,7 +8326,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
8267
8326
|
cacheDir,
|
|
8268
8327
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
8269
8328
|
);
|
|
8270
|
-
if ((0,
|
|
8329
|
+
if ((0, import_node_fs16.existsSync)(cached) && await sha256File(cached) === output.sha256) {
|
|
8271
8330
|
hasCache = true;
|
|
8272
8331
|
break;
|
|
8273
8332
|
}
|
|
@@ -8370,7 +8429,7 @@ async function nearestExistingDirectory(start) {
|
|
|
8370
8429
|
}
|
|
8371
8430
|
|
|
8372
8431
|
// src/pipeline/adopt.ts
|
|
8373
|
-
var
|
|
8432
|
+
var import_node_fs17 = require("fs");
|
|
8374
8433
|
var import_promises13 = require("fs/promises");
|
|
8375
8434
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
8376
8435
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
@@ -8383,7 +8442,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8383
8442
|
const localByHash = /* @__PURE__ */ new Map();
|
|
8384
8443
|
const unmatchedLocal = [];
|
|
8385
8444
|
for (const spec of specs) {
|
|
8386
|
-
if (!(0,
|
|
8445
|
+
if (!(0, import_node_fs17.existsSync)(spec.outFile)) {
|
|
8387
8446
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
8388
8447
|
continue;
|
|
8389
8448
|
}
|
|
@@ -8505,12 +8564,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
8505
8564
|
}
|
|
8506
8565
|
|
|
8507
8566
|
// src/pick/server.ts
|
|
8508
|
-
var
|
|
8567
|
+
var import_node_fs19 = require("fs");
|
|
8509
8568
|
var import_promises16 = require("fs/promises");
|
|
8510
8569
|
|
|
8511
8570
|
// src/pipeline/init.ts
|
|
8512
8571
|
var import_promises14 = require("fs/promises");
|
|
8513
|
-
var
|
|
8572
|
+
var import_node_fs18 = require("fs");
|
|
8514
8573
|
var import_node_path20 = __toESM(require("path"), 1);
|
|
8515
8574
|
function pngSize(buf) {
|
|
8516
8575
|
if (buf.length < 24) return null;
|
|
@@ -9083,7 +9142,7 @@ async function currentArt(entry, spec) {
|
|
|
9083
9142
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
9084
9143
|
if (!previous) return null;
|
|
9085
9144
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
9086
|
-
if (!(0,
|
|
9145
|
+
if (!(0, import_node_fs19.existsSync)(file)) return null;
|
|
9087
9146
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
9088
9147
|
if (!contentType) return null;
|
|
9089
9148
|
let width = entry.width;
|
|
@@ -9254,13 +9313,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
9254
9313
|
|
|
9255
9314
|
// src/gallery/snapshot.ts
|
|
9256
9315
|
var import_node_crypto6 = require("crypto");
|
|
9257
|
-
var
|
|
9316
|
+
var import_node_fs22 = require("fs");
|
|
9258
9317
|
var import_promises20 = require("fs/promises");
|
|
9259
9318
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
9260
9319
|
|
|
9261
9320
|
// src/workspace.ts
|
|
9262
9321
|
var import_promises17 = require("fs/promises");
|
|
9263
|
-
var
|
|
9322
|
+
var import_node_fs20 = require("fs");
|
|
9264
9323
|
var import_node_path21 = __toESM(require("path"), 1);
|
|
9265
9324
|
var import_zod6 = require("zod");
|
|
9266
9325
|
var WorkspaceProjectSchema = import_zod6.z.object({
|
|
@@ -9286,7 +9345,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
9286
9345
|
);
|
|
9287
9346
|
}
|
|
9288
9347
|
async function loadWorkspace(workspacePath) {
|
|
9289
|
-
if (!(0,
|
|
9348
|
+
if (!(0, import_node_fs20.existsSync)(workspacePath)) return { version: 1, projects: [] };
|
|
9290
9349
|
let raw;
|
|
9291
9350
|
try {
|
|
9292
9351
|
raw = JSON.parse(await (0, import_promises17.readFile)(workspacePath, "utf8"));
|
|
@@ -9338,14 +9397,14 @@ function validateWorkspace(ws, dir) {
|
|
|
9338
9397
|
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
9339
9398
|
});
|
|
9340
9399
|
}
|
|
9341
|
-
if (!(0,
|
|
9400
|
+
if (!(0, import_node_fs20.existsSync)(manifestPath)) {
|
|
9342
9401
|
diagnostics.push({
|
|
9343
9402
|
id: "missing-manifest",
|
|
9344
9403
|
level: "error",
|
|
9345
9404
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
9346
9405
|
});
|
|
9347
9406
|
}
|
|
9348
|
-
if (!(0,
|
|
9407
|
+
if (!(0, import_node_fs20.existsSync)(lockPath)) {
|
|
9349
9408
|
diagnostics.push({
|
|
9350
9409
|
id: "missing-lock",
|
|
9351
9410
|
level: "error",
|
|
@@ -9646,7 +9705,7 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
9646
9705
|
|
|
9647
9706
|
// src/pipeline/hand-edit.ts
|
|
9648
9707
|
var import_node_child_process3 = require("child_process");
|
|
9649
|
-
var
|
|
9708
|
+
var import_node_fs21 = require("fs");
|
|
9650
9709
|
var import_promises19 = require("fs/promises");
|
|
9651
9710
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
9652
9711
|
var import_zod8 = require("zod");
|
|
@@ -9659,36 +9718,63 @@ function handEditPath(loaded, spec) {
|
|
|
9659
9718
|
const file = inside ? relative : import_node_path23.default.basename(spec.outFile);
|
|
9660
9719
|
return import_node_path23.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
9661
9720
|
}
|
|
9662
|
-
function
|
|
9721
|
+
function handEditBases(spec, lock) {
|
|
9663
9722
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9664
9723
|
if (entry) {
|
|
9665
|
-
if (entry.outputs.length !== 1) return null;
|
|
9666
|
-
const
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9724
|
+
if (entry.outputs.length !== 1 && !isMemberSetEntry(entry)) return null;
|
|
9725
|
+
const members = entry.outputs.map((output, index) => ({
|
|
9726
|
+
role: entry.outputs.length === 1 ? null : output.role ?? null,
|
|
9727
|
+
path: currentEntryOutputPath(entry, spec, index),
|
|
9728
|
+
mediaType: output.mediaType
|
|
9729
|
+
}));
|
|
9730
|
+
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
9731
|
+
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
9732
|
+
if (!members.every((member) => (0, import_node_fs21.existsSync)(member.path))) return null;
|
|
9733
|
+
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
9734
|
+
}
|
|
9735
|
+
return (0, import_node_fs21.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
9736
|
+
}
|
|
9737
|
+
function handEditBase(spec, lock) {
|
|
9738
|
+
const bases = handEditBases(spec, lock);
|
|
9739
|
+
return bases && bases.length === 1 ? bases[0].path : null;
|
|
9740
|
+
}
|
|
9741
|
+
function handEditMembers(loaded, spec, lock, editPath) {
|
|
9742
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9743
|
+
const bases = handEditBases(spec, lock);
|
|
9744
|
+
if (entry && isMemberSetEntry(entry)) {
|
|
9745
|
+
return entry.outputs.map((output, index) => ({
|
|
9746
|
+
role: output.role ?? null,
|
|
9747
|
+
path: memberPath(editPath, output.role, index, entry.outputs.length, output.mediaType),
|
|
9748
|
+
base: bases?.[index]?.path ?? null
|
|
9749
|
+
}));
|
|
9670
9750
|
}
|
|
9671
|
-
return
|
|
9751
|
+
return [{ role: null, path: editPath, base: bases?.[0]?.path ?? null }];
|
|
9672
9752
|
}
|
|
9673
9753
|
async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
9674
9754
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9675
|
-
if (entry && entry.outputs.length > 1) {
|
|
9755
|
+
if (entry && entry.outputs.length > 1 && !isMemberSetEntry(entry)) {
|
|
9676
9756
|
throw new ManifestEditError(
|
|
9677
|
-
`${spec.styleId}/${spec.assetId}
|
|
9757
|
+
`${spec.styleId}/${spec.assetId} is a set of ${entry.outputs.length} outputs that are not all PNG; hand edits cover PNG images and sets of them`
|
|
9758
|
+
);
|
|
9759
|
+
}
|
|
9760
|
+
if (entry && spec.source && isMemberSetEntry(entry) && !sourceIsStem(spec.source, entry, loaded.root)) {
|
|
9761
|
+
throw new ManifestEditError(
|
|
9762
|
+
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
9678
9763
|
);
|
|
9679
9764
|
}
|
|
9680
9765
|
const existing = spec.source ? import_node_path23.default.resolve(loaded.root, spec.source) : null;
|
|
9681
9766
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
9767
|
+
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
9682
9768
|
let created = false;
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
if (!base) {
|
|
9769
|
+
for (const member of members) {
|
|
9770
|
+
if ((0, import_node_fs21.existsSync)(member.path)) continue;
|
|
9771
|
+
if (!member.base) {
|
|
9686
9772
|
throw new ManifestEditError(
|
|
9687
9773
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
9688
9774
|
);
|
|
9689
9775
|
}
|
|
9690
|
-
await (0, import_promises19.mkdir)(import_node_path23.default.dirname(
|
|
9691
|
-
await (0, import_promises19.copyFile)(base,
|
|
9776
|
+
await (0, import_promises19.mkdir)(import_node_path23.default.dirname(member.path), { recursive: true });
|
|
9777
|
+
await (0, import_promises19.copyFile)(member.base, member.path);
|
|
9692
9778
|
created = true;
|
|
9693
9779
|
}
|
|
9694
9780
|
const source = portableOutputPath(editPath, loaded.root);
|
|
@@ -9705,7 +9791,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
9705
9791
|
declared = result.changed;
|
|
9706
9792
|
manifestSha256 = result.sha256;
|
|
9707
9793
|
}
|
|
9708
|
-
return { editPath, source, created, declared, manifestSha256 };
|
|
9794
|
+
return { editPath, members, source, created, declared, manifestSha256 };
|
|
9709
9795
|
}
|
|
9710
9796
|
async function detachHandEdit(loaded, spec, opts = {}) {
|
|
9711
9797
|
if (!spec.source) {
|
|
@@ -9719,19 +9805,44 @@ async function detachHandEdit(loaded, spec, opts = {}) {
|
|
|
9719
9805
|
});
|
|
9720
9806
|
return { manifestSha256: result.sha256, changed: result.changed };
|
|
9721
9807
|
}
|
|
9722
|
-
var
|
|
9723
|
-
|
|
9724
|
-
|
|
9808
|
+
var CompanionOutputSchema = import_zod8.z.object({
|
|
9809
|
+
/** Output role for a set member; null for a single image. */
|
|
9810
|
+
role: import_zod8.z.string().nullable(),
|
|
9811
|
+
/** sha256 of the generated file this member started from; null when it started from untracked art. */
|
|
9725
9812
|
basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
9726
|
-
/** sha256 of the
|
|
9727
|
-
sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/)
|
|
9813
|
+
/** sha256 of the member as saved, so a later change by another tool is visible. */
|
|
9814
|
+
sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/)
|
|
9815
|
+
}).strict();
|
|
9816
|
+
var CompanionV2Schema = import_zod8.z.object({
|
|
9817
|
+
version: import_zod8.z.literal(2),
|
|
9728
9818
|
/** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
|
|
9729
9819
|
editor: import_zod8.z.string().min(1),
|
|
9730
9820
|
protocol: import_zod8.z.number().int().positive(),
|
|
9731
9821
|
savedAt: import_zod8.z.string().datetime(),
|
|
9732
9822
|
/** Basename of the layered project file beside the edit, when one was kept. */
|
|
9823
|
+
project: import_zod8.z.string().min(1).nullable(),
|
|
9824
|
+
/** One entry per file, in output order. */
|
|
9825
|
+
outputs: import_zod8.z.array(CompanionOutputSchema).min(1)
|
|
9826
|
+
}).strict();
|
|
9827
|
+
var CompanionV1Schema = import_zod8.z.object({
|
|
9828
|
+
version: import_zod8.z.literal(1),
|
|
9829
|
+
basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
9830
|
+
sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9831
|
+
editor: import_zod8.z.string().min(1),
|
|
9832
|
+
protocol: import_zod8.z.number().int().positive(),
|
|
9833
|
+
savedAt: import_zod8.z.string().datetime(),
|
|
9733
9834
|
project: import_zod8.z.string().min(1).nullable()
|
|
9734
9835
|
}).strict();
|
|
9836
|
+
var HandEditCompanionSchema = import_zod8.z.union([CompanionV2Schema, CompanionV1Schema]).transform(
|
|
9837
|
+
(value) => value.version === 2 ? value : {
|
|
9838
|
+
version: 2,
|
|
9839
|
+
editor: value.editor,
|
|
9840
|
+
protocol: value.protocol,
|
|
9841
|
+
savedAt: value.savedAt,
|
|
9842
|
+
project: value.project,
|
|
9843
|
+
outputs: [{ role: null, basedOn: value.basedOn, sha256: value.sha256 }]
|
|
9844
|
+
}
|
|
9845
|
+
);
|
|
9735
9846
|
function handEditCompanionPath(editPath) {
|
|
9736
9847
|
return editPath.replace(/\.png$/i, "") + ".edit.json";
|
|
9737
9848
|
}
|
|
@@ -9753,45 +9864,79 @@ async function readHandEditCompanion(editPath) {
|
|
|
9753
9864
|
}
|
|
9754
9865
|
var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
|
|
9755
9866
|
async function saveHandEdit(loaded, lock, spec, input) {
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
}
|
|
9867
|
+
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9868
|
+
const memberSet = entry ? isMemberSetEntry(entry) : false;
|
|
9869
|
+
const incoming = input.frames ?? (input.png ? [{ role: null, png: input.png }] : []);
|
|
9870
|
+
if (!incoming.length) throw new ManifestEditError("the save carried no image");
|
|
9759
9871
|
if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
|
|
9760
9872
|
throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9761
9873
|
}
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9874
|
+
if (memberSet) {
|
|
9875
|
+
const expected = entry.outputs.map((output) => output.role ?? null);
|
|
9876
|
+
const got = incoming.map((frame) => frame.role);
|
|
9877
|
+
if (got.length !== expected.length || expected.some((role, index) => got[index] !== role)) {
|
|
9878
|
+
throw new ManifestEditError(
|
|
9879
|
+
`${spec.styleId}/${spec.assetId} is a set of ${expected.length} members (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")}) \u2014 keep the frames it opened with`
|
|
9880
|
+
);
|
|
9881
|
+
}
|
|
9882
|
+
} else if (incoming.length !== 1) {
|
|
9883
|
+
throw new ManifestEditError(`${spec.styleId}/${spec.assetId} is one image; the editor returned ${incoming.length} frames \u2014 keep a single frame`);
|
|
9768
9884
|
}
|
|
9769
|
-
const
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9885
|
+
const decoded = incoming.map((frame, index) => {
|
|
9886
|
+
if (frame.png.length > MAX_HAND_EDIT_BYTES) {
|
|
9887
|
+
throw new ManifestEditError(`the edit is ${frame.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
|
|
9888
|
+
}
|
|
9889
|
+
try {
|
|
9890
|
+
validateMedia(frame.png, MediaType.PNG);
|
|
9891
|
+
return decodePng(frame.png);
|
|
9892
|
+
} catch (error) {
|
|
9893
|
+
const which = memberSet ? `member ${frame.role ?? index}` : "the edit";
|
|
9894
|
+
throw new ManifestEditError(`${which} is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
|
|
9895
|
+
}
|
|
9896
|
+
});
|
|
9897
|
+
const bases = handEditBases(spec, lock);
|
|
9898
|
+
const expectedSize = bases?.[0] ? decodePng(await (0, import_promises19.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
9899
|
+
for (const [index, image] of decoded.entries()) {
|
|
9900
|
+
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
9901
|
+
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
9902
|
+
throw new ManifestEditError(
|
|
9903
|
+
`${which} is ${image.width}\xD7${image.height}; ${spec.styleId}/${spec.assetId} is ${expectedSize.width}\xD7${expectedSize.height}`
|
|
9904
|
+
);
|
|
9905
|
+
}
|
|
9775
9906
|
}
|
|
9776
9907
|
const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
|
|
9777
|
-
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
9778
|
-
const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
|
|
9779
|
-
const digest2 = sha256(input.png);
|
|
9780
9908
|
const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
|
|
9781
9909
|
const companion = {
|
|
9782
|
-
version:
|
|
9783
|
-
basedOn,
|
|
9784
|
-
sha256: digest2,
|
|
9910
|
+
version: 2,
|
|
9785
9911
|
editor: input.editor,
|
|
9786
9912
|
protocol: input.protocol,
|
|
9787
9913
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9788
|
-
project: projectPath ? import_node_path23.default.basename(projectPath) : null
|
|
9914
|
+
project: projectPath ? import_node_path23.default.basename(projectPath) : null,
|
|
9915
|
+
outputs: incoming.map((frame, index) => ({
|
|
9916
|
+
role: frame.role,
|
|
9917
|
+
basedOn: basedOnHash(entry, index),
|
|
9918
|
+
sha256: sha256(frame.png)
|
|
9919
|
+
}))
|
|
9789
9920
|
};
|
|
9790
|
-
|
|
9921
|
+
for (const [index, member] of started.members.entries()) {
|
|
9922
|
+
await replaceFile(member.path, incoming[index].png);
|
|
9923
|
+
}
|
|
9791
9924
|
if (projectPath && input.project) await replaceFile(projectPath, input.project);
|
|
9792
9925
|
const companionPath = handEditCompanionPath(started.editPath);
|
|
9793
9926
|
await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
|
|
9794
|
-
return {
|
|
9927
|
+
return {
|
|
9928
|
+
...started,
|
|
9929
|
+
sha256: companion.outputs[0].sha256,
|
|
9930
|
+
basedOn: companion.outputs[0].basedOn,
|
|
9931
|
+
companion,
|
|
9932
|
+
companionPath,
|
|
9933
|
+
projectPath
|
|
9934
|
+
};
|
|
9935
|
+
}
|
|
9936
|
+
function basedOnHash(entry, index) {
|
|
9937
|
+
if (!entry) return null;
|
|
9938
|
+
if (entry.outputs.length === 1) return primaryOutput(entry)?.sha256 ?? null;
|
|
9939
|
+
return entry.outputs[index]?.sha256 ?? null;
|
|
9795
9940
|
}
|
|
9796
9941
|
async function replaceFile(file, bytes) {
|
|
9797
9942
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -9838,7 +9983,10 @@ var HandEditRequestSchema = import_zod9.z.discriminatedUnion("action", [
|
|
|
9838
9983
|
assetId: import_zod9.z.string().min(1),
|
|
9839
9984
|
styleId: import_zod9.z.string().min(1),
|
|
9840
9985
|
expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
|
|
9841
|
-
|
|
9986
|
+
/** The flattened image; for a frame set, `frames` instead. */
|
|
9987
|
+
png: Base64.min(1).optional(),
|
|
9988
|
+
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
9989
|
+
frames: import_zod9.z.array(import_zod9.z.object({ role: import_zod9.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
9842
9990
|
/** The editor's layered project file, kept beside the edit. */
|
|
9843
9991
|
pxo: Base64.optional(),
|
|
9844
9992
|
/** Editor identity from the bridge's ready message. */
|
|
@@ -9861,18 +10009,23 @@ function createGalleryEditHandler(opts) {
|
|
|
9861
10009
|
const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
|
|
9862
10010
|
log(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
|
|
9863
10011
|
if (request.open) {
|
|
9864
|
-
const command = (opts.openEditor ?? openInEditor)(started.
|
|
10012
|
+
const command = (opts.openEditor ?? openInEditor)(started.members[0].path);
|
|
9865
10013
|
log(` opened with: ${command}`);
|
|
9866
10014
|
}
|
|
9867
10015
|
} else if (request.action === "save-edit") {
|
|
10016
|
+
if (request.png !== void 0 === (request.frames !== void 0)) {
|
|
10017
|
+
throw new ManifestEditError("invalid edit: send png for one image or frames for a set, not both");
|
|
10018
|
+
}
|
|
9868
10019
|
const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
|
|
9869
|
-
png: Buffer.from(request.png, "base64"),
|
|
10020
|
+
...request.png !== void 0 ? { png: Buffer.from(request.png, "base64") } : {},
|
|
10021
|
+
...request.frames ? { frames: request.frames.map((frame) => ({ role: frame.role, png: Buffer.from(frame.png, "base64") })) } : {},
|
|
9870
10022
|
...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
|
|
9871
10023
|
editor: request.editor,
|
|
9872
10024
|
protocol: request.protocol,
|
|
9873
10025
|
expectedSha256: request.expectedSha256
|
|
9874
10026
|
});
|
|
9875
|
-
|
|
10027
|
+
const what = saved.members.length > 1 ? `${saved.source} (${saved.members.length} frames)` : saved.source;
|
|
10028
|
+
log(` hand edit saved from ${request.editor}: ${what}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
|
|
9876
10029
|
} else {
|
|
9877
10030
|
const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
|
|
9878
10031
|
if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
|
|
@@ -9920,6 +10073,18 @@ function matchesFilter(filter, styleId, assetId) {
|
|
|
9920
10073
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
|
|
9921
10074
|
return true;
|
|
9922
10075
|
}
|
|
10076
|
+
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
10077
|
+
async function samePixels(a, b) {
|
|
10078
|
+
try {
|
|
10079
|
+
const [left, right] = await Promise.all([(0, import_promises20.readFile)(a), (0, import_promises20.readFile)(b)]);
|
|
10080
|
+
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
10081
|
+
const x = decodePng(left);
|
|
10082
|
+
const y = decodePng(right);
|
|
10083
|
+
return x.width === y.width && x.height === y.height && x.pixels.equals(y.pixels);
|
|
10084
|
+
} catch {
|
|
10085
|
+
return false;
|
|
10086
|
+
}
|
|
10087
|
+
}
|
|
9923
10088
|
function metadataFps(entry) {
|
|
9924
10089
|
if (!entry) return null;
|
|
9925
10090
|
const namespace = entry.providerMetadata?.[entry.provider];
|
|
@@ -9967,7 +10132,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
9967
10132
|
outputPaths.map((file) => describeOutput(media, root, import_node_path25.default.resolve(root, file)))
|
|
9968
10133
|
);
|
|
9969
10134
|
const recordPath = import_node_path25.default.resolve(root, inspection.record);
|
|
9970
|
-
const recordExists = (0,
|
|
10135
|
+
const recordExists = (0, import_node_fs22.existsSync)(recordPath);
|
|
9971
10136
|
let options = null;
|
|
9972
10137
|
let check = null;
|
|
9973
10138
|
if (recordExists) {
|
|
@@ -10037,6 +10202,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
10037
10202
|
const asset = loaded.manifest.assets[spec.assetId] ?? null;
|
|
10038
10203
|
let outputs;
|
|
10039
10204
|
let edit = null;
|
|
10205
|
+
let edits = [];
|
|
10206
|
+
let editChanged = [];
|
|
10040
10207
|
let editStatus = null;
|
|
10041
10208
|
let editMeta = null;
|
|
10042
10209
|
if (entry && entry.outputs.length) {
|
|
@@ -10047,13 +10214,19 @@ async function buildGallerySnapshot(opts) {
|
|
|
10047
10214
|
);
|
|
10048
10215
|
if (spec.source) {
|
|
10049
10216
|
const editPath = import_node_path25.default.resolve(root, spec.source);
|
|
10050
|
-
const
|
|
10051
|
-
|
|
10052
|
-
|
|
10217
|
+
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
10218
|
+
const editShas = await Promise.all(
|
|
10219
|
+
Array.from({ length: memberCount }, async (_, index) => {
|
|
10220
|
+
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
10221
|
+
return { file, sha256: (0, import_node_fs22.existsSync)(file) ? await sha256File(file) : null };
|
|
10222
|
+
})
|
|
10223
|
+
);
|
|
10224
|
+
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
10225
|
+
edit = edits[0] ?? null;
|
|
10053
10226
|
const companion = await readHandEditCompanion(editPath);
|
|
10054
10227
|
if (companion) {
|
|
10055
10228
|
const projectPath = handEditProjectPath(editPath);
|
|
10056
|
-
const project = companion.project && (0,
|
|
10229
|
+
const project = companion.project && (0, import_node_fs22.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
|
|
10057
10230
|
let projectUrl = null;
|
|
10058
10231
|
if (project) {
|
|
10059
10232
|
const id = galleryMediaId(projectPath);
|
|
@@ -10063,18 +10236,34 @@ async function buildGallerySnapshot(opts) {
|
|
|
10063
10236
|
editMeta = {
|
|
10064
10237
|
editor: companion.editor,
|
|
10065
10238
|
savedAt: companion.savedAt,
|
|
10066
|
-
basedOn: companion.basedOn,
|
|
10067
|
-
changedSince:
|
|
10239
|
+
basedOn: companion.outputs[0]?.basedOn ?? null,
|
|
10240
|
+
changedSince: editShas.some(({ sha256: editSha }, index) => {
|
|
10241
|
+
const recorded = companion.outputs[index]?.sha256;
|
|
10242
|
+
return editSha !== null && recorded !== void 0 && editSha !== recorded;
|
|
10243
|
+
}),
|
|
10068
10244
|
project: project ? portableOutputPath(projectPath, root) : null,
|
|
10069
10245
|
projectUrl
|
|
10070
10246
|
};
|
|
10071
10247
|
}
|
|
10072
|
-
const regenerated =
|
|
10073
|
-
|
|
10248
|
+
const regenerated = edits.some((member, index) => {
|
|
10249
|
+
const generated = outputs[index];
|
|
10250
|
+
if (companion) {
|
|
10251
|
+
const recorded = companion.outputs[index];
|
|
10252
|
+
return recorded !== void 0 && generated.sha256 !== null && recorded.basedOn !== generated.sha256;
|
|
10253
|
+
}
|
|
10254
|
+
return Boolean(generated.modifiedAt && member.modifiedAt && generated.modifiedAt > member.modifiedAt && generated.exists);
|
|
10255
|
+
});
|
|
10256
|
+
editChanged = await Promise.all(edits.map(async (member, index) => {
|
|
10257
|
+
const generated = outputs[index];
|
|
10258
|
+
if (!member.exists || !generated.exists) return true;
|
|
10259
|
+
if (member.sha256 === generated.sha256) return false;
|
|
10260
|
+
return !await samePixels(member.absolutePath, generated.absolutePath);
|
|
10261
|
+
}));
|
|
10262
|
+
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
10074
10263
|
}
|
|
10075
10264
|
} else if (spec.source) {
|
|
10076
10265
|
outputs = [await describeOutput(media, root, import_node_path25.default.resolve(root, spec.source))];
|
|
10077
|
-
} else if (!entry && (0,
|
|
10266
|
+
} else if (!entry && (0, import_node_fs22.existsSync)(spec.outFile)) {
|
|
10078
10267
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
10079
10268
|
} else {
|
|
10080
10269
|
outputs = [];
|
|
@@ -10119,6 +10308,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
10119
10308
|
asset,
|
|
10120
10309
|
source: spec.source ?? null,
|
|
10121
10310
|
edit,
|
|
10311
|
+
edits,
|
|
10312
|
+
editChanged,
|
|
10122
10313
|
editStatus,
|
|
10123
10314
|
editMeta,
|
|
10124
10315
|
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
@@ -10176,6 +10367,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
10176
10367
|
asset: null,
|
|
10177
10368
|
source: null,
|
|
10178
10369
|
edit: null,
|
|
10370
|
+
edits: [],
|
|
10371
|
+
editChanged: [],
|
|
10179
10372
|
editStatus: null,
|
|
10180
10373
|
editMeta: null,
|
|
10181
10374
|
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
@@ -11104,10 +11297,15 @@ window.addEventListener('message', (e) => {
|
|
|
11104
11297
|
// declares the file the same way pixelkiln edit does.
|
|
11105
11298
|
|
|
11106
11299
|
let SHEET = null;
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
const
|
|
11110
|
-
|
|
11300
|
+
// What the editor gets: the edit files when they are all there, else the
|
|
11301
|
+
// generated outputs (one PNG, or every frame of a set).
|
|
11302
|
+
const editorSources = (item) => {
|
|
11303
|
+
if (item.edits.length && item.edits.every((e) => e.exists)) return { files: item.edits, fromEdit: true };
|
|
11304
|
+
if (editableOutputs(item)) return { files: item.outputs, fromEdit: false };
|
|
11305
|
+
return null;
|
|
11306
|
+
};
|
|
11307
|
+
const editorSource = (item) => { const s = editorSources(item); return s ? s.files[0] : null; };
|
|
11308
|
+
const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSources(item);
|
|
11111
11309
|
const toBase64 = (buf) => new Promise((resolve, reject) => {
|
|
11112
11310
|
const r = new FileReader();
|
|
11113
11311
|
r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
@@ -11136,7 +11334,7 @@ function openEditorSheet(item) {
|
|
|
11136
11334
|
const bar = el('div', 'rbar');
|
|
11137
11335
|
const close = el('button', null, 'Close'); close.type = 'button';
|
|
11138
11336
|
close.onclick = () => closeEditorSheet();
|
|
11139
|
-
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
|
|
11337
|
+
const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height + (item.outputs.length > 1 ? ' \xB7 ' + item.outputs.length + ' ' + memberNoun(item, item.outputs.length) : ''));
|
|
11140
11338
|
const status = el('span', 'st');
|
|
11141
11339
|
const acts = el('div', 'acts');
|
|
11142
11340
|
const msg = el('span', 'msg');
|
|
@@ -11157,23 +11355,34 @@ function openEditorSheet(item) {
|
|
|
11157
11355
|
stage.append(frame, loading);
|
|
11158
11356
|
panel.append(bar, stage);
|
|
11159
11357
|
host.append(scrim, panel);
|
|
11160
|
-
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false };
|
|
11358
|
+
SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
|
|
11161
11359
|
sheetStatus('cool', 'loading');
|
|
11162
11360
|
sheetButtons();
|
|
11163
11361
|
close.focus({ preventScroll: true });
|
|
11164
11362
|
}
|
|
11165
|
-
// The edit
|
|
11166
|
-
// project beside
|
|
11167
|
-
// falling back to the flattened
|
|
11363
|
+
// The edit files are what the editor gets; when a browser save kept the
|
|
11364
|
+
// layered project beside them, that goes along too and the editor restores
|
|
11365
|
+
// the layers, falling back to the flattened PNGs if the file cannot be read.
|
|
11366
|
+
// A frame set is sent as frames, one per member, and comes back the same way.
|
|
11168
11367
|
async function sendOpen() {
|
|
11169
|
-
const { item
|
|
11368
|
+
const { item } = SHEET;
|
|
11369
|
+
const sources = editorSources(item);
|
|
11170
11370
|
const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
|
|
11171
|
-
const projectUrl =
|
|
11172
|
-
|
|
11371
|
+
const projectUrl = sources.fromEdit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
|
|
11372
|
+
const set = sources.files.length > 1;
|
|
11373
|
+
if (set && SHEET.ready.version < 3) {
|
|
11374
|
+
SHEET.msg.textContent = 'This editor build cannot open sets; reinstall it with pixelkiln tools install editor.';
|
|
11375
|
+
sheetStatus('bad', 'unsupported');
|
|
11376
|
+
return;
|
|
11377
|
+
}
|
|
11378
|
+
const pngs = [];
|
|
11379
|
+
let pxo = null;
|
|
11173
11380
|
try {
|
|
11174
|
-
const
|
|
11175
|
-
|
|
11176
|
-
|
|
11381
|
+
for (const file of sources.files) {
|
|
11382
|
+
const res = await fetch(file.url, { cache: 'no-store' });
|
|
11383
|
+
if (!res.ok) throw new Error(await res.text());
|
|
11384
|
+
pngs.push(await res.arrayBuffer());
|
|
11385
|
+
}
|
|
11177
11386
|
if (projectUrl) {
|
|
11178
11387
|
const p = await fetch(projectUrl, { cache: 'no-store' });
|
|
11179
11388
|
if (p.ok) pxo = await p.arrayBuffer();
|
|
@@ -11185,12 +11394,22 @@ async function sendOpen() {
|
|
|
11185
11394
|
if (!SHEET) return;
|
|
11186
11395
|
const request = 'open-' + (++SHEET.requests);
|
|
11187
11396
|
SHEET.sentProject = !!pxo;
|
|
11397
|
+
SHEET.roles = set ? item.outputs.map((o) => o.role || null) : null;
|
|
11188
11398
|
const message = {
|
|
11189
11399
|
type: 'pixelkiln:open', request,
|
|
11190
11400
|
asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
|
|
11191
|
-
png, palette: style ? style.palette : [],
|
|
11401
|
+
png: pngs[0], palette: style ? style.palette : [],
|
|
11192
11402
|
};
|
|
11193
|
-
const transfer = [
|
|
11403
|
+
const transfer = [pngs[0]];
|
|
11404
|
+
if (set) {
|
|
11405
|
+
message.frames = pngs.map((png, i) => ({ role: SHEET.roles[i], png }));
|
|
11406
|
+
// A tile set is not an animation; a slow rate keeps its timeline still-ish.
|
|
11407
|
+
message.fps = isFrameSet(item) ? (item.fps || 12) : 1;
|
|
11408
|
+
for (const png of pngs.slice(1)) transfer.push(png);
|
|
11409
|
+
// The first frame is both png (older editors) and frames[0]; copy it so both transfer.
|
|
11410
|
+
message.png = pngs[0].slice(0);
|
|
11411
|
+
transfer.push(message.png);
|
|
11412
|
+
}
|
|
11194
11413
|
if (pxo) { message.pxo = pxo; transfer.push(pxo); }
|
|
11195
11414
|
SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
|
|
11196
11415
|
sheetStatus('cool', 'opening');
|
|
@@ -11218,8 +11437,9 @@ async function onEditorMessage(m) {
|
|
|
11218
11437
|
case 'pixelkiln:opened':
|
|
11219
11438
|
SHEET.opened = true;
|
|
11220
11439
|
SHEET.loading.remove();
|
|
11221
|
-
if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file
|
|
11222
|
-
else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file;
|
|
11440
|
+
if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file' + (m.frames > 1 ? 's' : '') + ' with ' + (m.layers === 1 ? 'the layer' : m.layers + ' layers') + (m.frames > 1 ? ' and ' + m.frames + ' ' + memberNoun(SHEET.item, m.frames) : '') + ' restored');
|
|
11441
|
+
else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
|
|
11442
|
+
else if (m.source === 'frames') sheetStatus('ok', (SHEET.item.edits.length ? 'editing the edit files' : 'editing a copy of the generated ' + memberNoun(SHEET.item, m.frames)) + ' as ' + m.frames + ' frames' + (isFrameSet(SHEET.item) ? '' : ', one per ' + memberNoun(SHEET.item, 1)));
|
|
11223
11443
|
else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
|
|
11224
11444
|
sheetButtons();
|
|
11225
11445
|
break;
|
|
@@ -11232,14 +11452,21 @@ async function onEditorMessage(m) {
|
|
|
11232
11452
|
const pending = SHEET.pending;
|
|
11233
11453
|
if (!pending || pending.request !== m.request) return;
|
|
11234
11454
|
try {
|
|
11235
|
-
const body = {
|
|
11455
|
+
const body = { editor: SHEET.ready.editor, protocol: SHEET.ready.version };
|
|
11456
|
+
if (SHEET.roles) {
|
|
11457
|
+
if (!Array.isArray(m.frames)) throw new Error('the editor returned no frames');
|
|
11458
|
+
body.frames = [];
|
|
11459
|
+
for (const f of m.frames) body.frames.push({ role: f.role === undefined ? null : f.role, png: await toBase64(f.png) });
|
|
11460
|
+
} else {
|
|
11461
|
+
body.png = await toBase64(m.png);
|
|
11462
|
+
}
|
|
11236
11463
|
if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
|
|
11237
11464
|
await postHandEdit(SHEET.item, 'save-edit', body);
|
|
11238
11465
|
const saved = snap.items.find((i) => i.id === SHEET.item.id);
|
|
11239
11466
|
if (saved) SHEET.item = saved;
|
|
11240
11467
|
SHEET.dirty = false;
|
|
11241
11468
|
SHEET.pending = null;
|
|
11242
|
-
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place it in place of the generated art.' };
|
|
11469
|
+
ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edits.length > 1 ? saved.edits.length + ' ' + memberNoun(saved, saved.edits.length) + ' to ' + saved.source : saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place ' + (saved && saved.edits.length > 1 ? 'them' : 'it') + ' in place of the generated art.' };
|
|
11243
11470
|
render();
|
|
11244
11471
|
if (pending.close) { closeEditorSheet(true); return; }
|
|
11245
11472
|
sheetStatus('ok', 'saved');
|
|
@@ -11387,7 +11614,7 @@ function clearFilters() {
|
|
|
11387
11614
|
function thumb(item) {
|
|
11388
11615
|
const cell = el('div', 'cell');
|
|
11389
11616
|
// A hand edit is what ships, so it is what the card shows.
|
|
11390
|
-
const shown = item.edit && item.
|
|
11617
|
+
const shown = item.edit && item.editStatus === 'edited' && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
|
|
11391
11618
|
if (!shown.length) {
|
|
11392
11619
|
cell.classList.add('none');
|
|
11393
11620
|
cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
|
|
@@ -11801,7 +12028,7 @@ function openCompare() {
|
|
|
11801
12028
|
// ---- hand edits -------------------------------------------------------------
|
|
11802
12029
|
|
|
11803
12030
|
const EDIT_STATUS_TEXT = {
|
|
11804
|
-
same: '
|
|
12031
|
+
same: 'The generated art\u2019s pixels, not changed yet. Open it in your editor and save.',
|
|
11805
12032
|
edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
|
|
11806
12033
|
'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
|
|
11807
12034
|
missing: 'The declared file is not on disk.',
|
|
@@ -11812,12 +12039,18 @@ async function postHandEdit(item, action, extra) {
|
|
|
11812
12039
|
if (item.project) body.project = item.project;
|
|
11813
12040
|
snap = await postEdit(body);
|
|
11814
12041
|
}
|
|
12042
|
+
// A single PNG or a set of them \u2014 frames, tiles \u2014 is hand-editable, one file
|
|
12043
|
+
// per member; a set with a GIF in it is not.
|
|
12044
|
+
const editableOutputs = (item) => item.outputs.length && item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
|
|
12045
|
+
// What the members of a set are called: frames for an animation, tiles for a tile set.
|
|
12046
|
+
const memberNoun = (item, n) => isFrameSet(item) ? (n === 1 ? 'frame' : 'frames')
|
|
12047
|
+
: item.outputs.every((o) => o.role && /^tile/i.test(o.role)) ? (n === 1 ? 'tile' : 'tiles') : (n === 1 ? 'member' : 'members');
|
|
11815
12048
|
function handEditSection(item) {
|
|
11816
|
-
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
|
|
11817
|
-
item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
|
|
12049
|
+
const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editableOutputs(item);
|
|
11818
12050
|
if (!item.edit && !canStart) return null;
|
|
12051
|
+
const set = item.edits.length > 1;
|
|
11819
12052
|
const s = el('section', 'meta');
|
|
11820
|
-
s.append(el('h3', null, 'Hand edit'));
|
|
12053
|
+
s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ')' : 'Hand edit'));
|
|
11821
12054
|
if (item.edit) {
|
|
11822
12055
|
const pair = el('div', 'pair');
|
|
11823
12056
|
const shared = displayScale(item.width, item.height, 200, 160);
|
|
@@ -11831,24 +12064,36 @@ function handEditSection(item) {
|
|
|
11831
12064
|
f.append(el('figcaption', null, label));
|
|
11832
12065
|
return f;
|
|
11833
12066
|
};
|
|
11834
|
-
|
|
12067
|
+
// For a set, the member the preview is on; the strip there steps through them.
|
|
12068
|
+
const at = set ? Math.min(ui.member, item.edits.length - 1) : 0;
|
|
12069
|
+
const which = set ? ' \xB7 ' + (item.outputs[at].role || memberNoun(item, 1) + ' ' + (at + 1)) : '';
|
|
12070
|
+
pair.append(fig(item.outputs[at], 'generated' + which), fig(item.edits[at], 'edit' + which));
|
|
11835
12071
|
s.append(pair);
|
|
11836
12072
|
const dl = el('dl');
|
|
11837
12073
|
const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
|
|
11838
12074
|
const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
|
|
11839
12075
|
row(dl, 'status', st);
|
|
11840
12076
|
row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
12077
|
+
if (set) {
|
|
12078
|
+
row(dl, 'files', item.source + ' \u2014 one per ' + memberNoun(item, 1) + ': ' + item.edits.map((e) => e.path.split('/').pop()).join(', '), { mono: true, copy: item.edits.map((e) => e.absolutePath).join('\\n') });
|
|
12079
|
+
const differing = item.editChanged.filter(Boolean).length;
|
|
12080
|
+
row(dl, 'changed', differing + ' of ' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ' differ from the generated art' +
|
|
12081
|
+
(differing ? ': ' + item.edits.filter((e, i) => item.editChanged[i]).map((e) => item.outputs[item.edits.indexOf(e)].role || e.path.split('/').pop()).join(', ') : ''));
|
|
12082
|
+
} else {
|
|
12083
|
+
row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
|
|
12084
|
+
if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
|
|
12085
|
+
}
|
|
12086
|
+
row(dl, 'saved', fmtWhen(item.edits.reduce((latest, e) => e.modifiedAt && (!latest || e.modifiedAt > latest) ? e.modifiedAt : latest, null)));
|
|
11844
12087
|
if (item.editMeta) {
|
|
11845
|
-
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014
|
|
11846
|
-
if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
12088
|
+
row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 a file changed since' : ''));
|
|
12089
|
+
if (item.editMeta.basedOn && !set) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
|
|
11847
12090
|
if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
|
|
11848
12091
|
}
|
|
11849
12092
|
s.append(dl);
|
|
11850
12093
|
} else {
|
|
11851
|
-
s.append(el('div', 'state-dim',
|
|
12094
|
+
s.append(el('div', 'state-dim', item.outputs.length > 1
|
|
12095
|
+
? 'Touch the ' + memberNoun(item, 2) + ' up in your own editor or in the browser, where they open as ' + (isFrameSet(item) ? 'one animation' : 'the frames of one project') + '. The generated files stay untouched; the edit is one sibling file per ' + memberNoun(item, 1) + ' that the manifest points at, and mount, pack, and export place those instead.'
|
|
12096
|
+
: '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.'));
|
|
11852
12097
|
}
|
|
11853
12098
|
if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
|
|
11854
12099
|
const acts = el('div', 'hand-actions');
|
|
@@ -11876,10 +12121,11 @@ function handEditSection(item) {
|
|
|
11876
12121
|
}
|
|
11877
12122
|
if (item.edit) {
|
|
11878
12123
|
go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
|
|
11879
|
-
go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.
|
|
12124
|
+
go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
|
|
11880
12125
|
} else if (canStart) {
|
|
11881
|
-
|
|
11882
|
-
go('
|
|
12126
|
+
const many = item.outputs.length > 1;
|
|
12127
|
+
go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file' + (many ? 's and opened the first ' + memberNoun(item, 1) : ' and opened it') + ' in your editor. Save there, then Refresh to see it here.');
|
|
12128
|
+
go('Create edit file' + (many ? 's' : '') + ' only', 'start-edit', { open: false }, 'Created the edit file' + (many ? 's' : '') + ' and declared ' + (many ? 'them' : 'it') + ' in the manifest.');
|
|
11883
12129
|
}
|
|
11884
12130
|
acts.append(msg);
|
|
11885
12131
|
s.append(acts);
|
|
@@ -12632,11 +12878,305 @@ if (ui.compare.length >= 2 && !ui.open) openCompare();
|
|
|
12632
12878
|
}
|
|
12633
12879
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
12634
12880
|
|
|
12881
|
+
// tools/pixelorama-bridge/pin.json
|
|
12882
|
+
var pin_default = {
|
|
12883
|
+
pixelorama: "v1.2.2",
|
|
12884
|
+
godot: "4.7.2",
|
|
12885
|
+
extensionsApi: 9,
|
|
12886
|
+
protocol: 3,
|
|
12887
|
+
release: "editor-pixelorama-v1.2.2-pk.14",
|
|
12888
|
+
files: {
|
|
12889
|
+
"index.apple-touch-icon.png": {
|
|
12890
|
+
sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
|
|
12891
|
+
bytes: 1036
|
|
12892
|
+
},
|
|
12893
|
+
"index.audio.position.worklet.js": {
|
|
12894
|
+
sha256: "be33985bc7160d6bf9646f259cd86b259cd67b02ccb297ee5c44f8ac84327bc8",
|
|
12895
|
+
bytes: 2973
|
|
12896
|
+
},
|
|
12897
|
+
"index.audio.worklet.js": {
|
|
12898
|
+
sha256: "5b476a9c9ce642c0ee4256436d1bc31d9c38f868aca0f9a8e2a57c18d2dec2a3",
|
|
12899
|
+
bytes: 7298
|
|
12900
|
+
},
|
|
12901
|
+
"index.html": {
|
|
12902
|
+
sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
|
|
12903
|
+
bytes: 5442
|
|
12904
|
+
},
|
|
12905
|
+
"index.icon.png": {
|
|
12906
|
+
sha256: "42a327d23178ddc26cc97e3560ebca6bc7c575d2a1b12a4837f2462049ca5745",
|
|
12907
|
+
bytes: 1070
|
|
12908
|
+
},
|
|
12909
|
+
"index.js": {
|
|
12910
|
+
sha256: "33c94cb3175f3333b82e2a3be5e8e86f77986f0aa2042b1631f6367a4e5bb6ba",
|
|
12911
|
+
bytes: 279815
|
|
12912
|
+
},
|
|
12913
|
+
"index.pck": {
|
|
12914
|
+
sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
|
|
12915
|
+
bytes: 6429628
|
|
12916
|
+
},
|
|
12917
|
+
"index.png": {
|
|
12918
|
+
sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
|
|
12919
|
+
bytes: 13558
|
|
12920
|
+
},
|
|
12921
|
+
"index.wasm": {
|
|
12922
|
+
sha256: "fc74679e3b97f76878947fcd4fbe1268cbfa6188182a2e33bbc3f5dc9bfa57d0",
|
|
12923
|
+
bytes: 39514754
|
|
12924
|
+
}
|
|
12925
|
+
}
|
|
12926
|
+
};
|
|
12927
|
+
|
|
12928
|
+
// src/editor/pin.ts
|
|
12929
|
+
var EDITOR_PIN = pin_default;
|
|
12930
|
+
var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
|
|
12931
|
+
|
|
12932
|
+
// src/editor/install.ts
|
|
12933
|
+
var import_node_crypto7 = require("crypto");
|
|
12934
|
+
var import_node_fs23 = require("fs");
|
|
12935
|
+
var import_promises21 = require("fs/promises");
|
|
12936
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
12937
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
12938
|
+
var EditorInstallError = class extends Error {
|
|
12939
|
+
constructor(message6, file) {
|
|
12940
|
+
super(message6);
|
|
12941
|
+
this.file = file;
|
|
12942
|
+
this.name = "EditorInstallError";
|
|
12943
|
+
}
|
|
12944
|
+
file;
|
|
12945
|
+
};
|
|
12946
|
+
function editorToolsRoot() {
|
|
12947
|
+
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
12948
|
+
if (override) return import_node_path26.default.resolve(override);
|
|
12949
|
+
if (process.platform === "darwin") return import_node_path26.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
12950
|
+
if (process.platform === "win32") {
|
|
12951
|
+
return import_node_path26.default.join(process.env.LOCALAPPDATA ?? import_node_path26.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
12952
|
+
}
|
|
12953
|
+
return import_node_path26.default.join(process.env.XDG_CACHE_HOME ?? import_node_path26.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
|
|
12954
|
+
}
|
|
12955
|
+
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
12956
|
+
return import_node_path26.default.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
12957
|
+
}
|
|
12958
|
+
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
12959
|
+
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
12960
|
+
if (override) return override.replace(/\/+$/, "");
|
|
12961
|
+
if (!pin.release) return null;
|
|
12962
|
+
return `${EDITOR_RELEASE_BASE}/${encodeURIComponent(pin.release)}`;
|
|
12963
|
+
}
|
|
12964
|
+
async function sha256Of(file) {
|
|
12965
|
+
try {
|
|
12966
|
+
return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises21.readFile)(file)).digest("hex");
|
|
12967
|
+
} catch {
|
|
12968
|
+
return null;
|
|
12969
|
+
}
|
|
12970
|
+
}
|
|
12971
|
+
async function editorStatus(opts = {}) {
|
|
12972
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
12973
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
12974
|
+
const missing = [];
|
|
12975
|
+
let installedBytes = 0;
|
|
12976
|
+
let totalBytes = 0;
|
|
12977
|
+
for (const [name, expected] of Object.entries(pin.files)) {
|
|
12978
|
+
totalBytes += expected.bytes;
|
|
12979
|
+
const file = import_node_path26.default.join(dir, name);
|
|
12980
|
+
const info = (0, import_node_fs23.existsSync)(file) ? await (0, import_promises21.stat)(file).catch(() => null) : null;
|
|
12981
|
+
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
12982
|
+
installedBytes += expected.bytes;
|
|
12983
|
+
} else {
|
|
12984
|
+
missing.push(name);
|
|
12985
|
+
}
|
|
12986
|
+
}
|
|
12987
|
+
const pinned = Object.keys(pin.files).length > 0 && pin.release !== null;
|
|
12988
|
+
return {
|
|
12989
|
+
release: pin.release,
|
|
12990
|
+
dir,
|
|
12991
|
+
installed: pinned && missing.length === 0,
|
|
12992
|
+
missing: pinned ? missing : Object.keys(pin.files),
|
|
12993
|
+
totalBytes,
|
|
12994
|
+
installedBytes
|
|
12995
|
+
};
|
|
12996
|
+
}
|
|
12997
|
+
async function installEditor(opts = {}) {
|
|
12998
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
12999
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
13000
|
+
const baseUrl = opts.baseUrl ?? editorBaseUrl(pin);
|
|
13001
|
+
const doFetch = opts.fetch ?? fetch;
|
|
13002
|
+
const before = await editorStatus({ pin, dir });
|
|
13003
|
+
if (!pin.release || !Object.keys(pin.files).length) {
|
|
13004
|
+
throw new EditorInstallError(
|
|
13005
|
+
"this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust"
|
|
13006
|
+
);
|
|
13007
|
+
}
|
|
13008
|
+
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
13009
|
+
await (0, import_promises21.mkdir)(dir, { recursive: true });
|
|
13010
|
+
const downloaded = [];
|
|
13011
|
+
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
13012
|
+
let fetchedBytes = before.installedBytes;
|
|
13013
|
+
const names = before.missing;
|
|
13014
|
+
for (const [index, name] of names.entries()) {
|
|
13015
|
+
const expected = pin.files[name];
|
|
13016
|
+
const report = (phase, received) => opts.onProgress?.({
|
|
13017
|
+
file: name,
|
|
13018
|
+
index,
|
|
13019
|
+
count: names.length,
|
|
13020
|
+
bytes: expected.bytes,
|
|
13021
|
+
totalBytes: before.totalBytes,
|
|
13022
|
+
fetchedBytes: fetchedBytes + received,
|
|
13023
|
+
phase
|
|
13024
|
+
});
|
|
13025
|
+
report("start", 0);
|
|
13026
|
+
const url = `${baseUrl}/${encodeURIComponent(name)}`;
|
|
13027
|
+
const response = await doFetch(url, { signal: opts.signal, redirect: "follow" });
|
|
13028
|
+
if (!response.ok) throw new EditorInstallError(`${name}: ${response.status} ${response.statusText} from ${url}`, name);
|
|
13029
|
+
const bytes = await readBody(response, expected.bytes, (received) => report("progress", received));
|
|
13030
|
+
if (bytes.length !== expected.bytes) {
|
|
13031
|
+
throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
|
|
13032
|
+
}
|
|
13033
|
+
const digest2 = (0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex");
|
|
13034
|
+
if (digest2 !== expected.sha256) {
|
|
13035
|
+
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
13036
|
+
}
|
|
13037
|
+
const final = import_node_path26.default.join(dir, name);
|
|
13038
|
+
const tmp = `${final}.${process.pid}.part`;
|
|
13039
|
+
try {
|
|
13040
|
+
await (0, import_promises21.writeFile)(tmp, bytes);
|
|
13041
|
+
await (0, import_promises21.rename)(tmp, final);
|
|
13042
|
+
} finally {
|
|
13043
|
+
await (0, import_promises21.rm)(tmp, { force: true });
|
|
13044
|
+
}
|
|
13045
|
+
fetchedBytes += bytes.length;
|
|
13046
|
+
downloaded.push(name);
|
|
13047
|
+
report("done", 0);
|
|
13048
|
+
}
|
|
13049
|
+
const after = await editorStatus({ pin, dir });
|
|
13050
|
+
if (!after.installed) {
|
|
13051
|
+
throw new EditorInstallError(`editor install is incomplete: ${after.missing.join(", ")}`);
|
|
13052
|
+
}
|
|
13053
|
+
return { ...after, downloaded, skipped };
|
|
13054
|
+
}
|
|
13055
|
+
async function readBody(response, expectedBytes, onBytes) {
|
|
13056
|
+
if (!response.body) return Buffer.from(await response.arrayBuffer());
|
|
13057
|
+
const chunks = [];
|
|
13058
|
+
let received = 0;
|
|
13059
|
+
const reader = response.body.getReader();
|
|
13060
|
+
for (; ; ) {
|
|
13061
|
+
const { done, value } = await reader.read();
|
|
13062
|
+
if (done) break;
|
|
13063
|
+
chunks.push(value);
|
|
13064
|
+
received += value.length;
|
|
13065
|
+
if (received > expectedBytes) {
|
|
13066
|
+
await reader.cancel().catch(() => {
|
|
13067
|
+
});
|
|
13068
|
+
break;
|
|
13069
|
+
}
|
|
13070
|
+
onBytes(received);
|
|
13071
|
+
}
|
|
13072
|
+
return Buffer.concat(chunks);
|
|
13073
|
+
}
|
|
13074
|
+
var EDITOR_CONTENT_TYPES = {
|
|
13075
|
+
".html": "text/html; charset=utf-8",
|
|
13076
|
+
".js": "text/javascript; charset=utf-8",
|
|
13077
|
+
".wasm": "application/wasm",
|
|
13078
|
+
".pck": "application/octet-stream",
|
|
13079
|
+
".png": "image/png",
|
|
13080
|
+
".json": "application/json; charset=utf-8"
|
|
13081
|
+
};
|
|
13082
|
+
function editorContentType(name) {
|
|
13083
|
+
return EDITOR_CONTENT_TYPES[import_node_path26.default.extname(name).toLowerCase()] ?? null;
|
|
13084
|
+
}
|
|
13085
|
+
|
|
13086
|
+
// src/gallery/editor.ts
|
|
13087
|
+
var import_promises22 = require("fs/promises");
|
|
13088
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
13089
|
+
function editorRoute(release, name = "index.html") {
|
|
13090
|
+
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
13091
|
+
}
|
|
13092
|
+
function createGalleryEditorHandlers(opts = {}) {
|
|
13093
|
+
const pin = opts.pin ?? EDITOR_PIN;
|
|
13094
|
+
const dir = opts.dir ?? editorDir(pin);
|
|
13095
|
+
const log = opts.onProgress ?? (() => {
|
|
13096
|
+
});
|
|
13097
|
+
let verified = null;
|
|
13098
|
+
let checking = null;
|
|
13099
|
+
let installing = null;
|
|
13100
|
+
let running = null;
|
|
13101
|
+
let error = null;
|
|
13102
|
+
const check = () => {
|
|
13103
|
+
if (!checking) {
|
|
13104
|
+
checking = editorStatus({ pin, dir }).then((status) => {
|
|
13105
|
+
verified = status;
|
|
13106
|
+
return status;
|
|
13107
|
+
}).finally(() => {
|
|
13108
|
+
checking = null;
|
|
13109
|
+
});
|
|
13110
|
+
}
|
|
13111
|
+
return checking;
|
|
13112
|
+
};
|
|
13113
|
+
const describe = (status) => ({
|
|
13114
|
+
...status,
|
|
13115
|
+
pixelorama: pin.pixelorama,
|
|
13116
|
+
protocol: pin.protocol,
|
|
13117
|
+
url: status.installed && pin.release ? editorRoute(pin.release) : null,
|
|
13118
|
+
installing,
|
|
13119
|
+
error
|
|
13120
|
+
});
|
|
13121
|
+
return {
|
|
13122
|
+
release: pin.release,
|
|
13123
|
+
status: async () => describe(verified ?? await check()),
|
|
13124
|
+
install: async () => {
|
|
13125
|
+
if (!running) {
|
|
13126
|
+
error = null;
|
|
13127
|
+
installing = { file: "", index: 0, count: 0, bytes: 0, totalBytes: 0, fetchedBytes: 0, phase: "start" };
|
|
13128
|
+
log(` installing the editor (Pixelorama ${pin.pixelorama}) into ${dir}`);
|
|
13129
|
+
running = installEditor({
|
|
13130
|
+
pin,
|
|
13131
|
+
dir,
|
|
13132
|
+
baseUrl: opts.baseUrl ?? editorBaseUrl(pin) ?? void 0,
|
|
13133
|
+
...opts.fetch ? { fetch: opts.fetch } : {},
|
|
13134
|
+
onProgress: (progress) => {
|
|
13135
|
+
installing = progress;
|
|
13136
|
+
}
|
|
13137
|
+
}).then((result) => {
|
|
13138
|
+
verified = result;
|
|
13139
|
+
const mb = (result.totalBytes / 1e6).toFixed(1);
|
|
13140
|
+
log(` editor ready: ${result.downloaded.length} file(s) fetched, ${result.skipped.length} already present (${mb} MB)`);
|
|
13141
|
+
}).catch((err) => {
|
|
13142
|
+
error = err instanceof Error ? err.message : String(err);
|
|
13143
|
+
verified = null;
|
|
13144
|
+
log(` editor install failed: ${error}`);
|
|
13145
|
+
}).finally(() => {
|
|
13146
|
+
installing = null;
|
|
13147
|
+
running = null;
|
|
13148
|
+
});
|
|
13149
|
+
}
|
|
13150
|
+
return describe(verified ?? await check());
|
|
13151
|
+
},
|
|
13152
|
+
file: async (release, name) => {
|
|
13153
|
+
if (!pin.release || release !== pin.release) return null;
|
|
13154
|
+
const expected = pin.files[name];
|
|
13155
|
+
const contentType = editorContentType(name);
|
|
13156
|
+
if (!expected || !contentType) return null;
|
|
13157
|
+
const status = verified ?? await check();
|
|
13158
|
+
if (!status.installed) return null;
|
|
13159
|
+
let bytes;
|
|
13160
|
+
try {
|
|
13161
|
+
bytes = await (0, import_promises22.readFile)(import_node_path27.default.join(dir, name));
|
|
13162
|
+
} catch {
|
|
13163
|
+
verified = null;
|
|
13164
|
+
return null;
|
|
13165
|
+
}
|
|
13166
|
+
if (bytes.length !== expected.bytes) {
|
|
13167
|
+
verified = null;
|
|
13168
|
+
return null;
|
|
13169
|
+
}
|
|
13170
|
+
return { bytes, contentType };
|
|
13171
|
+
}
|
|
13172
|
+
};
|
|
13173
|
+
}
|
|
13174
|
+
|
|
12635
13175
|
// src/gallery/server.ts
|
|
12636
13176
|
var import_node_child_process4 = require("child_process");
|
|
12637
|
-
var
|
|
13177
|
+
var import_node_crypto8 = require("crypto");
|
|
12638
13178
|
var import_node_http2 = require("http");
|
|
12639
|
-
var
|
|
13179
|
+
var import_promises23 = require("fs/promises");
|
|
12640
13180
|
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
12641
13181
|
var EDITOR_CSP = [
|
|
12642
13182
|
"default-src 'self'",
|
|
@@ -12664,7 +13204,7 @@ async function readJsonBody(req) {
|
|
|
12664
13204
|
async function serveGallery(opts) {
|
|
12665
13205
|
const log = opts.onProgress ?? (() => {
|
|
12666
13206
|
});
|
|
12667
|
-
const session = opts.edit || opts.generate || opts.editor ? (0,
|
|
13207
|
+
const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
|
|
12668
13208
|
let media = /* @__PURE__ */ new Map();
|
|
12669
13209
|
let loading = null;
|
|
12670
13210
|
const reviewAssets = /* @__PURE__ */ new Map();
|
|
@@ -12795,7 +13335,7 @@ async function serveGallery(opts) {
|
|
|
12795
13335
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
12796
13336
|
if (!asset) return fail(404, "no such review asset");
|
|
12797
13337
|
try {
|
|
12798
|
-
const bytes = await (0,
|
|
13338
|
+
const bytes = await (0, import_promises23.readFile)(asset.path);
|
|
12799
13339
|
res.writeHead(200, {
|
|
12800
13340
|
"Content-Type": asset.contentType,
|
|
12801
13341
|
"Cache-Control": "no-store",
|
|
@@ -12871,7 +13411,7 @@ async function serveGallery(opts) {
|
|
|
12871
13411
|
if (!asset) return fail(404, "no such gallery media");
|
|
12872
13412
|
let bytes;
|
|
12873
13413
|
try {
|
|
12874
|
-
bytes = await (0,
|
|
13414
|
+
bytes = await (0, import_promises23.readFile)(asset.path);
|
|
12875
13415
|
} catch {
|
|
12876
13416
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
12877
13417
|
}
|
|
@@ -12916,7 +13456,7 @@ async function serveGallery(opts) {
|
|
|
12916
13456
|
}
|
|
12917
13457
|
|
|
12918
13458
|
// src/gallery/generate.ts
|
|
12919
|
-
var
|
|
13459
|
+
var import_node_crypto9 = require("crypto");
|
|
12920
13460
|
var import_zod10 = require("zod");
|
|
12921
13461
|
var GenerateRequestSchema = import_zod10.z.object({
|
|
12922
13462
|
project: import_zod10.z.string().min(1).optional(),
|
|
@@ -13116,7 +13656,7 @@ function createGenerateHandlers(opts) {
|
|
|
13116
13656
|
}
|
|
13117
13657
|
}
|
|
13118
13658
|
const job = {
|
|
13119
|
-
id: (0,
|
|
13659
|
+
id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
|
|
13120
13660
|
project: request.project ?? null,
|
|
13121
13661
|
keys: request.keys,
|
|
13122
13662
|
mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
|
|
@@ -13181,19 +13721,19 @@ function createGenerateHandlers(opts) {
|
|
|
13181
13721
|
}
|
|
13182
13722
|
|
|
13183
13723
|
// src/pipeline/salvage.ts
|
|
13184
|
-
var
|
|
13185
|
-
var
|
|
13186
|
-
var
|
|
13724
|
+
var import_promises24 = require("fs/promises");
|
|
13725
|
+
var import_node_fs24 = require("fs");
|
|
13726
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
13187
13727
|
function providerClaimId(provider, objectId) {
|
|
13188
13728
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
13189
13729
|
}
|
|
13190
13730
|
async function loadClaims(lockPaths, opts = {}) {
|
|
13191
13731
|
const claimed = /* @__PURE__ */ new Set();
|
|
13192
13732
|
for (const p of lockPaths) {
|
|
13193
|
-
if (!(0,
|
|
13733
|
+
if (!(0, import_node_fs24.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
13194
13734
|
let parsed;
|
|
13195
13735
|
try {
|
|
13196
|
-
parsed = parseLock(JSON.parse(await (0,
|
|
13736
|
+
parsed = parseLock(JSON.parse(await (0, import_promises24.readFile)(p, "utf8")));
|
|
13197
13737
|
} catch {
|
|
13198
13738
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
13199
13739
|
}
|
|
@@ -13246,20 +13786,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
13246
13786
|
return matchStyleByPattern(prompt, manifest);
|
|
13247
13787
|
}
|
|
13248
13788
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
13249
|
-
const own =
|
|
13789
|
+
const own = import_node_path28.default.resolve(ownManifestPath);
|
|
13250
13790
|
const siblingManifestPaths = [
|
|
13251
13791
|
.../* @__PURE__ */ new Set([
|
|
13252
13792
|
...workspaceManifestPaths,
|
|
13253
|
-
...claimPaths.map((c) =>
|
|
13793
|
+
...claimPaths.map((c) => import_node_path28.default.join(import_node_path28.default.dirname(import_node_path28.default.resolve(c)), "pixelkiln.manifest.json"))
|
|
13254
13794
|
])
|
|
13255
13795
|
];
|
|
13256
13796
|
const siblings = [];
|
|
13257
13797
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
13258
|
-
if (
|
|
13259
|
-
if (!(0,
|
|
13798
|
+
if (import_node_path28.default.resolve(siblingManifestPath) === own) continue;
|
|
13799
|
+
if (!(0, import_node_fs24.existsSync)(siblingManifestPath)) continue;
|
|
13260
13800
|
try {
|
|
13261
13801
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
13262
|
-
siblings.push({ label:
|
|
13802
|
+
siblings.push({ label: import_node_path28.default.basename(import_node_path28.default.dirname(siblingManifestPath)), manifest });
|
|
13263
13803
|
} catch {
|
|
13264
13804
|
}
|
|
13265
13805
|
}
|
|
@@ -13375,8 +13915,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
13375
13915
|
}
|
|
13376
13916
|
|
|
13377
13917
|
// src/pick/salvage-server.ts
|
|
13378
|
-
var
|
|
13379
|
-
var
|
|
13918
|
+
var import_promises25 = require("fs/promises");
|
|
13919
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
13380
13920
|
|
|
13381
13921
|
// src/pick/salvage-sheet.ts
|
|
13382
13922
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13541,7 +14081,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13541
14081
|
});
|
|
13542
14082
|
const html = renderSalvageSheet(orphans, {
|
|
13543
14083
|
styleId: ctx.styleId,
|
|
13544
|
-
importDir:
|
|
14084
|
+
importDir: import_node_path29.default.relative(process.cwd(), ctx.importDir) || "."
|
|
13545
14085
|
});
|
|
13546
14086
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13547
14087
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13570,10 +14110,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13570
14110
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13571
14111
|
decodePng(buf);
|
|
13572
14112
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13573
|
-
const rel =
|
|
13574
|
-
const outFile =
|
|
13575
|
-
await (0,
|
|
13576
|
-
await (0,
|
|
14113
|
+
const rel = import_node_path29.default.join("_salvaged", `${assetId}.png`);
|
|
14114
|
+
const outFile = import_node_path29.default.resolve(ctx.importDir, rel);
|
|
14115
|
+
await (0, import_promises25.mkdir)(import_node_path29.default.dirname(outFile), { recursive: true });
|
|
14116
|
+
await (0, import_promises25.writeFile)(outFile, buf);
|
|
13577
14117
|
ctx.manifest.assets[assetId] = {
|
|
13578
14118
|
prompt: orphan.prompt,
|
|
13579
14119
|
promptByStyle: {},
|
|
@@ -13605,7 +14145,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13605
14145
|
sourceUrl: durableSource,
|
|
13606
14146
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13607
14147
|
outputs: [{
|
|
13608
|
-
path: portableOutputPath(outFile,
|
|
14148
|
+
path: portableOutputPath(outFile, import_node_path29.default.dirname(ctx.manifestPath)),
|
|
13609
14149
|
sha256: sha256(buf)
|
|
13610
14150
|
}],
|
|
13611
14151
|
submittedAt: orphan.createdAt,
|
|
@@ -13629,9 +14169,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13629
14169
|
}
|
|
13630
14170
|
}
|
|
13631
14171
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
13632
|
-
const raw = JSON.parse(await (0,
|
|
14172
|
+
const raw = JSON.parse(await (0, import_promises25.readFile)(ctx.manifestPath, "utf8"));
|
|
13633
14173
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
13634
|
-
await (0,
|
|
14174
|
+
await (0, import_promises25.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
13635
14175
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
13636
14176
|
return result;
|
|
13637
14177
|
}
|
|
@@ -13746,17 +14286,23 @@ async function workspaceStatus(ws, dir) {
|
|
|
13746
14286
|
ComfyUIProvider,
|
|
13747
14287
|
DEFAULT_QUALITY_TOLERANCES,
|
|
13748
14288
|
DEFAULT_RATE_LIMIT,
|
|
14289
|
+
EDITOR_PIN,
|
|
14290
|
+
EDITOR_RELEASE_BASE,
|
|
14291
|
+
EditorInstallError,
|
|
13749
14292
|
FAKE_PNG,
|
|
13750
14293
|
FakeProvider,
|
|
13751
14294
|
GenerateRequestError,
|
|
13752
14295
|
GenerateRequestSchema,
|
|
13753
14296
|
GeneratorSchema,
|
|
13754
14297
|
GridConfidenceSchema,
|
|
14298
|
+
HAND_EDIT_DIR,
|
|
14299
|
+
HandEditCompanionSchema,
|
|
13755
14300
|
ImageQualityMetricsSchema,
|
|
13756
14301
|
InheritedStyleSchema,
|
|
13757
14302
|
LockEntrySchema,
|
|
13758
14303
|
LockSchema,
|
|
13759
14304
|
MAX_DOWNLOAD_BYTES,
|
|
14305
|
+
MAX_HAND_EDIT_BYTES,
|
|
13760
14306
|
MAX_RETRIES,
|
|
13761
14307
|
ManifestDriftError,
|
|
13762
14308
|
ManifestEditError,
|
|
@@ -13803,12 +14349,19 @@ async function workspaceStatus(ws, dir) {
|
|
|
13803
14349
|
countNumberedDescriptions,
|
|
13804
14350
|
createArtifactBundleManifest,
|
|
13805
14351
|
createGalleryEditHandler,
|
|
14352
|
+
createGalleryEditorHandlers,
|
|
13806
14353
|
createGenerateHandlers,
|
|
13807
14354
|
createProvider,
|
|
13808
14355
|
currentEntryOutputPath,
|
|
13809
14356
|
currentOutputPath,
|
|
14357
|
+
detachHandEdit,
|
|
13810
14358
|
detectMediaType,
|
|
13811
14359
|
doctor,
|
|
14360
|
+
editorBaseUrl,
|
|
14361
|
+
editorDir,
|
|
14362
|
+
editorRoute,
|
|
14363
|
+
editorStatus,
|
|
14364
|
+
editorToolsRoot,
|
|
13812
14365
|
evaluateAudit,
|
|
13813
14366
|
expectedOutputPath,
|
|
13814
14367
|
exportTileset,
|
|
@@ -13820,13 +14373,22 @@ async function workspaceStatus(ws, dir) {
|
|
|
13820
14373
|
galleryMediaRoute,
|
|
13821
14374
|
generationCost,
|
|
13822
14375
|
groupOrphansByStyle,
|
|
14376
|
+
handEditBase,
|
|
14377
|
+
handEditBases,
|
|
14378
|
+
handEditCompanionPath,
|
|
14379
|
+
handEditMembers,
|
|
14380
|
+
handEditPath,
|
|
14381
|
+
handEditProjectPath,
|
|
13823
14382
|
hex,
|
|
13824
14383
|
idFromPrompt,
|
|
13825
14384
|
imageMetadata,
|
|
13826
14385
|
inspectCaches,
|
|
13827
14386
|
inspectQualityProfile,
|
|
13828
14387
|
inspectRevisionReadiness,
|
|
14388
|
+
installEditor,
|
|
13829
14389
|
installRecipe,
|
|
14390
|
+
isFrameSetEntry,
|
|
14391
|
+
isMemberSetEntry,
|
|
13830
14392
|
isSensitiveSourceUrl,
|
|
13831
14393
|
listBundledRecipes,
|
|
13832
14394
|
loadClaims,
|
|
@@ -13841,12 +14403,14 @@ async function workspaceStatus(ws, dir) {
|
|
|
13841
14403
|
measureImageQuality,
|
|
13842
14404
|
mediaExtension,
|
|
13843
14405
|
mediaTypeFromExtension,
|
|
14406
|
+
memberPath,
|
|
13844
14407
|
mergePalettes,
|
|
13845
14408
|
mountSprites,
|
|
13846
14409
|
mountStyle,
|
|
13847
14410
|
normalizeLockOutputPaths,
|
|
13848
14411
|
normalizeRefinementPalette,
|
|
13849
14412
|
normalizeTileRules,
|
|
14413
|
+
openInEditor,
|
|
13850
14414
|
outliers,
|
|
13851
14415
|
outputId,
|
|
13852
14416
|
packSprites,
|
|
@@ -13866,6 +14430,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
13866
14430
|
qualityRecordPath,
|
|
13867
14431
|
quantizeToPalette,
|
|
13868
14432
|
readArtifactBundleManifest,
|
|
14433
|
+
readHandEditCompanion,
|
|
13869
14434
|
readQualityBaseline,
|
|
13870
14435
|
recipeDigest,
|
|
13871
14436
|
refineAsset,
|
|
@@ -13898,6 +14463,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
13898
14463
|
retryAfterMs,
|
|
13899
14464
|
runPicker,
|
|
13900
14465
|
runSalvage,
|
|
14466
|
+
saveHandEdit,
|
|
13901
14467
|
saveLock,
|
|
13902
14468
|
saveWorkspace,
|
|
13903
14469
|
scanAssets,
|
|
@@ -13909,8 +14475,11 @@ async function workspaceStatus(ws, dir) {
|
|
|
13909
14475
|
shouldRetry,
|
|
13910
14476
|
slugify,
|
|
13911
14477
|
snapshotQualityBaseline,
|
|
14478
|
+
sourceIsStem,
|
|
14479
|
+
sourceOutputPath,
|
|
13912
14480
|
specHash,
|
|
13913
14481
|
spendByUnit,
|
|
14482
|
+
startHandEdit,
|
|
13914
14483
|
styleImagesBase64,
|
|
13915
14484
|
submit,
|
|
13916
14485
|
summarize,
|