pixelkiln 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,8 +65,8 @@ provenance and no long-lived npm token.
65
65
 
66
66
  ### Local human review
67
67
 
68
- `pixelkiln pick` opens a local candidate sheet — native aspect ratios, crisp
69
- small sprites, and no model choosing artwork for you.
68
+ `pixelkiln pick` opens a local candidate sheet with native aspect ratios and
69
+ crisp small sprites. No model chooses artwork for you.
70
70
 
71
71
  ![PixelKiln candidate review UI](./website/public/review-ui-showcase.jpg)
72
72
 
@@ -84,7 +84,7 @@ reviews from the page under that ceiling.
84
84
  Hand edits live beside the art, not in place of the record. [`pixelkiln edit`](docs/CLI.md#edit)
85
85
  opens a copy in your own editor; with `--edit` the gallery does the same, or opens
86
86
  it in a pinned, hash-verified [Pixelorama](docs/CLI.md#tools) build right in the page
87
- and saves it back — layers kept, frame and tile sets one file per member.
87
+ and saves it back with its layers kept, one file per member for frame and tile sets.
88
88
 
89
89
  ![PixelKiln in-browser editor](./website/public/gallery-editor-showcase.jpg)
90
90
 
package/dist/cli.js CHANGED
@@ -181,6 +181,54 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
181
181
  );
182
182
  }
183
183
 
184
+ // src/http.ts
185
+ var MAX_RETRIES = 4;
186
+ var DEFAULT_TIMEOUT_MS = 12e4;
187
+ var sleepFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
188
+ function shouldRetry(status) {
189
+ return status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
190
+ }
191
+ function backoffMs(attempt) {
192
+ const base = Math.min(1e3 * 2 ** attempt, 16e3);
193
+ return base + Math.floor(Math.random() * 400);
194
+ }
195
+ function retryAfterMs(value, now = Date.now()) {
196
+ if (!value) return null;
197
+ const seconds = Number(value);
198
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
199
+ const date = Date.parse(value);
200
+ if (!Number.isFinite(date)) return null;
201
+ return Math.max(0, date - now);
202
+ }
203
+ function fetchWithRetry(request, opts = {}) {
204
+ const sleep3 = opts.sleep ?? sleepFor;
205
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
206
+ const send = request ?? ((input, init) => globalThis.fetch(input, init));
207
+ return async (input, init) => {
208
+ const { retries: perCall, ...rest } = init ?? {};
209
+ const retries = Math.max(0, perCall ?? opts.retries ?? MAX_RETRIES);
210
+ for (let attempt = 0; ; attempt++) {
211
+ const signal = rest.signal ?? AbortSignal.timeout(timeoutMs);
212
+ let res;
213
+ try {
214
+ res = await send(input, { ...rest, signal });
215
+ } catch (error) {
216
+ if (rest.signal?.aborted || attempt >= retries) throw error;
217
+ const waitMs2 = backoffMs(attempt);
218
+ opts.onRetry?.({ attempt, waitMs: waitMs2, error });
219
+ await sleep3(waitMs2);
220
+ continue;
221
+ }
222
+ if (res.ok || !shouldRetry(res.status) || attempt >= retries) return res;
223
+ const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
224
+ await res.body?.cancel().catch(() => {
225
+ });
226
+ opts.onRetry?.({ attempt, waitMs, status: res.status });
227
+ await sleep3(waitMs);
228
+ }
229
+ };
230
+ }
231
+
184
232
  // src/png.ts
185
233
  import { deflateSync, inflateSync } from "zlib";
186
234
  var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
@@ -636,14 +684,15 @@ var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
636
684
  "strength"
637
685
  ]);
638
686
  var ComfyUIClient = class {
639
- constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL, request = fetch) {
640
- this.request = request;
687
+ baseUrl;
688
+ request;
689
+ constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL, request, retry) {
641
690
  this.baseUrl = normalizeBaseUrl(baseUrl);
691
+ this.request = fetchWithRetry(request, retry);
642
692
  }
643
- request;
644
- baseUrl;
693
+ /** A connectivity check should answer fast, so it does not retry. */
645
694
  async checkConnection() {
646
- const value = await this.json("system_stats");
695
+ const value = await this.json("system_stats", { retries: 0 });
647
696
  if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
648
697
  }
