@koda-sl/baker-cli 0.147.0 → 0.149.0-dev.7b64ca6b5

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/cli.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  toModelSafeImage,
35
35
  ulid,
36
36
  validateCanvasDeep
37
- } from "./chunk-2F5SCTNS.js";
37
+ } from "./chunk-ZBRVPPJP.js";
38
38
  import {
39
39
  csvOrJson,
40
40
  daysAgoIso,
@@ -2544,6 +2544,13 @@ var imageDocSchema = z8.object({
2544
2544
  width: z8.number().optional(),
2545
2545
  height: z8.number().optional(),
2546
2546
  aspectRatio: z8.number().optional(),
2547
+ /** Any non-opaque pixel in the decoded image. */
2548
+ hasAlpha: z8.boolean().optional(),
2549
+ /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2550
+ * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2551
+ * before placing it — the library keeps assets forever, so a bad ingest is
2552
+ * otherwise indistinguishable from a good one months later. */
2553
+ solidity: z8.number().optional(),
2547
2554
  dominantColor: z8.string().optional(),
2548
2555
  imagePalette: z8.array(z8.string()).optional(),
2549
2556
  thumbhashDataUri: z8.string().optional(),
@@ -2626,6 +2633,13 @@ var imageSearchResultSchema = z8.object({
2626
2633
  width: z8.number().optional(),
2627
2634
  height: z8.number().optional(),
2628
2635
  aspectRatio: z8.number().optional(),
2636
+ /** Any non-opaque pixel in the decoded image. */
2637
+ hasAlpha: z8.boolean().optional(),
2638
+ /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2639
+ * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2640
+ * before placing it — the library keeps assets forever, so a bad ingest is
2641
+ * otherwise indistinguishable from a good one months later. */
2642
+ solidity: z8.number().optional(),
2629
2643
  dominantColor: z8.string().optional(),
2630
2644
  imagePalette: z8.array(z8.string()).optional(),
2631
2645
  source: z8.string(),
@@ -2749,10 +2763,13 @@ var imagesLogoRequestSchema = z8.object({
2749
2763
  descriptionContext: z8.string().optional()
2750
2764
  });
2751
2765
  var imagesLogoResponseSchema = providerHitsResponseSchema();
2766
+ var hexColorSchema = z8.string().transform((value) => value.trim().replace(/^%23/i, "#")).refine((value) => /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value), {
2767
+ message: "Expected a hex color like #D7FFA4"
2768
+ }).transform((value) => value.startsWith("#") ? value : `#${value}`);
2752
2769
  var imagesIconRequestSchema = z8.object({
2753
2770
  name: z8.string().min(1),
2754
2771
  set: z8.string().optional(),
2755
- color: z8.string().optional(),
2772
+ color: hexColorSchema.optional(),
2756
2773
  width: z8.coerce.number().int().positive().optional(),
2757
2774
  autoIngest: z8.coerce.number().int().min(0).max(20).optional(),
2758
2775
  descriptionContext: z8.string().optional()
@@ -2802,6 +2819,19 @@ var imagesIngestResponseSchema = z8.object({
2802
2819
  contentHash: z8.string()
2803
2820
  });
2804
2821
 
2822
+ // ../api/src/logoShape.ts
2823
+ var PLATE_SOLIDITY_THRESHOLD = 0.6;
2824
+ var WORDMARK_MIN_ASPECT = 1.8;
2825
+ function classifyLogoShape({
2826
+ aspectRatio: aspectRatio2,
2827
+ hasAlpha,
2828
+ solidity
2829
+ }) {
2830
+ if (solidity === void 0 || aspectRatio2 === void 0) return "unknown";
2831
+ if (solidity >= PLATE_SOLIDITY_THRESHOLD || hasAlpha === false) return "plated-icon";
2832
+ return aspectRatio2 >= WORDMARK_MIN_ASPECT ? "wordmark" : "symbol";
2833
+ }
2834
+
2805
2835
  // ../api/src/tags.ts
2806
2836
  import { z as z9 } from "zod";
2807
2837
  var TAG_TYPES = [
@@ -24470,6 +24500,31 @@ var ingestCommand = defineCommand119({
24470
24500
 
24471
24501
  // src/commands/images/library.ts
24472
24502
  import { defineCommand as defineCommand120 } from "citty";
24503
+
24504
+ // src/commands/images/logoHints.ts
24505
+ var LOGO_QUERY_RE = /\blogos?\b|\bwordmark\b|\bbrand mark\b/i;
24506
+ function withLogoShape(row) {
24507
+ return {
24508
+ ...row,
24509
+ logoShape: classifyLogoShape({
24510
+ aspectRatio: row.aspectRatio,
24511
+ hasAlpha: row.hasAlpha,
24512
+ solidity: row.solidity
24513
+ })
24514
+ };
24515
+ }
24516
+ function buildLogoLibraryHints(query, rows) {
24517
+ if (!LOGO_QUERY_RE.test(query)) return [];
24518
+ const plates = rows.filter((row) => row.logoShape === "plated-icon");
24519
+ if (plates.length === 0) return [];
24520
+ const names = plates.map((row) => row.name).filter((name) => Boolean(name)).slice(0, 4);
24521
+ const subject = names.length > 0 ? names.join(", ") : `${plates.length} result(s)`;
24522
+ return [
24523
+ `PLATED ICON: ${subject} \u2014 solid tile with the mark knocked out, not a wordmark. In a logo strip it renders as a filled box and verify blocks it (logo-plate). Re-source with 'baker images logo <domain> --variant logo', or strip the plate with 'baker images normalize <file> --remove-bg --shrink-to-content'. Only use a plate in slots under ~32px.`
24524
+ ];
24525
+ }
24526
+
24527
+ // src/commands/images/library.ts
24473
24528
  registerSchema({
24474
24529
  command: "images.library",
24475
24530
  description: "Search the company image library. Returns only ready images.",
@@ -24534,8 +24589,10 @@ var libraryCommand = defineCommand120({
24534
24589
  if (minScore !== void 0) {
24535
24590
  data = data.filter((r) => typeof r.score === "number" && r.score >= minScore);
24536
24591
  }
24592
+ const shaped = data.map(withLogoShape);
24593
+ const hints = buildLogoLibraryHints(query, shaped);
24537
24594
  writeOutput(
24538
- { ok: true, data },
24595
+ { ok: true, data: shaped, ...hints.length > 0 ? { hints } : {} },
24539
24596
  args.output || "json",
24540
24597
  args.fields ? args.fields.split(",") : void 0,
24541
24598
  args.full
@@ -24553,11 +24610,16 @@ var libraryCommand = defineCommand120({
24553
24610
 
24554
24611
  // src/commands/images/logo.ts
24555
24612
  import { defineCommand as defineCommand121 } from "citty";
24613
+ var MAX_DOMAINS = 20;
24556
24614
  registerSchema({
24557
24615
  command: "images.logo",
24558
24616
  description: "Brand logo lookup via Brandfetch CDN (fallback/404). Auto-ingests by default.",
24559
24617
  args: {
24560
- domain: { type: "string", description: "Brand domain (e.g. stripe.com)", required: true },
24618
+ domain: {
24619
+ type: "string",
24620
+ description: "Brand domain, or a comma-separated list for a whole strip (e.g. stripe.com,intercom.com)",
24621
+ required: true
24622
+ },
24561
24623
  variant: { type: "string", description: "icon | logo | symbol", required: false },
24562
24624
  "auto-ingest": {
24563
24625
  type: "number",
@@ -24580,10 +24642,10 @@ registerSchema({
24580
24642
  var logoCommand = defineCommand121({
24581
24643
  meta: {
24582
24644
  name: "logo",
24583
- description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nExample: baker images logo stripe.com --variant logo"
24645
+ description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nSource a whole strip in one call by passing a comma-separated list \u2014 a text-rendered brand name is never the cheaper option.\n\nExamples:\n baker images logo stripe.com --variant logo\n baker images logo stripe.com,intercom.com,notion.so --variant logo"
24584
24646
  },
24585
24647
  args: {
24586
- domain: { type: "positional", description: "Brand domain", required: false },
24648
+ domain: { type: "positional", description: "Brand domain, or a comma-separated list", required: false },
24587
24649
  variant: { type: "string", description: "icon|logo|symbol", required: false },
24588
24650
  "auto-ingest": { type: "string", description: "Ingest top N (0-20, default 1)", required: false },
24589
24651
  "no-auto-ingest": { type: "boolean", description: "Skip auto-ingest", required: false },
@@ -24591,18 +24653,50 @@ var logoCommand = defineCommand121({
24591
24653
  },
24592
24654
  run: async ({ args }) => {
24593
24655
  try {
24594
- const domain = args.domain;
24595
- if (!domain) {
24656
+ const raw = args.domain;
24657
+ const domains = [
24658
+ ...new Set(
24659
+ (raw ?? "").split(",").map((d) => d.trim()).filter(Boolean)
24660
+ )
24661
+ ];
24662
+ if (domains.length === 0) {
24596
24663
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Domain is required" } });
24597
24664
  process.exit(1);
24598
24665
  }
24599
- const body = { domain };
24600
- if (args.variant) body.variant = args.variant;
24601
- if (args["auto-ingest"] !== void 0) body.autoIngest = Number(args["auto-ingest"]);
24602
- else if (args["no-auto-ingest"]) body.autoIngest = 0;
24603
- if (args.context) body.descriptionContext = args.context;
24604
- const data = await apiPost("/api/images/logo", body);
24605
- writeJson({ ok: true, data });
24666
+ if (domains.length > MAX_DOMAINS) {
24667
+ writeJson({
24668
+ ok: false,
24669
+ error: {
24670
+ code: "VALIDATION_ERROR",
24671
+ message: `Too many domains (${domains.length}); pass at most ${MAX_DOMAINS} per call`
24672
+ }
24673
+ });
24674
+ process.exit(1);
24675
+ }
24676
+ const base = {};
24677
+ if (args.variant) base.variant = args.variant;
24678
+ if (args["auto-ingest"] !== void 0) base.autoIngest = Number(args["auto-ingest"]);
24679
+ else if (args["no-auto-ingest"]) base.autoIngest = 0;
24680
+ if (args.context) base.descriptionContext = args.context;
24681
+ const fetchOne = (domain) => apiPost("/api/images/logo", { ...base, domain });
24682
+ if (domains.length === 1) {
24683
+ writeJson({ ok: true, data: await fetchOne(domains[0]) });
24684
+ return;
24685
+ }
24686
+ const settled = await Promise.all(
24687
+ domains.map(async (domain) => {
24688
+ try {
24689
+ return { domain, ...await fetchOne(domain) };
24690
+ } catch (err) {
24691
+ return { domain, hits: [], error: err instanceof ApiError ? err.message : "Lookup failed" };
24692
+ }
24693
+ })
24694
+ );
24695
+ const empty = settled.filter((r) => r.hits.length === 0).map((r) => r.domain);
24696
+ const hints = empty.length > 0 ? [
24697
+ `NO LOGO: ${empty.join(", ")} \u2014 Brandfetch doesn't know ${empty.length === 1 ? "this brand" : "these brands"}. Fall back per domain: 'baker images extract <domain> --auto-ingest 5', then 'baker images icon <brand> --set simple-icons'. Still nothing \u2192 'baker actions create' to acquire the file. Do not render the name as text.`
24698
+ ] : [];
24699
+ writeJson({ ok: true, data: { results: settled }, ...hints.length > 0 ? { hints } : {} });
24606
24700
  } catch (err) {
24607
24701
  if (err instanceof ApiError) {
24608
24702
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
@@ -24646,6 +24740,7 @@ function getDominantEdgeColor(data, width, height) {
24646
24740
  const colorCount = {};
24647
24741
  function accumulateColor(i, j) {
24648
24742
  const idx = (i * width + j) * 4;
24743
+ if ((data[idx + 3] ?? 0) < 10) return;
24649
24744
  const colorKey = `${data[idx]},${data[idx + 1]},${data[idx + 2]}`;
24650
24745
  colorCount[colorKey] = (colorCount[colorKey] ?? 0) + 1;
24651
24746
  }
@@ -24658,7 +24753,7 @@ function getDominantEdgeColor(data, width, height) {
24658
24753
  accumulateColor(i, width - 1);
24659
24754
  }
24660
24755
  let maxCount = 0;
24661
- let dominantColor = { r: 0, g: 0, b: 0 };
24756
+ let dominantColor = null;
24662
24757
  for (const key in colorCount) {
24663
24758
  const count = colorCount[key];
24664
24759
  if (count > maxCount) {
@@ -24669,6 +24764,27 @@ function getDominantEdgeColor(data, width, height) {
24669
24764
  }
24670
24765
  return dominantColor;
24671
24766
  }
24767
+ function opaqueSolidity(data, width, height) {
24768
+ let minX = width;
24769
+ let minY = height;
24770
+ let maxX = -1;
24771
+ let maxY = -1;
24772
+ let opaque = 0;
24773
+ for (let y = 0; y < height; y++) {
24774
+ for (let x = 0; x < width; x++) {
24775
+ if ((data[(y * width + x) * 4 + 3] ?? 0) <= 200) continue;
24776
+ opaque++;
24777
+ if (x < minX) minX = x;
24778
+ if (x > maxX) maxX = x;
24779
+ if (y < minY) minY = y;
24780
+ if (y > maxY) maxY = y;
24781
+ }
24782
+ }
24783
+ if (maxX < 0) return 0;
24784
+ const boxArea = (maxX - minX + 1) * (maxY - minY + 1);
24785
+ return boxArea === 0 ? 0 : opaque / boxArea;
24786
+ }
24787
+ var PLATE_SOLIDITY_THRESHOLD2 = 0.6;
24672
24788
  function hasTransparency(data, threshold = 0.02) {
24673
24789
  let transparentPixels = 0;
24674
24790
  let totalPixels = 0;
@@ -24733,6 +24849,9 @@ function removeBackground(data, width, height, colorRangeThreshold = COLOR_RANGE
24733
24849
  return data;
24734
24850
  }
24735
24851
  const dominantEdgeColor = getDominantEdgeColor(data, width, height);
24852
+ if (!dominantEdgeColor) {
24853
+ return data;
24854
+ }
24736
24855
  const isGradient = hasGradientColors(data);
24737
24856
  const result = Buffer.from(data);
24738
24857
  if (isGradient) {
@@ -25010,8 +25129,8 @@ async function processInternal(inputBuffer, isSVG, options) {
25010
25129
  const metadata = await sharp3(inputBuffer).metadata();
25011
25130
  let alreadyTransparent = false;
25012
25131
  if (metadata.hasAlpha) {
25013
- const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25014
- alreadyTransparent = hasTransparency(alphaData, 0.05);
25132
+ const { data: alphaData, info: info2 } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25133
+ alreadyTransparent = opaqueSolidity(alphaData, info2.width, info2.height) < PLATE_SOLIDITY_THRESHOLD2;
25015
25134
  }
25016
25135
  let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
25017
25136
  if (options.color) {