create-opentray 0.22.0 → 0.23.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.
@@ -9767,22 +9767,7 @@ const scrapeUrl = async (url, options = {}) => {
9767
9767
  ...c,
9768
9768
  order
9769
9769
  }));
9770
- const solids = [];
9771
- for (const original of originals) for (const variant of ["solid-black", "solid-white"]) {
9772
- const solid = await renderSolidSilhouette(original.path, variant === "solid-black" ? black : white);
9773
- if (solid === void 0) continue;
9774
- const hash = await iconPerceptualHash(solid);
9775
- if (hash !== void 0 && solids.some((s) => s.hash !== void 0 && hamming(hash, s.hash) <= 6)) continue;
9776
- const path = await writeIconTemp(solid, dir);
9777
- solids.push({
9778
- url: original.url,
9779
- path,
9780
- variant,
9781
- variantOf: original.order,
9782
- hash
9783
- });
9784
- }
9785
- const icons = [...originals.map((c) => ({
9770
+ const icons = originals.map((c) => ({
9786
9771
  index: c.order,
9787
9772
  url: c.url,
9788
9773
  path: c.path,
@@ -9790,16 +9775,7 @@ const scrapeUrl = async (url, options = {}) => {
9790
9775
  height: c.height,
9791
9776
  format: c.format,
9792
9777
  variant: "original"
9793
- })), ...solids.map((s, i) => ({
9794
- index: originals.length + i,
9795
- url: s.url,
9796
- path: s.path,
9797
- width: SOLID_SIZE,
9798
- height: SOLID_SIZE,
9799
- format: "png",
9800
- variant: s.variant,
9801
- variantOf: s.variantOf
9802
- }))];
9778
+ }));
9803
9779
  return {
9804
9780
  ok: true,
9805
9781
  title,
@@ -9827,25 +9803,15 @@ const deriveUrlPresets = async (url, options = {}) => {
9827
9803
  ...scraped.frameEmbeddable === void 0 ? {} : { frameEmbeddable: scraped.frameEmbeddable }
9828
9804
  };
9829
9805
  };
9830
- const black = {
9831
- r: 0,
9832
- g: 0,
9833
- b: 0
9834
- };
9835
- const white = {
9836
- r: 255,
9837
- g: 255,
9838
- b: 255
9839
- };
9840
- const SOLID_SIZE = 128;
9841
9806
  /**
9842
9807
  * Render a solid-color silhouette from an icon's alpha mask (RGB discarded,
9843
- * alpha kept) — the shape language macOS tray templates want. Non-decodable
9844
- * sources (e.g. corrupt bytes) return undefined instead of failing the scrape.
9808
+ * alpha kept) — the shape language macOS tray templates want. Called on the
9809
+ * browser-extracted SUBJECT (its alpha mask IS the subject shape); returns
9810
+ * undefined for non-decodable sources instead of failing the caller.
9845
9811
  */
9846
9812
  const renderSolidSilhouette = async (sourcePath, color) => {
9847
9813
  try {
9848
- const image = await containImage(await decodeImageFile(sourcePath), SOLID_SIZE, SOLID_SIZE);
9814
+ const image = await containImage(await decodeImageFile(sourcePath), 128, 128);
9849
9815
  for (let i = 0; i < image.data.length; i += 4) {
9850
9816
  image.data[i] = color.r;
9851
9817
  image.data[i + 1] = color.g;
@@ -17321,6 +17287,7 @@ const createWizardSession = (options) => {
17321
17287
  let trayIconIsSolid = false;
17322
17288
  let iconCandidates = [];
17323
17289
  let iconPort;
17290
+ let iconGeneration = 0;
17324
17291
  let currentTokens = [];
17325
17292
  let currentCommand = "";
17326
17293
  let urlSource;
@@ -17459,13 +17426,15 @@ const createWizardSession = (options) => {
17459
17426
  if (stateNow === "frozen" || stateNow === "materializing" || stateNow === "success") return;
17460
17427
  iconCandidates = scraped.icons;
17461
17428
  iconPort = port;
17429
+ iconGeneration += 1;
17462
17430
  currentIconPath = scraped.icons[0]?.path;
17463
17431
  currentIconUrl = scraped.icons[0]?.url;
17464
17432
  if (scraped.title !== void 0) scrapedTitle = scraped.title;
17465
17433
  emit({
17466
17434
  type: "icons",
17467
17435
  port,
17468
- icons: scraped.icons
17436
+ icons: scraped.icons,
17437
+ generation: iconGeneration
17469
17438
  });
17470
17439
  emit({
17471
17440
  type: "scrape",
@@ -17570,6 +17539,7 @@ const createWizardSession = (options) => {
17570
17539
  ...selectedPort === void 0 ? {} : { selectedPort },
17571
17540
  iconCandidates: [...iconCandidates],
17572
17541
  ...iconPort === void 0 ? {} : { iconsPort: iconPort },
17542
+ iconsGeneration: iconGeneration,
17573
17543
  interactive
17574
17544
  };
17575
17545
  },
@@ -17595,6 +17565,64 @@ const createWizardSession = (options) => {
17595
17565
  replaceIconCandidates(port, icons) {
17596
17566
  iconPort = port;
17597
17567
  iconCandidates = icons;
17568
+ iconGeneration += 1;
17569
+ },
17570
+ async addIconCandidate(port, candidate) {
17571
+ if (state === "frozen" || state === "materializing" || state === "success") return;
17572
+ if (iconPort !== port) return;
17573
+ const source = iconCandidates.find((icon) => icon.index === candidate.variantOf);
17574
+ if (source === void 0) return;
17575
+ const dir = tempIconDir ?? (tempIconDir = await mkdtemp(join(tmpdir(), "create-opentray-")));
17576
+ const path = join(dir, `subject-${createHash("sha256").update(candidate.bytes).digest("hex").slice(0, 16)}.bin`);
17577
+ await writeFile(path, candidate.bytes);
17578
+ const priorSubject = iconCandidates.find((icon) => icon.variant === "subject" && icon.variantOf === source.index);
17579
+ const base = priorSubject === void 0 ? iconCandidates : iconCandidates.filter((icon) => icon !== priorSubject && !(icon.variantOf === priorSubject.index && icon.variant !== "original" && icon.variant !== "subject"));
17580
+ let nextIndex = base.reduce((max, icon) => Math.max(max, icon.index), -1) + 1;
17581
+ const appended = {
17582
+ index: nextIndex,
17583
+ url: source.url,
17584
+ path,
17585
+ width: candidate.width,
17586
+ height: candidate.height,
17587
+ format: candidate.format,
17588
+ variant: "subject",
17589
+ variantOf: source.index
17590
+ };
17591
+ let candidates = [...base, appended];
17592
+ for (const [variant, color] of [["solid-black", {
17593
+ r: 0,
17594
+ g: 0,
17595
+ b: 0
17596
+ }], ["solid-white", {
17597
+ r: 255,
17598
+ g: 255,
17599
+ b: 255
17600
+ }]]) {
17601
+ const solid = await renderSolidSilhouette(path, color);
17602
+ if (solid === void 0) continue;
17603
+ const solidPath = join(dir, `solid-${variant}-${createHash("sha256").update(solid).digest("hex").slice(0, 12)}.bin`);
17604
+ await writeFile(solidPath, solid);
17605
+ nextIndex += 1;
17606
+ candidates = [...candidates, {
17607
+ index: nextIndex,
17608
+ url: source.url,
17609
+ path: solidPath,
17610
+ width: 128,
17611
+ height: 128,
17612
+ format: "png",
17613
+ variant,
17614
+ variantOf: appended.index
17615
+ }];
17616
+ }
17617
+ iconCandidates = candidates;
17618
+ if (priorSubject !== void 0) iconGeneration += 1;
17619
+ emit({
17620
+ type: "icons",
17621
+ port,
17622
+ icons: iconCandidates,
17623
+ generation: iconGeneration
17624
+ });
17625
+ return appended;
17598
17626
  },
17599
17627
  selectTrayIconCandidate(port, index) {
17600
17628
  if (state === "frozen" || state === "materializing" || state === "success") return false;
@@ -17603,7 +17631,7 @@ const createWizardSession = (options) => {
17603
17631
  touched.trayIconPath = true;
17604
17632
  currentTrayIconPath = candidate.path;
17605
17633
  currentTrayIconUrl = candidate.url;
17606
- trayIconIsSolid = candidate.variant !== "original";
17634
+ trayIconIsSolid = candidate.variant === "solid-black" || candidate.variant === "solid-white";
17607
17635
  form = {
17608
17636
  ...form,
17609
17637
  trayIconPath: candidate.path
@@ -17621,7 +17649,7 @@ const createWizardSession = (options) => {
17621
17649
  if (!touched.trayIconPath) {
17622
17650
  currentTrayIconPath = candidate.path;
17623
17651
  currentTrayIconUrl = candidate.url;
17624
- trayIconIsSolid = candidate.variant !== "original";
17652
+ trayIconIsSolid = candidate.variant === "solid-black" || candidate.variant === "solid-white";
17625
17653
  form = {
17626
17654
  ...form,
17627
17655
  iconPath: candidate.path,
@@ -17649,6 +17677,7 @@ const createWizardSession = (options) => {
17649
17677
  scrapedTitle = void 0;
17650
17678
  iconCandidates = [];
17651
17679
  iconPort = void 0;
17680
+ iconGeneration += 1;
17652
17681
  currentIconPath = void 0;
17653
17682
  currentIconUrl = void 0;
17654
17683
  stopTimers();
@@ -17678,6 +17707,7 @@ const createWizardSession = (options) => {
17678
17707
  const scraped = await (options.scrapeUrl ?? scrapeUrl)(parsed.href, tempIconDir === void 0 ? {} : { tempDir: tempIconDir });
17679
17708
  iconCandidates = scraped.ok ? scraped.icons : [];
17680
17709
  iconPort = 0;
17710
+ iconGeneration += 1;
17681
17711
  currentIconPath = scraped.ok ? scraped.icons[0]?.path : void 0;
17682
17712
  currentIconUrl = scraped.ok ? scraped.icons[0]?.url : void 0;
17683
17713
  scrapedTitle = scraped.ok ? scraped.title : void 0;
@@ -17687,7 +17717,8 @@ const createWizardSession = (options) => {
17687
17717
  emit({
17688
17718
  type: "icons",
17689
17719
  port: 0,
17690
- icons: iconCandidates
17720
+ icons: iconCandidates,
17721
+ generation: iconGeneration
17691
17722
  });
17692
17723
  emit({
17693
17724
  type: "scrape",
@@ -17741,6 +17772,7 @@ const createWizardSession = (options) => {
17741
17772
  currentTrayIconUrl = void 0;
17742
17773
  trayIconIsSolid = false;
17743
17774
  iconCandidates = [];
17775
+ iconGeneration += 1;
17744
17776
  touched.appId = false;
17745
17777
  touched.appName = false;
17746
17778
  touched.pm = false;
@@ -18859,6 +18891,8 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
18859
18891
  ]);
18860
18892
  const createWizardServer = async (createSession, options = {}) => {
18861
18893
  const token = randomBytes(16).toString("hex");
18894
+ const imglyUpstream = options.imglyUpstream ?? IMGLY_UPSTREAM;
18895
+ const imglyCacheRoot = options.imglyCacheRoot ?? join(homedir(), ".opentray", "cache", "imgly-data");
18862
18896
  const clients = /* @__PURE__ */ new Set();
18863
18897
  const eventLog = [];
18864
18898
  const EVENT_LOG_LIMIT = 200;
@@ -18933,6 +18967,10 @@ const createWizardServer = async (createSession, options = {}) => {
18933
18967
  await handleAssetFile(url.pathname, response);
18934
18968
  return;
18935
18969
  }
18970
+ if (url.pathname.startsWith("/imgly-data/")) {
18971
+ await handleImglyAsset(url, response, imglyCacheRoot, imglyUpstream);
18972
+ return;
18973
+ }
18936
18974
  if (url.pathname === "/logo.png" || FAVICON_PATHS.has(url.pathname)) {
18937
18975
  await handleAssetFile(url.pathname, response);
18938
18976
  return;
@@ -19137,6 +19175,83 @@ const handleVendorAsset = async (pathname, response) => {
19137
19175
  }
19138
19176
  respond(response, 404, "text/plain", "terminal renderer asset is missing\n");
19139
19177
  };
19178
+ /**
19179
+ * Backend model proxy/cache for the browser subject extraction (owner
19180
+ * round-8): the wizard rebinds a RANDOM port every launch, so a browser-side
19181
+ * cache can never survive — the backend downloads model assets from the
19182
+ * vendor CDN ONCE into a persistent disk cache and serves the frontend from
19183
+ * loopback. Content-addressed chunks are immutable → long-lived cache headers.
19184
+ */
19185
+ const IMGLY_CONTENT_TYPES = {
19186
+ ".json": "application/json",
19187
+ ".wasm": "application/wasm",
19188
+ ".mjs": "text/javascript; charset=utf-8"
19189
+ };
19190
+ /** Model chunks are 4 MiB; bound the proxy to stop unbounded disk writes. */
19191
+ const IMGLY_FILE_LIMIT = 16 * 1024 * 1024;
19192
+ const IMGLY_UPSTREAM = (version) => `https://staticimgly.com/@imgly/background-removal-data/${version}/dist/`;
19193
+ const handleImglyAsset = async (url, response, cacheRoot, upstream) => {
19194
+ const match = /^\/imgly-data\/([\w.-]+)\/([\w.-]+)$/.exec(url.pathname);
19195
+ if (match === null) {
19196
+ respond(response, 404, "text/plain", "not found\n");
19197
+ return;
19198
+ }
19199
+ const version = match[1];
19200
+ const name = match[2];
19201
+ if (version.includes("..") || name.includes("..")) {
19202
+ respond(response, 404, "text/plain", "not found\n");
19203
+ return;
19204
+ }
19205
+ const contentType = IMGLY_CONTENT_TYPES[extname(name).toLowerCase()] ?? "application/octet-stream";
19206
+ const respondBytes = (bytes, fromCache) => {
19207
+ response.writeHead(200, {
19208
+ "content-type": contentType,
19209
+ "content-length": bytes.length,
19210
+ "cache-control": "public, max-age=31536000, immutable",
19211
+ "x-opentray-imgly-cache": fromCache ? "hit" : "miss"
19212
+ });
19213
+ response.end(bytes);
19214
+ };
19215
+ const dir = join(cacheRoot, version);
19216
+ const path = join(dir, name);
19217
+ const cached = await readFile(path).catch(() => void 0);
19218
+ if (cached !== void 0) {
19219
+ respondBytes(cached, true);
19220
+ return;
19221
+ }
19222
+ const fetchFromUpstream = async () => {
19223
+ const upstreamResponse = await fetch(`${upstream(version)}${name}`);
19224
+ if (!upstreamResponse.ok) throw new Error(`upstream ${upstreamResponse.status}`);
19225
+ const bytes = Buffer.from(await upstreamResponse.arrayBuffer());
19226
+ if (bytes.length === 0 || bytes.length > IMGLY_FILE_LIMIT) throw new Error(`upstream size out of bounds: ${bytes.length}`);
19227
+ await mkdir(dir, { recursive: true });
19228
+ const tempPath = join(dir, `.${name}.${randomBytes(6).toString("hex")}.tmp`);
19229
+ await writeFile(tempPath, bytes);
19230
+ await rm(path, { force: true });
19231
+ await rename(tempPath, path);
19232
+ return bytes;
19233
+ };
19234
+ const inFlight = handleImglyAsset.inFlight.get(path);
19235
+ if (inFlight !== void 0) {
19236
+ try {
19237
+ respondBytes(await inFlight, true);
19238
+ } catch {
19239
+ respond(response, 502, "text/plain", "model asset upstream failed\n");
19240
+ }
19241
+ return;
19242
+ }
19243
+ const download = fetchFromUpstream();
19244
+ handleImglyAsset.inFlight.set(path, download);
19245
+ try {
19246
+ respondBytes(await download, false);
19247
+ } catch (error) {
19248
+ respond(response, 502, "text/plain", `model asset upstream failed: ${String(error)}\n`);
19249
+ } finally {
19250
+ handleImglyAsset.inFlight.delete(path);
19251
+ }
19252
+ };
19253
+ /** In-flight upstream downloads, keyed by cache path (per server instance). */
19254
+ handleImglyAsset.inFlight = /* @__PURE__ */ new Map();
19140
19255
  /** Containment: icon routes must only read sources the wizard itself
19141
19256
  * produced (its temp dirs / saved uploads), never arbitrary paths. */
19142
19257
  const isWizardOwnedIconPath = (session, target) => {
@@ -19163,6 +19278,41 @@ const handleApi = async (pathname, request, response, session, preReadBody) => {
19163
19278
  respond(response, 200, "application/json", JSON.stringify({ path }) + "\n");
19164
19279
  return;
19165
19280
  }
19281
+ if (pathname === "/api/icon-candidate") {
19282
+ const query = new URL(request.url ?? "/", "http://127.0.0.1").searchParams;
19283
+ const port = Number.parseInt(query.get("port") ?? "", 10);
19284
+ const variantOf = Number.parseInt(query.get("variantOf") ?? "", 10);
19285
+ const width = Number.parseInt(query.get("width") ?? "", 10);
19286
+ const height = Number.parseInt(query.get("height") ?? "", 10);
19287
+ if (!Number.isInteger(port) || !Number.isInteger(variantOf) || !Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
19288
+ respond(response, 400, "application/json", "{\"error\":\"port, variantOf, width and height are required\"}\n");
19289
+ return;
19290
+ }
19291
+ const chunks = [];
19292
+ for await (const chunk of request) {
19293
+ const piece = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
19294
+ chunks.push(piece);
19295
+ }
19296
+ const bytes = Buffer.concat(chunks);
19297
+ if (bytes.length < 64) {
19298
+ respond(response, 400, "application/json", "{\"error\":\"image bytes are required\"}\n");
19299
+ return;
19300
+ }
19301
+ const format = query.get("format") ?? "png";
19302
+ const appended = await session.addIconCandidate(port, {
19303
+ bytes,
19304
+ variantOf,
19305
+ width,
19306
+ height,
19307
+ format
19308
+ });
19309
+ if (appended === void 0) {
19310
+ respond(response, 409, "application/json", "{\"error\":\"candidate not acceptable for this port/state\"}\n");
19311
+ return;
19312
+ }
19313
+ respond(response, 200, "application/json", JSON.stringify({ index: appended.index }) + "\n");
19314
+ return;
19315
+ }
19166
19316
  const body = preReadBody ?? await readJsonBody(request);
19167
19317
  switch (pathname) {
19168
19318
  case "/api/url": {
@@ -20807,4 +20957,4 @@ if (isMainModule()) main(process.argv.slice(2)).then((code) => {
20807
20957
  //#endregion
20808
20958
  export { resolveOnPath as A, parseNetstatPorts as C, waitForTcpPort as D, verifyHttpService as E, toProjectDirectoryName as F, deriveDefaultAppId as M, deriveDefaultAppName as N, parseShebangInterpreter as O, isValidAppId as P, parseLsofPorts as S, serviceUrl as T, faviconCandidateSize as _, isAuthorized as a, scrapeService as b, openMaterializedApp as c, expectedDarwinBundlePath as d, isDirectoryOccupied$1 as f, extractTitle as g, extractFaviconCandidates as h, createWizardServer as i, tokenizeCommandLine as j, resolveLaunchVector as k, pinningHint as l, writeScaffold as m, openBrowser as n, isLoopbackHost as o, materialize as p, parseWizardCli as r, createWizardSession as s, main as t, detectPackageManager as u, rankFaviconCandidates as v, parsePowerShellPorts as w, createPortDiscovery as x, resolveFaviconUrl as y };
20809
20959
 
20810
- //# sourceMappingURL=bin-Dyfucd3C.mjs.map
20960
+ //# sourceMappingURL=bin-DDNgskyb.mjs.map