pixelkiln 0.2.0 → 0.3.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 +48 -0
- package/NAMING.md +15 -15
- package/PROVIDERS.md +13 -13
- package/README.md +22 -9
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +588 -142
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +291 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +168 -1
- package/dist/index.d.ts +168 -1
- package/dist/index.js +280 -21
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CLI.md +47 -0
- package/docs/ENDPOINTS.md +39 -38
- package/docs/GENERATORS.md +1 -1
- package/docs/RECOVERY.md +44 -0
- package/docs/TILES.md +1 -1
- package/examples/minimal/README.md +2 -2
- package/package.json +1 -1
- package/schema/workspace.schema.json +54 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { readFile as
|
|
4
|
+
import path18 from "path";
|
|
5
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -154,11 +154,11 @@ var PixelLabClient = class {
|
|
|
154
154
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
155
155
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
156
156
|
*/
|
|
157
|
-
async request(
|
|
157
|
+
async request(path19, init, attempt = 0) {
|
|
158
158
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
159
159
|
let res;
|
|
160
160
|
try {
|
|
161
|
-
res = await fetch(`${BASE}${
|
|
161
|
+
res = await fetch(`${BASE}${path19}`, {
|
|
162
162
|
...init,
|
|
163
163
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
164
164
|
headers: {
|
|
@@ -170,24 +170,24 @@ var PixelLabClient = class {
|
|
|
170
170
|
} catch (err) {
|
|
171
171
|
if (attempt < MAX_RETRIES) {
|
|
172
172
|
await sleep(backoffMs(attempt));
|
|
173
|
-
return this.request(
|
|
173
|
+
return this.request(path19, init, attempt + 1);
|
|
174
174
|
}
|
|
175
175
|
throw err;
|
|
176
176
|
}
|
|
177
177
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
178
178
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
179
179
|
await sleep(waitMs);
|
|
180
|
-
return this.request(
|
|
180
|
+
return this.request(path19, init, attempt + 1);
|
|
181
181
|
}
|
|
182
182
|
const text = await res.text();
|
|
183
183
|
if (!res.ok) {
|
|
184
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
184
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path19} \u2192 ${res.status}`, res.status, text);
|
|
185
185
|
}
|
|
186
186
|
if (!text) return {};
|
|
187
187
|
try {
|
|
188
188
|
return JSON.parse(text);
|
|
189
189
|
} catch {
|
|
190
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
190
|
+
throw new Error(`${init?.method ?? "GET"} ${path19} returned invalid JSON`);
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
async balance() {
|
|
@@ -1353,8 +1353,8 @@ import { readFile } from "fs/promises";
|
|
|
1353
1353
|
function sha256(data) {
|
|
1354
1354
|
return createHash("sha256").update(data).digest("hex");
|
|
1355
1355
|
}
|
|
1356
|
-
async function sha256File(
|
|
1357
|
-
return sha256(await readFile(
|
|
1356
|
+
async function sha256File(path19) {
|
|
1357
|
+
return sha256(await readFile(path19));
|
|
1358
1358
|
}
|
|
1359
1359
|
function specHash(spec, styleImageHashes) {
|
|
1360
1360
|
return sha256(
|
|
@@ -2715,10 +2715,10 @@ function isSha256Hash(value) {
|
|
|
2715
2715
|
function parseCache(value) {
|
|
2716
2716
|
return HashCacheSchema.parse(value);
|
|
2717
2717
|
}
|
|
2718
|
-
async function loadCache(
|
|
2719
|
-
if (!existsSync8(
|
|
2718
|
+
async function loadCache(path19) {
|
|
2719
|
+
if (!existsSync8(path19)) return { version: 1, hashes: {} };
|
|
2720
2720
|
try {
|
|
2721
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(
|
|
2721
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(path19, "utf8")));
|
|
2722
2722
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
2723
2723
|
return {
|
|
2724
2724
|
version: 1,
|
|
@@ -2730,18 +2730,18 @@ async function loadCache(path17) {
|
|
|
2730
2730
|
return { version: 1, hashes: {} };
|
|
2731
2731
|
}
|
|
2732
2732
|
}
|
|
2733
|
-
async function saveCache(
|
|
2733
|
+
async function saveCache(path19, cache) {
|
|
2734
2734
|
const sorted = {};
|
|
2735
2735
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
2736
2736
|
const hash = cache.hashes[key];
|
|
2737
2737
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
2738
2738
|
sorted[key] = hash;
|
|
2739
2739
|
}
|
|
2740
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
2741
|
-
const tmp = `${
|
|
2740
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path19)), { recursive: true });
|
|
2741
|
+
const tmp = `${path19}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2742
2742
|
try {
|
|
2743
2743
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
2744
|
-
await rename3(tmp,
|
|
2744
|
+
await rename3(tmp, path19);
|
|
2745
2745
|
} finally {
|
|
2746
2746
|
await rm3(tmp, { force: true });
|
|
2747
2747
|
}
|
|
@@ -3382,6 +3382,7 @@ async function writeManifestFile(target, manifest) {
|
|
|
3382
3382
|
// src/pipeline/salvage.ts
|
|
3383
3383
|
import { readFile as readFile8 } from "fs/promises";
|
|
3384
3384
|
import { existsSync as existsSync11 } from "fs";
|
|
3385
|
+
import path12 from "path";
|
|
3385
3386
|
async function loadClaims(lockPaths) {
|
|
3386
3387
|
const claimed = /* @__PURE__ */ new Set();
|
|
3387
3388
|
for (const p of lockPaths) {
|
|
@@ -3429,6 +3430,26 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
3429
3430
|
}
|
|
3430
3431
|
return null;
|
|
3431
3432
|
}
|
|
3433
|
+
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
3434
|
+
const own = path12.resolve(ownManifestPath);
|
|
3435
|
+
const siblingManifestPaths = [
|
|
3436
|
+
.../* @__PURE__ */ new Set([
|
|
3437
|
+
...workspaceManifestPaths,
|
|
3438
|
+
...claimPaths.map((c) => path12.join(path12.dirname(path12.resolve(c)), "pixelkiln.manifest.json"))
|
|
3439
|
+
])
|
|
3440
|
+
];
|
|
3441
|
+
const siblings = [];
|
|
3442
|
+
for (const siblingManifestPath of siblingManifestPaths) {
|
|
3443
|
+
if (path12.resolve(siblingManifestPath) === own) continue;
|
|
3444
|
+
if (!existsSync11(siblingManifestPath)) continue;
|
|
3445
|
+
try {
|
|
3446
|
+
const { manifest } = await loadManifest(siblingManifestPath);
|
|
3447
|
+
siblings.push({ label: path12.basename(path12.dirname(siblingManifestPath)), manifest });
|
|
3448
|
+
} catch {
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
return siblings;
|
|
3452
|
+
}
|
|
3432
3453
|
function groupOrphansByStyle(orphans, manifest, siblings = []) {
|
|
3433
3454
|
const matched = /* @__PURE__ */ new Map();
|
|
3434
3455
|
const elsewhere = /* @__PURE__ */ new Map();
|
|
@@ -3538,10 +3559,237 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
3538
3559
|
return { tagged, failed };
|
|
3539
3560
|
}
|
|
3540
3561
|
|
|
3541
|
-
// src/
|
|
3542
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
3562
|
+
// src/workspace.ts
|
|
3563
|
+
import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
|
|
3543
3564
|
import { existsSync as existsSync12 } from "fs";
|
|
3544
|
-
import
|
|
3565
|
+
import path13 from "path";
|
|
3566
|
+
import { z as z4 } from "zod";
|
|
3567
|
+
var WorkspaceProjectSchema = z4.object({
|
|
3568
|
+
id: z4.string().min(1),
|
|
3569
|
+
/** Manifest path, relative to the catalog file's own directory. */
|
|
3570
|
+
manifest: z4.string().min(1),
|
|
3571
|
+
/** Lockfile path, relative to the catalog file's own directory. */
|
|
3572
|
+
lock: z4.string().min(1),
|
|
3573
|
+
provider: z4.string().min(1).default("pixellab"),
|
|
3574
|
+
/** Free-form label for a shared account, e.g. distinguishing sandboxes. */
|
|
3575
|
+
account: z4.string().optional()
|
|
3576
|
+
}).strict();
|
|
3577
|
+
var WorkspaceSchema = z4.object({
|
|
3578
|
+
version: z4.literal(1),
|
|
3579
|
+
projects: z4.array(WorkspaceProjectSchema).default([])
|
|
3580
|
+
}).strict();
|
|
3581
|
+
function parseWorkspace(raw) {
|
|
3582
|
+
const parsed = WorkspaceSchema.safeParse(raw);
|
|
3583
|
+
if (parsed.success) return parsed.data;
|
|
3584
|
+
throw new Error(
|
|
3585
|
+
`Workspace catalog is not valid v1:
|
|
3586
|
+
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
async function loadWorkspace(workspacePath) {
|
|
3590
|
+
if (!existsSync12(workspacePath)) return { version: 1, projects: [] };
|
|
3591
|
+
let raw;
|
|
3592
|
+
try {
|
|
3593
|
+
raw = JSON.parse(await readFile9(workspacePath, "utf8"));
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
throw new Error(
|
|
3596
|
+
`Workspace catalog at ${workspacePath} is malformed:
|
|
3597
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
return parseWorkspace(raw);
|
|
3601
|
+
}
|
|
3602
|
+
async function saveWorkspace(workspacePath, ws) {
|
|
3603
|
+
const sorted = {
|
|
3604
|
+
version: 1,
|
|
3605
|
+
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
3606
|
+
};
|
|
3607
|
+
await mkdir4(path13.dirname(path13.resolve(workspacePath)), { recursive: true });
|
|
3608
|
+
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3609
|
+
try {
|
|
3610
|
+
await writeFile6(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
3611
|
+
await rename4(tmp, workspacePath);
|
|
3612
|
+
} finally {
|
|
3613
|
+
await rm4(tmp, { force: true });
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function toPortablePath(dir, absolute) {
|
|
3617
|
+
return path13.relative(dir, absolute).split(path13.sep).join("/");
|
|
3618
|
+
}
|
|
3619
|
+
function resolveProject(dir, project) {
|
|
3620
|
+
return {
|
|
3621
|
+
manifestPath: path13.resolve(dir, project.manifest.split("/").join(path13.sep)),
|
|
3622
|
+
lockPath: path13.resolve(dir, project.lock.split("/").join(path13.sep))
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
function validateWorkspace(ws, dir) {
|
|
3626
|
+
const diagnostics = [];
|
|
3627
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
3628
|
+
const lockOwners = /* @__PURE__ */ new Map();
|
|
3629
|
+
const manifestOwners = /* @__PURE__ */ new Map();
|
|
3630
|
+
for (const project of ws.projects) {
|
|
3631
|
+
idCounts.set(project.id, (idCounts.get(project.id) ?? 0) + 1);
|
|
3632
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
3633
|
+
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
3634
|
+
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
3635
|
+
if (path13.isAbsolute(project.manifest) || path13.isAbsolute(project.lock)) {
|
|
3636
|
+
diagnostics.push({
|
|
3637
|
+
id: "absolute-path",
|
|
3638
|
+
level: "warning",
|
|
3639
|
+
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
if (!existsSync12(manifestPath)) {
|
|
3643
|
+
diagnostics.push({
|
|
3644
|
+
id: "missing-manifest",
|
|
3645
|
+
level: "error",
|
|
3646
|
+
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
if (!existsSync12(lockPath)) {
|
|
3650
|
+
diagnostics.push({
|
|
3651
|
+
id: "missing-lock",
|
|
3652
|
+
level: "error",
|
|
3653
|
+
message: `project "${project.id}" lockfile not found: ${lockPath}`
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
for (const [id, count] of idCounts) {
|
|
3658
|
+
if (count > 1) {
|
|
3659
|
+
diagnostics.push({
|
|
3660
|
+
id: "duplicate-id",
|
|
3661
|
+
level: "error",
|
|
3662
|
+
message: `project id "${id}" is registered ${count} times`
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
for (const [lockPath, ids] of lockOwners) {
|
|
3667
|
+
if (ids.length > 1) {
|
|
3668
|
+
diagnostics.push({
|
|
3669
|
+
id: "duplicate-lock",
|
|
3670
|
+
level: "error",
|
|
3671
|
+
message: `${ids.join(", ")} all register the same lockfile: ${lockPath}`
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
for (const [manifestPath, ids] of manifestOwners) {
|
|
3676
|
+
if (ids.length > 1) {
|
|
3677
|
+
diagnostics.push({
|
|
3678
|
+
id: "duplicate-manifest",
|
|
3679
|
+
level: "warning",
|
|
3680
|
+
message: `${ids.join(", ")} share manifest ${manifestPath} \u2014 expected only when they are variant lockfiles beside one manifest`
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
const providers = new Set(ws.projects.map((p) => p.provider));
|
|
3685
|
+
if (providers.size > 1) {
|
|
3686
|
+
diagnostics.push({
|
|
3687
|
+
id: "mixed-provider",
|
|
3688
|
+
level: "warning",
|
|
3689
|
+
message: `registered projects use different providers: ${[...providers].sort().join(", ")} \u2014 spend totals are kept separate per unit, but confirm this is intentional`
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
return diagnostics;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// src/pipeline/workspace.ts
|
|
3696
|
+
async function workspaceClaims(ws, dir) {
|
|
3697
|
+
const lockPaths = [];
|
|
3698
|
+
const byProject = {};
|
|
3699
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3700
|
+
for (const project of ws.projects) {
|
|
3701
|
+
const { lockPath } = resolveProject(dir, project);
|
|
3702
|
+
lockPaths.push(lockPath);
|
|
3703
|
+
let projectClaims;
|
|
3704
|
+
try {
|
|
3705
|
+
projectClaims = await loadClaims([lockPath]);
|
|
3706
|
+
} catch (err) {
|
|
3707
|
+
throw new Error(
|
|
3708
|
+
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
byProject[project.id] = projectClaims.size;
|
|
3712
|
+
for (const id of projectClaims) claimed.add(id);
|
|
3713
|
+
}
|
|
3714
|
+
return { claimed, byProject, lockPaths };
|
|
3715
|
+
}
|
|
3716
|
+
function emptyStateCounts() {
|
|
3717
|
+
return {
|
|
3718
|
+
ok: 0,
|
|
3719
|
+
missing: 0,
|
|
3720
|
+
untracked: 0,
|
|
3721
|
+
stale: 0,
|
|
3722
|
+
orphaned: 0,
|
|
3723
|
+
"in-flight": 0,
|
|
3724
|
+
recoverable: 0,
|
|
3725
|
+
failed: 0
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
async function workspaceStatus(ws, dir) {
|
|
3729
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
3730
|
+
const provider = PixelLabProvider.forOffline();
|
|
3731
|
+
const projects = [];
|
|
3732
|
+
const totalsByState = emptyStateCounts();
|
|
3733
|
+
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
3734
|
+
for (const project of ws.projects) {
|
|
3735
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
3736
|
+
const base = {
|
|
3737
|
+
id: project.id,
|
|
3738
|
+
provider: project.provider,
|
|
3739
|
+
account: project.account ?? null,
|
|
3740
|
+
manifest: manifestPath,
|
|
3741
|
+
lock: lockPath
|
|
3742
|
+
};
|
|
3743
|
+
try {
|
|
3744
|
+
const loaded = await loadManifest(manifestPath);
|
|
3745
|
+
const specs = await resolveSpecs(loaded, { provider });
|
|
3746
|
+
const lock = await loadLock(lockPath);
|
|
3747
|
+
normalizeLockOutputPaths(lock, specs);
|
|
3748
|
+
const plan = await buildPlan(specs, lock);
|
|
3749
|
+
const byState = summarize(plan);
|
|
3750
|
+
const spend = spendByUnit(lock);
|
|
3751
|
+
for (const state of Object.keys(byState)) {
|
|
3752
|
+
totalsByState[state] += byState[state];
|
|
3753
|
+
}
|
|
3754
|
+
for (const unit of Object.keys(spend)) {
|
|
3755
|
+
totalsSpend[unit] += spend[unit];
|
|
3756
|
+
}
|
|
3757
|
+
projects.push({
|
|
3758
|
+
...base,
|
|
3759
|
+
entries: Object.keys(lock.entries).length,
|
|
3760
|
+
byState,
|
|
3761
|
+
spendByUnit: spend,
|
|
3762
|
+
error: null
|
|
3763
|
+
});
|
|
3764
|
+
} catch (err) {
|
|
3765
|
+
projects.push({
|
|
3766
|
+
...base,
|
|
3767
|
+
entries: 0,
|
|
3768
|
+
byState: emptyStateCounts(),
|
|
3769
|
+
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
3770
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
let claims = 0;
|
|
3775
|
+
try {
|
|
3776
|
+
claims = (await workspaceClaims(ws, dir)).claimed.size;
|
|
3777
|
+
} catch {
|
|
3778
|
+
}
|
|
3779
|
+
return {
|
|
3780
|
+
version: 1,
|
|
3781
|
+
safe: !diagnostics.some((d) => d.level === "error") && projects.every((p) => !p.error),
|
|
3782
|
+
dir,
|
|
3783
|
+
projects,
|
|
3784
|
+
totals: { byState: totalsByState, spendByUnit: totalsSpend, claims },
|
|
3785
|
+
diagnostics
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// src/pipeline/audit.ts
|
|
3790
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3791
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3792
|
+
import path14 from "path";
|
|
3545
3793
|
function colorDistance(a, b) {
|
|
3546
3794
|
const rmean = (a.r + b.r) / 2;
|
|
3547
3795
|
const dr = a.r - b.r;
|
|
@@ -3586,12 +3834,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3586
3834
|
const unreadable = [];
|
|
3587
3835
|
for (const spec of mine) {
|
|
3588
3836
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
3589
|
-
if (!
|
|
3837
|
+
if (!existsSync13(output.absolutePath)) {
|
|
3590
3838
|
missing.push(output.id);
|
|
3591
3839
|
continue;
|
|
3592
3840
|
}
|
|
3593
3841
|
try {
|
|
3594
|
-
const png = decodePng(await
|
|
3842
|
+
const png = decodePng(await readFile10(output.absolutePath));
|
|
3595
3843
|
const palette = extractPalette(png, 12);
|
|
3596
3844
|
assets.push({
|
|
3597
3845
|
assetId: spec.assetId,
|
|
@@ -3615,10 +3863,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3615
3863
|
let referenceFromStyleImages = false;
|
|
3616
3864
|
const refPalettes = [];
|
|
3617
3865
|
for (const img of style.styleImages) {
|
|
3618
|
-
const abs =
|
|
3619
|
-
if (!
|
|
3866
|
+
const abs = path14.resolve(loaded.root, img.path);
|
|
3867
|
+
if (!existsSync13(abs)) continue;
|
|
3620
3868
|
try {
|
|
3621
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
3869
|
+
refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
|
|
3622
3870
|
} catch {
|
|
3623
3871
|
}
|
|
3624
3872
|
}
|
|
@@ -3694,16 +3942,16 @@ function hex(c) {
|
|
|
3694
3942
|
}
|
|
3695
3943
|
|
|
3696
3944
|
// src/pipeline/cache-health.ts
|
|
3697
|
-
import { existsSync as
|
|
3698
|
-
import { readFile as
|
|
3699
|
-
import
|
|
3945
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3946
|
+
import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
|
|
3947
|
+
import path15 from "path";
|
|
3700
3948
|
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3701
3949
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
3702
|
-
if (options.prune && !
|
|
3703
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
3950
|
+
if (options.prune && !existsSync14(lockPath)) {
|
|
3951
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path15.resolve(lockPath)}`);
|
|
3704
3952
|
}
|
|
3705
|
-
const contentDir =
|
|
3706
|
-
const remotePath =
|
|
3953
|
+
const contentDir = path15.resolve(path15.dirname(lockPath), ".pixelkiln", "cache");
|
|
3954
|
+
const remotePath = path15.resolve(cachePathFor(lockPath));
|
|
3707
3955
|
const referenced = new Set(
|
|
3708
3956
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
3709
3957
|
);
|
|
@@ -3717,7 +3965,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3717
3965
|
]);
|
|
3718
3966
|
for (const name of names) {
|
|
3719
3967
|
try {
|
|
3720
|
-
await
|
|
3968
|
+
await rm5(path15.join(contentDir, name), { force: true });
|
|
3721
3969
|
removed.contentFiles++;
|
|
3722
3970
|
} catch {
|
|
3723
3971
|
}
|
|
@@ -3726,7 +3974,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3726
3974
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
3727
3975
|
removed.resetRemoteHashCache = true;
|
|
3728
3976
|
} else if (remoteHashes.invalidIds.length) {
|
|
3729
|
-
const cache = parseCache(JSON.parse(await
|
|
3977
|
+
const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
3730
3978
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
3731
3979
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
3732
3980
|
await saveCache(remotePath, cache);
|
|
@@ -3744,7 +3992,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3744
3992
|
async function inspectContentCache(contentDir, referenced) {
|
|
3745
3993
|
const report = {
|
|
3746
3994
|
path: contentDir,
|
|
3747
|
-
exists:
|
|
3995
|
+
exists: existsSync14(contentDir),
|
|
3748
3996
|
files: 0,
|
|
3749
3997
|
bytes: 0,
|
|
3750
3998
|
valid: 0,
|
|
@@ -3765,11 +4013,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3765
4013
|
continue;
|
|
3766
4014
|
}
|
|
3767
4015
|
report.files++;
|
|
3768
|
-
const file =
|
|
4016
|
+
const file = path15.join(contentDir, entry.name);
|
|
3769
4017
|
const expected = entry.name.endsWith(".png") ? entry.name.slice(0, -4) : "";
|
|
3770
4018
|
let bytes;
|
|
3771
4019
|
try {
|
|
3772
|
-
bytes = await
|
|
4020
|
+
bytes = await readFile11(file);
|
|
3773
4021
|
report.bytes += bytes.length;
|
|
3774
4022
|
} catch (err) {
|
|
3775
4023
|
report.invalid.push({
|
|
@@ -3811,7 +4059,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3811
4059
|
async function inspectRemoteHashCache(remotePath) {
|
|
3812
4060
|
const report = {
|
|
3813
4061
|
path: remotePath,
|
|
3814
|
-
exists:
|
|
4062
|
+
exists: existsSync14(remotePath),
|
|
3815
4063
|
entries: 0,
|
|
3816
4064
|
valid: 0,
|
|
3817
4065
|
invalidIds: [],
|
|
@@ -3820,7 +4068,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3820
4068
|
if (!report.exists) return report;
|
|
3821
4069
|
let cache;
|
|
3822
4070
|
try {
|
|
3823
|
-
cache = parseCache(JSON.parse(await
|
|
4071
|
+
cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
3824
4072
|
} catch (err) {
|
|
3825
4073
|
report.error = err instanceof Error ? err.message : String(err);
|
|
3826
4074
|
return report;
|
|
@@ -4091,8 +4339,8 @@ function isRecord(value) {
|
|
|
4091
4339
|
}
|
|
4092
4340
|
|
|
4093
4341
|
// src/pick/salvage-server.ts
|
|
4094
|
-
import { mkdir as
|
|
4095
|
-
import
|
|
4342
|
+
import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
|
|
4343
|
+
import path16 from "path";
|
|
4096
4344
|
|
|
4097
4345
|
// src/pick/salvage-sheet.ts
|
|
4098
4346
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -4257,7 +4505,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4257
4505
|
});
|
|
4258
4506
|
const html = renderSalvageSheet(orphans, {
|
|
4259
4507
|
styleId: ctx.styleId,
|
|
4260
|
-
importDir:
|
|
4508
|
+
importDir: path16.relative(process.cwd(), ctx.importDir) || "."
|
|
4261
4509
|
});
|
|
4262
4510
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
4263
4511
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -4286,10 +4534,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4286
4534
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE3)) throw new Error("not a PNG");
|
|
4287
4535
|
decodePng(buf);
|
|
4288
4536
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4289
|
-
const rel =
|
|
4290
|
-
const outFile =
|
|
4291
|
-
await
|
|
4292
|
-
await
|
|
4537
|
+
const rel = path16.join("_salvaged", `${assetId}.png`);
|
|
4538
|
+
const outFile = path16.resolve(ctx.importDir, rel);
|
|
4539
|
+
await mkdir5(path16.dirname(outFile), { recursive: true });
|
|
4540
|
+
await writeFile7(outFile, buf);
|
|
4293
4541
|
ctx.manifest.assets[assetId] = {
|
|
4294
4542
|
prompt: orphan.prompt,
|
|
4295
4543
|
promptByStyle: {},
|
|
@@ -4315,7 +4563,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4315
4563
|
error: null,
|
|
4316
4564
|
sourceUrl: orphan.previewUrl,
|
|
4317
4565
|
outputs: [{
|
|
4318
|
-
path: portableOutputPath(outFile,
|
|
4566
|
+
path: portableOutputPath(outFile, path16.dirname(ctx.manifestPath)),
|
|
4319
4567
|
sha256: sha256(buf)
|
|
4320
4568
|
}],
|
|
4321
4569
|
submittedAt: orphan.createdAt,
|
|
@@ -4339,9 +4587,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4339
4587
|
}
|
|
4340
4588
|
}
|
|
4341
4589
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
4342
|
-
const raw = JSON.parse(await
|
|
4590
|
+
const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
|
|
4343
4591
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
4344
|
-
await
|
|
4592
|
+
await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
4345
4593
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
4346
4594
|
return result;
|
|
4347
4595
|
}
|
|
@@ -4349,9 +4597,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4349
4597
|
}
|
|
4350
4598
|
|
|
4351
4599
|
// src/artifacts.ts
|
|
4352
|
-
import
|
|
4600
|
+
import path17 from "path";
|
|
4353
4601
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
4354
|
-
import { mkdir as
|
|
4602
|
+
import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
4355
4603
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
4356
4604
|
function message(error) {
|
|
4357
4605
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -4360,7 +4608,7 @@ function digest(data) {
|
|
|
4360
4608
|
return createHash2("sha256").update(data).digest("hex");
|
|
4361
4609
|
}
|
|
4362
4610
|
function portableRelative(from, to) {
|
|
4363
|
-
return
|
|
4611
|
+
return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
|
|
4364
4612
|
}
|
|
4365
4613
|
function canonical(value) {
|
|
4366
4614
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -4411,7 +4659,7 @@ function parseArtifactManifest(absolute, data) {
|
|
|
4411
4659
|
}
|
|
4412
4660
|
async function readOptional(file) {
|
|
4413
4661
|
try {
|
|
4414
|
-
return await
|
|
4662
|
+
return await readFile13(file);
|
|
4415
4663
|
} catch (error) {
|
|
4416
4664
|
if (isCode(error, "ENOENT")) return null;
|
|
4417
4665
|
throw error;
|
|
@@ -4430,8 +4678,8 @@ function processIsAlive(pid) {
|
|
|
4430
4678
|
}
|
|
4431
4679
|
}
|
|
4432
4680
|
function validTemporaryPath(candidate, destination, type) {
|
|
4433
|
-
return
|
|
4434
|
-
`.${
|
|
4681
|
+
return path17.dirname(candidate) === path17.dirname(destination) && path17.basename(candidate).startsWith(
|
|
4682
|
+
`.${path17.basename(destination)}.pixelkiln-${type}-`
|
|
4435
4683
|
);
|
|
4436
4684
|
}
|
|
4437
4685
|
function parseTransaction(journal, data) {
|
|
@@ -4447,14 +4695,14 @@ function parseTransaction(journal, data) {
|
|
|
4447
4695
|
return raw;
|
|
4448
4696
|
}
|
|
4449
4697
|
async function removeTransactionFiles(journal) {
|
|
4450
|
-
await
|
|
4451
|
-
await
|
|
4698
|
+
await rm6(journal, { force: true });
|
|
4699
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
4452
4700
|
}
|
|
4453
4701
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
4454
|
-
const journal =
|
|
4702
|
+
const journal = path17.resolve(recoveryFile);
|
|
4455
4703
|
const bytes = await readOptional(journal);
|
|
4456
4704
|
if (!bytes) {
|
|
4457
|
-
await
|
|
4705
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
4458
4706
|
return;
|
|
4459
4707
|
}
|
|
4460
4708
|
if (activeTransactions.has(journal)) {
|
|
@@ -4467,7 +4715,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4467
4715
|
);
|
|
4468
4716
|
}
|
|
4469
4717
|
for (const entry of transaction.entries) {
|
|
4470
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
4718
|
+
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")) {
|
|
4471
4719
|
throw new Error(
|
|
4472
4720
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
4473
4721
|
);
|
|
@@ -4493,7 +4741,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4493
4741
|
}
|
|
4494
4742
|
await remove2(entry.destination, errors);
|
|
4495
4743
|
try {
|
|
4496
|
-
await
|
|
4744
|
+
await rename5(entry.backup, entry.destination);
|
|
4497
4745
|
} catch (error) {
|
|
4498
4746
|
errors.push(
|
|
4499
4747
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -4513,7 +4761,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4513
4761
|
await removeTransactionFiles(journal);
|
|
4514
4762
|
}
|
|
4515
4763
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
4516
|
-
const root =
|
|
4764
|
+
const root = path17.dirname(path17.resolve(manifestPath));
|
|
4517
4765
|
const sources = provenance.sources.map((source) => ({
|
|
4518
4766
|
...source,
|
|
4519
4767
|
path: portableRelative(root, source.path)
|
|
@@ -4543,11 +4791,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
4543
4791
|
];
|
|
4544
4792
|
}
|
|
4545
4793
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
4546
|
-
const absoluteManifest =
|
|
4794
|
+
const absoluteManifest = path17.resolve(manifestPath);
|
|
4547
4795
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
4548
4796
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
4549
4797
|
absoluteManifest,
|
|
4550
|
-
...outputs.map((output) =>
|
|
4798
|
+
...outputs.map((output) => path17.resolve(output.path))
|
|
4551
4799
|
]);
|
|
4552
4800
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
4553
4801
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -4565,13 +4813,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
4565
4813
|
}
|
|
4566
4814
|
}
|
|
4567
4815
|
if (!options.force) {
|
|
4568
|
-
const root =
|
|
4816
|
+
const root = path17.dirname(absoluteManifest);
|
|
4569
4817
|
const recorded = new Map(
|
|
4570
|
-
previous?.outputs.map((output) => [
|
|
4818
|
+
previous?.outputs.map((output) => [path17.resolve(root, output.path), output.sha256]) ?? []
|
|
4571
4819
|
);
|
|
4572
4820
|
const conflicts = [];
|
|
4573
4821
|
for (const output of outputs) {
|
|
4574
|
-
const destination =
|
|
4822
|
+
const destination = path17.resolve(output.path);
|
|
4575
4823
|
const current = await readOptional(destination);
|
|
4576
4824
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
4577
4825
|
const expected = recorded.get(destination);
|
|
@@ -4596,7 +4844,7 @@ function isCode(error, code) {
|
|
|
4596
4844
|
}
|
|
4597
4845
|
async function remove2(file, errors) {
|
|
4598
4846
|
try {
|
|
4599
|
-
await
|
|
4847
|
+
await rm6(file, { force: true });
|
|
4600
4848
|
} catch (error) {
|
|
4601
4849
|
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
4602
4850
|
}
|
|
@@ -4609,7 +4857,7 @@ async function rollback(prepared) {
|
|
|
4609
4857
|
for (const artifact of [...prepared].reverse()) {
|
|
4610
4858
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
4611
4859
|
try {
|
|
4612
|
-
await
|
|
4860
|
+
await rename5(artifact.backup, artifact.destination);
|
|
4613
4861
|
artifact.backedUp = false;
|
|
4614
4862
|
} catch (error) {
|
|
4615
4863
|
errors.push(
|
|
@@ -4626,7 +4874,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4626
4874
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
4627
4875
|
const normalized = files.map((file) => {
|
|
4628
4876
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
4629
|
-
return { destination:
|
|
4877
|
+
return { destination: path17.resolve(file.path), data: Buffer.from(file.data) };
|
|
4630
4878
|
});
|
|
4631
4879
|
const destinations = /* @__PURE__ */ new Set();
|
|
4632
4880
|
for (const artifact of normalized) {
|
|
@@ -4642,7 +4890,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4642
4890
|
const unchanged = [];
|
|
4643
4891
|
for (const artifact of normalized) {
|
|
4644
4892
|
try {
|
|
4645
|
-
const current = await
|
|
4893
|
+
const current = await readFile13(artifact.destination);
|
|
4646
4894
|
if (current.equals(artifact.data)) {
|
|
4647
4895
|
unchanged.push(artifact.destination);
|
|
4648
4896
|
} else {
|
|
@@ -4656,21 +4904,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4656
4904
|
if (!changed.length) return { changed: [], unchanged };
|
|
4657
4905
|
const token = randomUUID2();
|
|
4658
4906
|
for (const [index, artifact] of changed.entries()) {
|
|
4659
|
-
const basename =
|
|
4660
|
-
artifact.stage =
|
|
4661
|
-
|
|
4907
|
+
const basename = path17.basename(artifact.destination);
|
|
4908
|
+
artifact.stage = path17.join(
|
|
4909
|
+
path17.dirname(artifact.destination),
|
|
4662
4910
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
4663
4911
|
);
|
|
4664
4912
|
if (artifact.existed) {
|
|
4665
|
-
artifact.backup =
|
|
4666
|
-
|
|
4913
|
+
artifact.backup = path17.join(
|
|
4914
|
+
path17.dirname(artifact.destination),
|
|
4667
4915
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
4668
4916
|
);
|
|
4669
4917
|
}
|
|
4670
4918
|
}
|
|
4671
|
-
const journal = options.recoveryFile ?
|
|
4919
|
+
const journal = options.recoveryFile ? path17.resolve(options.recoveryFile) : null;
|
|
4672
4920
|
if (journal) {
|
|
4673
|
-
await
|
|
4921
|
+
await mkdir6(path17.dirname(journal), { recursive: true });
|
|
4674
4922
|
const transaction = {
|
|
4675
4923
|
format: "pixelkiln-artifact-transaction",
|
|
4676
4924
|
version: 1,
|
|
@@ -4684,7 +4932,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4684
4932
|
}))
|
|
4685
4933
|
};
|
|
4686
4934
|
try {
|
|
4687
|
-
await
|
|
4935
|
+
await writeFile8(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
4688
4936
|
activeTransactions.add(journal);
|
|
4689
4937
|
} catch (error) {
|
|
4690
4938
|
throw new Error(
|
|
@@ -4695,9 +4943,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4695
4943
|
}
|
|
4696
4944
|
try {
|
|
4697
4945
|
for (const [index, artifact] of changed.entries()) {
|
|
4698
|
-
await
|
|
4946
|
+
await mkdir6(path17.dirname(artifact.destination), { recursive: true });
|
|
4699
4947
|
await options.beforeStage?.(artifact.destination, index);
|
|
4700
|
-
await
|
|
4948
|
+
await writeFile8(artifact.stage, artifact.data, { flag: "wx" });
|
|
4701
4949
|
}
|
|
4702
4950
|
} catch (error) {
|
|
4703
4951
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -4710,17 +4958,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4710
4958
|
try {
|
|
4711
4959
|
for (const artifact of changed) {
|
|
4712
4960
|
if (!artifact.existed) continue;
|
|
4713
|
-
await
|
|
4961
|
+
await rename5(artifact.destination, artifact.backup);
|
|
4714
4962
|
artifact.backedUp = true;
|
|
4715
4963
|
}
|
|
4716
4964
|
for (const [index, artifact] of changed.entries()) {
|
|
4717
4965
|
await options.beforePromote?.(artifact.destination, index);
|
|
4718
|
-
await
|
|
4966
|
+
await rename5(artifact.stage, artifact.destination);
|
|
4719
4967
|
artifact.stage = void 0;
|
|
4720
4968
|
artifact.promoted = true;
|
|
4721
4969
|
}
|
|
4722
4970
|
if (journal) {
|
|
4723
|
-
await
|
|
4971
|
+
await writeFile8(transactionMarker(journal), `${token}
|
|
4724
4972
|
`, { flag: "wx" });
|
|
4725
4973
|
durableCommit = true;
|
|
4726
4974
|
await options.afterCommit?.();
|
|
@@ -4765,11 +5013,11 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4765
5013
|
// src/cli.ts
|
|
4766
5014
|
var log = (msg = "") => console.log(msg);
|
|
4767
5015
|
async function provenanceFile(id, file) {
|
|
4768
|
-
const absolute =
|
|
5016
|
+
const absolute = path18.resolve(file);
|
|
4769
5017
|
return {
|
|
4770
5018
|
id,
|
|
4771
5019
|
path: absolute,
|
|
4772
|
-
sha256:
|
|
5020
|
+
sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
|
|
4773
5021
|
included: true
|
|
4774
5022
|
};
|
|
4775
5023
|
}
|
|
@@ -4793,7 +5041,10 @@ var VALUE_FLAGS = [
|
|
|
4793
5041
|
"--max-distance",
|
|
4794
5042
|
"--min-transparency",
|
|
4795
5043
|
"--max-colors",
|
|
4796
|
-
"--sigma"
|
|
5044
|
+
"--sigma",
|
|
5045
|
+
"--workspace",
|
|
5046
|
+
"--provider",
|
|
5047
|
+
"--account"
|
|
4797
5048
|
];
|
|
4798
5049
|
var BOOL_FLAGS = [
|
|
4799
5050
|
"--force",
|
|
@@ -4832,18 +5083,43 @@ var COMMANDS = [
|
|
|
4832
5083
|
"tag",
|
|
4833
5084
|
"balance",
|
|
4834
5085
|
"status",
|
|
5086
|
+
"workspace",
|
|
4835
5087
|
"help",
|
|
4836
5088
|
"--help",
|
|
4837
5089
|
"-h",
|
|
4838
5090
|
"--version",
|
|
4839
5091
|
"-v"
|
|
4840
5092
|
];
|
|
5093
|
+
var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
4841
5094
|
function parseArgs(argv) {
|
|
4842
5095
|
const [command = "help"] = argv;
|
|
4843
5096
|
if (!COMMANDS.includes(command)) {
|
|
4844
5097
|
throw new Error(`Unknown command "${command}". Run \`pixelkiln help\` for the list.`);
|
|
4845
5098
|
}
|
|
4846
|
-
|
|
5099
|
+
let rest = argv.slice(1);
|
|
5100
|
+
let subcommand;
|
|
5101
|
+
let target;
|
|
5102
|
+
if (command === "workspace") {
|
|
5103
|
+
subcommand = rest[0];
|
|
5104
|
+
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
5105
|
+
throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
|
|
5106
|
+
}
|
|
5107
|
+
if (!WORKSPACE_SUBCOMMANDS.includes(subcommand)) {
|
|
5108
|
+
throw new Error(
|
|
5109
|
+
`Unknown workspace subcommand "${subcommand}". Known: ${WORKSPACE_SUBCOMMANDS.join(", ")}`
|
|
5110
|
+
);
|
|
5111
|
+
}
|
|
5112
|
+
rest = rest.slice(1);
|
|
5113
|
+
if (subcommand === "add" || subcommand === "remove") {
|
|
5114
|
+
target = rest[0];
|
|
5115
|
+
if (target === void 0 || target.startsWith("-")) {
|
|
5116
|
+
throw new Error(
|
|
5117
|
+
subcommand === "add" ? "workspace add needs a manifest path." : "workspace remove needs a project id or manifest path."
|
|
5118
|
+
);
|
|
5119
|
+
}
|
|
5120
|
+
rest = rest.slice(1);
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
4847
5123
|
for (let i = 0; i < rest.length; i++) {
|
|
4848
5124
|
const token = rest[i];
|
|
4849
5125
|
if (!token.startsWith("-")) {
|
|
@@ -4920,7 +5196,8 @@ function parseArgs(argv) {
|
|
|
4920
5196
|
return {
|
|
4921
5197
|
command,
|
|
4922
5198
|
manifest,
|
|
4923
|
-
lock: get("--lock") ??
|
|
5199
|
+
lock: get("--lock") ?? path18.join(path18.dirname(path18.resolve(manifest)), "pixelkiln.lock.json"),
|
|
5200
|
+
explicitLock: get("--lock"),
|
|
4924
5201
|
styles: list("--style"),
|
|
4925
5202
|
assets: list("--only"),
|
|
4926
5203
|
force: rest.includes("--force"),
|
|
@@ -4949,7 +5226,12 @@ function parseArgs(argv) {
|
|
|
4949
5226
|
maxDistance: numberOption("--max-distance", { min: 0 }),
|
|
4950
5227
|
minTransparency: numberOption("--min-transparency", { min: 0, max: 1 }),
|
|
4951
5228
|
maxColors: numberOption("--max-colors", { min: 1, integer: true }),
|
|
4952
|
-
sigma: numberOption("--sigma", { min: Number.EPSILON })
|
|
5229
|
+
sigma: numberOption("--sigma", { min: Number.EPSILON }),
|
|
5230
|
+
subcommand,
|
|
5231
|
+
workspace: get("--workspace"),
|
|
5232
|
+
target,
|
|
5233
|
+
provider: get("--provider"),
|
|
5234
|
+
account: get("--account")
|
|
4953
5235
|
};
|
|
4954
5236
|
}
|
|
4955
5237
|
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation (PixelLab)
|
|
@@ -4982,6 +5264,8 @@ Commands
|
|
|
4982
5264
|
tag Push manifest tags to the objects upstream (free).
|
|
4983
5265
|
balance Show the provider's remaining balance.
|
|
4984
5266
|
status Summarise the lockfile.
|
|
5267
|
+
workspace Register sibling projects and derive account-wide claims/status.
|
|
5268
|
+
add/remove/list/status/claims. Offline.
|
|
4985
5269
|
|
|
4986
5270
|
Options
|
|
4987
5271
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
@@ -5009,6 +5293,8 @@ Options
|
|
|
5009
5293
|
--no-open Do not auto-open the browser during pick
|
|
5010
5294
|
--tag Also push tags upstream after fetch
|
|
5011
5295
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
5296
|
+
--workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
|
|
5297
|
+
derives salvage's claim set instead of repeated --claims.
|
|
5012
5298
|
--from <dir> Source tree for init
|
|
5013
5299
|
--write-prompts adopt: recover prompts into the manifest
|
|
5014
5300
|
|
|
@@ -5021,6 +5307,9 @@ Examples
|
|
|
5021
5307
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
5022
5308
|
pixelkiln mount --style ground
|
|
5023
5309
|
pixelkiln export --style ground --only terrain --format tiled
|
|
5310
|
+
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
5311
|
+
pixelkiln workspace status --json
|
|
5312
|
+
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
5024
5313
|
`;
|
|
5025
5314
|
function printPlan(plan) {
|
|
5026
5315
|
const counts = summarize(plan);
|
|
@@ -5061,6 +5350,23 @@ async function confirm(question, auto) {
|
|
|
5061
5350
|
process.stdin.pause();
|
|
5062
5351
|
return answer === "y" || answer === "yes";
|
|
5063
5352
|
}
|
|
5353
|
+
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
5354
|
+
if (!existsSync15(workspacePath)) {
|
|
5355
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
5356
|
+
}
|
|
5357
|
+
const dir = path18.dirname(path18.resolve(workspacePath));
|
|
5358
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5359
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
5360
|
+
const errors = diagnostics.filter((d) => d.level === "error");
|
|
5361
|
+
if (errors.length) {
|
|
5362
|
+
throw new Error(
|
|
5363
|
+
`Workspace catalog at ${workspacePath} is not safe to derive a claim set from:
|
|
5364
|
+
` + errors.map((d) => ` ${d.id}: ${d.message}`).join("\n")
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
const claims = await workspaceClaims(ws, dir);
|
|
5368
|
+
return { ws, dir, diagnostics, claims };
|
|
5369
|
+
}
|
|
5064
5370
|
async function main() {
|
|
5065
5371
|
const args = parseArgs(process.argv.slice(2));
|
|
5066
5372
|
if (args.command === "help" || args.command === "--help" || args.command === "-h") {
|
|
@@ -5069,13 +5375,13 @@ async function main() {
|
|
|
5069
5375
|
}
|
|
5070
5376
|
if (args.command === "--version" || args.command === "-v") {
|
|
5071
5377
|
const pkg = JSON.parse(
|
|
5072
|
-
await
|
|
5378
|
+
await readFile14(new URL("../package.json", import.meta.url), "utf8")
|
|
5073
5379
|
);
|
|
5074
5380
|
log(`${pkg.name} ${pkg.version}`);
|
|
5075
5381
|
return;
|
|
5076
5382
|
}
|
|
5077
5383
|
if (args.command === "balance") {
|
|
5078
|
-
loadEnvFiles(
|
|
5384
|
+
loadEnvFiles(path18.dirname(path18.resolve(args.manifest)));
|
|
5079
5385
|
loadEnvFiles(process.cwd());
|
|
5080
5386
|
const p = PixelLabProvider.fromEnv();
|
|
5081
5387
|
const b = await p.balance();
|
|
@@ -5086,31 +5392,31 @@ async function main() {
|
|
|
5086
5392
|
}
|
|
5087
5393
|
if (args.command === "init") {
|
|
5088
5394
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
5089
|
-
const root =
|
|
5090
|
-
if (!
|
|
5395
|
+
const root = path18.resolve(args.from);
|
|
5396
|
+
if (!existsSync15(root)) throw new Error(`No directory at ${root}`);
|
|
5091
5397
|
const generator = args.generator ?? "map";
|
|
5092
5398
|
if (generator !== "1dir" && generator !== "map") {
|
|
5093
5399
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
5094
5400
|
}
|
|
5095
|
-
const target =
|
|
5401
|
+
const target = path18.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
5096
5402
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
5097
5403
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
5098
5404
|
const manifest = buildManifest(
|
|
5099
|
-
args.name ??
|
|
5405
|
+
args.name ?? path18.basename(path18.dirname(target)),
|
|
5100
5406
|
args.styles[0] ?? "base",
|
|
5101
5407
|
generator,
|
|
5102
|
-
|
|
5408
|
+
path18.relative(path18.dirname(target), root) || ".",
|
|
5103
5409
|
assets
|
|
5104
5410
|
);
|
|
5105
5411
|
await writeManifestFile(target, manifest);
|
|
5106
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
5412
|
+
log(` scanned ${assets.length} PNG(s) under ${path18.relative(process.cwd(), root)}`);
|
|
5107
5413
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
5108
|
-
log(` wrote ${
|
|
5414
|
+
log(` wrote ${path18.relative(process.cwd(), target)}`);
|
|
5109
5415
|
log(`
|
|
5110
5416
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
5111
5417
|
log(` PixelLab account instead of inventing them:`);
|
|
5112
5418
|
log(`
|
|
5113
|
-
pixelkiln adopt --manifest ${
|
|
5419
|
+
pixelkiln adopt --manifest ${path18.relative(process.cwd(), target)} --write-prompts
|
|
5114
5420
|
`);
|
|
5115
5421
|
return;
|
|
5116
5422
|
}
|
|
@@ -5158,10 +5464,10 @@ async function main() {
|
|
|
5158
5464
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
5159
5465
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
5160
5466
|
}
|
|
5161
|
-
const raw = JSON.parse(await
|
|
5467
|
+
const raw = JSON.parse(await readFile14(path18.resolve(args.inputs), "utf8"));
|
|
5162
5468
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
5163
5469
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
5164
|
-
const base =
|
|
5470
|
+
const base = path18.resolve(args.out.replace(/\.png$/, ""));
|
|
5165
5471
|
const outputs = [
|
|
5166
5472
|
{ path: `${base}.png`, data: png },
|
|
5167
5473
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -5174,18 +5480,143 @@ async function main() {
|
|
|
5174
5480
|
log(
|
|
5175
5481
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
5176
5482
|
);
|
|
5177
|
-
log(` ${
|
|
5483
|
+
log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
5178
5484
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5179
5485
|
return;
|
|
5180
5486
|
}
|
|
5181
|
-
if (
|
|
5487
|
+
if (args.command === "workspace") {
|
|
5488
|
+
const workspacePath = path18.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
5489
|
+
const dir = path18.dirname(workspacePath);
|
|
5490
|
+
if (args.subcommand === "add") {
|
|
5491
|
+
const manifestPath = path18.resolve(args.target);
|
|
5492
|
+
const loadedTarget = await loadManifest(manifestPath);
|
|
5493
|
+
const lockPath = args.explicitLock ? path18.resolve(args.explicitLock) : path18.join(path18.dirname(manifestPath), "pixelkiln.lock.json");
|
|
5494
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5495
|
+
const id = args.name ?? loadedTarget.manifest.name;
|
|
5496
|
+
if (ws.projects.some((p) => p.id === id)) {
|
|
5497
|
+
throw new Error(
|
|
5498
|
+
`Project id "${id}" is already registered in ${workspacePath}. Pass --name for a different id.`
|
|
5499
|
+
);
|
|
5500
|
+
}
|
|
5501
|
+
const lockOwner = ws.projects.find((p) => resolveProject(dir, p).lockPath === lockPath);
|
|
5502
|
+
if (lockOwner) {
|
|
5503
|
+
throw new Error(`Lockfile ${lockPath} is already registered under project id "${lockOwner.id}".`);
|
|
5504
|
+
}
|
|
5505
|
+
const project = {
|
|
5506
|
+
id,
|
|
5507
|
+
manifest: toPortablePath(dir, manifestPath),
|
|
5508
|
+
lock: toPortablePath(dir, lockPath),
|
|
5509
|
+
provider: args.provider ?? "pixellab",
|
|
5510
|
+
...args.account ? { account: args.account } : {}
|
|
5511
|
+
};
|
|
5512
|
+
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
5513
|
+
log(` registered "${id}" in ${path18.relative(process.cwd(), workspacePath)}`);
|
|
5514
|
+
log(` manifest: ${project.manifest}`);
|
|
5515
|
+
log(` lock: ${project.lock}`);
|
|
5516
|
+
if (!existsSync15(lockPath)) {
|
|
5517
|
+
log(
|
|
5518
|
+
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
5519
|
+
);
|
|
5520
|
+
}
|
|
5521
|
+
return;
|
|
5522
|
+
}
|
|
5523
|
+
if (args.subcommand === "remove") {
|
|
5524
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5525
|
+
const resolvedTarget = path18.resolve(args.target);
|
|
5526
|
+
const match = ws.projects.find(
|
|
5527
|
+
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
5528
|
+
);
|
|
5529
|
+
if (!match) {
|
|
5530
|
+
throw new Error(`No registered project matches "${args.target}" (checked id and manifest path).`);
|
|
5531
|
+
}
|
|
5532
|
+
await saveWorkspace(workspacePath, {
|
|
5533
|
+
version: 1,
|
|
5534
|
+
projects: ws.projects.filter((p) => p !== match)
|
|
5535
|
+
});
|
|
5536
|
+
log(` removed "${match.id}" from ${path18.relative(process.cwd(), workspacePath)}`);
|
|
5537
|
+
return;
|
|
5538
|
+
}
|
|
5539
|
+
if (args.subcommand === "list") {
|
|
5540
|
+
if (!existsSync15(workspacePath)) {
|
|
5541
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
5542
|
+
}
|
|
5543
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5544
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
5545
|
+
if (args.json) {
|
|
5546
|
+
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
5547
|
+
} else if (!ws.projects.length) {
|
|
5548
|
+
log(` no projects registered in ${path18.relative(process.cwd(), workspacePath)}`);
|
|
5549
|
+
} else {
|
|
5550
|
+
log(` ${ws.projects.length} project(s) in ${path18.relative(process.cwd(), workspacePath)}:`);
|
|
5551
|
+
for (const p of ws.projects) {
|
|
5552
|
+
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
5553
|
+
}
|
|
5554
|
+
for (const d of diagnostics) log(` ${d.level === "error" ? "ERROR" : "WARN "} ${d.id.padEnd(18)} ${d.message}`);
|
|
5555
|
+
}
|
|
5556
|
+
if (args.check && diagnostics.some((d) => d.level === "error")) process.exitCode = 1;
|
|
5557
|
+
return;
|
|
5558
|
+
}
|
|
5559
|
+
if (args.subcommand === "status") {
|
|
5560
|
+
if (!existsSync15(workspacePath)) {
|
|
5561
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
5562
|
+
}
|
|
5563
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5564
|
+
const report = await workspaceStatus(ws, dir);
|
|
5565
|
+
if (args.json) {
|
|
5566
|
+
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
5567
|
+
} else {
|
|
5568
|
+
log(` workspace: ${path18.relative(process.cwd(), workspacePath)}`);
|
|
5569
|
+
for (const p of report.projects) {
|
|
5570
|
+
if (p.error) {
|
|
5571
|
+
log(`
|
|
5572
|
+
${p.id} \u2014 ERROR: ${p.error}`);
|
|
5573
|
+
continue;
|
|
5574
|
+
}
|
|
5575
|
+
log(`
|
|
5576
|
+
${p.id} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
5577
|
+
log(` ${p.entries} lock entries`);
|
|
5578
|
+
for (const [state, n] of Object.entries(p.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
5579
|
+
for (const unit of ["generations", "usd", "free"]) {
|
|
5580
|
+
if (p.spendByUnit[unit]) log(` spend: ${formatCost(unit, p.spendByUnit[unit])}`);
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
log(`
|
|
5584
|
+
totals:`);
|
|
5585
|
+
for (const [state, n] of Object.entries(report.totals.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
5586
|
+
for (const unit of ["generations", "usd", "free"]) {
|
|
5587
|
+
if (report.totals.spendByUnit[unit]) log(` spend: ${formatCost(unit, report.totals.spendByUnit[unit])}`);
|
|
5588
|
+
}
|
|
5589
|
+
log(` claims: ${report.totals.claims}`);
|
|
5590
|
+
for (const d of report.diagnostics) log(` ${d.level === "error" ? "ERROR" : "WARN "} ${d.id.padEnd(18)} ${d.message}`);
|
|
5591
|
+
}
|
|
5592
|
+
if (args.check && !report.safe) process.exitCode = 1;
|
|
5593
|
+
return;
|
|
5594
|
+
}
|
|
5595
|
+
if (args.subcommand === "claims") {
|
|
5596
|
+
const { claims, diagnostics } = await requireCompleteWorkspaceClaims(workspacePath);
|
|
5597
|
+
if (args.json) {
|
|
5598
|
+
log(JSON.stringify({
|
|
5599
|
+
version: 1,
|
|
5600
|
+
claimed: [...claims.claimed].sort(),
|
|
5601
|
+
byProject: claims.byProject,
|
|
5602
|
+
lockPaths: claims.lockPaths
|
|
5603
|
+
}, null, 2));
|
|
5604
|
+
} else {
|
|
5605
|
+
log(` ${claims.claimed.size} claimed id(s) across ${claims.lockPaths.length} lockfile(s):`);
|
|
5606
|
+
for (const [id, n] of Object.entries(claims.byProject).sort()) log(` ${id.padEnd(24)} ${n}`);
|
|
5607
|
+
for (const d of diagnostics) log(` WARN ${d.id.padEnd(18)} ${d.message}`);
|
|
5608
|
+
}
|
|
5609
|
+
return;
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
if (!existsSync15(path18.resolve(args.manifest))) {
|
|
5182
5613
|
throw new Error(
|
|
5183
|
-
`No manifest at ${
|
|
5614
|
+
`No manifest at ${path18.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
5184
5615
|
);
|
|
5185
5616
|
}
|
|
5186
|
-
const manifestDir =
|
|
5617
|
+
const manifestDir = path18.dirname(path18.resolve(args.manifest));
|
|
5187
5618
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
5188
|
-
if (
|
|
5619
|
+
if (path18.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
5189
5620
|
const loaded = await loadManifest(args.manifest);
|
|
5190
5621
|
const estimator = PixelLabProvider.forOffline();
|
|
5191
5622
|
const specs = await resolveSpecs(loaded, {
|
|
@@ -5260,7 +5691,7 @@ async function main() {
|
|
|
5260
5691
|
let intact = true;
|
|
5261
5692
|
for (const output of entry.outputs) {
|
|
5262
5693
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
5263
|
-
if (!
|
|
5694
|
+
if (!existsSync15(file) || await sha256File(file) !== output.sha256) {
|
|
5264
5695
|
intact = false;
|
|
5265
5696
|
break;
|
|
5266
5697
|
}
|
|
@@ -5318,7 +5749,7 @@ async function main() {
|
|
|
5318
5749
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
5319
5750
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
5320
5751
|
}
|
|
5321
|
-
const manifestDir2 =
|
|
5752
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5322
5753
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
5323
5754
|
for (const styleId of styleIds) {
|
|
5324
5755
|
const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
|
|
@@ -5327,7 +5758,7 @@ async function main() {
|
|
|
5327
5758
|
primaryOnly: args.primaryOnly
|
|
5328
5759
|
});
|
|
5329
5760
|
const style = loaded.manifest.styles[styleId];
|
|
5330
|
-
const base = args.out ?
|
|
5761
|
+
const base = args.out ? path18.resolve(args.out.replace(/\.png$/, "")) : path18.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
5331
5762
|
const outputs = [
|
|
5332
5763
|
{ path: `${base}.png`, data: png },
|
|
5333
5764
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -5350,13 +5781,13 @@ async function main() {
|
|
|
5350
5781
|
log(
|
|
5351
5782
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
5352
5783
|
);
|
|
5353
|
-
log(` ${
|
|
5784
|
+
log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
5354
5785
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5355
5786
|
}
|
|
5356
5787
|
return;
|
|
5357
5788
|
}
|
|
5358
5789
|
if (args.command === "mount") {
|
|
5359
|
-
const manifestDir2 =
|
|
5790
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5360
5791
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
5361
5792
|
for (const styleId of styleIds) {
|
|
5362
5793
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -5386,7 +5817,7 @@ async function main() {
|
|
|
5386
5817
|
sources,
|
|
5387
5818
|
outputRoles
|
|
5388
5819
|
);
|
|
5389
|
-
const out =
|
|
5820
|
+
const out = path18.resolve(manifestDir2, style.mount.out);
|
|
5390
5821
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
5391
5822
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
5392
5823
|
const outputs = [
|
|
@@ -5399,7 +5830,7 @@ async function main() {
|
|
|
5399
5830
|
await provenanceFile("$manifest", args.manifest),
|
|
5400
5831
|
await provenanceFile("$lock", args.lock),
|
|
5401
5832
|
...artifactSources.filter(
|
|
5402
|
-
(source) => source.id !== "$base" ||
|
|
5833
|
+
(source) => source.id !== "$base" || path18.resolve(source.path) !== out
|
|
5403
5834
|
)
|
|
5404
5835
|
],
|
|
5405
5836
|
options: {
|
|
@@ -5412,7 +5843,7 @@ async function main() {
|
|
|
5412
5843
|
log(
|
|
5413
5844
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
5414
5845
|
);
|
|
5415
|
-
log(` ${
|
|
5846
|
+
log(` ${path18.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
5416
5847
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5417
5848
|
}
|
|
5418
5849
|
return;
|
|
@@ -5468,7 +5899,7 @@ async function main() {
|
|
|
5468
5899
|
}
|
|
5469
5900
|
if (args.command === "export") {
|
|
5470
5901
|
const format = args.format ?? "generic";
|
|
5471
|
-
const manifestDir2 =
|
|
5902
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5472
5903
|
const selected = specs.filter((spec) => {
|
|
5473
5904
|
if (spec.generator !== "tiles") return false;
|
|
5474
5905
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -5484,12 +5915,12 @@ async function main() {
|
|
|
5484
5915
|
for (const spec of selected) {
|
|
5485
5916
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5486
5917
|
const style = loaded.manifest.styles[spec.styleId];
|
|
5487
|
-
const defaultBase =
|
|
5488
|
-
const base = args.out ?
|
|
5918
|
+
const defaultBase = path18.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
5919
|
+
const base = args.out ? path18.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
5489
5920
|
const result = exportTileset(entry, spec, {
|
|
5490
5921
|
format,
|
|
5491
5922
|
manifestDir: manifestDir2,
|
|
5492
|
-
imageName:
|
|
5923
|
+
imageName: path18.basename(`${base}.png`),
|
|
5493
5924
|
columns: args.columns
|
|
5494
5925
|
});
|
|
5495
5926
|
const outputs = [
|
|
@@ -5507,7 +5938,7 @@ async function main() {
|
|
|
5507
5938
|
asset: spec.assetId,
|
|
5508
5939
|
columns: args.columns ?? null,
|
|
5509
5940
|
format,
|
|
5510
|
-
image:
|
|
5941
|
+
image: path18.basename(`${base}.png`),
|
|
5511
5942
|
providerRules: result.generic.providerRules,
|
|
5512
5943
|
style: spec.styleId,
|
|
5513
5944
|
tileType: spec.tileType ?? null
|
|
@@ -5517,7 +5948,7 @@ async function main() {
|
|
|
5517
5948
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
5518
5949
|
);
|
|
5519
5950
|
log(
|
|
5520
|
-
` ${
|
|
5951
|
+
` ${path18.relative(process.cwd(), base)}.png + ${path18.basename(base)}${result.extension} + .pixelkiln.json`
|
|
5521
5952
|
);
|
|
5522
5953
|
}
|
|
5523
5954
|
return;
|
|
@@ -5557,10 +5988,10 @@ async function main() {
|
|
|
5557
5988
|
tagged ${n} object(s) upstream`);
|
|
5558
5989
|
}
|
|
5559
5990
|
if (args.writePrompts) {
|
|
5560
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
5991
|
+
const { filled, stillEmpty } = await writePromptsBack(path18.resolve(args.manifest), lock, {
|
|
5561
5992
|
onProgress: log
|
|
5562
5993
|
});
|
|
5563
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
5994
|
+
log(` recovered ${filled} prompt(s) into ${path18.relative(process.cwd(), args.manifest)}`);
|
|
5564
5995
|
const reloaded = await loadManifest(args.manifest);
|
|
5565
5996
|
const rebased = await resolveSpecs(reloaded, {
|
|
5566
5997
|
styles: args.styles,
|
|
@@ -5589,19 +6020,39 @@ async function main() {
|
|
|
5589
6020
|
if (args.command === "salvage") {
|
|
5590
6021
|
const jsonMode = args.dryRun && args.json;
|
|
5591
6022
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
5592
|
-
const ownLock =
|
|
6023
|
+
const ownLock = path18.resolve(args.lock);
|
|
6024
|
+
let workspaceProjects = [];
|
|
6025
|
+
let workspaceDir = "";
|
|
6026
|
+
if (args.workspace) {
|
|
6027
|
+
const workspacePath = path18.resolve(args.workspace);
|
|
6028
|
+
workspaceDir = path18.dirname(workspacePath);
|
|
6029
|
+
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
6030
|
+
workspaceProjects = complete.ws.projects;
|
|
6031
|
+
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
6032
|
+
}
|
|
6033
|
+
const workspaceLockPaths = workspaceProjects.map((p) => resolveProject(workspaceDir, p).lockPath);
|
|
5593
6034
|
const lockPaths = [
|
|
5594
|
-
|
|
5595
|
-
|
|
6035
|
+
.../* @__PURE__ */ new Set([
|
|
6036
|
+
...workspaceLockPaths,
|
|
6037
|
+
...existsSync15(ownLock) ? [ownLock] : [],
|
|
6038
|
+
...args.claims.map((c) => path18.resolve(c))
|
|
6039
|
+
])
|
|
5596
6040
|
];
|
|
5597
6041
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
5598
|
-
for (const p of lockPaths) diag(` ${
|
|
5599
|
-
if (!args.claims.length) {
|
|
6042
|
+
for (const p of lockPaths) diag(` ${path18.relative(process.cwd(), p)}`);
|
|
6043
|
+
if (!args.claims.length && !args.workspace) {
|
|
5600
6044
|
diag(
|
|
5601
6045
|
`
|
|
5602
6046
|
Only this project's lockfile was consulted. If the account is shared,
|
|
5603
|
-
pass every other project's lockfile via --claims a.json,b.json or
|
|
5604
|
-
|
|
6047
|
+
pass every other project's lockfile via --claims a.json,b.json, or
|
|
6048
|
+
register every project in a workspace catalog and pass --workspace.`
|
|
6049
|
+
);
|
|
6050
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path18.resolve(args.manifest))) {
|
|
6051
|
+
diag(
|
|
6052
|
+
`
|
|
6053
|
+
This project's manifest is not registered in the workspace catalog. Its own
|
|
6054
|
+
lockfile is still included above, so this run's claim set is complete \u2014 but
|
|
6055
|
+
\`pixelkiln workspace add ${args.manifest}\` would keep it aggregated too.`
|
|
5605
6056
|
);
|
|
5606
6057
|
}
|
|
5607
6058
|
const claimed = await loadClaims(lockPaths);
|
|
@@ -5613,16 +6064,11 @@ async function main() {
|
|
|
5613
6064
|
nothing to triage`);
|
|
5614
6065
|
return;
|
|
5615
6066
|
}
|
|
5616
|
-
const siblings =
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
const siblingLoaded = await loadManifest(siblingManifestPath);
|
|
5622
|
-
siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest: siblingLoaded.manifest });
|
|
5623
|
-
} catch {
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
6067
|
+
const siblings = await loadSiblingManifests(
|
|
6068
|
+
args.manifest,
|
|
6069
|
+
workspaceProjects.map((p) => resolveProject(workspaceDir, p).manifestPath),
|
|
6070
|
+
args.claims
|
|
6071
|
+
);
|
|
5626
6072
|
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans, loaded.manifest, siblings);
|
|
5627
6073
|
const multiStyle = Object.keys(loaded.manifest.styles).length > 1;
|
|
5628
6074
|
if (multiStyle) {
|
|
@@ -5685,7 +6131,7 @@ async function main() {
|
|
|
5685
6131
|
manifestPath: loaded.path,
|
|
5686
6132
|
manifest: loaded.manifest,
|
|
5687
6133
|
styleId,
|
|
5688
|
-
importDir:
|
|
6134
|
+
importDir: path18.resolve(loaded.root, style.outDir),
|
|
5689
6135
|
lock,
|
|
5690
6136
|
lockPath: args.lock
|
|
5691
6137
|
},
|