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/dist/index.js CHANGED
@@ -409,11 +409,11 @@ var PixelLabClient = class {
409
409
  * common than the failure mode of retrying (a duplicate object), and a
410
410
  * duplicate is visible and free to delete whereas a silent gap is neither.
411
411
  */
412
- async request(path15, init, attempt = 0) {
412
+ async request(path17, init, attempt = 0) {
413
413
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
414
414
  let res;
415
415
  try {
416
- res = await fetch(`${BASE}${path15}`, {
416
+ res = await fetch(`${BASE}${path17}`, {
417
417
  ...init,
418
418
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
419
419
  headers: {
@@ -425,24 +425,24 @@ var PixelLabClient = class {
425
425
  } catch (err) {
426
426
  if (attempt < MAX_RETRIES) {
427
427
  await sleep(backoffMs(attempt));
428
- return this.request(path15, init, attempt + 1);
428
+ return this.request(path17, init, attempt + 1);
429
429
  }
430
430
  throw err;
431
431
  }
432
432
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
433
433
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
434
434
  await sleep(waitMs);
435
- return this.request(path15, init, attempt + 1);
435
+ return this.request(path17, init, attempt + 1);
436
436
  }
437
437
  const text = await res.text();
438
438
  if (!res.ok) {
439
- throw new PixelLabError(`${init?.method ?? "GET"} ${path15} \u2192 ${res.status}`, res.status, text);
439
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path17} \u2192 ${res.status}`, res.status, text);
440
440
  }
441
441
  if (!text) return {};
442
442
  try {
443
443
  return JSON.parse(text);
444
444
  } catch {
445
- throw new Error(`${init?.method ?? "GET"} ${path15} returned invalid JSON`);
445
+ throw new Error(`${init?.method ?? "GET"} ${path17} returned invalid JSON`);
446
446
  }
447
447
  }
448
448
  async balance() {
@@ -1460,8 +1460,8 @@ import { readFile } from "fs/promises";
1460
1460
  function sha256(data) {
1461
1461
  return createHash2("sha256").update(data).digest("hex");
1462
1462
  }
1463
- async function sha256File(path15) {
1464
- return sha256(await readFile(path15));
1463
+ async function sha256File(path17) {
1464
+ return sha256(await readFile(path17));
1465
1465
  }
1466
1466
  function specHash(spec, styleImageHashes) {
1467
1467
  return sha256(
@@ -2705,10 +2705,10 @@ function isSha256Hash(value) {
2705
2705
  function parseCache(value) {
2706
2706
  return HashCacheSchema.parse(value);
2707
2707
  }
2708
- async function loadCache(path15) {
2709
- if (!existsSync6(path15)) return { version: 1, hashes: {} };
2708
+ async function loadCache(path17) {
2709
+ if (!existsSync6(path17)) return { version: 1, hashes: {} };
2710
2710
  try {
2711
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path15, "utf8")));
2711
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path17, "utf8")));
2712
2712
  if (!parsed.success) return { version: 1, hashes: {} };
2713
2713
  return {
2714
2714
  version: 1,
@@ -2720,18 +2720,18 @@ async function loadCache(path15) {
2720
2720
  return { version: 1, hashes: {} };
2721
2721
  }
2722
2722
  }
2723
- async function saveCache(path15, cache) {
2723
+ async function saveCache(path17, cache) {
2724
2724
  const sorted = {};
2725
2725
  for (const key of Object.keys(cache.hashes).sort()) {
2726
2726
  const hash = cache.hashes[key];
2727
2727
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
2728
2728
  sorted[key] = hash;
2729
2729
  }
2730
- await mkdir3(pathModule.dirname(pathModule.resolve(path15)), { recursive: true });
2731
- const tmp = `${path15}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2730
+ await mkdir3(pathModule.dirname(pathModule.resolve(path17)), { recursive: true });
2731
+ const tmp = `${path17}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2732
2732
  try {
2733
2733
  await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
2734
- await rename3(tmp, path15);
2734
+ await rename3(tmp, path17);
2735
2735
  } finally {
2736
2736
  await rm3(tmp, { force: true });
2737
2737
  }
@@ -4464,6 +4464,7 @@ function mode(values) {
4464
4464
  // src/pipeline/salvage.ts
4465
4465
  import { readFile as readFile11 } from "fs/promises";
4466
4466
  import { existsSync as existsSync12 } from "fs";
4467
+ import path14 from "path";
4467
4468
  async function loadClaims(lockPaths) {
4468
4469
  const claimed = /* @__PURE__ */ new Set();
4469
4470
  for (const p of lockPaths) {
@@ -4516,6 +4517,26 @@ function matchOrphanStyle(prompt, manifest) {
4516
4517
  if (styleIds.length <= 1) return styleIds[0] ?? null;
4517
4518
  return matchStyleByPattern(prompt, manifest);
4518
4519
  }
4520
+ async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
4521
+ const own = path14.resolve(ownManifestPath);
4522
+ const siblingManifestPaths = [
4523
+ .../* @__PURE__ */ new Set([
4524
+ ...workspaceManifestPaths,
4525
+ ...claimPaths.map((c) => path14.join(path14.dirname(path14.resolve(c)), "pixelkiln.manifest.json"))
4526
+ ])
4527
+ ];
4528
+ const siblings = [];
4529
+ for (const siblingManifestPath of siblingManifestPaths) {
4530
+ if (path14.resolve(siblingManifestPath) === own) continue;
4531
+ if (!existsSync12(siblingManifestPath)) continue;
4532
+ try {
4533
+ const { manifest } = await loadManifest(siblingManifestPath);
4534
+ siblings.push({ label: path14.basename(path14.dirname(siblingManifestPath)), manifest });
4535
+ } catch {
4536
+ }
4537
+ }
4538
+ return siblings;
4539
+ }
4519
4540
  function groupOrphansByStyle(orphans, manifest, siblings = []) {
4520
4541
  const matched = /* @__PURE__ */ new Map();
4521
4542
  const elsewhere = /* @__PURE__ */ new Map();
@@ -4627,7 +4648,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
4627
4648
 
4628
4649
  // src/pick/salvage-server.ts
4629
4650
  import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
4630
- import path14 from "path";
4651
+ import path15 from "path";
4631
4652
 
4632
4653
  // src/pick/salvage-sheet.ts
4633
4654
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -4792,7 +4813,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
4792
4813
  });
4793
4814
  const html = renderSalvageSheet(orphans, {
4794
4815
  styleId: ctx.styleId,
4795
- importDir: path14.relative(process.cwd(), ctx.importDir) || "."
4816
+ importDir: path15.relative(process.cwd(), ctx.importDir) || "."
4796
4817
  });
4797
4818
  const byId = new Map(orphans.map((o) => [o.id, o]));
4798
4819
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -4821,9 +4842,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
4821
4842
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE3)) throw new Error("not a PNG");
4822
4843
  decodePng(buf);
4823
4844
  const assetId = idFromPrompt(orphan.prompt, taken);
4824
- const rel = path14.join("_salvaged", `${assetId}.png`);
4825
- const outFile = path14.resolve(ctx.importDir, rel);
4826
- await mkdir5(path14.dirname(outFile), { recursive: true });
4845
+ const rel = path15.join("_salvaged", `${assetId}.png`);
4846
+ const outFile = path15.resolve(ctx.importDir, rel);
4847
+ await mkdir5(path15.dirname(outFile), { recursive: true });
4827
4848
  await writeFile7(outFile, buf);
4828
4849
  ctx.manifest.assets[assetId] = {
4829
4850
  prompt: orphan.prompt,
@@ -4850,7 +4871,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
4850
4871
  error: null,
4851
4872
  sourceUrl: orphan.previewUrl,
4852
4873
  outputs: [{
4853
- path: portableOutputPath(outFile, path14.dirname(ctx.manifestPath)),
4874
+ path: portableOutputPath(outFile, path15.dirname(ctx.manifestPath)),
4854
4875
  sha256: sha256(buf)
4855
4876
  }],
4856
4877
  submittedAt: orphan.createdAt,
@@ -4882,6 +4903,233 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
4882
4903
  }
4883
4904
  });
4884
4905
  }
4906
+
4907
+ // src/workspace.ts
4908
+ import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
4909
+ import { existsSync as existsSync13 } from "fs";
4910
+ import path16 from "path";
4911
+ import { z as z4 } from "zod";
4912
+ var WorkspaceProjectSchema = z4.object({
4913
+ id: z4.string().min(1),
4914
+ /** Manifest path, relative to the catalog file's own directory. */
4915
+ manifest: z4.string().min(1),
4916
+ /** Lockfile path, relative to the catalog file's own directory. */
4917
+ lock: z4.string().min(1),
4918
+ provider: z4.string().min(1).default("pixellab"),
4919
+ /** Free-form label for a shared account, e.g. distinguishing sandboxes. */
4920
+ account: z4.string().optional()
4921
+ }).strict();
4922
+ var WorkspaceSchema = z4.object({
4923
+ version: z4.literal(1),
4924
+ projects: z4.array(WorkspaceProjectSchema).default([])
4925
+ }).strict();
4926
+ function parseWorkspace(raw) {
4927
+ const parsed = WorkspaceSchema.safeParse(raw);
4928
+ if (parsed.success) return parsed.data;
4929
+ throw new Error(
4930
+ `Workspace catalog is not valid v1:
4931
+ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
4932
+ );
4933
+ }
4934
+ async function loadWorkspace(workspacePath) {
4935
+ if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
4936
+ let raw;
4937
+ try {
4938
+ raw = JSON.parse(await readFile13(workspacePath, "utf8"));
4939
+ } catch (err) {
4940
+ throw new Error(
4941
+ `Workspace catalog at ${workspacePath} is malformed:
4942
+ ${err instanceof Error ? err.message : String(err)}`
4943
+ );
4944
+ }
4945
+ return parseWorkspace(raw);
4946
+ }
4947
+ async function saveWorkspace(workspacePath, ws) {
4948
+ const sorted = {
4949
+ version: 1,
4950
+ projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
4951
+ };
4952
+ await mkdir6(path16.dirname(path16.resolve(workspacePath)), { recursive: true });
4953
+ const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
4954
+ try {
4955
+ await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
4956
+ await rename5(tmp, workspacePath);
4957
+ } finally {
4958
+ await rm6(tmp, { force: true });
4959
+ }
4960
+ }
4961
+ function toPortablePath(dir, absolute) {
4962
+ return path16.relative(dir, absolute).split(path16.sep).join("/");
4963
+ }
4964
+ function resolveProject(dir, project) {
4965
+ return {
4966
+ manifestPath: path16.resolve(dir, project.manifest.split("/").join(path16.sep)),
4967
+ lockPath: path16.resolve(dir, project.lock.split("/").join(path16.sep))
4968
+ };
4969
+ }
4970
+ function validateWorkspace(ws, dir) {
4971
+ const diagnostics = [];
4972
+ const idCounts = /* @__PURE__ */ new Map();
4973
+ const lockOwners = /* @__PURE__ */ new Map();
4974
+ const manifestOwners = /* @__PURE__ */ new Map();
4975
+ for (const project of ws.projects) {
4976
+ idCounts.set(project.id, (idCounts.get(project.id) ?? 0) + 1);
4977
+ const { manifestPath, lockPath } = resolveProject(dir, project);
4978
+ lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
4979
+ manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
4980
+ if (path16.isAbsolute(project.manifest) || path16.isAbsolute(project.lock)) {
4981
+ diagnostics.push({
4982
+ id: "absolute-path",
4983
+ level: "warning",
4984
+ message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
4985
+ });
4986
+ }
4987
+ if (!existsSync13(manifestPath)) {
4988
+ diagnostics.push({
4989
+ id: "missing-manifest",
4990
+ level: "error",
4991
+ message: `project "${project.id}" manifest not found: ${manifestPath}`
4992
+ });
4993
+ }
4994
+ if (!existsSync13(lockPath)) {
4995
+ diagnostics.push({
4996
+ id: "missing-lock",
4997
+ level: "error",
4998
+ message: `project "${project.id}" lockfile not found: ${lockPath}`
4999
+ });
5000
+ }
5001
+ }
5002
+ for (const [id, count] of idCounts) {
5003
+ if (count > 1) {
5004
+ diagnostics.push({
5005
+ id: "duplicate-id",
5006
+ level: "error",
5007
+ message: `project id "${id}" is registered ${count} times`
5008
+ });
5009
+ }
5010
+ }
5011
+ for (const [lockPath, ids] of lockOwners) {
5012
+ if (ids.length > 1) {
5013
+ diagnostics.push({
5014
+ id: "duplicate-lock",
5015
+ level: "error",
5016
+ message: `${ids.join(", ")} all register the same lockfile: ${lockPath}`
5017
+ });
5018
+ }
5019
+ }
5020
+ for (const [manifestPath, ids] of manifestOwners) {
5021
+ if (ids.length > 1) {
5022
+ diagnostics.push({
5023
+ id: "duplicate-manifest",
5024
+ level: "warning",
5025
+ message: `${ids.join(", ")} share manifest ${manifestPath} \u2014 expected only when they are variant lockfiles beside one manifest`
5026
+ });
5027
+ }
5028
+ }
5029
+ const providers = new Set(ws.projects.map((p) => p.provider));
5030
+ if (providers.size > 1) {
5031
+ diagnostics.push({
5032
+ id: "mixed-provider",
5033
+ level: "warning",
5034
+ message: `registered projects use different providers: ${[...providers].sort().join(", ")} \u2014 spend totals are kept separate per unit, but confirm this is intentional`
5035
+ });
5036
+ }
5037
+ return diagnostics;
5038
+ }
5039
+
5040
+ // src/pipeline/workspace.ts
5041
+ async function workspaceClaims(ws, dir) {
5042
+ const lockPaths = [];
5043
+ const byProject = {};
5044
+ const claimed = /* @__PURE__ */ new Set();
5045
+ for (const project of ws.projects) {
5046
+ const { lockPath } = resolveProject(dir, project);
5047
+ lockPaths.push(lockPath);
5048
+ let projectClaims;
5049
+ try {
5050
+ projectClaims = await loadClaims([lockPath]);
5051
+ } catch (err) {
5052
+ throw new Error(
5053
+ `Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
5054
+ );
5055
+ }
5056
+ byProject[project.id] = projectClaims.size;
5057
+ for (const id of projectClaims) claimed.add(id);
5058
+ }
5059
+ return { claimed, byProject, lockPaths };
5060
+ }
5061
+ function emptyStateCounts() {
5062
+ return {
5063
+ ok: 0,
5064
+ missing: 0,
5065
+ untracked: 0,
5066
+ stale: 0,
5067
+ orphaned: 0,
5068
+ "in-flight": 0,
5069
+ recoverable: 0,
5070
+ failed: 0
5071
+ };
5072
+ }
5073
+ async function workspaceStatus(ws, dir) {
5074
+ const diagnostics = validateWorkspace(ws, dir);
5075
+ const provider = PixelLabProvider.forOffline();
5076
+ const projects = [];
5077
+ const totalsByState = emptyStateCounts();
5078
+ const totalsSpend = { generations: 0, usd: 0, free: 0 };
5079
+ for (const project of ws.projects) {
5080
+ const { manifestPath, lockPath } = resolveProject(dir, project);
5081
+ const base = {
5082
+ id: project.id,
5083
+ provider: project.provider,
5084
+ account: project.account ?? null,
5085
+ manifest: manifestPath,
5086
+ lock: lockPath
5087
+ };
5088
+ try {
5089
+ const loaded = await loadManifest(manifestPath);
5090
+ const specs = await resolveSpecs(loaded, { provider });
5091
+ const lock = await loadLock(lockPath);
5092
+ normalizeLockOutputPaths(lock, specs);
5093
+ const plan = await buildPlan(specs, lock);
5094
+ const byState = summarize(plan);
5095
+ const spend = spendByUnit(lock);
5096
+ for (const state of Object.keys(byState)) {
5097
+ totalsByState[state] += byState[state];
5098
+ }
5099
+ for (const unit of Object.keys(spend)) {
5100
+ totalsSpend[unit] += spend[unit];
5101
+ }
5102
+ projects.push({
5103
+ ...base,
5104
+ entries: Object.keys(lock.entries).length,
5105
+ byState,
5106
+ spendByUnit: spend,
5107
+ error: null
5108
+ });
5109
+ } catch (err) {
5110
+ projects.push({
5111
+ ...base,
5112
+ entries: 0,
5113
+ byState: emptyStateCounts(),
5114
+ spendByUnit: { generations: 0, usd: 0, free: 0 },
5115
+ error: err instanceof Error ? err.message : String(err)
5116
+ });
5117
+ }
5118
+ }
5119
+ let claims = 0;
5120
+ try {
5121
+ claims = (await workspaceClaims(ws, dir)).claimed.size;
5122
+ } catch {
5123
+ }
5124
+ return {
5125
+ version: 1,
5126
+ safe: !diagnostics.some((d) => d.level === "error") && projects.every((p) => !p.error),
5127
+ dir,
5128
+ projects,
5129
+ totals: { byState: totalsByState, spendByUnit: totalsSpend, claims },
5130
+ diagnostics
5131
+ };
5132
+ }
4885
5133
  export {
4886
5134
  AssetSchema,
4887
5135
  DEFAULT_RATE_LIMIT,
@@ -4898,6 +5146,8 @@ export {
4898
5146
  PixelLabProvider,
4899
5147
  StyleSchema,
4900
5148
  UnsupportedCapabilityError,
5149
+ WorkspaceProjectSchema,
5150
+ WorkspaceSchema,
4901
5151
  adopt,
4902
5152
  applyTags,
4903
5153
  auditStyle,
@@ -4928,6 +5178,8 @@ export {
4928
5178
  loadClaims,
4929
5179
  loadLock,
4930
5180
  loadManifest,
5181
+ loadSiblingManifests,
5182
+ loadWorkspace,
4931
5183
  lockKey,
4932
5184
  matchOrphanStyle,
4933
5185
  measureBalanceChange,
@@ -4942,6 +5194,7 @@ export {
4942
5194
  packStyle,
4943
5195
  paletteDistance,
4944
5196
  parseLock,
5197
+ parseWorkspace,
4945
5198
  pngSize,
4946
5199
  poll,
4947
5200
  portableOutputPath,
@@ -4955,6 +5208,7 @@ export {
4955
5208
  resolveEntryOutputs,
4956
5209
  resolveOutputPath,
4957
5210
  resolvePackInputs,
5211
+ resolveProject,
4958
5212
  resolveSpecEntryOutputs,
4959
5213
  resolveSpecOutputs,
4960
5214
  resolveSpecs,
@@ -4964,6 +5218,7 @@ export {
4964
5218
  runPicker,
4965
5219
  runSalvage,
4966
5220
  saveLock,
5221
+ saveWorkspace,
4967
5222
  scanAssets,
4968
5223
  selectEntryOutput,
4969
5224
  sha256,
@@ -4979,11 +5234,15 @@ export {
4979
5234
  tileFeatureOutputCount,
4980
5235
  tileVariationCount,
4981
5236
  tilesCost,
5237
+ toPortablePath,
4982
5238
  totalSpend,
4983
5239
  upsert,
4984
5240
  validateCostEstimate,
5241
+ validateWorkspace,
4985
5242
  verifyArtifactBundle,
4986
5243
  withArtifactManifest,
5244
+ workspaceClaims,
5245
+ workspaceStatus,
4987
5246
  writeArtifactBundle,
4988
5247
  writeManagedArtifactBundle
4989
5248
  };