pixelkiln 0.9.0 → 0.11.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/CONTRIBUTING.md +2 -1
- package/PROVIDERS.md +13 -14
- package/README.md +19 -9
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +1433 -925
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +500 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -2
- package/dist/index.d.ts +124 -2
- package/dist/index.js +475 -105
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +3 -2
- package/docs/ARTIFACTS.md +28 -1
- package/docs/CLI.md +66 -6
- package/docs/COMFYUI.md +62 -11
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/LIBRARY.md +21 -0
- package/docs/MANIFEST.md +3 -2
- package/docs/PROVIDER_BENCHMARK.md +24 -8
- package/docs/QUALITY.md +31 -0
- package/docs/README.md +1 -1
- package/package.json +2 -2
- package/skills/pixelkiln/SKILL.md +5 -1
- package/skills/pixelkiln/references/comfyui.md +15 -11
- package/skills/pixelkiln/references/mixed-providers.md +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path20 from "path";
|
|
5
5
|
import { existsSync as existsSync15 } from "fs";
|
|
6
|
-
import { readFile as
|
|
6
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -114,8 +114,8 @@ import { readFile } from "fs/promises";
|
|
|
114
114
|
function sha256(data) {
|
|
115
115
|
return createHash("sha256").update(data).digest("hex");
|
|
116
116
|
}
|
|
117
|
-
async function sha256File(
|
|
118
|
-
return sha256(await readFile(
|
|
117
|
+
async function sha256File(path21) {
|
|
118
|
+
return sha256(await readFile(path21));
|
|
119
119
|
}
|
|
120
120
|
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
|
|
121
121
|
return sha256(
|
|
@@ -974,9 +974,9 @@ function normalizeBaseUrl(value) {
|
|
|
974
974
|
}
|
|
975
975
|
function historyError(status) {
|
|
976
976
|
const messages = Array.isArray(status.messages) ? status.messages : [];
|
|
977
|
-
for (const
|
|
978
|
-
if (!Array.isArray(
|
|
979
|
-
const detail =
|
|
977
|
+
for (const message3 of messages) {
|
|
978
|
+
if (!Array.isArray(message3) || message3[0] !== "execution_error" || !isObject(message3[1])) continue;
|
|
979
|
+
const detail = message3[1].exception_message;
|
|
980
980
|
if (typeof detail === "string" && detail.trim()) {
|
|
981
981
|
return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
|
|
982
982
|
}
|
|
@@ -1097,8 +1097,8 @@ function validateResponse(schema, raw, operation) {
|
|
|
1097
1097
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
1098
1098
|
}
|
|
1099
1099
|
var PixelLabError = class extends Error {
|
|
1100
|
-
constructor(
|
|
1101
|
-
super(
|
|
1100
|
+
constructor(message3, status, body) {
|
|
1101
|
+
super(message3);
|
|
1102
1102
|
this.status = status;
|
|
1103
1103
|
this.body = body;
|
|
1104
1104
|
this.name = "PixelLabError";
|
|
@@ -1123,11 +1123,11 @@ var PixelLabClient = class {
|
|
|
1123
1123
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1124
1124
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1125
1125
|
*/
|
|
1126
|
-
async request(
|
|
1126
|
+
async request(path21, init, attempt = 0) {
|
|
1127
1127
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1128
1128
|
let res;
|
|
1129
1129
|
try {
|
|
1130
|
-
res = await fetch(`${BASE}${
|
|
1130
|
+
res = await fetch(`${BASE}${path21}`, {
|
|
1131
1131
|
...init,
|
|
1132
1132
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1133
1133
|
headers: {
|
|
@@ -1139,24 +1139,24 @@ var PixelLabClient = class {
|
|
|
1139
1139
|
} catch (err) {
|
|
1140
1140
|
if (attempt < MAX_RETRIES) {
|
|
1141
1141
|
await sleep(backoffMs(attempt));
|
|
1142
|
-
return this.request(
|
|
1142
|
+
return this.request(path21, init, attempt + 1);
|
|
1143
1143
|
}
|
|
1144
1144
|
throw err;
|
|
1145
1145
|
}
|
|
1146
1146
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1147
1147
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1148
1148
|
await sleep(waitMs);
|
|
1149
|
-
return this.request(
|
|
1149
|
+
return this.request(path21, init, attempt + 1);
|
|
1150
1150
|
}
|
|
1151
1151
|
const text = await res.text();
|
|
1152
1152
|
if (!res.ok) {
|
|
1153
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1153
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path21} \u2192 ${res.status}`, res.status, text);
|
|
1154
1154
|
}
|
|
1155
1155
|
if (!text) return {};
|
|
1156
1156
|
try {
|
|
1157
1157
|
return JSON.parse(text);
|
|
1158
1158
|
} catch {
|
|
1159
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1159
|
+
throw new Error(`${init?.method ?? "GET"} ${path21} returned invalid JSON`);
|
|
1160
1160
|
}
|
|
1161
1161
|
}
|
|
1162
1162
|
async balance() {
|
|
@@ -1996,9 +1996,9 @@ var RetroDiffusionClient = class {
|
|
|
1996
1996
|
}
|
|
1997
1997
|
return balance;
|
|
1998
1998
|
}
|
|
1999
|
-
async call(
|
|
1999
|
+
async call(path21, init = {}) {
|
|
2000
2000
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2001
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2001
|
+
const response = await this.request(`${this.baseUrl}${path21}`, {
|
|
2002
2002
|
...init,
|
|
2003
2003
|
headers: {
|
|
2004
2004
|
"Content-Type": "application/json",
|
|
@@ -2262,8 +2262,8 @@ function retroError(value) {
|
|
|
2262
2262
|
if (details.length) return details.join("; ");
|
|
2263
2263
|
}
|
|
2264
2264
|
if (record.detail && typeof record.detail === "object") {
|
|
2265
|
-
const
|
|
2266
|
-
if (typeof
|
|
2265
|
+
const message3 = record.detail.message;
|
|
2266
|
+
if (typeof message3 === "string") return message3;
|
|
2267
2267
|
}
|
|
2268
2268
|
return JSON.stringify(value);
|
|
2269
2269
|
}
|
|
@@ -3262,9 +3262,9 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3262
3262
|
);
|
|
3263
3263
|
} catch (err) {
|
|
3264
3264
|
failed++;
|
|
3265
|
-
const
|
|
3266
|
-
upsert(lock, key, { status: "failed", error:
|
|
3267
|
-
log2(` FAILED ${key}: ${
|
|
3265
|
+
const message3 = err instanceof Error ? err.message : String(err);
|
|
3266
|
+
upsert(lock, key, { status: "failed", error: message3, cost: 0 });
|
|
3267
|
+
log2(` FAILED ${key}: ${message3}`);
|
|
3268
3268
|
}
|
|
3269
3269
|
await saveLock(lockPath, lock);
|
|
3270
3270
|
}
|
|
@@ -3452,14 +3452,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3452
3452
|
});
|
|
3453
3453
|
result.downloaded++;
|
|
3454
3454
|
} catch (err) {
|
|
3455
|
-
const
|
|
3455
|
+
const message3 = err instanceof Error ? err.message : String(err);
|
|
3456
3456
|
upsert(lock, key, {
|
|
3457
3457
|
status: "download-failed",
|
|
3458
3458
|
outputs: mergeOutputs(entry.outputs, outputs),
|
|
3459
|
-
error: `download failed: ${
|
|
3459
|
+
error: `download failed: ${message3}`
|
|
3460
3460
|
});
|
|
3461
3461
|
result.failed++;
|
|
3462
|
-
log2(` FAILED ${key}: ${
|
|
3462
|
+
log2(` FAILED ${key}: ${message3}`);
|
|
3463
3463
|
}
|
|
3464
3464
|
await saveLock(lockPath, lock);
|
|
3465
3465
|
}
|
|
@@ -3529,7 +3529,7 @@ import { access, stat as stat2 } from "fs/promises";
|
|
|
3529
3529
|
import path10 from "path";
|
|
3530
3530
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
3531
3531
|
const checks = [];
|
|
3532
|
-
const add = (id, level,
|
|
3532
|
+
const add = (id, level, message3) => checks.push({ id, level, message: message3 });
|
|
3533
3533
|
add(
|
|
3534
3534
|
"manifest",
|
|
3535
3535
|
"ok",
|
|
@@ -3679,10 +3679,10 @@ function isSha256Hash(value) {
|
|
|
3679
3679
|
function parseCache(value) {
|
|
3680
3680
|
return HashCacheSchema.parse(value);
|
|
3681
3681
|
}
|
|
3682
|
-
async function loadCache(
|
|
3683
|
-
if (!existsSync8(
|
|
3682
|
+
async function loadCache(path21) {
|
|
3683
|
+
if (!existsSync8(path21)) return { version: 1, hashes: {} };
|
|
3684
3684
|
try {
|
|
3685
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(
|
|
3685
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path21, "utf8")));
|
|
3686
3686
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
3687
3687
|
return {
|
|
3688
3688
|
version: 1,
|
|
@@ -3694,18 +3694,18 @@ async function loadCache(path20) {
|
|
|
3694
3694
|
return { version: 1, hashes: {} };
|
|
3695
3695
|
}
|
|
3696
3696
|
}
|
|
3697
|
-
async function saveCache(
|
|
3697
|
+
async function saveCache(path21, cache) {
|
|
3698
3698
|
const sorted = {};
|
|
3699
3699
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
3700
3700
|
const hash = cache.hashes[key];
|
|
3701
3701
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
3702
3702
|
sorted[key] = hash;
|
|
3703
3703
|
}
|
|
3704
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
3705
|
-
const tmp = `${
|
|
3704
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path21)), { recursive: true });
|
|
3705
|
+
const tmp = `${path21}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3706
3706
|
try {
|
|
3707
3707
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
3708
|
-
await rename3(tmp,
|
|
3708
|
+
await rename3(tmp, path21);
|
|
3709
3709
|
} finally {
|
|
3710
3710
|
await rm3(tmp, { force: true });
|
|
3711
3711
|
}
|
|
@@ -4156,10 +4156,10 @@ function serveReviewPage(opts) {
|
|
|
4156
4156
|
res.end(JSON.stringify(result));
|
|
4157
4157
|
server.close(() => resolve(result));
|
|
4158
4158
|
} catch (err) {
|
|
4159
|
-
const
|
|
4160
|
-
log2(` apply failed: ${
|
|
4159
|
+
const message3 = err instanceof Error ? err.message : String(err);
|
|
4160
|
+
log2(` apply failed: ${message3}`);
|
|
4161
4161
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4162
|
-
res.end(
|
|
4162
|
+
res.end(message3);
|
|
4163
4163
|
}
|
|
4164
4164
|
return;
|
|
4165
4165
|
}
|
|
@@ -5044,938 +5044,1341 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
5044
5044
|
return report;
|
|
5045
5045
|
}
|
|
5046
5046
|
|
|
5047
|
-
// src/pipeline/
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5047
|
+
// src/pipeline/refine.ts
|
|
5048
|
+
import path18 from "path";
|
|
5049
|
+
import { execFile } from "child_process";
|
|
5050
|
+
import { mkdtemp, readFile as readFile14, rm as rm7 } from "fs/promises";
|
|
5051
|
+
import { tmpdir } from "os";
|
|
5052
|
+
import { promisify } from "util";
|
|
5053
|
+
|
|
5054
|
+
// src/artifacts.ts
|
|
5055
|
+
import path17 from "path";
|
|
5056
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5057
|
+
import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
5058
|
+
var activeTransactions = /* @__PURE__ */ new Set();
|
|
5059
|
+
function message(error) {
|
|
5060
|
+
return error instanceof Error ? error.message : String(error);
|
|
5061
|
+
}
|
|
5062
|
+
function digest(data) {
|
|
5063
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
5064
|
+
}
|
|
5065
|
+
function portableRelative(from, to) {
|
|
5066
|
+
return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
|
|
5067
|
+
}
|
|
5068
|
+
function canonical2(value) {
|
|
5069
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
5070
|
+
if (typeof value === "number") {
|
|
5071
|
+
if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
|
|
5072
|
+
return value;
|
|
5073
|
+
}
|
|
5074
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
5075
|
+
if (typeof value === "object") {
|
|
5076
|
+
const sorted = {};
|
|
5077
|
+
for (const key of Object.keys(value).sort()) {
|
|
5078
|
+
const item = value[key];
|
|
5079
|
+
if (item !== void 0) sorted[key] = canonical2(item);
|
|
5060
5080
|
}
|
|
5081
|
+
return sorted;
|
|
5061
5082
|
}
|
|
5062
|
-
|
|
5063
|
-
const terrains = Array.isArray(raw.terrains) ? raw.terrains.filter((item) => typeof item === "string") : [];
|
|
5064
|
-
return {
|
|
5065
|
-
ruleType,
|
|
5066
|
-
arity,
|
|
5067
|
-
...connectivity ? { connectivity } : {},
|
|
5068
|
-
terrains,
|
|
5069
|
-
masks,
|
|
5070
|
-
raw
|
|
5071
|
-
};
|
|
5083
|
+
throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
|
|
5072
5084
|
}
|
|
5073
|
-
function
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
);
|
|
5085
|
+
function fingerprint(provenance) {
|
|
5086
|
+
return digest(JSON.stringify(canonical2({
|
|
5087
|
+
kind: provenance.kind,
|
|
5088
|
+
sources: provenance.sources,
|
|
5089
|
+
options: provenance.options,
|
|
5090
|
+
outputs: provenance.outputs
|
|
5091
|
+
})));
|
|
5092
|
+
}
|
|
5093
|
+
function parseArtifactManifest(absolute, data) {
|
|
5094
|
+
let raw;
|
|
5095
|
+
try {
|
|
5096
|
+
raw = JSON.parse(data);
|
|
5097
|
+
} catch (error) {
|
|
5098
|
+
throw new Error(`${absolute} is not valid JSON: ${message(error)}`, { cause: error });
|
|
5087
5099
|
}
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5100
|
+
if (raw.format !== "pixelkiln-artifact-bundle" || raw.version !== 1 || raw.kind !== "pack" && raw.kind !== "mount" && raw.kind !== "tileset" && raw.kind !== "refine" || typeof raw.fingerprint !== "string" || raw.options === void 0 || !Array.isArray(raw.sources) || !Array.isArray(raw.outputs)) {
|
|
5101
|
+
throw new Error(`${absolute} is not a PixelKiln artifact bundle manifest.`);
|
|
5102
|
+
}
|
|
5103
|
+
for (const source of raw.sources) {
|
|
5104
|
+
if (!source || typeof source.id !== "string" || typeof source.path !== "string" || source.sha256 !== null && typeof source.sha256 !== "string" || typeof source.included !== "boolean") {
|
|
5105
|
+
throw new Error(`${absolute} contains an invalid artifact source.`);
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
for (const output of raw.outputs) {
|
|
5109
|
+
if (!output || typeof output.path !== "string" || typeof output.sha256 !== "string") {
|
|
5110
|
+
throw new Error(`${absolute} contains an invalid artifact output.`);
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return raw;
|
|
5114
|
+
}
|
|
5115
|
+
async function readArtifactBundleManifest(manifestPath) {
|
|
5116
|
+
const absolute = path17.resolve(manifestPath);
|
|
5117
|
+
return parseArtifactManifest(absolute, await readFile13(absolute, "utf8"));
|
|
5118
|
+
}
|
|
5119
|
+
async function readOptional(file) {
|
|
5120
|
+
try {
|
|
5121
|
+
return await readFile13(file);
|
|
5122
|
+
} catch (error) {
|
|
5123
|
+
if (isCode(error, "ENOENT")) return null;
|
|
5124
|
+
throw error;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
function transactionMarker(journal) {
|
|
5128
|
+
return `${journal}.committed`;
|
|
5129
|
+
}
|
|
5130
|
+
function processIsAlive(pid) {
|
|
5131
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
5132
|
+
try {
|
|
5133
|
+
process.kill(pid, 0);
|
|
5134
|
+
return true;
|
|
5135
|
+
} catch (error) {
|
|
5136
|
+
return !isCode(error, "ESRCH");
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
function validTemporaryPath(candidate, destination, type) {
|
|
5140
|
+
return path17.dirname(candidate) === path17.dirname(destination) && path17.basename(candidate).startsWith(
|
|
5141
|
+
`.${path17.basename(destination)}.pixelkiln-${type}-`
|
|
5099
5142
|
);
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
};
|
|
5143
|
+
}
|
|
5144
|
+
function parseTransaction(journal, data) {
|
|
5145
|
+
let raw;
|
|
5146
|
+
try {
|
|
5147
|
+
raw = JSON.parse(data);
|
|
5148
|
+
} catch (error) {
|
|
5149
|
+
throw new Error(`${journal} is not valid transaction JSON: ${message(error)}`, { cause: error });
|
|
5108
5150
|
}
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
throw new Error(
|
|
5112
|
-
`${options.format} export needs recognized adjacency rules; use --format generic to retain this provider rule object without guessing`
|
|
5113
|
-
);
|
|
5151
|
+
if (raw.format !== "pixelkiln-artifact-transaction" || raw.version !== 1 || !Number.isInteger(raw.pid) || typeof raw.createdAt !== "string" || !Array.isArray(raw.entries) || !raw.entries.length) {
|
|
5152
|
+
throw new Error(`${journal} is not a PixelKiln artifact transaction.`);
|
|
5114
5153
|
}
|
|
5115
|
-
|
|
5154
|
+
return raw;
|
|
5155
|
+
}
|
|
5156
|
+
async function removeTransactionFiles(journal) {
|
|
5157
|
+
await rm6(journal, { force: true });
|
|
5158
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
5159
|
+
}
|
|
5160
|
+
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
5161
|
+
const journal = path17.resolve(recoveryFile);
|
|
5162
|
+
const bytes = await readOptional(journal);
|
|
5163
|
+
if (!bytes) {
|
|
5164
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
if (activeTransactions.has(journal)) {
|
|
5168
|
+
throw new Error(`Another artifact write in this process is already using ${journal}.`);
|
|
5169
|
+
}
|
|
5170
|
+
const transaction = parseTransaction(journal, bytes.toString("utf8"));
|
|
5171
|
+
if (transaction.pid !== process.pid && processIsAlive(transaction.pid)) {
|
|
5116
5172
|
throw new Error(
|
|
5117
|
-
|
|
5173
|
+
`Artifact transaction ${journal} is still owned by live process ${transaction.pid}; refusing concurrent recovery.`
|
|
5118
5174
|
);
|
|
5119
5175
|
}
|
|
5120
|
-
|
|
5176
|
+
for (const entry of transaction.entries) {
|
|
5177
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path17.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
5178
|
+
throw new Error(
|
|
5179
|
+
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
5180
|
+
);
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
const committed = await readOptional(transactionMarker(journal)) !== null;
|
|
5184
|
+
const errors = [];
|
|
5185
|
+
if (committed) {
|
|
5186
|
+
for (const entry of transaction.entries) {
|
|
5187
|
+
if (entry.backup) await remove2(entry.backup, errors);
|
|
5188
|
+
await remove2(entry.stage, errors);
|
|
5189
|
+
}
|
|
5190
|
+
} else {
|
|
5191
|
+
for (const entry of [...transaction.entries].reverse()) {
|
|
5192
|
+
const backup = entry.backup ? await readOptional(entry.backup) : null;
|
|
5193
|
+
const destination = await readOptional(entry.destination);
|
|
5194
|
+
if (backup) {
|
|
5195
|
+
if (destination && digest(destination) !== entry.sha256) {
|
|
5196
|
+
errors.push(
|
|
5197
|
+
`destination changed during interrupted recovery: ${entry.destination}; previous file remains at ${entry.backup}`
|
|
5198
|
+
);
|
|
5199
|
+
continue;
|
|
5200
|
+
}
|
|
5201
|
+
await remove2(entry.destination, errors);
|
|
5202
|
+
try {
|
|
5203
|
+
await rename5(entry.backup, entry.destination);
|
|
5204
|
+
} catch (error) {
|
|
5205
|
+
errors.push(
|
|
5206
|
+
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
5207
|
+
);
|
|
5208
|
+
}
|
|
5209
|
+
} else if (destination && digest(destination) === entry.sha256) {
|
|
5210
|
+
await remove2(entry.destination, errors);
|
|
5211
|
+
}
|
|
5212
|
+
await remove2(entry.stage, errors);
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
if (errors.length) {
|
|
5121
5216
|
throw new Error(
|
|
5122
|
-
|
|
5217
|
+
`Artifact transaction recovery needs attention; journal retained at ${journal}: ` + errors.join("; ")
|
|
5123
5218
|
);
|
|
5124
5219
|
}
|
|
5125
|
-
|
|
5126
|
-
return {
|
|
5127
|
-
png: packed.png,
|
|
5128
|
-
extension: ".tsj",
|
|
5129
|
-
document: JSON.stringify(buildTiled(generic), null, 2) + "\n",
|
|
5130
|
-
generic,
|
|
5131
|
-
sources: packed.sources
|
|
5132
|
-
};
|
|
5133
|
-
}
|
|
5134
|
-
return {
|
|
5135
|
-
png: packed.png,
|
|
5136
|
-
extension: ".tres",
|
|
5137
|
-
document: buildGodot(generic, spec),
|
|
5138
|
-
generic,
|
|
5139
|
-
sources: packed.sources
|
|
5140
|
-
};
|
|
5220
|
+
await removeTransactionFiles(journal);
|
|
5141
5221
|
}
|
|
5142
|
-
function
|
|
5222
|
+
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
5223
|
+
const root = path17.dirname(path17.resolve(manifestPath));
|
|
5224
|
+
const sources = provenance.sources.map((source) => ({
|
|
5225
|
+
...source,
|
|
5226
|
+
path: portableRelative(root, source.path)
|
|
5227
|
+
}));
|
|
5228
|
+
const normalized = {
|
|
5229
|
+
kind: provenance.kind,
|
|
5230
|
+
sources,
|
|
5231
|
+
options: canonical2(provenance.options)
|
|
5232
|
+
};
|
|
5233
|
+
const manifestOutputs = outputs.map((output) => ({
|
|
5234
|
+
path: portableRelative(root, output.path),
|
|
5235
|
+
sha256: digest(output.data)
|
|
5236
|
+
}));
|
|
5143
5237
|
return {
|
|
5144
|
-
format: "pixelkiln-
|
|
5238
|
+
format: "pixelkiln-artifact-bundle",
|
|
5145
5239
|
version: 1,
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
image: imageName,
|
|
5150
|
-
tile: { ...atlas.cell },
|
|
5151
|
-
sheet: { ...atlas.sheet, columns: atlas.columns },
|
|
5152
|
-
tiles: frames.map((frame, index) => {
|
|
5153
|
-
const sourceIndex = tileIndex(frame.id) ?? index;
|
|
5154
|
-
return {
|
|
5155
|
-
id: index,
|
|
5156
|
-
sourceIndex,
|
|
5157
|
-
role: frame.id,
|
|
5158
|
-
x: frame.x,
|
|
5159
|
-
y: frame.y,
|
|
5160
|
-
width: frame.width,
|
|
5161
|
-
height: frame.height,
|
|
5162
|
-
...rules?.masks[sourceIndex] !== void 0 ? { bitmask: rules.masks[sourceIndex] } : {},
|
|
5163
|
-
stampOnly: rules ? rules.masks[sourceIndex] === void 0 : true
|
|
5164
|
-
};
|
|
5165
|
-
}),
|
|
5166
|
-
providerRules,
|
|
5167
|
-
rules
|
|
5168
|
-
};
|
|
5169
|
-
}
|
|
5170
|
-
function buildTiled(generic) {
|
|
5171
|
-
const rules = generic.rules;
|
|
5172
|
-
const tiled = {
|
|
5173
|
-
type: "tileset",
|
|
5174
|
-
version: "1.10",
|
|
5175
|
-
tiledversion: "1.12.2",
|
|
5176
|
-
name: generic.name.replace("/", "-"),
|
|
5177
|
-
tilewidth: generic.tile.width,
|
|
5178
|
-
tileheight: generic.tile.height,
|
|
5179
|
-
tilecount: generic.tiles.length,
|
|
5180
|
-
columns: generic.sheet.columns,
|
|
5181
|
-
image: generic.image,
|
|
5182
|
-
imagewidth: generic.sheet.width,
|
|
5183
|
-
imageheight: generic.sheet.height,
|
|
5184
|
-
margin: 0,
|
|
5185
|
-
spacing: 0
|
|
5240
|
+
...normalized,
|
|
5241
|
+
fingerprint: fingerprint({ ...normalized, outputs: manifestOutputs }),
|
|
5242
|
+
outputs: manifestOutputs
|
|
5186
5243
|
};
|
|
5187
|
-
if (!rules || rules.ruleType === "outline") return tiled;
|
|
5188
|
-
const terrainNames = terrainNamesFor(rules);
|
|
5189
|
-
tiled.wangsets = [{
|
|
5190
|
-
name: generic.name,
|
|
5191
|
-
type: rules.ruleType,
|
|
5192
|
-
tile: representativeTile(generic, 0),
|
|
5193
|
-
colors: terrainNames.map((name, index) => ({
|
|
5194
|
-
name,
|
|
5195
|
-
color: index === 0 ? "#5ab552" : "#4c78a8",
|
|
5196
|
-
tile: representativeTile(generic, index),
|
|
5197
|
-
probability: 1
|
|
5198
|
-
})),
|
|
5199
|
-
properties: rules.connectivity ? [{ name: "pixelkiln:connectivity", type: "string", value: rules.connectivity }] : [],
|
|
5200
|
-
wangtiles: generic.tiles.filter((tile) => tile.bitmask !== void 0).map((tile) => ({ tileid: tile.id, wangid: tiledWangId(rules, tile.bitmask) }))
|
|
5201
|
-
}];
|
|
5202
|
-
return tiled;
|
|
5203
|
-
}
|
|
5204
|
-
function tiledWangId(rules, mask) {
|
|
5205
|
-
const bitColor = (bit) => (mask & 1 << bit) !== 0 ? 1 : 2;
|
|
5206
|
-
if (rules.ruleType === "corner") {
|
|
5207
|
-
return [0, bitColor(2), 0, bitColor(0), 0, bitColor(1), 0, bitColor(3)];
|
|
5208
|
-
}
|
|
5209
|
-
return [bitColor(0), 0, bitColor(1), 0, bitColor(2), 0, bitColor(3), 0];
|
|
5210
5244
|
}
|
|
5211
|
-
function
|
|
5212
|
-
const
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
""
|
|
5216
|
-
`[ext_resource type="Texture2D" path=${JSON.stringify(`./${generic.image}`)} id="1_texture"]`,
|
|
5217
|
-
"",
|
|
5218
|
-
`[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_pixelkiln"]`,
|
|
5219
|
-
`texture = ExtResource("1_texture")`,
|
|
5220
|
-
`texture_region_size = Vector2i(${generic.tile.width}, ${generic.tile.height})`
|
|
5245
|
+
function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
5246
|
+
const manifest = createArtifactBundleManifest(manifestPath, outputs, provenance);
|
|
5247
|
+
return [
|
|
5248
|
+
...outputs,
|
|
5249
|
+
{ path: manifestPath, data: JSON.stringify(manifest, null, 2) + "\n" }
|
|
5221
5250
|
];
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5251
|
+
}
|
|
5252
|
+
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
5253
|
+
const absoluteManifest = path17.resolve(manifestPath);
|
|
5254
|
+
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
5255
|
+
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
5256
|
+
absoluteManifest,
|
|
5257
|
+
...outputs.map((output) => path17.resolve(output.path))
|
|
5258
|
+
]);
|
|
5259
|
+
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
5260
|
+
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
5261
|
+
let previous = null;
|
|
5262
|
+
if (existingManifestBytes && !options.force) {
|
|
5263
|
+
try {
|
|
5264
|
+
previous = parseArtifactManifest(absoluteManifest, existingManifestBytes.toString("utf8"));
|
|
5265
|
+
const valid = previous.fingerprint === fingerprint(previous);
|
|
5266
|
+
if (!valid) throw new Error("its integrity fingerprint does not match");
|
|
5267
|
+
} catch (error) {
|
|
5268
|
+
throw new Error(
|
|
5269
|
+
`Refusing to replace unrecognized or modified provenance at ${absoluteManifest}: ${message(error)}. Pass { force: true } (CLI: --force) to take ownership of this artifact bundle.`,
|
|
5270
|
+
{ cause: error }
|
|
5271
|
+
);
|
|
5232
5272
|
}
|
|
5233
5273
|
}
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
for (const
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5274
|
+
if (!options.force) {
|
|
5275
|
+
const root = path17.dirname(absoluteManifest);
|
|
5276
|
+
const recorded = new Map(
|
|
5277
|
+
previous?.outputs.map((output) => [path17.resolve(root, output.path), output.sha256]) ?? []
|
|
5278
|
+
);
|
|
5279
|
+
const conflicts = [];
|
|
5280
|
+
for (const output of outputs) {
|
|
5281
|
+
const destination = path17.resolve(output.path);
|
|
5282
|
+
const current = await readOptional(destination);
|
|
5283
|
+
if (!current || digest(current) === digest(output.data)) continue;
|
|
5284
|
+
const expected = recorded.get(destination);
|
|
5285
|
+
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
5286
|
+
}
|
|
5287
|
+
if (conflicts.length) {
|
|
5288
|
+
throw new Error(
|
|
5289
|
+
`Refusing to overwrite modified or unowned artifact file(s):
|
|
5290
|
+
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
5291
|
+
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`
|
|
5244
5292
|
);
|
|
5245
5293
|
}
|
|
5246
5294
|
}
|
|
5247
|
-
|
|
5248
|
-
return
|
|
5295
|
+
const { force: _force, ...bundleOptions } = options;
|
|
5296
|
+
return writeArtifactBundle(
|
|
5297
|
+
withArtifactManifest(absoluteManifest, outputs, provenance),
|
|
5298
|
+
{ ...bundleOptions, recoveryFile }
|
|
5299
|
+
);
|
|
5249
5300
|
}
|
|
5250
|
-
function
|
|
5251
|
-
const
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5301
|
+
async function verifyArtifactBundle(manifestPath) {
|
|
5302
|
+
const absolute = path17.resolve(manifestPath);
|
|
5303
|
+
const raw = await readArtifactBundleManifest(absolute);
|
|
5304
|
+
const root = path17.dirname(absolute);
|
|
5305
|
+
const changedSources = [];
|
|
5306
|
+
const changedOutputs = [];
|
|
5307
|
+
for (const source of raw.sources) {
|
|
5308
|
+
let actual = null;
|
|
5309
|
+
try {
|
|
5310
|
+
actual = digest(await readFile13(path17.resolve(root, source.path)));
|
|
5311
|
+
} catch (error) {
|
|
5312
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5313
|
+
}
|
|
5314
|
+
if (actual !== source.sha256) changedSources.push(source.id);
|
|
5259
5315
|
}
|
|
5316
|
+
for (const output of raw.outputs) {
|
|
5317
|
+
try {
|
|
5318
|
+
if (digest(await readFile13(path17.resolve(root, output.path))) !== output.sha256) {
|
|
5319
|
+
changedOutputs.push(output.path);
|
|
5320
|
+
}
|
|
5321
|
+
} catch (error) {
|
|
5322
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5323
|
+
changedOutputs.push(output.path);
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
const fingerprintValid = raw.fingerprint === fingerprint({
|
|
5327
|
+
kind: raw.kind,
|
|
5328
|
+
sources: raw.sources,
|
|
5329
|
+
options: raw.options,
|
|
5330
|
+
outputs: raw.outputs
|
|
5331
|
+
});
|
|
5260
5332
|
return {
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5333
|
+
current: fingerprintValid && !changedSources.length && !changedOutputs.length,
|
|
5334
|
+
fingerprintValid,
|
|
5335
|
+
changedSources,
|
|
5336
|
+
changedOutputs
|
|
5265
5337
|
};
|
|
5266
5338
|
}
|
|
5267
|
-
function
|
|
5268
|
-
|
|
5269
|
-
return terrainZero * 2 >= Object.keys(values).length ? 0 : 1;
|
|
5270
|
-
}
|
|
5271
|
-
function godotTileShape(tileType) {
|
|
5272
|
-
if (tileType === "isometric" || tileType === "oblique") return 1;
|
|
5273
|
-
if (tileType === "hex" || tileType === "hex_pointy") return 3;
|
|
5274
|
-
return 0;
|
|
5275
|
-
}
|
|
5276
|
-
function representativeTile(generic, terrainIndex) {
|
|
5277
|
-
const rules = generic.rules;
|
|
5278
|
-
const full = (1 << rules.arity) - 1;
|
|
5279
|
-
const wanted = terrainIndex === 0 ? full : 0;
|
|
5280
|
-
return generic.tiles.find((tile) => tile.bitmask === wanted)?.id ?? -1;
|
|
5281
|
-
}
|
|
5282
|
-
function terrainNamesFor(rules) {
|
|
5283
|
-
return [rules.terrains[0] ?? "feature", rules.terrains[1] ?? "background"];
|
|
5339
|
+
function isCode(error, code) {
|
|
5340
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
5284
5341
|
}
|
|
5285
|
-
function
|
|
5286
|
-
|
|
5287
|
-
(
|
|
5288
|
-
)
|
|
5289
|
-
|
|
5290
|
-
throw new Error(
|
|
5291
|
-
`Engine tilesets require uniform ${generic.tile.width}x${generic.tile.height} cells; mismatched: ${mismatched.map((tile) => tile.role).join(", ")}. Use --format generic.`
|
|
5292
|
-
);
|
|
5342
|
+
async function remove2(file, errors) {
|
|
5343
|
+
try {
|
|
5344
|
+
await rm6(file, { force: true });
|
|
5345
|
+
} catch (error) {
|
|
5346
|
+
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
5293
5347
|
}
|
|
5294
5348
|
}
|
|
5295
|
-
function
|
|
5296
|
-
const
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5349
|
+
async function rollback(prepared) {
|
|
5350
|
+
const errors = [];
|
|
5351
|
+
for (const artifact of [...prepared].reverse()) {
|
|
5352
|
+
if (artifact.promoted) await remove2(artifact.destination, errors);
|
|
5353
|
+
}
|
|
5354
|
+
for (const artifact of [...prepared].reverse()) {
|
|
5355
|
+
if (!artifact.backup || !artifact.backedUp) continue;
|
|
5356
|
+
try {
|
|
5357
|
+
await rename5(artifact.backup, artifact.destination);
|
|
5358
|
+
artifact.backedUp = false;
|
|
5359
|
+
} catch (error) {
|
|
5360
|
+
errors.push(
|
|
5361
|
+
`could not restore ${artifact.destination}; previous file remains at ${artifact.backup}: ${message(error)}`
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
for (const artifact of prepared) {
|
|
5366
|
+
if (artifact.stage) await remove2(artifact.stage, errors);
|
|
5367
|
+
}
|
|
5368
|
+
return errors;
|
|
5301
5369
|
}
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
return `<!doctype html>
|
|
5315
|
-
<html lang="en">
|
|
5316
|
-
<head>
|
|
5317
|
-
<meta charset="utf-8">
|
|
5318
|
-
<title>pixelkiln salvage${styleId ? ` \u2014 ${styleId}` : ""}</title>
|
|
5319
|
-
<style>
|
|
5320
|
-
:root { --bg:#0f1115; --panel:#171a21; --line:#262b36; --text:#e6e8ee; --dim:#8b93a7;
|
|
5321
|
-
--import:#6ee7a8; --keep:#7cc4ff; --discard:#f87171; }
|
|
5322
|
-
@media (prefers-color-scheme: light) {
|
|
5323
|
-
:root { --bg:#f6f7f9; --panel:#fff; --line:#dfe3ea; --text:#1a1d23; --dim:#666e80;
|
|
5324
|
-
--import:#0a8f52; --keep:#1a6fd4; --discard:#c0392b; }
|
|
5370
|
+
async function writeArtifactBundle(files, options = {}) {
|
|
5371
|
+
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
5372
|
+
const normalized = files.map((file) => {
|
|
5373
|
+
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
5374
|
+
return { destination: path17.resolve(file.path), data: Buffer.from(file.data) };
|
|
5375
|
+
});
|
|
5376
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
5377
|
+
for (const artifact of normalized) {
|
|
5378
|
+
if (destinations.has(artifact.destination)) {
|
|
5379
|
+
throw new Error(`Artifact bundle contains duplicate destination: ${artifact.destination}`);
|
|
5380
|
+
}
|
|
5381
|
+
destinations.add(artifact.destination);
|
|
5325
5382
|
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
font:14px/1.5 ui-sans-serif,-apple-system,"SF Pro Text",Inter,system-ui,sans-serif; }
|
|
5329
|
-
header { position:sticky; top:0; z-index:10; background:var(--panel);
|
|
5330
|
-
border-bottom:1px solid var(--line); padding:12px 20px; display:flex; gap:14px;
|
|
5331
|
-
align-items:center; flex-wrap:wrap; }
|
|
5332
|
-
h1 { font-size:15px; margin:0; font-weight:600; }
|
|
5333
|
-
h1 .ctx { font-weight:500; color:var(--dim); }
|
|
5334
|
-
h1 .dest { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px; }
|
|
5335
|
-
.tally { color:var(--dim); font-variant-numeric:tabular-nums; }
|
|
5336
|
-
.tally b { color:var(--text); font-weight:600; }
|
|
5337
|
-
button { font:inherit; font-weight:550; border-radius:7px; padding:7px 14px;
|
|
5338
|
-
border:1px solid var(--line); background:transparent; color:var(--text); cursor:pointer; }
|
|
5339
|
-
button.primary { background:var(--import); border-color:var(--import); color:#08130d; }
|
|
5340
|
-
button:disabled { opacity:.45; cursor:not-allowed; }
|
|
5341
|
-
main { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:12px; padding:18px 20px 80px; }
|
|
5342
|
-
.card { border:2px solid var(--line); border-radius:10px; background:var(--panel);
|
|
5343
|
-
padding:10px; display:flex; flex-direction:column; gap:8px; }
|
|
5344
|
-
.card.import { border-color:var(--import); }
|
|
5345
|
-
.card.keep { border-color:var(--keep); }
|
|
5346
|
-
.card.discard { border-color:var(--discard); opacity:.5; }
|
|
5347
|
-
.thumb { height:120px; display:flex; align-items:center; justify-content:center;
|
|
5348
|
-
border-radius:6px;
|
|
5349
|
-
background-image:
|
|
5350
|
-
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
5351
|
-
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
|
|
5352
|
-
background-size:12px 12px; background-position:0 0,6px 6px; }
|
|
5353
|
-
.thumb img { image-rendering:pixelated; max-height:112px; max-width:100%; }
|
|
5354
|
-
.meta { font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums; }
|
|
5355
|
-
.prompt { font-size:11.5px; color:var(--dim); max-height:3.2em; overflow:hidden; }
|
|
5356
|
-
.acts { display:flex; gap:5px; }
|
|
5357
|
-
.acts button { flex:1; padding:5px 0; font-size:11.5px; }
|
|
5358
|
-
.acts button.on-import { background:var(--import); border-color:var(--import); color:#08130d; }
|
|
5359
|
-
.acts button.on-keep { background:var(--keep); border-color:var(--keep); color:#04121f; }
|
|
5360
|
-
.acts button.on-discard { background:var(--discard); border-color:var(--discard); color:#fff; }
|
|
5361
|
-
footer { position:fixed; bottom:0; left:0; right:0; background:var(--panel);
|
|
5362
|
-
border-top:1px solid var(--line); padding:10px 20px; font-size:12px; color:var(--dim); }
|
|
5363
|
-
kbd { border:1px solid var(--line); border-bottom-width:2px; border-radius:4px;
|
|
5364
|
-
padding:1px 5px; font-size:11px; font-family:ui-monospace,Menlo,monospace; }
|
|
5365
|
-
</style>
|
|
5366
|
-
</head>
|
|
5367
|
-
<body>
|
|
5368
|
-
<header>
|
|
5369
|
-
<h1>pixelkiln salvage${styleId ? ` <span class="ctx">\u2014 ${styleId}<span class="dest"> \u2192 ${importDir}/_salvaged/</span></span>` : ""}</h1>
|
|
5370
|
-
<span class="tally"><b id="ti">0</b> import \xB7 <b id="tk">0</b> keep \xB7 <b id="td">0</b> discard \xB7
|
|
5371
|
-
<b id="tu">0</b> undecided</span>
|
|
5372
|
-
<span style="flex:1"></span>
|
|
5373
|
-
<button id="allDiscard">Discard all undecided</button>
|
|
5374
|
-
<button id="allKeep">Keep all undecided</button>
|
|
5375
|
-
<button id="submit" class="primary" disabled>Apply</button>
|
|
5376
|
-
</header>
|
|
5377
|
-
<main id="grid"></main>
|
|
5378
|
-
<footer>
|
|
5379
|
-
<b>import</b> downloads it and adds it to your manifest \xB7 <b>keep</b> tags it and leaves it \xB7
|
|
5380
|
-
<b>discard</b> only tags it \u2014 nothing is deleted here.
|
|
5381
|
-
Hover a card and press <kbd>i</kbd> / <kbd>k</kbd> / <kbd>d</kbd>, or <kbd>u</kbd> to undo.
|
|
5382
|
-
</footer>
|
|
5383
|
-
<script>
|
|
5384
|
-
const ITEMS = ${data};
|
|
5385
|
-
const picks = new Map();
|
|
5386
|
-
const grid = document.getElementById('grid');
|
|
5387
|
-
let hovered = null;
|
|
5388
|
-
|
|
5389
|
-
ITEMS.forEach((o, i) => {
|
|
5390
|
-
const el = document.createElement('div');
|
|
5391
|
-
el.className = 'card';
|
|
5392
|
-
el.dataset.i = String(i);
|
|
5393
|
-
el.innerHTML =
|
|
5394
|
-
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
5395
|
-
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
5396
|
-
'<div class="prompt">' + o.prompt + '</div>' +
|
|
5397
|
-
'<div class="acts">' +
|
|
5398
|
-
'<button data-a="import">import</button>' +
|
|
5399
|
-
'<button data-a="keep">keep</button>' +
|
|
5400
|
-
'<button data-a="discard">discard</button>' +
|
|
5401
|
-
'</div>';
|
|
5402
|
-
el.querySelectorAll('.acts button').forEach(b => {
|
|
5403
|
-
b.onclick = () => choose(i, b.dataset.a);
|
|
5404
|
-
});
|
|
5405
|
-
el.onmouseenter = () => { hovered = i; };
|
|
5406
|
-
grid.appendChild(el);
|
|
5407
|
-
});
|
|
5408
|
-
|
|
5409
|
-
function choose(i, action) {
|
|
5410
|
-
if (picks.get(i) === action) picks.delete(i); else picks.set(i, action);
|
|
5411
|
-
const el = grid.children[i];
|
|
5412
|
-
el.className = 'card' + (picks.has(i) ? ' ' + picks.get(i) : '');
|
|
5413
|
-
el.querySelectorAll('.acts button').forEach(b => {
|
|
5414
|
-
b.className = picks.get(i) === b.dataset.a ? 'on-' + b.dataset.a : '';
|
|
5415
|
-
});
|
|
5416
|
-
refresh();
|
|
5417
|
-
}
|
|
5418
|
-
|
|
5419
|
-
function refresh() {
|
|
5420
|
-
const c = { import: 0, keep: 0, discard: 0 };
|
|
5421
|
-
for (const a of picks.values()) c[a]++;
|
|
5422
|
-
ti.textContent = c.import; tk.textContent = c.keep; td.textContent = c.discard;
|
|
5423
|
-
tu.textContent = ITEMS.length - picks.size;
|
|
5424
|
-
document.getElementById('submit').disabled = picks.size === 0;
|
|
5425
|
-
}
|
|
5426
|
-
|
|
5427
|
-
document.addEventListener('keydown', e => {
|
|
5428
|
-
if (hovered === null) return;
|
|
5429
|
-
const map = { i: 'import', k: 'keep', d: 'discard' };
|
|
5430
|
-
if (map[e.key]) { choose(hovered, map[e.key]); e.preventDefault(); }
|
|
5431
|
-
else if (e.key === 'u') { picks.delete(hovered); choose(hovered, '__none__'); }
|
|
5432
|
-
});
|
|
5433
|
-
|
|
5434
|
-
document.getElementById('allKeep').onclick = () => {
|
|
5435
|
-
ITEMS.forEach((_, i) => { if (!picks.has(i)) choose(i, 'keep'); });
|
|
5436
|
-
};
|
|
5437
|
-
document.getElementById('allDiscard').onclick = () => {
|
|
5438
|
-
ITEMS.forEach((_, i) => { if (!picks.has(i)) choose(i, 'discard'); });
|
|
5439
|
-
};
|
|
5440
|
-
|
|
5441
|
-
document.getElementById('submit').onclick = async () => {
|
|
5442
|
-
const btn = document.getElementById('submit');
|
|
5443
|
-
btn.disabled = true; btn.textContent = 'applying\u2026';
|
|
5444
|
-
const decisions = [...picks.entries()].map(([i, action]) => ({ id: ITEMS[i].id, action }));
|
|
5445
|
-
try {
|
|
5446
|
-
const res = await fetch('/apply', {
|
|
5447
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
5448
|
-
body: JSON.stringify({ decisions }),
|
|
5449
|
-
});
|
|
5450
|
-
if (!res.ok) throw new Error(await res.text());
|
|
5451
|
-
btn.textContent = 'done \u2014 close this tab';
|
|
5452
|
-
document.body.style.opacity = '.6';
|
|
5453
|
-
} catch (err) {
|
|
5454
|
-
btn.textContent = 'failed: ' + err.message; btn.disabled = false;
|
|
5383
|
+
if (options.recoveryFile) {
|
|
5384
|
+
await recoverArtifactTransaction(options.recoveryFile, destinations);
|
|
5455
5385
|
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
5466
|
-
const log2 = opts.onProgress ?? (() => {
|
|
5467
|
-
});
|
|
5468
|
-
const html = renderSalvageSheet(orphans, {
|
|
5469
|
-
styleId: ctx.styleId,
|
|
5470
|
-
importDir: path17.relative(process.cwd(), ctx.importDir) || "."
|
|
5471
|
-
});
|
|
5472
|
-
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
5473
|
-
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
5474
|
-
return serveReviewPage({
|
|
5475
|
-
html,
|
|
5476
|
-
port: opts.port,
|
|
5477
|
-
open: opts.open,
|
|
5478
|
-
onProgress: log2,
|
|
5479
|
-
onReady: (url) => {
|
|
5480
|
-
log2(`
|
|
5481
|
-
${orphans.length} unclaimed object(s) to triage: ${url}`);
|
|
5482
|
-
log2(` (nothing is deleted here \u2014 discard only tags)
|
|
5483
|
-
`);
|
|
5484
|
-
},
|
|
5485
|
-
handleApply: async (body) => {
|
|
5486
|
-
const { decisions } = body;
|
|
5487
|
-
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
5488
|
-
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
5489
|
-
const importedAssetIds = [];
|
|
5490
|
-
for (const decision of decisions) {
|
|
5491
|
-
const orphan = byId.get(decision.id);
|
|
5492
|
-
if (!orphan) continue;
|
|
5493
|
-
if (decision.action === "import") {
|
|
5494
|
-
try {
|
|
5495
|
-
const buf = await provider.download(orphan.previewUrl);
|
|
5496
|
-
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
5497
|
-
decodePng(buf);
|
|
5498
|
-
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
5499
|
-
const rel = path17.join("_salvaged", `${assetId}.png`);
|
|
5500
|
-
const outFile = path17.resolve(ctx.importDir, rel);
|
|
5501
|
-
await mkdir5(path17.dirname(outFile), { recursive: true });
|
|
5502
|
-
await writeFile7(outFile, buf);
|
|
5503
|
-
ctx.manifest.assets[assetId] = {
|
|
5504
|
-
prompt: orphan.prompt,
|
|
5505
|
-
promptByStyle: {},
|
|
5506
|
-
category: "_salvaged",
|
|
5507
|
-
file: rel,
|
|
5508
|
-
tags: ["salvaged"],
|
|
5509
|
-
styles: [ctx.styleId],
|
|
5510
|
-
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
5511
|
-
};
|
|
5512
|
-
upsert(ctx.lock, lockKey(ctx.styleId, assetId), {
|
|
5513
|
-
styleId: ctx.styleId,
|
|
5514
|
-
assetId,
|
|
5515
|
-
specHash: SALVAGED_SPEC_HASH,
|
|
5516
|
-
generator: orphan.width === orphan.height ? "1dir" : "map",
|
|
5517
|
-
prompt: orphan.prompt,
|
|
5518
|
-
width: orphan.width,
|
|
5519
|
-
height: orphan.height,
|
|
5520
|
-
jobId: orphan.id,
|
|
5521
|
-
reviewObjectId: null,
|
|
5522
|
-
objectId: orphan.id,
|
|
5523
|
-
candidateIndex: null,
|
|
5524
|
-
status: "downloaded",
|
|
5525
|
-
error: null,
|
|
5526
|
-
sourceUrl: orphan.previewUrl,
|
|
5527
|
-
outputs: [{
|
|
5528
|
-
path: portableOutputPath(outFile, path17.dirname(ctx.manifestPath)),
|
|
5529
|
-
sha256: sha256(buf)
|
|
5530
|
-
}],
|
|
5531
|
-
submittedAt: orphan.createdAt,
|
|
5532
|
-
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5533
|
-
cost: 0,
|
|
5534
|
-
// already paid for, in an earlier period
|
|
5535
|
-
costUnit: "generations",
|
|
5536
|
-
provider: provider.id
|
|
5537
|
-
});
|
|
5538
|
-
importedAssetIds.push(assetId);
|
|
5539
|
-
result.imported++;
|
|
5540
|
-
log2(` imported ${assetId} \u2190 ${orphan.id}`);
|
|
5541
|
-
} catch (err) {
|
|
5542
|
-
result.failed++;
|
|
5543
|
-
log2(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5544
|
-
}
|
|
5545
|
-
} else if (decision.action === "keep") {
|
|
5546
|
-
result.kept++;
|
|
5547
|
-
} else {
|
|
5548
|
-
result.discarded++;
|
|
5549
|
-
}
|
|
5386
|
+
const changed = [];
|
|
5387
|
+
const unchanged = [];
|
|
5388
|
+
for (const artifact of normalized) {
|
|
5389
|
+
try {
|
|
5390
|
+
const current = await readFile13(artifact.destination);
|
|
5391
|
+
if (current.equals(artifact.data)) {
|
|
5392
|
+
unchanged.push(artifact.destination);
|
|
5393
|
+
} else {
|
|
5394
|
+
changed.push({ ...artifact, existed: true, backedUp: false, promoted: false });
|
|
5550
5395
|
}
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
5555
|
-
await saveLock(ctx.lockPath, ctx.lock);
|
|
5556
|
-
return result;
|
|
5396
|
+
} catch (error) {
|
|
5397
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5398
|
+
changed.push({ ...artifact, existed: false, backedUp: false, promoted: false });
|
|
5557
5399
|
}
|
|
5558
|
-
}
|
|
5400
|
+
}
|
|
5401
|
+
if (!changed.length) return { changed: [], unchanged };
|
|
5402
|
+
const token = randomUUID3();
|
|
5403
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5404
|
+
const basename = path17.basename(artifact.destination);
|
|
5405
|
+
artifact.stage = path17.join(
|
|
5406
|
+
path17.dirname(artifact.destination),
|
|
5407
|
+
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
5408
|
+
);
|
|
5409
|
+
if (artifact.existed) {
|
|
5410
|
+
artifact.backup = path17.join(
|
|
5411
|
+
path17.dirname(artifact.destination),
|
|
5412
|
+
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
5413
|
+
);
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
const journal = options.recoveryFile ? path17.resolve(options.recoveryFile) : null;
|
|
5417
|
+
if (journal) {
|
|
5418
|
+
await mkdir5(path17.dirname(journal), { recursive: true });
|
|
5419
|
+
const transaction = {
|
|
5420
|
+
format: "pixelkiln-artifact-transaction",
|
|
5421
|
+
version: 1,
|
|
5422
|
+
pid: process.pid,
|
|
5423
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5424
|
+
entries: changed.map((artifact) => ({
|
|
5425
|
+
destination: artifact.destination,
|
|
5426
|
+
stage: artifact.stage,
|
|
5427
|
+
...artifact.backup ? { backup: artifact.backup } : {},
|
|
5428
|
+
sha256: digest(artifact.data)
|
|
5429
|
+
}))
|
|
5430
|
+
};
|
|
5431
|
+
try {
|
|
5432
|
+
await writeFile7(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
5433
|
+
activeTransactions.add(journal);
|
|
5434
|
+
} catch (error) {
|
|
5435
|
+
throw new Error(
|
|
5436
|
+
`Could not start durable artifact transaction at ${journal}: ${message(error)}`,
|
|
5437
|
+
{ cause: error }
|
|
5438
|
+
);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
try {
|
|
5442
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5443
|
+
await mkdir5(path17.dirname(artifact.destination), { recursive: true });
|
|
5444
|
+
await options.beforeStage?.(artifact.destination, index);
|
|
5445
|
+
await writeFile7(artifact.stage, artifact.data, { flag: "wx" });
|
|
5446
|
+
}
|
|
5447
|
+
} catch (error) {
|
|
5448
|
+
const cleanupErrors2 = await rollback(changed);
|
|
5449
|
+
if (journal && !cleanupErrors2.length) await removeTransactionFiles(journal);
|
|
5450
|
+
if (journal) activeTransactions.delete(journal);
|
|
5451
|
+
const suffix = cleanupErrors2.length ? ` Cleanup also failed: ${cleanupErrors2.join("; ")}` : "";
|
|
5452
|
+
throw new Error(`Could not stage artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5453
|
+
}
|
|
5454
|
+
let durableCommit = false;
|
|
5455
|
+
try {
|
|
5456
|
+
for (const artifact of changed) {
|
|
5457
|
+
if (!artifact.existed) continue;
|
|
5458
|
+
await rename5(artifact.destination, artifact.backup);
|
|
5459
|
+
artifact.backedUp = true;
|
|
5460
|
+
}
|
|
5461
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5462
|
+
await options.beforePromote?.(artifact.destination, index);
|
|
5463
|
+
await rename5(artifact.stage, artifact.destination);
|
|
5464
|
+
artifact.stage = void 0;
|
|
5465
|
+
artifact.promoted = true;
|
|
5466
|
+
}
|
|
5467
|
+
if (journal) {
|
|
5468
|
+
await writeFile7(transactionMarker(journal), `${token}
|
|
5469
|
+
`, { flag: "wx" });
|
|
5470
|
+
durableCommit = true;
|
|
5471
|
+
await options.afterCommit?.();
|
|
5472
|
+
}
|
|
5473
|
+
} catch (error) {
|
|
5474
|
+
if (durableCommit) {
|
|
5475
|
+
if (journal) activeTransactions.delete(journal);
|
|
5476
|
+
throw new Error(
|
|
5477
|
+
`Artifact bundle was committed, but final cleanup was interrupted: ${message(error)}. The next write will finish cleanup from ${journal}.`,
|
|
5478
|
+
{ cause: error }
|
|
5479
|
+
);
|
|
5480
|
+
}
|
|
5481
|
+
const rollbackErrors = await rollback(changed);
|
|
5482
|
+
if (journal && !rollbackErrors.length) await removeTransactionFiles(journal);
|
|
5483
|
+
if (journal) activeTransactions.delete(journal);
|
|
5484
|
+
const suffix = rollbackErrors.length ? ` Rollback needs attention: ${rollbackErrors.join("; ")}` : " Previous bundle restored.";
|
|
5485
|
+
throw new Error(`Could not promote artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5486
|
+
}
|
|
5487
|
+
const cleanupErrors = [];
|
|
5488
|
+
for (const artifact of changed) {
|
|
5489
|
+
if (artifact.backup) await remove2(artifact.backup, cleanupErrors);
|
|
5490
|
+
}
|
|
5491
|
+
if (cleanupErrors.length) {
|
|
5492
|
+
if (journal) activeTransactions.delete(journal);
|
|
5493
|
+
throw new Error(
|
|
5494
|
+
`Artifact bundle was written, but old backup cleanup failed: ${cleanupErrors.join("; ")}`
|
|
5495
|
+
);
|
|
5496
|
+
}
|
|
5497
|
+
if (journal) {
|
|
5498
|
+
try {
|
|
5499
|
+
await removeTransactionFiles(journal);
|
|
5500
|
+
} finally {
|
|
5501
|
+
activeTransactions.delete(journal);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
return {
|
|
5505
|
+
changed: changed.map((artifact) => artifact.destination),
|
|
5506
|
+
unchanged
|
|
5507
|
+
};
|
|
5559
5508
|
}
|
|
5560
5509
|
|
|
5561
|
-
// src/
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
function message(error) {
|
|
5510
|
+
// src/pipeline/refine.ts
|
|
5511
|
+
var execFileAsync = promisify(execFile);
|
|
5512
|
+
var PIXEL_ART_FIXER_URL = "https://github.com/Retro-Diffusion/pixel-art-fixer";
|
|
5513
|
+
var PIXEL_ART_FIXER_REVISION = "ef376e57e1c272633ca2dbf5f29ec3fcf6596465";
|
|
5514
|
+
function message2(error) {
|
|
5567
5515
|
return error instanceof Error ? error.message : String(error);
|
|
5568
5516
|
}
|
|
5569
|
-
function
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5517
|
+
function qualityRecordPath(output) {
|
|
5518
|
+
if (!/\.png$/i.test(output)) throw new Error("Refined output must be a .png file.");
|
|
5519
|
+
return output.replace(/\.png$/i, ".pixelkiln.json");
|
|
5520
|
+
}
|
|
5521
|
+
function confidenceFor(consensus) {
|
|
5522
|
+
if (consensus.startsWith("fast:")) return "high";
|
|
5523
|
+
if (consensus === "arbitrated" || consensus === "forced" || consensus.startsWith("fastmode:") && consensus.includes("+")) return "medium";
|
|
5524
|
+
return "low";
|
|
5525
|
+
}
|
|
5526
|
+
var confidenceRank = { low: 0, medium: 1, high: 2 };
|
|
5527
|
+
function requireConfidence(value) {
|
|
5528
|
+
if (value === "low" || value === "medium" || value === "high") return value;
|
|
5529
|
+
throw new Error(`Unknown grid confidence "${value}"; expected low, medium, or high.`);
|
|
5530
|
+
}
|
|
5531
|
+
function firstJsonObject(text) {
|
|
5532
|
+
const start = text.indexOf("{");
|
|
5533
|
+
if (start < 0) throw new Error("Pixel Art Fixer did not print detection JSON.");
|
|
5534
|
+
let depth = 0;
|
|
5535
|
+
let string = false;
|
|
5536
|
+
let escaped = false;
|
|
5537
|
+
for (let i = start; i < text.length; i++) {
|
|
5538
|
+
const char = text[i];
|
|
5539
|
+
if (string) {
|
|
5540
|
+
if (escaped) escaped = false;
|
|
5541
|
+
else if (char === "\\") escaped = true;
|
|
5542
|
+
else if (char === '"') string = false;
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
if (char === '"') string = true;
|
|
5546
|
+
else if (char === "{") depth++;
|
|
5547
|
+
else if (char === "}" && --depth === 0) {
|
|
5548
|
+
try {
|
|
5549
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
5550
|
+
} catch (error) {
|
|
5551
|
+
throw new Error(`Pixel Art Fixer printed invalid detection JSON: ${message2(error)}`);
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5580
5554
|
}
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5555
|
+
throw new Error("Pixel Art Fixer printed incomplete detection JSON.");
|
|
5556
|
+
}
|
|
5557
|
+
var PYTHON_FIXER_BRIDGE = [
|
|
5558
|
+
"import json, sys",
|
|
5559
|
+
"from pixelfixer.api import process",
|
|
5560
|
+
"result = process(open(sys.argv[1], 'rb').read(), mode='full')",
|
|
5561
|
+
"open(sys.argv[2], 'wb').write(result.pop('png'))",
|
|
5562
|
+
"print(json.dumps(result))"
|
|
5563
|
+
].join("; ");
|
|
5564
|
+
function parseDetection(raw) {
|
|
5565
|
+
if (!raw || typeof raw !== "object") throw new Error("Pixel Art Fixer returned no detection result.");
|
|
5566
|
+
const value = raw;
|
|
5567
|
+
const stepX = Number(value.step_x);
|
|
5568
|
+
const stepY = Number(value.step_y);
|
|
5569
|
+
const columns = Number(value.cols);
|
|
5570
|
+
const rows = Number(value.rows);
|
|
5571
|
+
const consensus = value.consensus;
|
|
5572
|
+
if (!Number.isFinite(stepX) || stepX <= 0 || !Number.isFinite(stepY) || stepY <= 0 || !Number.isInteger(columns) || columns < 1 || !Number.isInteger(rows) || rows < 1 || typeof consensus !== "string" || !consensus) {
|
|
5573
|
+
throw new Error("Pixel Art Fixer returned an invalid grid detection result.");
|
|
5574
|
+
}
|
|
5575
|
+
return {
|
|
5576
|
+
stepX,
|
|
5577
|
+
stepY,
|
|
5578
|
+
columns,
|
|
5579
|
+
rows,
|
|
5580
|
+
consensus,
|
|
5581
|
+
confidence: confidenceFor(consensus)
|
|
5582
|
+
};
|
|
5583
|
+
}
|
|
5584
|
+
function normalizePalette(values) {
|
|
5585
|
+
const colors = /* @__PURE__ */ new Map();
|
|
5586
|
+
for (const value of values) {
|
|
5587
|
+
const rgb = parseHex(value);
|
|
5588
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5589
|
+
colors.set(hex2, { hex: hex2, ...rgb });
|
|
5590
|
+
}
|
|
5591
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
5592
|
+
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5593
|
+
}
|
|
5594
|
+
return [...colors.values()];
|
|
5595
|
+
}
|
|
5596
|
+
function quantizeToPalette(png, palette) {
|
|
5597
|
+
const colors = normalizePalette(palette);
|
|
5598
|
+
const pixels = Buffer.from(png.pixels);
|
|
5599
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
5600
|
+
const alpha = pixels[i + 3];
|
|
5601
|
+
if (alpha === 0) {
|
|
5602
|
+
pixels[i] = 0;
|
|
5603
|
+
pixels[i + 1] = 0;
|
|
5604
|
+
pixels[i + 2] = 0;
|
|
5605
|
+
continue;
|
|
5587
5606
|
}
|
|
5588
|
-
|
|
5607
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5608
|
+
let nearest = colors[0];
|
|
5609
|
+
let distance = colorDistance(pixel, nearest);
|
|
5610
|
+
for (let j = 1; j < colors.length; j++) {
|
|
5611
|
+
const candidate = colors[j];
|
|
5612
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
5613
|
+
if (candidateDistance < distance) {
|
|
5614
|
+
nearest = candidate;
|
|
5615
|
+
distance = candidateDistance;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
pixels[i] = nearest.r;
|
|
5619
|
+
pixels[i + 1] = nearest.g;
|
|
5620
|
+
pixels[i + 2] = nearest.b;
|
|
5589
5621
|
}
|
|
5590
|
-
|
|
5622
|
+
return { width: png.width, height: png.height, pixels };
|
|
5591
5623
|
}
|
|
5592
|
-
function
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5624
|
+
function colorCount(png) {
|
|
5625
|
+
const colors = /* @__PURE__ */ new Set();
|
|
5626
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5627
|
+
if (png.pixels[i + 3] < 128) continue;
|
|
5628
|
+
colors.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
5629
|
+
}
|
|
5630
|
+
return colors.size;
|
|
5599
5631
|
}
|
|
5600
|
-
function
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
}
|
|
5605
|
-
|
|
5632
|
+
function unexpectedPaletteColors(png, palette) {
|
|
5633
|
+
const allowed = new Set(palette.map((hex2) => {
|
|
5634
|
+
const color = parseHex(hex2);
|
|
5635
|
+
return color.r << 16 | color.g << 8 | color.b;
|
|
5636
|
+
}));
|
|
5637
|
+
const unexpected = /* @__PURE__ */ new Set();
|
|
5638
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5639
|
+
if (png.pixels[i + 3] === 0) continue;
|
|
5640
|
+
const color = png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2];
|
|
5641
|
+
if (!allowed.has(color)) unexpected.add(color);
|
|
5642
|
+
}
|
|
5643
|
+
return unexpected.size;
|
|
5644
|
+
}
|
|
5645
|
+
function auditRefinement(png, detection, palette, minTransparency, minGridConfidence) {
|
|
5646
|
+
const colors = colorCount(png);
|
|
5647
|
+
const unexpectedColors = unexpectedPaletteColors(png, palette);
|
|
5648
|
+
const transparency = transparencyRatio(png);
|
|
5649
|
+
const reasons = [];
|
|
5650
|
+
if (png.width !== detection.columns || png.height !== detection.rows) {
|
|
5651
|
+
reasons.push(
|
|
5652
|
+
`native output is ${png.width}x${png.height}, expected detected grid ${detection.columns}x${detection.rows}`
|
|
5653
|
+
);
|
|
5606
5654
|
}
|
|
5607
|
-
if (
|
|
5608
|
-
|
|
5655
|
+
if (colors > palette.length) reasons.push(`opaque color count ${colors} exceeds ${palette.length}`);
|
|
5656
|
+
if (unexpectedColors) reasons.push(`${unexpectedColors} visible color(s) are outside the final palette`);
|
|
5657
|
+
if (minTransparency !== void 0 && transparency < minTransparency) {
|
|
5658
|
+
reasons.push(
|
|
5659
|
+
`transparency ${(transparency * 100).toFixed(1)}% is below ${(minTransparency * 100).toFixed(1)}%`
|
|
5660
|
+
);
|
|
5609
5661
|
}
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5662
|
+
if (confidenceRank[detection.confidence] < confidenceRank[minGridConfidence]) {
|
|
5663
|
+
reasons.push(
|
|
5664
|
+
`grid confidence ${detection.confidence} is below required ${minGridConfidence} (${detection.consensus})`
|
|
5665
|
+
);
|
|
5614
5666
|
}
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5667
|
+
return {
|
|
5668
|
+
width: png.width,
|
|
5669
|
+
height: png.height,
|
|
5670
|
+
colorCount: colors,
|
|
5671
|
+
paletteConformant: unexpectedColors === 0,
|
|
5672
|
+
transparency,
|
|
5673
|
+
thresholds: {
|
|
5674
|
+
maxColors: palette.length,
|
|
5675
|
+
minTransparency: minTransparency ?? null,
|
|
5676
|
+
minGridConfidence
|
|
5677
|
+
},
|
|
5678
|
+
safe: reasons.length === 0,
|
|
5679
|
+
reasons
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
function isGridConfidence(value) {
|
|
5683
|
+
return value === "low" || value === "medium" || value === "high";
|
|
5684
|
+
}
|
|
5685
|
+
function parseRecordOptions(manifest, record) {
|
|
5686
|
+
if (manifest.kind !== "refine") throw new Error(`${record} is not a PixelKiln refinement record.`);
|
|
5687
|
+
const raw = manifest.options;
|
|
5688
|
+
if (!raw || typeof raw !== "object") throw new Error(`${record} has invalid quality metadata.`);
|
|
5689
|
+
const options = raw;
|
|
5690
|
+
const grid = options.nativeGrid;
|
|
5691
|
+
const palette = options.palette;
|
|
5692
|
+
const audit = options.audit;
|
|
5693
|
+
const review = options.review;
|
|
5694
|
+
if (options.schema !== "pixelkiln-quality" || options.version !== 1 || !grid || grid.tool !== "pixel-art-fixer" || grid.url !== PIXEL_ART_FIXER_URL || typeof grid.revision !== "string" || !grid.revision || grid.protocol !== "python-api-v1" && grid.protocol !== "python-cli-v1" || !isGridConfidence(grid.confidence) || typeof grid.consensus !== "string" || !Number.isFinite(grid.stepX) || !Number.isFinite(grid.stepY) || !Number.isInteger(grid.sourceWidth) || !Number.isInteger(grid.sourceHeight) || !Number.isInteger(grid.nativeWidth) || !Number.isInteger(grid.nativeHeight) || !palette || !Array.isArray(palette.colors) || palette.colors.length < 2 || !palette.colors.every((color) => typeof color === "string") || palette.dither !== "none" || palette.distance !== "redmean" || !audit || typeof audit.safe !== "boolean" || typeof audit.paletteConformant !== "boolean" || !Array.isArray(audit.reasons) || !review || review.status !== "pending" && review.status !== "approved") {
|
|
5695
|
+
throw new Error(`${record} has invalid quality metadata.`);
|
|
5619
5696
|
}
|
|
5620
|
-
return
|
|
5697
|
+
return options;
|
|
5621
5698
|
}
|
|
5622
|
-
async function
|
|
5699
|
+
async function refineAsset(options) {
|
|
5700
|
+
const source = path18.resolve(options.source);
|
|
5701
|
+
const output = path18.resolve(options.output);
|
|
5702
|
+
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
5703
|
+
const record = path18.resolve(qualityRecordPath(output));
|
|
5704
|
+
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
5705
|
+
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
5706
|
+
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
5707
|
+
let sourcePng;
|
|
5623
5708
|
try {
|
|
5624
|
-
|
|
5709
|
+
sourcePng = decodePng(await readFile14(source));
|
|
5625
5710
|
} catch (error) {
|
|
5626
|
-
|
|
5627
|
-
throw error;
|
|
5711
|
+
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5628
5712
|
}
|
|
5713
|
+
const temp = await mkdtemp(path18.join(tmpdir(), "pixelkiln-refine-"));
|
|
5714
|
+
const recovered = path18.join(temp, "native.png");
|
|
5715
|
+
const compatibilityCommand = options.fixerCommand;
|
|
5716
|
+
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
5717
|
+
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
5718
|
+
try {
|
|
5719
|
+
let stdout;
|
|
5720
|
+
try {
|
|
5721
|
+
const result = await execFileAsync(fixerCommand, args, {
|
|
5722
|
+
encoding: "utf8",
|
|
5723
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
5724
|
+
timeout: 10 * 60 * 1e3
|
|
5725
|
+
});
|
|
5726
|
+
stdout = result.stdout;
|
|
5727
|
+
} catch (error) {
|
|
5728
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
5729
|
+
const install = `Install the pinned Python package from ${PIXEL_ART_FIXER_URL} at ${options.fixerRevision ?? PIXEL_ART_FIXER_REVISION}, or pass --fixer-python.`;
|
|
5730
|
+
throw new Error(
|
|
5731
|
+
code === "ENOENT" ? `Pixel Art Fixer command not found: ${fixerCommand}. ${install}` : `Pixel Art Fixer failed: ${message2(error)}. ${install}`,
|
|
5732
|
+
{ cause: error }
|
|
5733
|
+
);
|
|
5734
|
+
}
|
|
5735
|
+
const detection = parseDetection(firstJsonObject(stdout));
|
|
5736
|
+
const native = decodePng(await readFile14(recovered));
|
|
5737
|
+
const quantized = quantizeToPalette(native, palette);
|
|
5738
|
+
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5739
|
+
const audit = auditRefinement(
|
|
5740
|
+
quantized,
|
|
5741
|
+
detection,
|
|
5742
|
+
palette,
|
|
5743
|
+
options.minTransparency,
|
|
5744
|
+
minGridConfidence
|
|
5745
|
+
);
|
|
5746
|
+
if (!audit.safe) {
|
|
5747
|
+
throw new Error(`Refinement failed its quality audit: ${audit.reasons.join("; ")}`);
|
|
5748
|
+
}
|
|
5749
|
+
const provenanceSource = {
|
|
5750
|
+
id: "source",
|
|
5751
|
+
path: source,
|
|
5752
|
+
sha256: await sha256File(source),
|
|
5753
|
+
included: true
|
|
5754
|
+
};
|
|
5755
|
+
const recordOptions = {
|
|
5756
|
+
schema: "pixelkiln-quality",
|
|
5757
|
+
version: 1,
|
|
5758
|
+
nativeGrid: {
|
|
5759
|
+
tool: "pixel-art-fixer",
|
|
5760
|
+
url: PIXEL_ART_FIXER_URL,
|
|
5761
|
+
revision: options.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
5762
|
+
protocol: compatibilityCommand ? "python-cli-v1" : "python-api-v1",
|
|
5763
|
+
confidence: detection.confidence,
|
|
5764
|
+
consensus: detection.consensus,
|
|
5765
|
+
stepX: detection.stepX,
|
|
5766
|
+
stepY: detection.stepY,
|
|
5767
|
+
sourceWidth: sourcePng.width,
|
|
5768
|
+
sourceHeight: sourcePng.height,
|
|
5769
|
+
nativeWidth: quantized.width,
|
|
5770
|
+
nativeHeight: quantized.height
|
|
5771
|
+
},
|
|
5772
|
+
palette: { colors: palette, dither: "none", distance: "redmean" },
|
|
5773
|
+
audit,
|
|
5774
|
+
review: { status: "pending" }
|
|
5775
|
+
};
|
|
5776
|
+
await writeManagedArtifactBundle(
|
|
5777
|
+
record,
|
|
5778
|
+
[{ path: output, data: png }],
|
|
5779
|
+
{ kind: "refine", sources: [provenanceSource], options: recordOptions },
|
|
5780
|
+
{ force: options.force }
|
|
5781
|
+
);
|
|
5782
|
+
return { source, output, record, detection, palette, audit };
|
|
5783
|
+
} finally {
|
|
5784
|
+
await rm7(temp, { recursive: true, force: true });
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
async function checkQualityRecord(recordPath) {
|
|
5788
|
+
const record = path18.resolve(recordPath);
|
|
5789
|
+
const manifest = await readArtifactBundleManifest(record);
|
|
5790
|
+
const options = parseRecordOptions(manifest, record);
|
|
5791
|
+
const verification = await verifyArtifactBundle(record);
|
|
5792
|
+
const output = manifest.outputs.length === 1 ? path18.resolve(path18.dirname(record), manifest.outputs[0].path) : "";
|
|
5793
|
+
const reasons = [];
|
|
5794
|
+
if (!verification.fingerprintValid) reasons.push("quality record fingerprint changed");
|
|
5795
|
+
for (const source of verification.changedSources) reasons.push(`source changed: ${source}`);
|
|
5796
|
+
for (const changed of verification.changedOutputs) reasons.push(`output changed: ${changed}`);
|
|
5797
|
+
if (manifest.sources.length !== 1) reasons.push("quality record must contain exactly one source");
|
|
5798
|
+
if (manifest.outputs.length !== 1) reasons.push("quality record must contain exactly one output");
|
|
5799
|
+
if (!options.audit.safe) reasons.push(...options.audit.reasons.map((reason) => `audit: ${reason}`));
|
|
5800
|
+
if (options.review.status !== "approved") reasons.push("human 1\xD7 review is pending");
|
|
5801
|
+
return {
|
|
5802
|
+
safe: reasons.length === 0,
|
|
5803
|
+
current: verification.current,
|
|
5804
|
+
approved: options.review.status === "approved",
|
|
5805
|
+
auditSafe: options.audit.safe,
|
|
5806
|
+
record,
|
|
5807
|
+
output,
|
|
5808
|
+
options,
|
|
5809
|
+
reasons
|
|
5810
|
+
};
|
|
5629
5811
|
}
|
|
5630
|
-
function
|
|
5631
|
-
|
|
5812
|
+
async function approveQualityRecord(recordPath, approval) {
|
|
5813
|
+
const reviewer = approval.reviewer.trim();
|
|
5814
|
+
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
5815
|
+
const record = path18.resolve(recordPath);
|
|
5816
|
+
const manifest = await readArtifactBundleManifest(record);
|
|
5817
|
+
const options = parseRecordOptions(manifest, record);
|
|
5818
|
+
const verification = await verifyArtifactBundle(record);
|
|
5819
|
+
if (!verification.current) {
|
|
5820
|
+
const changes = [
|
|
5821
|
+
...verification.changedSources.map((source) => `source ${source}`),
|
|
5822
|
+
...verification.changedOutputs.map((output2) => `output ${output2}`),
|
|
5823
|
+
...!verification.fingerprintValid ? ["quality metadata"] : []
|
|
5824
|
+
];
|
|
5825
|
+
throw new Error(`Cannot approve a stale refinement; changed: ${changes.join(", ")}. Run refine again.`);
|
|
5826
|
+
}
|
|
5827
|
+
if (!options.audit.safe) {
|
|
5828
|
+
throw new Error(`Cannot approve a failed quality audit: ${options.audit.reasons.join("; ")}`);
|
|
5829
|
+
}
|
|
5830
|
+
if (manifest.sources.length !== 1 || manifest.outputs.length !== 1) {
|
|
5831
|
+
throw new Error("A refinement record must contain exactly one source and one output.");
|
|
5832
|
+
}
|
|
5833
|
+
const root = path18.dirname(record);
|
|
5834
|
+
const outputPath = path18.resolve(root, manifest.outputs[0].path);
|
|
5835
|
+
const output = await readFile14(outputPath);
|
|
5836
|
+
const sources = manifest.sources.map((source) => ({
|
|
5837
|
+
...source,
|
|
5838
|
+
path: path18.resolve(root, source.path)
|
|
5839
|
+
}));
|
|
5840
|
+
const approved = {
|
|
5841
|
+
...options,
|
|
5842
|
+
review: {
|
|
5843
|
+
status: "approved",
|
|
5844
|
+
reviewer,
|
|
5845
|
+
approvedAt: (approval.approvedAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
5846
|
+
checklist: {
|
|
5847
|
+
nativeScale: true,
|
|
5848
|
+
crispEdges: true,
|
|
5849
|
+
subjectReadable: true,
|
|
5850
|
+
paletteSeparation: true,
|
|
5851
|
+
alphaAndSeams: true
|
|
5852
|
+
},
|
|
5853
|
+
...approval.note?.trim() ? { note: approval.note.trim() } : {}
|
|
5854
|
+
}
|
|
5855
|
+
};
|
|
5856
|
+
await writeManagedArtifactBundle(
|
|
5857
|
+
record,
|
|
5858
|
+
[{ path: outputPath, data: output }],
|
|
5859
|
+
{ kind: "refine", sources, options: approved }
|
|
5860
|
+
);
|
|
5861
|
+
return checkQualityRecord(record);
|
|
5632
5862
|
}
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5863
|
+
|
|
5864
|
+
// src/pipeline/tileset-export.ts
|
|
5865
|
+
function normalizeTileRules(raw) {
|
|
5866
|
+
if (!isRecord(raw)) return null;
|
|
5867
|
+
const ruleType = raw.rule_type;
|
|
5868
|
+
const arity = raw.arity;
|
|
5869
|
+
if (ruleType !== "edge" && ruleType !== "corner" && ruleType !== "outline") return null;
|
|
5870
|
+
if (arity !== 4 && arity !== 6) return null;
|
|
5871
|
+
const masks = {};
|
|
5872
|
+
if (isRecord(raw.tiles)) {
|
|
5873
|
+
for (const [key, value] of Object.entries(raw.tiles)) {
|
|
5874
|
+
const index = tileIndex(key);
|
|
5875
|
+
const mask = typeof value === "number" ? value : isRecord(value) && typeof value.bitmask === "number" ? value.bitmask : isRecord(value) && typeof value.mask === "number" ? value.mask : null;
|
|
5876
|
+
if (index !== null && mask !== null && Number.isInteger(mask) && mask >= 0) masks[index] = mask;
|
|
5877
|
+
}
|
|
5640
5878
|
}
|
|
5879
|
+
const connectivity = raw.connectivity === "same" || raw.connectivity === "other" ? raw.connectivity : void 0;
|
|
5880
|
+
const terrains = Array.isArray(raw.terrains) ? raw.terrains.filter((item) => typeof item === "string") : [];
|
|
5881
|
+
return {
|
|
5882
|
+
ruleType,
|
|
5883
|
+
arity,
|
|
5884
|
+
...connectivity ? { connectivity } : {},
|
|
5885
|
+
terrains,
|
|
5886
|
+
masks,
|
|
5887
|
+
raw
|
|
5888
|
+
};
|
|
5641
5889
|
}
|
|
5642
|
-
function
|
|
5643
|
-
|
|
5644
|
-
|
|
5890
|
+
function exportTileset(entry, spec, options) {
|
|
5891
|
+
const outputs = resolveSpecEntryOutputs(entry, spec);
|
|
5892
|
+
if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
|
|
5893
|
+
const packed = packSprites(
|
|
5894
|
+
outputs.map((output) => ({
|
|
5895
|
+
id: output.role ?? fallbackOutputRole(output.index),
|
|
5896
|
+
path: output.absolutePath
|
|
5897
|
+
})),
|
|
5898
|
+
{ columns: options.columns, order: "input" }
|
|
5645
5899
|
);
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
raw = JSON.parse(data);
|
|
5651
|
-
} catch (error) {
|
|
5652
|
-
throw new Error(`${journal} is not valid transaction JSON: ${message(error)}`, { cause: error });
|
|
5653
|
-
}
|
|
5654
|
-
if (raw.format !== "pixelkiln-artifact-transaction" || raw.version !== 1 || !Number.isInteger(raw.pid) || typeof raw.createdAt !== "string" || !Array.isArray(raw.entries) || !raw.entries.length) {
|
|
5655
|
-
throw new Error(`${journal} is not a PixelKiln artifact transaction.`);
|
|
5656
|
-
}
|
|
5657
|
-
return raw;
|
|
5658
|
-
}
|
|
5659
|
-
async function removeTransactionFiles(journal) {
|
|
5660
|
-
await rm6(journal, { force: true });
|
|
5661
|
-
await rm6(transactionMarker(journal), { force: true });
|
|
5662
|
-
}
|
|
5663
|
-
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
5664
|
-
const journal = path18.resolve(recoveryFile);
|
|
5665
|
-
const bytes = await readOptional(journal);
|
|
5666
|
-
if (!bytes) {
|
|
5667
|
-
await rm6(transactionMarker(journal), { force: true });
|
|
5668
|
-
return;
|
|
5900
|
+
if (packed.skipped.length) {
|
|
5901
|
+
throw new Error(
|
|
5902
|
+
`Cannot export ${spec.styleId}/${spec.assetId}: ` + packed.skipped.map((item) => `${item.id}: ${item.reason}`).join("; ")
|
|
5903
|
+
);
|
|
5669
5904
|
}
|
|
5670
|
-
|
|
5671
|
-
|
|
5905
|
+
const metadata = entry.providerMetadata?.[entry.provider];
|
|
5906
|
+
const rawRules = isRecord(metadata) ? metadata.tileRules : void 0;
|
|
5907
|
+
const rules = normalizeTileRules(rawRules);
|
|
5908
|
+
const frames = packed.atlas.frames;
|
|
5909
|
+
const generic = buildGeneric(
|
|
5910
|
+
spec,
|
|
5911
|
+
options.imageName,
|
|
5912
|
+
packed.atlas,
|
|
5913
|
+
frames,
|
|
5914
|
+
isRecord(rawRules) ? rawRules : null,
|
|
5915
|
+
rules
|
|
5916
|
+
);
|
|
5917
|
+
if (options.format === "generic") {
|
|
5918
|
+
return {
|
|
5919
|
+
png: packed.png,
|
|
5920
|
+
extension: ".json",
|
|
5921
|
+
document: JSON.stringify(generic, null, 2) + "\n",
|
|
5922
|
+
generic,
|
|
5923
|
+
sources: packed.sources
|
|
5924
|
+
};
|
|
5672
5925
|
}
|
|
5673
|
-
|
|
5674
|
-
if (
|
|
5926
|
+
assertUniformTiles(generic);
|
|
5927
|
+
if ((rawRules !== void 0 || entry.tileFeature) && !rules) {
|
|
5675
5928
|
throw new Error(
|
|
5676
|
-
|
|
5929
|
+
`${options.format} export needs recognized adjacency rules; use --format generic to retain this provider rule object without guessing`
|
|
5677
5930
|
);
|
|
5678
5931
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
);
|
|
5684
|
-
}
|
|
5685
|
-
}
|
|
5686
|
-
const committed = await readOptional(transactionMarker(journal)) !== null;
|
|
5687
|
-
const errors = [];
|
|
5688
|
-
if (committed) {
|
|
5689
|
-
for (const entry of transaction.entries) {
|
|
5690
|
-
if (entry.backup) await remove2(entry.backup, errors);
|
|
5691
|
-
await remove2(entry.stage, errors);
|
|
5692
|
-
}
|
|
5693
|
-
} else {
|
|
5694
|
-
for (const entry of [...transaction.entries].reverse()) {
|
|
5695
|
-
const backup = entry.backup ? await readOptional(entry.backup) : null;
|
|
5696
|
-
const destination = await readOptional(entry.destination);
|
|
5697
|
-
if (backup) {
|
|
5698
|
-
if (destination && digest(destination) !== entry.sha256) {
|
|
5699
|
-
errors.push(
|
|
5700
|
-
`destination changed during interrupted recovery: ${entry.destination}; previous file remains at ${entry.backup}`
|
|
5701
|
-
);
|
|
5702
|
-
continue;
|
|
5703
|
-
}
|
|
5704
|
-
await remove2(entry.destination, errors);
|
|
5705
|
-
try {
|
|
5706
|
-
await rename5(entry.backup, entry.destination);
|
|
5707
|
-
} catch (error) {
|
|
5708
|
-
errors.push(
|
|
5709
|
-
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
5710
|
-
);
|
|
5711
|
-
}
|
|
5712
|
-
} else if (destination && digest(destination) === entry.sha256) {
|
|
5713
|
-
await remove2(entry.destination, errors);
|
|
5714
|
-
}
|
|
5715
|
-
await remove2(entry.stage, errors);
|
|
5716
|
-
}
|
|
5932
|
+
if (rules?.arity === 6) {
|
|
5933
|
+
throw new Error(
|
|
5934
|
+
`${options.format} export does not yet map six-edge hex masks safely; use --format generic`
|
|
5935
|
+
);
|
|
5717
5936
|
}
|
|
5718
|
-
if (
|
|
5937
|
+
if (rules?.ruleType === "outline") {
|
|
5719
5938
|
throw new Error(
|
|
5720
|
-
|
|
5939
|
+
`${options.format} export cannot infer adjacency from outline/stamp-only rules; use --format generic`
|
|
5721
5940
|
);
|
|
5722
5941
|
}
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5942
|
+
if (options.format === "tiled") {
|
|
5943
|
+
return {
|
|
5944
|
+
png: packed.png,
|
|
5945
|
+
extension: ".tsj",
|
|
5946
|
+
document: JSON.stringify(buildTiled(generic), null, 2) + "\n",
|
|
5947
|
+
generic,
|
|
5948
|
+
sources: packed.sources
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
return {
|
|
5952
|
+
png: packed.png,
|
|
5953
|
+
extension: ".tres",
|
|
5954
|
+
document: buildGodot(generic, spec),
|
|
5955
|
+
generic,
|
|
5956
|
+
sources: packed.sources
|
|
5735
5957
|
};
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
sha256: digest(output.data)
|
|
5739
|
-
}));
|
|
5958
|
+
}
|
|
5959
|
+
function buildGeneric(spec, imageName, atlas, frames, providerRules, rules) {
|
|
5740
5960
|
return {
|
|
5741
|
-
format: "pixelkiln-
|
|
5961
|
+
format: "pixelkiln-tileset",
|
|
5742
5962
|
version: 1,
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5963
|
+
name: `${spec.styleId}/${spec.assetId}`,
|
|
5964
|
+
style: spec.styleId,
|
|
5965
|
+
asset: spec.assetId,
|
|
5966
|
+
image: imageName,
|
|
5967
|
+
tile: { ...atlas.cell },
|
|
5968
|
+
sheet: { ...atlas.sheet, columns: atlas.columns },
|
|
5969
|
+
tiles: frames.map((frame, index) => {
|
|
5970
|
+
const sourceIndex = tileIndex(frame.id) ?? index;
|
|
5971
|
+
return {
|
|
5972
|
+
id: index,
|
|
5973
|
+
sourceIndex,
|
|
5974
|
+
role: frame.id,
|
|
5975
|
+
x: frame.x,
|
|
5976
|
+
y: frame.y,
|
|
5977
|
+
width: frame.width,
|
|
5978
|
+
height: frame.height,
|
|
5979
|
+
...rules?.masks[sourceIndex] !== void 0 ? { bitmask: rules.masks[sourceIndex] } : {},
|
|
5980
|
+
stampOnly: rules ? rules.masks[sourceIndex] === void 0 : true
|
|
5981
|
+
};
|
|
5982
|
+
}),
|
|
5983
|
+
providerRules,
|
|
5984
|
+
rules
|
|
5746
5985
|
};
|
|
5747
5986
|
}
|
|
5748
|
-
function
|
|
5749
|
-
const
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5987
|
+
function buildTiled(generic) {
|
|
5988
|
+
const rules = generic.rules;
|
|
5989
|
+
const tiled = {
|
|
5990
|
+
type: "tileset",
|
|
5991
|
+
version: "1.10",
|
|
5992
|
+
tiledversion: "1.12.2",
|
|
5993
|
+
name: generic.name.replace("/", "-"),
|
|
5994
|
+
tilewidth: generic.tile.width,
|
|
5995
|
+
tileheight: generic.tile.height,
|
|
5996
|
+
tilecount: generic.tiles.length,
|
|
5997
|
+
columns: generic.sheet.columns,
|
|
5998
|
+
image: generic.image,
|
|
5999
|
+
imagewidth: generic.sheet.width,
|
|
6000
|
+
imageheight: generic.sheet.height,
|
|
6001
|
+
margin: 0,
|
|
6002
|
+
spacing: 0
|
|
6003
|
+
};
|
|
6004
|
+
if (!rules || rules.ruleType === "outline") return tiled;
|
|
6005
|
+
const terrainNames = terrainNamesFor(rules);
|
|
6006
|
+
tiled.wangsets = [{
|
|
6007
|
+
name: generic.name,
|
|
6008
|
+
type: rules.ruleType,
|
|
6009
|
+
tile: representativeTile(generic, 0),
|
|
6010
|
+
colors: terrainNames.map((name, index) => ({
|
|
6011
|
+
name,
|
|
6012
|
+
color: index === 0 ? "#5ab552" : "#4c78a8",
|
|
6013
|
+
tile: representativeTile(generic, index),
|
|
6014
|
+
probability: 1
|
|
6015
|
+
})),
|
|
6016
|
+
properties: rules.connectivity ? [{ name: "pixelkiln:connectivity", type: "string", value: rules.connectivity }] : [],
|
|
6017
|
+
wangtiles: generic.tiles.filter((tile) => tile.bitmask !== void 0).map((tile) => ({ tileid: tile.id, wangid: tiledWangId(rules, tile.bitmask) }))
|
|
6018
|
+
}];
|
|
6019
|
+
return tiled;
|
|
5754
6020
|
}
|
|
5755
|
-
|
|
5756
|
-
const
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
const
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
6021
|
+
function tiledWangId(rules, mask) {
|
|
6022
|
+
const bitColor = (bit) => (mask & 1 << bit) !== 0 ? 1 : 2;
|
|
6023
|
+
if (rules.ruleType === "corner") {
|
|
6024
|
+
return [0, bitColor(2), 0, bitColor(0), 0, bitColor(1), 0, bitColor(3)];
|
|
6025
|
+
}
|
|
6026
|
+
return [bitColor(0), 0, bitColor(1), 0, bitColor(2), 0, bitColor(3), 0];
|
|
6027
|
+
}
|
|
6028
|
+
function buildGodot(generic, spec) {
|
|
6029
|
+
const rules = generic.rules;
|
|
6030
|
+
const lines = [
|
|
6031
|
+
`[gd_resource type="TileSet" load_steps=3 format=3]`,
|
|
6032
|
+
"",
|
|
6033
|
+
`[ext_resource type="Texture2D" path=${JSON.stringify(`./${generic.image}`)} id="1_texture"]`,
|
|
6034
|
+
"",
|
|
6035
|
+
`[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_pixelkiln"]`,
|
|
6036
|
+
`texture = ExtResource("1_texture")`,
|
|
6037
|
+
`texture_region_size = Vector2i(${generic.tile.width}, ${generic.tile.height})`
|
|
6038
|
+
];
|
|
6039
|
+
for (const tile of generic.tiles) {
|
|
6040
|
+
const x = tile.id % generic.sheet.columns;
|
|
6041
|
+
const y = Math.floor(tile.id / generic.sheet.columns);
|
|
6042
|
+
lines.push(`${x}:${y}/0 = 0`);
|
|
6043
|
+
if (!rules || tile.bitmask === void 0 || rules.ruleType === "outline") continue;
|
|
6044
|
+
const values = godotTerrainBits(rules, tile.bitmask);
|
|
6045
|
+
lines.push(`${x}:${y}/0/terrain_set = 0`);
|
|
6046
|
+
lines.push(`${x}:${y}/0/terrain = ${dominantTerrain(values)}`);
|
|
6047
|
+
for (const [side, value] of Object.entries(values)) {
|
|
6048
|
+
lines.push(`${x}:${y}/0/terrains_peering_bit/${side} = ${value}`);
|
|
5775
6049
|
}
|
|
5776
6050
|
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
for (const
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
const expected = recorded.get(destination);
|
|
5788
|
-
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
5789
|
-
}
|
|
5790
|
-
if (conflicts.length) {
|
|
5791
|
-
throw new Error(
|
|
5792
|
-
`Refusing to overwrite modified or unowned artifact file(s):
|
|
5793
|
-
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
5794
|
-
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`
|
|
6051
|
+
lines.push("", "[resource]");
|
|
6052
|
+
lines.push(`tile_size = Vector2i(${generic.tile.width}, ${generic.tile.height})`);
|
|
6053
|
+
const shape = godotTileShape(spec.tileType ?? "isometric");
|
|
6054
|
+
if (shape !== 0) lines.push(`tile_shape = ${shape}`);
|
|
6055
|
+
if (rules && rules.ruleType !== "outline") {
|
|
6056
|
+
lines.push(`terrain_set_0/mode = ${rules.ruleType === "corner" ? 1 : 2}`);
|
|
6057
|
+
for (const [index, name] of terrainNamesFor(rules).entries()) {
|
|
6058
|
+
lines.push(`terrain_set_0/terrain_${index}/name = ${JSON.stringify(name)}`);
|
|
6059
|
+
lines.push(
|
|
6060
|
+
`terrain_set_0/terrain_${index}/color = ` + (index === 0 ? "Color(0.3529, 0.7098, 0.3216, 1)" : "Color(0.298, 0.4706, 0.6588, 1)")
|
|
5795
6061
|
);
|
|
5796
6062
|
}
|
|
5797
6063
|
}
|
|
5798
|
-
|
|
5799
|
-
return
|
|
5800
|
-
withArtifactManifest(absoluteManifest, outputs, provenance),
|
|
5801
|
-
{ ...bundleOptions, recoveryFile }
|
|
5802
|
-
);
|
|
5803
|
-
}
|
|
5804
|
-
function isCode(error, code) {
|
|
5805
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
6064
|
+
lines.push(`sources/0 = SubResource("TileSetAtlasSource_pixelkiln")`, "");
|
|
6065
|
+
return lines.join("\n");
|
|
5806
6066
|
}
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
6067
|
+
function godotTerrainBits(rules, mask) {
|
|
6068
|
+
const terrain = (bit) => (mask & 1 << bit) !== 0 ? 0 : 1;
|
|
6069
|
+
if (rules.ruleType === "corner") {
|
|
6070
|
+
return {
|
|
6071
|
+
top_left_corner: terrain(3),
|
|
6072
|
+
top_right_corner: terrain(2),
|
|
6073
|
+
bottom_left_corner: terrain(1),
|
|
6074
|
+
bottom_right_corner: terrain(0)
|
|
6075
|
+
};
|
|
5812
6076
|
}
|
|
6077
|
+
return {
|
|
6078
|
+
top_side: terrain(0),
|
|
6079
|
+
right_side: terrain(1),
|
|
6080
|
+
bottom_side: terrain(2),
|
|
6081
|
+
left_side: terrain(3)
|
|
6082
|
+
};
|
|
5813
6083
|
}
|
|
5814
|
-
|
|
5815
|
-
const
|
|
5816
|
-
|
|
5817
|
-
|
|
6084
|
+
function dominantTerrain(values) {
|
|
6085
|
+
const terrainZero = Object.values(values).filter((value) => value === 0).length;
|
|
6086
|
+
return terrainZero * 2 >= Object.keys(values).length ? 0 : 1;
|
|
6087
|
+
}
|
|
6088
|
+
function godotTileShape(tileType) {
|
|
6089
|
+
if (tileType === "isometric" || tileType === "oblique") return 1;
|
|
6090
|
+
if (tileType === "hex" || tileType === "hex_pointy") return 3;
|
|
6091
|
+
return 0;
|
|
6092
|
+
}
|
|
6093
|
+
function representativeTile(generic, terrainIndex) {
|
|
6094
|
+
const rules = generic.rules;
|
|
6095
|
+
const full = (1 << rules.arity) - 1;
|
|
6096
|
+
const wanted = terrainIndex === 0 ? full : 0;
|
|
6097
|
+
return generic.tiles.find((tile) => tile.bitmask === wanted)?.id ?? -1;
|
|
6098
|
+
}
|
|
6099
|
+
function terrainNamesFor(rules) {
|
|
6100
|
+
return [rules.terrains[0] ?? "feature", rules.terrains[1] ?? "background"];
|
|
6101
|
+
}
|
|
6102
|
+
function assertUniformTiles(generic) {
|
|
6103
|
+
const mismatched = generic.tiles.filter(
|
|
6104
|
+
(tile) => tile.width !== generic.tile.width || tile.height !== generic.tile.height
|
|
6105
|
+
);
|
|
6106
|
+
if (mismatched.length) {
|
|
6107
|
+
throw new Error(
|
|
6108
|
+
`Engine tilesets require uniform ${generic.tile.width}x${generic.tile.height} cells; mismatched: ${mismatched.map((tile) => tile.role).join(", ")}. Use --format generic.`
|
|
6109
|
+
);
|
|
5818
6110
|
}
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
6111
|
+
}
|
|
6112
|
+
function tileIndex(key) {
|
|
6113
|
+
const match = key.match(/(?:^|[_-])(\d+)$/);
|
|
6114
|
+
return match ? Number(match[1]) : null;
|
|
6115
|
+
}
|
|
6116
|
+
function isRecord(value) {
|
|
6117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
// src/pick/salvage-server.ts
|
|
6121
|
+
import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile15 } from "fs/promises";
|
|
6122
|
+
import path19 from "path";
|
|
6123
|
+
|
|
6124
|
+
// src/pick/salvage-sheet.ts
|
|
6125
|
+
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6126
|
+
function renderSalvageSheet(orphans, ctx) {
|
|
6127
|
+
const safe = orphans.map((o) => ({ ...o, prompt: escapeHtml2(o.prompt) }));
|
|
6128
|
+
const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
|
|
6129
|
+
const styleId = ctx ? escapeHtml2(ctx.styleId) : null;
|
|
6130
|
+
const importDir = ctx ? escapeHtml2(ctx.importDir) : null;
|
|
6131
|
+
return `<!doctype html>
|
|
6132
|
+
<html lang="en">
|
|
6133
|
+
<head>
|
|
6134
|
+
<meta charset="utf-8">
|
|
6135
|
+
<title>pixelkiln salvage${styleId ? ` \u2014 ${styleId}` : ""}</title>
|
|
6136
|
+
<style>
|
|
6137
|
+
:root { --bg:#0f1115; --panel:#171a21; --line:#262b36; --text:#e6e8ee; --dim:#8b93a7;
|
|
6138
|
+
--import:#6ee7a8; --keep:#7cc4ff; --discard:#f87171; }
|
|
6139
|
+
@media (prefers-color-scheme: light) {
|
|
6140
|
+
:root { --bg:#f6f7f9; --panel:#fff; --line:#dfe3ea; --text:#1a1d23; --dim:#666e80;
|
|
6141
|
+
--import:#0a8f52; --keep:#1a6fd4; --discard:#c0392b; }
|
|
5829
6142
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
6143
|
+
* { box-sizing:border-box; }
|
|
6144
|
+
body { margin:0; background:var(--bg); color:var(--text);
|
|
6145
|
+
font:14px/1.5 ui-sans-serif,-apple-system,"SF Pro Text",Inter,system-ui,sans-serif; }
|
|
6146
|
+
header { position:sticky; top:0; z-index:10; background:var(--panel);
|
|
6147
|
+
border-bottom:1px solid var(--line); padding:12px 20px; display:flex; gap:14px;
|
|
6148
|
+
align-items:center; flex-wrap:wrap; }
|
|
6149
|
+
h1 { font-size:15px; margin:0; font-weight:600; }
|
|
6150
|
+
h1 .ctx { font-weight:500; color:var(--dim); }
|
|
6151
|
+
h1 .dest { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px; }
|
|
6152
|
+
.tally { color:var(--dim); font-variant-numeric:tabular-nums; }
|
|
6153
|
+
.tally b { color:var(--text); font-weight:600; }
|
|
6154
|
+
button { font:inherit; font-weight:550; border-radius:7px; padding:7px 14px;
|
|
6155
|
+
border:1px solid var(--line); background:transparent; color:var(--text); cursor:pointer; }
|
|
6156
|
+
button.primary { background:var(--import); border-color:var(--import); color:#08130d; }
|
|
6157
|
+
button:disabled { opacity:.45; cursor:not-allowed; }
|
|
6158
|
+
main { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:12px; padding:18px 20px 80px; }
|
|
6159
|
+
.card { border:2px solid var(--line); border-radius:10px; background:var(--panel);
|
|
6160
|
+
padding:10px; display:flex; flex-direction:column; gap:8px; }
|
|
6161
|
+
.card.import { border-color:var(--import); }
|
|
6162
|
+
.card.keep { border-color:var(--keep); }
|
|
6163
|
+
.card.discard { border-color:var(--discard); opacity:.5; }
|
|
6164
|
+
.thumb { height:120px; display:flex; align-items:center; justify-content:center;
|
|
6165
|
+
border-radius:6px;
|
|
6166
|
+
background-image:
|
|
6167
|
+
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
6168
|
+
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
|
|
6169
|
+
background-size:12px 12px; background-position:0 0,6px 6px; }
|
|
6170
|
+
.thumb img { image-rendering:pixelated; max-height:112px; max-width:100%; }
|
|
6171
|
+
.meta { font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums; }
|
|
6172
|
+
.prompt { font-size:11.5px; color:var(--dim); max-height:3.2em; overflow:hidden; }
|
|
6173
|
+
.acts { display:flex; gap:5px; }
|
|
6174
|
+
.acts button { flex:1; padding:5px 0; font-size:11.5px; }
|
|
6175
|
+
.acts button.on-import { background:var(--import); border-color:var(--import); color:#08130d; }
|
|
6176
|
+
.acts button.on-keep { background:var(--keep); border-color:var(--keep); color:#04121f; }
|
|
6177
|
+
.acts button.on-discard { background:var(--discard); border-color:var(--discard); color:#fff; }
|
|
6178
|
+
footer { position:fixed; bottom:0; left:0; right:0; background:var(--panel);
|
|
6179
|
+
border-top:1px solid var(--line); padding:10px 20px; font-size:12px; color:var(--dim); }
|
|
6180
|
+
kbd { border:1px solid var(--line); border-bottom-width:2px; border-radius:4px;
|
|
6181
|
+
padding:1px 5px; font-size:11px; font-family:ui-monospace,Menlo,monospace; }
|
|
6182
|
+
</style>
|
|
6183
|
+
</head>
|
|
6184
|
+
<body>
|
|
6185
|
+
<header>
|
|
6186
|
+
<h1>pixelkiln salvage${styleId ? ` <span class="ctx">\u2014 ${styleId}<span class="dest"> \u2192 ${importDir}/_salvaged/</span></span>` : ""}</h1>
|
|
6187
|
+
<span class="tally"><b id="ti">0</b> import \xB7 <b id="tk">0</b> keep \xB7 <b id="td">0</b> discard \xB7
|
|
6188
|
+
<b id="tu">0</b> undecided</span>
|
|
6189
|
+
<span style="flex:1"></span>
|
|
6190
|
+
<button id="allDiscard">Discard all undecided</button>
|
|
6191
|
+
<button id="allKeep">Keep all undecided</button>
|
|
6192
|
+
<button id="submit" class="primary" disabled>Apply</button>
|
|
6193
|
+
</header>
|
|
6194
|
+
<main id="grid"></main>
|
|
6195
|
+
<footer>
|
|
6196
|
+
<b>import</b> downloads it and adds it to your manifest \xB7 <b>keep</b> tags it and leaves it \xB7
|
|
6197
|
+
<b>discard</b> only tags it \u2014 nothing is deleted here.
|
|
6198
|
+
Hover a card and press <kbd>i</kbd> / <kbd>k</kbd> / <kbd>d</kbd>, or <kbd>u</kbd> to undo.
|
|
6199
|
+
</footer>
|
|
6200
|
+
<script>
|
|
6201
|
+
const ITEMS = ${data};
|
|
6202
|
+
const picks = new Map();
|
|
6203
|
+
const grid = document.getElementById('grid');
|
|
6204
|
+
let hovered = null;
|
|
6205
|
+
|
|
6206
|
+
ITEMS.forEach((o, i) => {
|
|
6207
|
+
const el = document.createElement('div');
|
|
6208
|
+
el.className = 'card';
|
|
6209
|
+
el.dataset.i = String(i);
|
|
6210
|
+
el.innerHTML =
|
|
6211
|
+
'<div class="thumb"><img src="' + o.previewUrl + '" loading="lazy"></div>' +
|
|
6212
|
+
'<div class="meta">' + o.width + '\xD7' + o.height + ' \xB7 ' + o.createdAt.slice(0,10) + '</div>' +
|
|
6213
|
+
'<div class="prompt">' + o.prompt + '</div>' +
|
|
6214
|
+
'<div class="acts">' +
|
|
6215
|
+
'<button data-a="import">import</button>' +
|
|
6216
|
+
'<button data-a="keep">keep</button>' +
|
|
6217
|
+
'<button data-a="discard">discard</button>' +
|
|
6218
|
+
'</div>';
|
|
6219
|
+
el.querySelectorAll('.acts button').forEach(b => {
|
|
6220
|
+
b.onclick = () => choose(i, b.dataset.a);
|
|
6221
|
+
});
|
|
6222
|
+
el.onmouseenter = () => { hovered = i; };
|
|
6223
|
+
grid.appendChild(el);
|
|
6224
|
+
});
|
|
6225
|
+
|
|
6226
|
+
function choose(i, action) {
|
|
6227
|
+
if (picks.get(i) === action) picks.delete(i); else picks.set(i, action);
|
|
6228
|
+
const el = grid.children[i];
|
|
6229
|
+
el.className = 'card' + (picks.has(i) ? ' ' + picks.get(i) : '');
|
|
6230
|
+
el.querySelectorAll('.acts button').forEach(b => {
|
|
6231
|
+
b.className = picks.get(i) === b.dataset.a ? 'on-' + b.dataset.a : '';
|
|
6232
|
+
});
|
|
6233
|
+
refresh();
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
function refresh() {
|
|
6237
|
+
const c = { import: 0, keep: 0, discard: 0 };
|
|
6238
|
+
for (const a of picks.values()) c[a]++;
|
|
6239
|
+
ti.textContent = c.import; tk.textContent = c.keep; td.textContent = c.discard;
|
|
6240
|
+
tu.textContent = ITEMS.length - picks.size;
|
|
6241
|
+
document.getElementById('submit').disabled = picks.size === 0;
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
document.addEventListener('keydown', e => {
|
|
6245
|
+
if (hovered === null) return;
|
|
6246
|
+
const map = { i: 'import', k: 'keep', d: 'discard' };
|
|
6247
|
+
if (map[e.key]) { choose(hovered, map[e.key]); e.preventDefault(); }
|
|
6248
|
+
else if (e.key === 'u') { picks.delete(hovered); choose(hovered, '__none__'); }
|
|
6249
|
+
});
|
|
6250
|
+
|
|
6251
|
+
document.getElementById('allKeep').onclick = () => {
|
|
6252
|
+
ITEMS.forEach((_, i) => { if (!picks.has(i)) choose(i, 'keep'); });
|
|
6253
|
+
};
|
|
6254
|
+
document.getElementById('allDiscard').onclick = () => {
|
|
6255
|
+
ITEMS.forEach((_, i) => { if (!picks.has(i)) choose(i, 'discard'); });
|
|
6256
|
+
};
|
|
6257
|
+
|
|
6258
|
+
document.getElementById('submit').onclick = async () => {
|
|
6259
|
+
const btn = document.getElementById('submit');
|
|
6260
|
+
btn.disabled = true; btn.textContent = 'applying\u2026';
|
|
6261
|
+
const decisions = [...picks.entries()].map(([i, action]) => ({ id: ITEMS[i].id, action }));
|
|
6262
|
+
try {
|
|
6263
|
+
const res = await fetch('/apply', {
|
|
6264
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
6265
|
+
body: JSON.stringify({ decisions }),
|
|
6266
|
+
});
|
|
6267
|
+
if (!res.ok) throw new Error(await res.text());
|
|
6268
|
+
btn.textContent = 'done \u2014 close this tab';
|
|
6269
|
+
document.body.style.opacity = '.6';
|
|
6270
|
+
} catch (err) {
|
|
6271
|
+
btn.textContent = 'failed: ' + err.message; btn.disabled = false;
|
|
5832
6272
|
}
|
|
5833
|
-
|
|
6273
|
+
};
|
|
6274
|
+
refresh();
|
|
6275
|
+
</script>
|
|
6276
|
+
</body>
|
|
6277
|
+
</html>`;
|
|
5834
6278
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
6279
|
+
|
|
6280
|
+
// src/pick/salvage-server.ts
|
|
6281
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
6282
|
+
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
6283
|
+
const log2 = opts.onProgress ?? (() => {
|
|
5840
6284
|
});
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
6285
|
+
const html = renderSalvageSheet(orphans, {
|
|
6286
|
+
styleId: ctx.styleId,
|
|
6287
|
+
importDir: path19.relative(process.cwd(), ctx.importDir) || "."
|
|
6288
|
+
});
|
|
6289
|
+
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
6290
|
+
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
6291
|
+
return serveReviewPage({
|
|
6292
|
+
html,
|
|
6293
|
+
port: opts.port,
|
|
6294
|
+
open: opts.open,
|
|
6295
|
+
onProgress: log2,
|
|
6296
|
+
onReady: (url) => {
|
|
6297
|
+
log2(`
|
|
6298
|
+
${orphans.length} unclaimed object(s) to triage: ${url}`);
|
|
6299
|
+
log2(` (nothing is deleted here \u2014 discard only tags)
|
|
6300
|
+
`);
|
|
6301
|
+
},
|
|
6302
|
+
handleApply: async (body) => {
|
|
6303
|
+
const { decisions } = body;
|
|
6304
|
+
const result = { imported: 0, kept: 0, discarded: 0, failed: 0 };
|
|
6305
|
+
const taken = new Set(Object.keys(ctx.manifest.assets));
|
|
6306
|
+
const importedAssetIds = [];
|
|
6307
|
+
for (const decision of decisions) {
|
|
6308
|
+
const orphan = byId.get(decision.id);
|
|
6309
|
+
if (!orphan) continue;
|
|
6310
|
+
if (decision.action === "import") {
|
|
6311
|
+
try {
|
|
6312
|
+
const buf = await provider.download(orphan.previewUrl);
|
|
6313
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
6314
|
+
decodePng(buf);
|
|
6315
|
+
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
6316
|
+
const rel = path19.join("_salvaged", `${assetId}.png`);
|
|
6317
|
+
const outFile = path19.resolve(ctx.importDir, rel);
|
|
6318
|
+
await mkdir6(path19.dirname(outFile), { recursive: true });
|
|
6319
|
+
await writeFile8(outFile, buf);
|
|
6320
|
+
ctx.manifest.assets[assetId] = {
|
|
6321
|
+
prompt: orphan.prompt,
|
|
6322
|
+
promptByStyle: {},
|
|
6323
|
+
category: "_salvaged",
|
|
6324
|
+
file: rel,
|
|
6325
|
+
tags: ["salvaged"],
|
|
6326
|
+
styles: [ctx.styleId],
|
|
6327
|
+
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
6328
|
+
};
|
|
6329
|
+
upsert(ctx.lock, lockKey(ctx.styleId, assetId), {
|
|
6330
|
+
styleId: ctx.styleId,
|
|
6331
|
+
assetId,
|
|
6332
|
+
specHash: SALVAGED_SPEC_HASH,
|
|
6333
|
+
generator: orphan.width === orphan.height ? "1dir" : "map",
|
|
6334
|
+
prompt: orphan.prompt,
|
|
6335
|
+
width: orphan.width,
|
|
6336
|
+
height: orphan.height,
|
|
6337
|
+
jobId: orphan.id,
|
|
6338
|
+
reviewObjectId: null,
|
|
6339
|
+
objectId: orphan.id,
|
|
6340
|
+
candidateIndex: null,
|
|
6341
|
+
status: "downloaded",
|
|
6342
|
+
error: null,
|
|
6343
|
+
sourceUrl: orphan.previewUrl,
|
|
6344
|
+
outputs: [{
|
|
6345
|
+
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
6346
|
+
sha256: sha256(buf)
|
|
6347
|
+
}],
|
|
6348
|
+
submittedAt: orphan.createdAt,
|
|
6349
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6350
|
+
cost: 0,
|
|
6351
|
+
// already paid for, in an earlier period
|
|
6352
|
+
costUnit: "generations",
|
|
6353
|
+
provider: provider.id
|
|
6354
|
+
});
|
|
6355
|
+
importedAssetIds.push(assetId);
|
|
6356
|
+
result.imported++;
|
|
6357
|
+
log2(` imported ${assetId} \u2190 ${orphan.id}`);
|
|
6358
|
+
} catch (err) {
|
|
6359
|
+
result.failed++;
|
|
6360
|
+
log2(` import failed ${orphan.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6361
|
+
}
|
|
6362
|
+
} else if (decision.action === "keep") {
|
|
6363
|
+
result.kept++;
|
|
6364
|
+
} else {
|
|
6365
|
+
result.discarded++;
|
|
6366
|
+
}
|
|
5860
6367
|
}
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
const token = randomUUID3();
|
|
5868
|
-
for (const [index, artifact] of changed.entries()) {
|
|
5869
|
-
const basename = path18.basename(artifact.destination);
|
|
5870
|
-
artifact.stage = path18.join(
|
|
5871
|
-
path18.dirname(artifact.destination),
|
|
5872
|
-
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
5873
|
-
);
|
|
5874
|
-
if (artifact.existed) {
|
|
5875
|
-
artifact.backup = path18.join(
|
|
5876
|
-
path18.dirname(artifact.destination),
|
|
5877
|
-
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
5878
|
-
);
|
|
5879
|
-
}
|
|
5880
|
-
}
|
|
5881
|
-
const journal = options.recoveryFile ? path18.resolve(options.recoveryFile) : null;
|
|
5882
|
-
if (journal) {
|
|
5883
|
-
await mkdir6(path18.dirname(journal), { recursive: true });
|
|
5884
|
-
const transaction = {
|
|
5885
|
-
format: "pixelkiln-artifact-transaction",
|
|
5886
|
-
version: 1,
|
|
5887
|
-
pid: process.pid,
|
|
5888
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5889
|
-
entries: changed.map((artifact) => ({
|
|
5890
|
-
destination: artifact.destination,
|
|
5891
|
-
stage: artifact.stage,
|
|
5892
|
-
...artifact.backup ? { backup: artifact.backup } : {},
|
|
5893
|
-
sha256: digest(artifact.data)
|
|
5894
|
-
}))
|
|
5895
|
-
};
|
|
5896
|
-
try {
|
|
5897
|
-
await writeFile8(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
5898
|
-
activeTransactions.add(journal);
|
|
5899
|
-
} catch (error) {
|
|
5900
|
-
throw new Error(
|
|
5901
|
-
`Could not start durable artifact transaction at ${journal}: ${message(error)}`,
|
|
5902
|
-
{ cause: error }
|
|
5903
|
-
);
|
|
5904
|
-
}
|
|
5905
|
-
}
|
|
5906
|
-
try {
|
|
5907
|
-
for (const [index, artifact] of changed.entries()) {
|
|
5908
|
-
await mkdir6(path18.dirname(artifact.destination), { recursive: true });
|
|
5909
|
-
await options.beforeStage?.(artifact.destination, index);
|
|
5910
|
-
await writeFile8(artifact.stage, artifact.data, { flag: "wx" });
|
|
5911
|
-
}
|
|
5912
|
-
} catch (error) {
|
|
5913
|
-
const cleanupErrors2 = await rollback(changed);
|
|
5914
|
-
if (journal && !cleanupErrors2.length) await removeTransactionFiles(journal);
|
|
5915
|
-
if (journal) activeTransactions.delete(journal);
|
|
5916
|
-
const suffix = cleanupErrors2.length ? ` Cleanup also failed: ${cleanupErrors2.join("; ")}` : "";
|
|
5917
|
-
throw new Error(`Could not stage artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5918
|
-
}
|
|
5919
|
-
let durableCommit = false;
|
|
5920
|
-
try {
|
|
5921
|
-
for (const artifact of changed) {
|
|
5922
|
-
if (!artifact.existed) continue;
|
|
5923
|
-
await rename5(artifact.destination, artifact.backup);
|
|
5924
|
-
artifact.backedUp = true;
|
|
5925
|
-
}
|
|
5926
|
-
for (const [index, artifact] of changed.entries()) {
|
|
5927
|
-
await options.beforePromote?.(artifact.destination, index);
|
|
5928
|
-
await rename5(artifact.stage, artifact.destination);
|
|
5929
|
-
artifact.stage = void 0;
|
|
5930
|
-
artifact.promoted = true;
|
|
5931
|
-
}
|
|
5932
|
-
if (journal) {
|
|
5933
|
-
await writeFile8(transactionMarker(journal), `${token}
|
|
5934
|
-
`, { flag: "wx" });
|
|
5935
|
-
durableCommit = true;
|
|
5936
|
-
await options.afterCommit?.();
|
|
5937
|
-
}
|
|
5938
|
-
} catch (error) {
|
|
5939
|
-
if (durableCommit) {
|
|
5940
|
-
if (journal) activeTransactions.delete(journal);
|
|
5941
|
-
throw new Error(
|
|
5942
|
-
`Artifact bundle was committed, but final cleanup was interrupted: ${message(error)}. The next write will finish cleanup from ${journal}.`,
|
|
5943
|
-
{ cause: error }
|
|
5944
|
-
);
|
|
5945
|
-
}
|
|
5946
|
-
const rollbackErrors = await rollback(changed);
|
|
5947
|
-
if (journal && !rollbackErrors.length) await removeTransactionFiles(journal);
|
|
5948
|
-
if (journal) activeTransactions.delete(journal);
|
|
5949
|
-
const suffix = rollbackErrors.length ? ` Rollback needs attention: ${rollbackErrors.join("; ")}` : " Previous bundle restored.";
|
|
5950
|
-
throw new Error(`Could not promote artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5951
|
-
}
|
|
5952
|
-
const cleanupErrors = [];
|
|
5953
|
-
for (const artifact of changed) {
|
|
5954
|
-
if (artifact.backup) await remove2(artifact.backup, cleanupErrors);
|
|
5955
|
-
}
|
|
5956
|
-
if (cleanupErrors.length) {
|
|
5957
|
-
if (journal) activeTransactions.delete(journal);
|
|
5958
|
-
throw new Error(
|
|
5959
|
-
`Artifact bundle was written, but old backup cleanup failed: ${cleanupErrors.join("; ")}`
|
|
5960
|
-
);
|
|
5961
|
-
}
|
|
5962
|
-
if (journal) {
|
|
5963
|
-
try {
|
|
5964
|
-
await removeTransactionFiles(journal);
|
|
5965
|
-
} finally {
|
|
5966
|
-
activeTransactions.delete(journal);
|
|
6368
|
+
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
6369
|
+
const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
|
|
6370
|
+
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
6371
|
+
await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
6372
|
+
await saveLock(ctx.lockPath, ctx.lock);
|
|
6373
|
+
return result;
|
|
5967
6374
|
}
|
|
5968
|
-
}
|
|
5969
|
-
return {
|
|
5970
|
-
changed: changed.map((artifact) => artifact.destination),
|
|
5971
|
-
unchanged
|
|
5972
|
-
};
|
|
6375
|
+
});
|
|
5973
6376
|
}
|
|
5974
6377
|
|
|
5975
6378
|
// src/cli.ts
|
|
5976
6379
|
var log = (msg = "") => console.log(msg);
|
|
5977
6380
|
async function provenanceFile(id, file) {
|
|
5978
|
-
const absolute =
|
|
6381
|
+
const absolute = path20.resolve(file);
|
|
5979
6382
|
return {
|
|
5980
6383
|
id,
|
|
5981
6384
|
path: absolute,
|
|
@@ -6006,7 +6409,13 @@ var VALUE_FLAGS = [
|
|
|
6006
6409
|
"--sigma",
|
|
6007
6410
|
"--workspace",
|
|
6008
6411
|
"--provider",
|
|
6009
|
-
"--account"
|
|
6412
|
+
"--account",
|
|
6413
|
+
"--palette",
|
|
6414
|
+
"--fixer-python",
|
|
6415
|
+
"--fixer-revision",
|
|
6416
|
+
"--min-grid-confidence",
|
|
6417
|
+
"--reviewer",
|
|
6418
|
+
"--note"
|
|
6010
6419
|
];
|
|
6011
6420
|
var BOOL_FLAGS = [
|
|
6012
6421
|
"--force",
|
|
@@ -6045,6 +6454,7 @@ var COMMANDS = [
|
|
|
6045
6454
|
"tag",
|
|
6046
6455
|
"balance",
|
|
6047
6456
|
"status",
|
|
6457
|
+
"refine",
|
|
6048
6458
|
"workspace",
|
|
6049
6459
|
"help",
|
|
6050
6460
|
"--help",
|
|
@@ -6053,6 +6463,7 @@ var COMMANDS = [
|
|
|
6053
6463
|
"-v"
|
|
6054
6464
|
];
|
|
6055
6465
|
var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
6466
|
+
var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
6056
6467
|
function parseArgs(argv) {
|
|
6057
6468
|
const [command = "help"] = argv;
|
|
6058
6469
|
if (!COMMANDS.includes(command)) {
|
|
@@ -6081,6 +6492,14 @@ function parseArgs(argv) {
|
|
|
6081
6492
|
}
|
|
6082
6493
|
rest = rest.slice(1);
|
|
6083
6494
|
}
|
|
6495
|
+
} else if (command === "refine") {
|
|
6496
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
6497
|
+
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
6498
|
+
throw new Error(
|
|
6499
|
+
`Unknown refine subcommand "${subcommand}". Known: ${REFINE_SUBCOMMANDS.join(", ")}`
|
|
6500
|
+
);
|
|
6501
|
+
}
|
|
6502
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
6084
6503
|
}
|
|
6085
6504
|
for (let i = 0; i < rest.length; i++) {
|
|
6086
6505
|
const token = rest[i];
|
|
@@ -6155,10 +6574,16 @@ function parseArgs(argv) {
|
|
|
6155
6574
|
}
|
|
6156
6575
|
return value;
|
|
6157
6576
|
};
|
|
6577
|
+
const rawGridConfidence = get("--min-grid-confidence");
|
|
6578
|
+
if (rawGridConfidence !== void 0 && rawGridConfidence !== "low" && rawGridConfidence !== "medium" && rawGridConfidence !== "high") {
|
|
6579
|
+
throw new Error(
|
|
6580
|
+
`--min-grid-confidence must be low, medium, or high, got "${rawGridConfidence}"`
|
|
6581
|
+
);
|
|
6582
|
+
}
|
|
6158
6583
|
return {
|
|
6159
6584
|
command,
|
|
6160
6585
|
manifest,
|
|
6161
|
-
lock: get("--lock") ??
|
|
6586
|
+
lock: get("--lock") ?? path20.join(path20.dirname(path20.resolve(manifest)), "pixelkiln.lock.json"),
|
|
6162
6587
|
explicitLock: get("--lock"),
|
|
6163
6588
|
styles: list("--style"),
|
|
6164
6589
|
assets: list("--only"),
|
|
@@ -6189,6 +6614,12 @@ function parseArgs(argv) {
|
|
|
6189
6614
|
minTransparency: numberOption("--min-transparency", { min: 0, max: 1 }),
|
|
6190
6615
|
maxColors: numberOption("--max-colors", { min: 1, integer: true }),
|
|
6191
6616
|
sigma: numberOption("--sigma", { min: Number.EPSILON }),
|
|
6617
|
+
palette: list("--palette"),
|
|
6618
|
+
fixerPython: get("--fixer-python"),
|
|
6619
|
+
fixerRevision: get("--fixer-revision"),
|
|
6620
|
+
minGridConfidence: rawGridConfidence,
|
|
6621
|
+
reviewer: get("--reviewer"),
|
|
6622
|
+
note: get("--note"),
|
|
6192
6623
|
subcommand,
|
|
6193
6624
|
workspace: get("--workspace"),
|
|
6194
6625
|
target,
|
|
@@ -6215,6 +6646,8 @@ Commands
|
|
|
6215
6646
|
salvage Triage account objects no lockfile claims. Recovers usable art.
|
|
6216
6647
|
One session per matching style unless --style forces a single one.
|
|
6217
6648
|
audit Measure how consistently a style's assets hold together. Offline.
|
|
6649
|
+
refine Recover a native pixel grid, enforce a final palette, audit it,
|
|
6650
|
+
and attach a verifiable human approval. run/approve/check. Offline.
|
|
6218
6651
|
cache Inspect local recovery/object-hash caches; optionally verify or prune.
|
|
6219
6652
|
pack Composite sprites into a PNG/atlas/provenance bundle. Offline.
|
|
6220
6653
|
mount Write a style's sprites into their declared cells of an existing
|
|
@@ -6240,6 +6673,12 @@ Options
|
|
|
6240
6673
|
--min-transparency <0..1> audit: minimum transparent canvas share
|
|
6241
6674
|
--max-colors <n> audit: maximum distinct opaque colors
|
|
6242
6675
|
--sigma <n> audit: relative outlier cutoff (default: 1.5)
|
|
6676
|
+
--palette <hexes> refine: final comma-separated #rrggbb colors (repeatable)
|
|
6677
|
+
--fixer-python <path> refine: Python with Pixel Art Fixer installed
|
|
6678
|
+
--fixer-revision <sha> refine: Pixel Art Fixer revision to record
|
|
6679
|
+
--min-grid-confidence <level> refine: high (default), medium, or low
|
|
6680
|
+
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
6681
|
+
--note <text> refine approve: optional review note
|
|
6243
6682
|
--prune cache: remove invalid/unreferenced local cache data
|
|
6244
6683
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
6245
6684
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
@@ -6269,6 +6708,9 @@ Examples
|
|
|
6269
6708
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
6270
6709
|
pixelkiln mount --style ground
|
|
6271
6710
|
pixelkiln export --style ground --only terrain --format tiled
|
|
6711
|
+
pixelkiln refine --from source.png --out final.png --palette "#101820,#f2aa4c"
|
|
6712
|
+
pixelkiln refine approve --from final.pixelkiln.json --reviewer "Your Name"
|
|
6713
|
+
pixelkiln refine check --from final.pixelkiln.json
|
|
6272
6714
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
6273
6715
|
pixelkiln workspace status --json
|
|
6274
6716
|
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
@@ -6316,7 +6758,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
|
6316
6758
|
if (!existsSync15(workspacePath)) {
|
|
6317
6759
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6318
6760
|
}
|
|
6319
|
-
const dir =
|
|
6761
|
+
const dir = path20.dirname(path20.resolve(workspacePath));
|
|
6320
6762
|
const ws = await loadWorkspace(workspacePath);
|
|
6321
6763
|
const diagnostics = validateWorkspace(ws, dir);
|
|
6322
6764
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -6337,13 +6779,79 @@ async function main() {
|
|
|
6337
6779
|
}
|
|
6338
6780
|
if (args.command === "--version" || args.command === "-v") {
|
|
6339
6781
|
const pkg = JSON.parse(
|
|
6340
|
-
await
|
|
6782
|
+
await readFile16(new URL("../package.json", import.meta.url), "utf8")
|
|
6341
6783
|
);
|
|
6342
6784
|
log(`${pkg.name} ${pkg.version}`);
|
|
6343
6785
|
return;
|
|
6344
6786
|
}
|
|
6787
|
+
if (args.command === "refine") {
|
|
6788
|
+
if (!args.from) {
|
|
6789
|
+
throw new Error(
|
|
6790
|
+
args.subcommand === "run" ? "refine needs --from <source.png>." : `refine ${args.subcommand} needs --from <output.pixelkiln.json>.`
|
|
6791
|
+
);
|
|
6792
|
+
}
|
|
6793
|
+
if (args.subcommand === "run") {
|
|
6794
|
+
if (!args.out) throw new Error("refine needs --out <final.png>.");
|
|
6795
|
+
if (!args.palette.length) {
|
|
6796
|
+
throw new Error("refine needs an explicit final palette via --palette <#rrggbb,...>.");
|
|
6797
|
+
}
|
|
6798
|
+
const result2 = await refineAsset({
|
|
6799
|
+
source: args.from,
|
|
6800
|
+
output: args.out,
|
|
6801
|
+
palette: args.palette,
|
|
6802
|
+
fixerPython: args.fixerPython,
|
|
6803
|
+
fixerRevision: args.fixerRevision,
|
|
6804
|
+
minGridConfidence: args.minGridConfidence,
|
|
6805
|
+
minTransparency: args.minTransparency,
|
|
6806
|
+
force: args.force
|
|
6807
|
+
});
|
|
6808
|
+
if (args.json) {
|
|
6809
|
+
log(JSON.stringify(result2, null, 2));
|
|
6810
|
+
} else {
|
|
6811
|
+
log(
|
|
6812
|
+
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
6813
|
+
);
|
|
6814
|
+
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
6815
|
+
log(` wrote ${path20.relative(process.cwd(), result2.output)}`);
|
|
6816
|
+
log(` quality record: ${path20.relative(process.cwd(), result2.record)}`);
|
|
6817
|
+
log(`
|
|
6818
|
+
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
6819
|
+
log(
|
|
6820
|
+
` pixelkiln refine approve --from ${path20.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
6821
|
+
);
|
|
6822
|
+
}
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
if (args.subcommand === "approve") {
|
|
6826
|
+
if (!args.reviewer?.trim()) {
|
|
6827
|
+
throw new Error("refine approve needs --reviewer <name>.");
|
|
6828
|
+
}
|
|
6829
|
+
log(` Inspect the final PNG at 1\xD7 and integer zoom.`);
|
|
6830
|
+
log(` Confirm crisp edges, readable forms, palette separation, alpha, and seams.`);
|
|
6831
|
+
if (!await confirm(" Record this asset as human-approved?", args.yes)) {
|
|
6832
|
+
log(" approval not recorded");
|
|
6833
|
+
return;
|
|
6834
|
+
}
|
|
6835
|
+
const result2 = await approveQualityRecord(args.from, {
|
|
6836
|
+
reviewer: args.reviewer,
|
|
6837
|
+
note: args.note
|
|
6838
|
+
});
|
|
6839
|
+
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
6840
|
+
else log(` approved ${path20.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
|
|
6841
|
+
return;
|
|
6842
|
+
}
|
|
6843
|
+
const result = await checkQualityRecord(args.from);
|
|
6844
|
+
if (args.json) {
|
|
6845
|
+
log(JSON.stringify(result, null, 2));
|
|
6846
|
+
} else {
|
|
6847
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path20.relative(process.cwd(), result.output || result.record)}`);
|
|
6848
|
+
for (const reason of result.reasons) log(` ${reason}`);
|
|
6849
|
+
}
|
|
6850
|
+
if (!result.safe) process.exitCode = 1;
|
|
6851
|
+
return;
|
|
6852
|
+
}
|
|
6345
6853
|
if (args.command === "balance") {
|
|
6346
|
-
loadEnvFiles(
|
|
6854
|
+
loadEnvFiles(path20.dirname(path20.resolve(args.manifest)));
|
|
6347
6855
|
loadEnvFiles(process.cwd());
|
|
6348
6856
|
const loaded2 = await loadManifest(args.manifest);
|
|
6349
6857
|
const p = createProvider(loaded2.manifest.provider, "online");
|
|
@@ -6355,31 +6863,31 @@ async function main() {
|
|
|
6355
6863
|
}
|
|
6356
6864
|
if (args.command === "init") {
|
|
6357
6865
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
6358
|
-
const root =
|
|
6866
|
+
const root = path20.resolve(args.from);
|
|
6359
6867
|
if (!existsSync15(root)) throw new Error(`No directory at ${root}`);
|
|
6360
6868
|
const generator = args.generator ?? "map";
|
|
6361
6869
|
if (generator !== "1dir" && generator !== "map") {
|
|
6362
6870
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
6363
6871
|
}
|
|
6364
|
-
const target =
|
|
6872
|
+
const target = path20.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
6365
6873
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
6366
6874
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
6367
6875
|
const manifest = buildManifest(
|
|
6368
|
-
args.name ??
|
|
6876
|
+
args.name ?? path20.basename(path20.dirname(target)),
|
|
6369
6877
|
args.styles[0] ?? "base",
|
|
6370
6878
|
generator,
|
|
6371
|
-
|
|
6879
|
+
path20.relative(path20.dirname(target), root) || ".",
|
|
6372
6880
|
assets
|
|
6373
6881
|
);
|
|
6374
6882
|
await writeManifestFile(target, manifest);
|
|
6375
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
6883
|
+
log(` scanned ${assets.length} PNG(s) under ${path20.relative(process.cwd(), root)}`);
|
|
6376
6884
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
6377
|
-
log(` wrote ${
|
|
6885
|
+
log(` wrote ${path20.relative(process.cwd(), target)}`);
|
|
6378
6886
|
log(`
|
|
6379
6887
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
6380
6888
|
log(` PixelLab account instead of inventing them:`);
|
|
6381
6889
|
log(`
|
|
6382
|
-
pixelkiln adopt --manifest ${
|
|
6890
|
+
pixelkiln adopt --manifest ${path20.relative(process.cwd(), target)} --write-prompts
|
|
6383
6891
|
`);
|
|
6384
6892
|
return;
|
|
6385
6893
|
}
|
|
@@ -6427,10 +6935,10 @@ async function main() {
|
|
|
6427
6935
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
6428
6936
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
6429
6937
|
}
|
|
6430
|
-
const raw = JSON.parse(await
|
|
6938
|
+
const raw = JSON.parse(await readFile16(path20.resolve(args.inputs), "utf8"));
|
|
6431
6939
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
6432
6940
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
6433
|
-
const base =
|
|
6941
|
+
const base = path20.resolve(args.out.replace(/\.png$/, ""));
|
|
6434
6942
|
const outputs = [
|
|
6435
6943
|
{ path: `${base}.png`, data: png },
|
|
6436
6944
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -6443,17 +6951,17 @@ async function main() {
|
|
|
6443
6951
|
log(
|
|
6444
6952
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
6445
6953
|
);
|
|
6446
|
-
log(` ${
|
|
6954
|
+
log(` ${path20.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
6447
6955
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6448
6956
|
return;
|
|
6449
6957
|
}
|
|
6450
6958
|
if (args.command === "workspace") {
|
|
6451
|
-
const workspacePath =
|
|
6452
|
-
const dir =
|
|
6959
|
+
const workspacePath = path20.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
6960
|
+
const dir = path20.dirname(workspacePath);
|
|
6453
6961
|
if (args.subcommand === "add") {
|
|
6454
|
-
const manifestPath =
|
|
6962
|
+
const manifestPath = path20.resolve(args.target);
|
|
6455
6963
|
const loadedTarget = await loadManifest(manifestPath);
|
|
6456
|
-
const lockPath = args.explicitLock ?
|
|
6964
|
+
const lockPath = args.explicitLock ? path20.resolve(args.explicitLock) : path20.join(path20.dirname(manifestPath), "pixelkiln.lock.json");
|
|
6457
6965
|
const ws = await loadWorkspace(workspacePath);
|
|
6458
6966
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
6459
6967
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -6473,7 +6981,7 @@ async function main() {
|
|
|
6473
6981
|
...args.account ? { account: args.account } : {}
|
|
6474
6982
|
};
|
|
6475
6983
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
6476
|
-
log(` registered "${id}" in ${
|
|
6984
|
+
log(` registered "${id}" in ${path20.relative(process.cwd(), workspacePath)}`);
|
|
6477
6985
|
log(` manifest: ${project.manifest}`);
|
|
6478
6986
|
log(` lock: ${project.lock}`);
|
|
6479
6987
|
if (!existsSync15(lockPath)) {
|
|
@@ -6485,7 +6993,7 @@ async function main() {
|
|
|
6485
6993
|
}
|
|
6486
6994
|
if (args.subcommand === "remove") {
|
|
6487
6995
|
const ws = await loadWorkspace(workspacePath);
|
|
6488
|
-
const resolvedTarget =
|
|
6996
|
+
const resolvedTarget = path20.resolve(args.target);
|
|
6489
6997
|
const match = ws.projects.find(
|
|
6490
6998
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
6491
6999
|
);
|
|
@@ -6496,7 +7004,7 @@ async function main() {
|
|
|
6496
7004
|
version: 1,
|
|
6497
7005
|
projects: ws.projects.filter((p) => p !== match)
|
|
6498
7006
|
});
|
|
6499
|
-
log(` removed "${match.id}" from ${
|
|
7007
|
+
log(` removed "${match.id}" from ${path20.relative(process.cwd(), workspacePath)}`);
|
|
6500
7008
|
return;
|
|
6501
7009
|
}
|
|
6502
7010
|
if (args.subcommand === "list") {
|
|
@@ -6508,9 +7016,9 @@ async function main() {
|
|
|
6508
7016
|
if (args.json) {
|
|
6509
7017
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
6510
7018
|
} else if (!ws.projects.length) {
|
|
6511
|
-
log(` no projects registered in ${
|
|
7019
|
+
log(` no projects registered in ${path20.relative(process.cwd(), workspacePath)}`);
|
|
6512
7020
|
} else {
|
|
6513
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
7021
|
+
log(` ${ws.projects.length} project(s) in ${path20.relative(process.cwd(), workspacePath)}:`);
|
|
6514
7022
|
for (const p of ws.projects) {
|
|
6515
7023
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
6516
7024
|
}
|
|
@@ -6528,7 +7036,7 @@ async function main() {
|
|
|
6528
7036
|
if (args.json) {
|
|
6529
7037
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
6530
7038
|
} else {
|
|
6531
|
-
log(` workspace: ${
|
|
7039
|
+
log(` workspace: ${path20.relative(process.cwd(), workspacePath)}`);
|
|
6532
7040
|
for (const p of report.projects) {
|
|
6533
7041
|
if (p.error) {
|
|
6534
7042
|
log(`
|
|
@@ -6572,14 +7080,14 @@ async function main() {
|
|
|
6572
7080
|
return;
|
|
6573
7081
|
}
|
|
6574
7082
|
}
|
|
6575
|
-
if (!existsSync15(
|
|
7083
|
+
if (!existsSync15(path20.resolve(args.manifest))) {
|
|
6576
7084
|
throw new Error(
|
|
6577
|
-
`No manifest at ${
|
|
7085
|
+
`No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
6578
7086
|
);
|
|
6579
7087
|
}
|
|
6580
|
-
const manifestDir =
|
|
7088
|
+
const manifestDir = path20.dirname(path20.resolve(args.manifest));
|
|
6581
7089
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
6582
|
-
if (
|
|
7090
|
+
if (path20.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
6583
7091
|
const loaded = await loadManifest(args.manifest);
|
|
6584
7092
|
const estimator = createProvider(loaded.manifest.provider, "offline");
|
|
6585
7093
|
const specs = await resolveSpecs(loaded, {
|
|
@@ -6714,7 +7222,7 @@ async function main() {
|
|
|
6714
7222
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
6715
7223
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
6716
7224
|
}
|
|
6717
|
-
const manifestDir2 =
|
|
7225
|
+
const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
|
|
6718
7226
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
6719
7227
|
for (const styleId of styleIds) {
|
|
6720
7228
|
const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
|
|
@@ -6723,7 +7231,7 @@ async function main() {
|
|
|
6723
7231
|
primaryOnly: args.primaryOnly
|
|
6724
7232
|
});
|
|
6725
7233
|
const style = loaded.manifest.styles[styleId];
|
|
6726
|
-
const base = args.out ?
|
|
7234
|
+
const base = args.out ? path20.resolve(args.out.replace(/\.png$/, "")) : path20.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
6727
7235
|
const outputs = [
|
|
6728
7236
|
{ path: `${base}.png`, data: png },
|
|
6729
7237
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -6746,13 +7254,13 @@ async function main() {
|
|
|
6746
7254
|
log(
|
|
6747
7255
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
6748
7256
|
);
|
|
6749
|
-
log(` ${
|
|
7257
|
+
log(` ${path20.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
6750
7258
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6751
7259
|
}
|
|
6752
7260
|
return;
|
|
6753
7261
|
}
|
|
6754
7262
|
if (args.command === "mount") {
|
|
6755
|
-
const manifestDir2 =
|
|
7263
|
+
const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
|
|
6756
7264
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
6757
7265
|
for (const styleId of styleIds) {
|
|
6758
7266
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -6782,7 +7290,7 @@ async function main() {
|
|
|
6782
7290
|
sources,
|
|
6783
7291
|
outputRoles
|
|
6784
7292
|
);
|
|
6785
|
-
const out =
|
|
7293
|
+
const out = path20.resolve(manifestDir2, style.mount.out);
|
|
6786
7294
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
6787
7295
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
6788
7296
|
const outputs = [
|
|
@@ -6795,7 +7303,7 @@ async function main() {
|
|
|
6795
7303
|
await provenanceFile("$manifest", args.manifest),
|
|
6796
7304
|
await provenanceFile("$lock", args.lock),
|
|
6797
7305
|
...artifactSources.filter(
|
|
6798
|
-
(source) => source.id !== "$base" ||
|
|
7306
|
+
(source) => source.id !== "$base" || path20.resolve(source.path) !== out
|
|
6799
7307
|
)
|
|
6800
7308
|
],
|
|
6801
7309
|
options: {
|
|
@@ -6808,7 +7316,7 @@ async function main() {
|
|
|
6808
7316
|
log(
|
|
6809
7317
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
6810
7318
|
);
|
|
6811
|
-
log(` ${
|
|
7319
|
+
log(` ${path20.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
6812
7320
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6813
7321
|
}
|
|
6814
7322
|
return;
|
|
@@ -6864,7 +7372,7 @@ async function main() {
|
|
|
6864
7372
|
}
|
|
6865
7373
|
if (args.command === "export") {
|
|
6866
7374
|
const format = args.format ?? "generic";
|
|
6867
|
-
const manifestDir2 =
|
|
7375
|
+
const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
|
|
6868
7376
|
const selected = specs.filter((spec) => {
|
|
6869
7377
|
if (spec.generator !== "tiles") return false;
|
|
6870
7378
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -6880,12 +7388,12 @@ async function main() {
|
|
|
6880
7388
|
for (const spec of selected) {
|
|
6881
7389
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
6882
7390
|
const style = loaded.manifest.styles[spec.styleId];
|
|
6883
|
-
const defaultBase =
|
|
6884
|
-
const base = args.out ?
|
|
7391
|
+
const defaultBase = path20.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
7392
|
+
const base = args.out ? path20.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
6885
7393
|
const result = exportTileset(entry, spec, {
|
|
6886
7394
|
format,
|
|
6887
7395
|
manifestDir: manifestDir2,
|
|
6888
|
-
imageName:
|
|
7396
|
+
imageName: path20.basename(`${base}.png`),
|
|
6889
7397
|
columns: args.columns
|
|
6890
7398
|
});
|
|
6891
7399
|
const outputs = [
|
|
@@ -6903,7 +7411,7 @@ async function main() {
|
|
|
6903
7411
|
asset: spec.assetId,
|
|
6904
7412
|
columns: args.columns ?? null,
|
|
6905
7413
|
format,
|
|
6906
|
-
image:
|
|
7414
|
+
image: path20.basename(`${base}.png`),
|
|
6907
7415
|
providerRules: result.generic.providerRules,
|
|
6908
7416
|
style: spec.styleId,
|
|
6909
7417
|
tileType: spec.tileType ?? null
|
|
@@ -6913,7 +7421,7 @@ async function main() {
|
|
|
6913
7421
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
6914
7422
|
);
|
|
6915
7423
|
log(
|
|
6916
|
-
` ${
|
|
7424
|
+
` ${path20.relative(process.cwd(), base)}.png + ${path20.basename(base)}${result.extension} + .pixelkiln.json`
|
|
6917
7425
|
);
|
|
6918
7426
|
}
|
|
6919
7427
|
return;
|
|
@@ -6953,10 +7461,10 @@ async function main() {
|
|
|
6953
7461
|
tagged ${n} object(s) upstream`);
|
|
6954
7462
|
}
|
|
6955
7463
|
if (args.writePrompts) {
|
|
6956
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
7464
|
+
const { filled, stillEmpty } = await writePromptsBack(path20.resolve(args.manifest), lock, {
|
|
6957
7465
|
onProgress: log
|
|
6958
7466
|
});
|
|
6959
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
7467
|
+
log(` recovered ${filled} prompt(s) into ${path20.relative(process.cwd(), args.manifest)}`);
|
|
6960
7468
|
const reloaded = await loadManifest(args.manifest);
|
|
6961
7469
|
const rebased = await resolveSpecs(reloaded, {
|
|
6962
7470
|
styles: args.styles,
|
|
@@ -6985,12 +7493,12 @@ async function main() {
|
|
|
6985
7493
|
if (args.command === "salvage") {
|
|
6986
7494
|
const jsonMode = args.dryRun && args.json;
|
|
6987
7495
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
6988
|
-
const ownLock =
|
|
7496
|
+
const ownLock = path20.resolve(args.lock);
|
|
6989
7497
|
let workspaceProjects = [];
|
|
6990
7498
|
let workspaceDir = "";
|
|
6991
7499
|
if (args.workspace) {
|
|
6992
|
-
const workspacePath =
|
|
6993
|
-
workspaceDir =
|
|
7500
|
+
const workspacePath = path20.resolve(args.workspace);
|
|
7501
|
+
workspaceDir = path20.dirname(workspacePath);
|
|
6994
7502
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
6995
7503
|
workspaceProjects = complete.ws.projects;
|
|
6996
7504
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -7000,11 +7508,11 @@ async function main() {
|
|
|
7000
7508
|
.../* @__PURE__ */ new Set([
|
|
7001
7509
|
...workspaceLockPaths,
|
|
7002
7510
|
...existsSync15(ownLock) ? [ownLock] : [],
|
|
7003
|
-
...args.claims.map((c) =>
|
|
7511
|
+
...args.claims.map((c) => path20.resolve(c))
|
|
7004
7512
|
])
|
|
7005
7513
|
];
|
|
7006
7514
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
7007
|
-
for (const p of lockPaths) diag(` ${
|
|
7515
|
+
for (const p of lockPaths) diag(` ${path20.relative(process.cwd(), p)}`);
|
|
7008
7516
|
if (!args.claims.length && !args.workspace) {
|
|
7009
7517
|
diag(
|
|
7010
7518
|
`
|
|
@@ -7012,7 +7520,7 @@ async function main() {
|
|
|
7012
7520
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
7013
7521
|
register every project in a workspace catalog and pass --workspace.`
|
|
7014
7522
|
);
|
|
7015
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
7523
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path20.resolve(args.manifest))) {
|
|
7016
7524
|
diag(
|
|
7017
7525
|
`
|
|
7018
7526
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -7096,7 +7604,7 @@ async function main() {
|
|
|
7096
7604
|
manifestPath: loaded.path,
|
|
7097
7605
|
manifest: loaded.manifest,
|
|
7098
7606
|
styleId,
|
|
7099
|
-
importDir:
|
|
7607
|
+
importDir: path20.resolve(loaded.root, style.outDir),
|
|
7100
7608
|
lock,
|
|
7101
7609
|
lockPath: args.lock
|
|
7102
7610
|
},
|