649
698
  async submit(workflow) {
@@ -1505,24 +1554,7 @@ import path3 from "path";
1505
1554
  // src/client.ts
1506
1555
  import { z } from "zod";
1507
1556
  var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
1508
- var MAX_RETRIES = 4;
1509
1557
  var MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
1510
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1511
- function shouldRetry(status) {
1512
- return status === 429 || status === 408 || status >= 500;
1513
- }
1514
- function backoffMs(attempt) {
1515
- const base = Math.min(1e3 * 2 ** attempt, 16e3);
1516
- return base + Math.floor(Math.random() * 400);
1517
- }
1518
- function retryAfterMs(value, now = Date.now()) {
1519
- if (!value) return null;
1520
- const seconds = Number(value);
1521
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1522
- const date = Date.parse(value);
1523
- if (!Number.isFinite(date)) return null;
1524
- return Math.max(0, date - now);
1525
- }
1526
1558
  var BalanceResponseSchema = z.object({
1527
1559
  credits: z.object({ usd: z.number() }).passthrough(),
1528
1560
  subscription: z.object({
@@ -1598,45 +1630,22 @@ var PixelLabError = class extends Error {
1598
1630
  var PixelLabClient = class {
1599
1631
  constructor(apiKey, timeoutMs = 12e4) {
1600
1632
  this.apiKey = apiKey;
1601
- this.timeoutMs = timeoutMs;
1602
1633
  if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
1634
+ this.http = fetchWithRetry(void 0, { timeoutMs });
1603
1635
  }
1604
1636
  apiKey;
1605
- timeoutMs;
1606
- /**
1607
- * Retries only what is safe to retry: transport failures, 429, and 5xx.
1608
- * A 4xx other than 429 is a bad request and retrying it just wastes time.
1609
- *
1610
- * POSTs that create objects are included, which is a deliberate trade: the
1611
- * failure mode of not retrying (a dropped asset in a 65-item run) is more
1612
- * common than the failure mode of retrying (a duplicate object), and a
1613
- * duplicate is visible and free to delete whereas a silent gap is neither.
1614
- */
1615
- async request(path32, init, attempt = 0) {
1637
+ http;
1638
+ /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1639
+ async request(path32, init) {
1616
1640
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1617
- let res;
1618
- try {
1619
- res = await fetch(`${BASE}${path32}`, {
1620
- ...init,
1621
- signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
1622
- headers: {
1623
- Authorization: auth,
1624
- "Content-Type": "application/json",
1625
- ...init?.headers ?? {}
1626
- }
1627
- });
1628
- } catch (err) {
1629
- if (attempt < MAX_RETRIES) {
1630
- await sleep(backoffMs(attempt));
1631
- return this.request(path32, init, attempt + 1);
1641
+ const res = await this.http(`${BASE}${path32}`, {
1642
+ ...init,
1643
+ headers: {
1644
+ Authorization: auth,
1645
+ "Content-Type": "application/json",
1646
+ ...init?.headers ?? {}
1632
1647
  }
1633
- throw err;
1634
- }
1635
- if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
1636
- const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
1637
- await sleep(waitMs);
1638
- return this.request(path32, init, attempt + 1);
1639
- }
1648
+ });
1640
1649
  const text = await res.text();
1641
1650
  if (!res.ok) {
1642
1651
  throw new PixelLabError(`${init?.method ?? "GET"} ${path32} \u2192 ${res.status}`, res.status, text);
@@ -1824,7 +1833,7 @@ var PixelLabClient = class {
1824
1833
  }
1825
1834
  /** Storage URLs are public; no auth header, and sending one can break the CDN request. */
1826
1835
  async download(url) {
1827
- const res = await fetch(url, { signal: AbortSignal.timeout(this.timeoutMs) });
1836
+ const res = await this.http(url);
1828
1837
  if (!res.ok) throw new PixelLabError(`download ${url} \u2192 ${res.status}`, res.status, "");
1829
1838
  const declared = Number(res.headers.get("content-length"));
1830
1839
  if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
@@ -2631,10 +2640,10 @@ function firstUrl(urls) {
2631
2640
  var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
2632
2641
  var SOURCE_PROTOCOL = "retrodiffusion:";
2633
2642
  var RetroDiffusionClient = class {
2634
- constructor(token, baseUrl = DEFAULT_BASE_URL2, request = fetch) {
2643
+ constructor(token, baseUrl = DEFAULT_BASE_URL2, request, retry) {
2635
2644
  this.token = token;
2636
2645
  this.baseUrl = baseUrl;
2637
- this.request = request;
2646
+ this.request = fetchWithRetry(request, retry);
2638
2647
  }
2639
2648
  token;
2640
2649
  baseUrl;
@@ -2669,6 +2678,12 @@ var RetroDiffusionClient = class {
2669
2678
  }
2670
2679
  return balance;
2671
2680
  }
2681
+ /** Result URLs are signed; no token header, and the same retry policy as the API. */
2682
+ async download(url) {
2683
+ const response = await this.request(url);
2684
+ if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
2685
+ return Buffer.from(await response.arrayBuffer());
2686
+ }
2672
2687
  async call(path32, init = {}) {
2673
2688
  if (!this.token) throw new Error("RD_API_KEY is not set");
2674
2689
  const response = await this.request(`${this.baseUrl}${path32}`, {
@@ -2864,9 +2879,7 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
2864
2879
  async downloadResolved(url) {
2865
2880
  const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
2866
2881
  if (data) return Buffer.from(data, "base64");
2867
- const response = await fetch(url);
2868
- if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
2869
- return Buffer.from(await response.arrayBuffer());
2882
+ return this.client.download(url);
2870
2883
  }
2871
2884
  async balance() {
2872
2885
  return { unit: "usd", remaining: await this.client.balance() };
@@ -2993,11 +3006,11 @@ var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
2993
3006
  "width"
2994
3007
  ]);
2995
3008
  var ScenarioClient = class {
2996
- constructor(apiKey, apiSecret, baseUrl = DEFAULT_BASE_URL3, request = fetch) {
3009
+ constructor(apiKey, apiSecret, baseUrl = DEFAULT_BASE_URL3, request, retry) {
2997
3010
  this.apiKey = apiKey;
2998
3011
  this.apiSecret = apiSecret;
2999
3012
  this.baseUrl = baseUrl;
3000
- this.request = request;
3013
+ this.request = fetchWithRetry(request, retry);
3001
3014
  }
3002
3015
  apiKey;
3003
3016
  apiSecret;
@@ -3095,8 +3108,9 @@ var ScenarioClient = class {
3095
3108
  }
3096
3109
  return bytes;
3097
3110
  }
3111
+ /** A connectivity check should answer fast, so it does not retry. */
3098
3112
  async checkConnection() {
3099
- await this.call("/models", {}, { pageSize: "1" });
3113
+ await this.call("/models", { retries: 0 }, { pageSize: "1" });
3100
3114
  }
3101
3115
  async call(pathname, init = {}, query = {}) {
3102
3116
  if (!this.apiKey || !this.apiSecret) {
@@ -6628,7 +6642,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
6628
6642
 
6629
6643
  // src/pipeline/submit.ts
6630
6644
  var IN_FLIGHT_POLL_MS = 5e3;
6631
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6645
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6632
6646
  async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6633
6647
  const limits = provider.rateLimit?.() ?? DEFAULT_RATE_LIMIT;
6634
6648
  const maxInFlight = opts.maxInFlight ?? limits.maxInFlight;
@@ -6683,7 +6697,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6683
6697
  `Timed out waiting for a generation slot` + (lastSlotError ? `; last poll error: ${lastSlotError}` : "")
6684
6698
  );
6685
6699
  }
6686
- await sleep2(slotPollMs);
6700
+ await sleep(slotPollMs);
6687
6701
  await pruneInFlight();
6688
6702
  }
6689
6703
  }
@@ -6691,7 +6705,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6691
6705
  const estimate = estimates.get(key);
6692
6706
  await waitForSlot();
6693
6707
  const since = Date.now() - lastSubmitAt;
6694
- if (since < spacing) await sleep2(spacing - since);
6708
+ if (since < spacing) await sleep(spacing - since);
6695
6709
  await requireRevisionReady(spec, lock);
6696
6710
  const previousEntry = lock.entries[key];
6697
6711
  const resumesCheckpoint = Boolean(
@@ -6809,7 +6823,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6809
6823
  }
6810
6824
 
6811
6825
  // src/pipeline/poll.ts
6812
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6826
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6813
6827
  async function poll(provider, lock, lockPath, opts = {}) {
6814
6828
  const interval = opts.intervalMs ?? 5e3;
6815
6829
  const timeout = opts.timeoutMs ?? 15 * 60 * 1e3;
@@ -6886,7 +6900,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
6886
6900
  }
6887
6901
  }
6888
6902
  await saveLock(lockPath, lock);
6889
- if (pending().length > 0) await sleep3(interval);
6903
+ if (pending().length > 0) await sleep2(interval);
6890
6904
  }
6891
6905
  await saveLock(lockPath, lock);
6892
6906
  return result;
@@ -7800,8 +7814,25 @@ document.getElementById('submit').onclick = async () => {
7800
7814
 
7801
7815
  // src/pick/review-server.ts
7802
7816
  import { createServer } from "http";
7803
- import { spawn } from "child_process";
7804
7817
  import { readFile as readFile11 } from "fs/promises";
7818
+
7819
+ // src/open.ts
7820
+ import { spawn } from "child_process";
7821
+ function openExternal(target, command = defaultOpenCommand()) {
7822
+ const [program, ...args] = command;
7823
+ const child = spawn(program, [...args, target], { stdio: "ignore", detached: true });
7824
+ child.on("error", () => {
7825
+ });
7826
+ child.unref();
7827
+ return command.join(" ");
7828
+ }
7829
+ function defaultOpenCommand(platform = process.platform) {
7830
+ if (platform === "darwin") return ["open"];
7831
+ if (platform === "win32") return ["cmd", "/c", "start", ""];
7832
+ return ["xdg-open"];
7833
+ }
7834
+
7835
+ // src/pick/review-server.ts
7805
7836
  function serveReviewPage(opts) {
7806
7837
  const log2 = opts.onProgress ?? (() => {
7807
7838
  });
@@ -7873,10 +7904,9 @@ function serveReviewPage(opts) {
7873
7904
  server.listen(opts.port ?? 0, "127.0.0.1", () => {
7874
7905
  const address = server.address();
7875
7906
  const port = typeof address === "object" && address ? address.port : opts.port;
7876
- opts.onReady(`http://127.0.0.1:${port}/`);
7877
- if (opts.open !== false && process.platform === "darwin") {
7878
- spawn("open", [`http://127.0.0.1:${port}/`], { stdio: "ignore", detached: true }).unref();
7879
- }
7907
+ const url = `http://127.0.0.1:${port}/`;
7908
+ opts.onReady(url);
7909
+ if (opts.open !== false) openExternal(url);
7880
7910
  });
7881
7911
  });
7882
7912
  }
@@ -8449,7 +8479,6 @@ async function applyManifestEdit(manifestPath, edit) {
8449
8479
  }
8450
8480
 
8451
8481
  // src/pipeline/hand-edit.ts
8452
- import { spawn as spawn2 } from "child_process";
8453
8482
  import { existsSync as existsSync18 } from "fs";
8454
8483
  import { copyFile, mkdir as mkdir6, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
8455
8484
  import path21 from "path";
@@ -8689,13 +8718,8 @@ async function replaceFile(file, bytes) {
8689
8718
  }
8690
8719
  }
8691
8720
  function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
8692
- const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
8693
- const [program, ...args] = command;
8694
- const child = spawn2(program, [...args, file], { stdio: "ignore", detached: true });
8695
- child.on("error", () => {
8696
- });
8697
- child.unref();
8698
- return command.join(" ");
8721
+ const command = editor?.trim() ? editor.trim().split(/\s+/) : defaultOpenCommand();
8722
+ return openExternal(file, command);
8699
8723
  }
8700
8724
 
8701
8725
  // src/gallery/edit.ts
@@ -9346,7 +9370,6 @@ async function buildWorkspaceGallerySnapshot(opts) {
9346
9370
  }
9347
9371
 
9348
9372
  // src/gallery/server.ts
9349
- import { spawn as spawn3 } from "child_process";
9350
9373
  import { randomBytes } from "crypto";
9351
9374
  import { createServer as createServer2 } from "http";
9352
9375
  import { readFile as readFile17 } from "fs/promises";
@@ -12126,9 +12149,7 @@ async function serveGallery(opts) {
12126
12149
  });
12127
12150
  });
12128
12151
  opts.onReady?.(url);
12129
- if (opts.open !== false && process.platform === "darwin") {
12130
- spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
12131
- }
12152
+ if (opts.open !== false) openExternal(url);
12132
12153
  return {
12133
12154
  url,
12134
12155
  session,