@koda-sl/baker-cli 0.148.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 = [
@@ -5395,27 +5425,17 @@ var googleDraftStageRequestSchema = z13.object({
5395
5425
  chatId: z13.string(),
5396
5426
  op: googleDraftOpInputSchema
5397
5427
  });
5398
- var googleDraftStageResponseSchema = z13.discriminatedUnion("staged", [
5399
- z13.object({
5400
- staged: z13.literal(true),
5401
- ref: z13.string(),
5402
- kind: googleDraftOpKindSchema,
5403
- mode: googleWriteModeSchema,
5404
- dependsOn: z13.array(z13.string()),
5405
- summary: z13.string(),
5406
- warnings: z13.array(z13.string()),
5407
- /** True when the op amended an already-staged op in place instead of appending a new one. */
5408
- amended: z13.boolean().optional()
5409
- }),
5410
- z13.object({
5411
- staged: z13.literal(false),
5412
- noop: z13.literal(true),
5413
- kind: googleDraftOpKindSchema,
5414
- mode: googleWriteModeSchema,
5415
- summary: z13.string(),
5416
- reason: z13.string()
5417
- })
5418
- ]);
5428
+ var googleDraftStageResponseSchema = z13.object({
5429
+ staged: z13.literal(true),
5430
+ ref: z13.string(),
5431
+ kind: googleDraftOpKindSchema,
5432
+ mode: googleWriteModeSchema,
5433
+ dependsOn: z13.array(z13.string()),
5434
+ summary: z13.string(),
5435
+ warnings: z13.array(z13.string()),
5436
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
5437
+ amended: z13.boolean().optional()
5438
+ });
5419
5439
  var googleDraftAmendRequestSchema = z13.object({
5420
5440
  chatId: z13.string(),
5421
5441
  ref: z13.string(),
@@ -5442,8 +5462,7 @@ var googleDraftStageBatchResponseSchema = z13.object({
5442
5462
  summary: z13.string(),
5443
5463
  warnings: z13.array(z13.string())
5444
5464
  })
5445
- ),
5446
- skipped: z13.array(z13.object({ kind: googleDraftOpKindSchema, summary: z13.string(), reason: z13.string() })).optional()
5465
+ )
5447
5466
  });
5448
5467
  var googleDraftOpViewSchema = z13.object({
5449
5468
  ref: z13.string(),
@@ -5702,7 +5721,7 @@ function keywordEntries(args) {
5702
5721
  const seen = /* @__PURE__ */ new Set();
5703
5722
  for (const item of raw) {
5704
5723
  const entry = parseKeywordEntry(item, defaultMatch);
5705
- const key = JSON.stringify([entry.text, entry.matchType]);
5724
+ const key = `${entry.text}\0${entry.matchType}`;
5706
5725
  if (!seen.has(key)) {
5707
5726
  seen.add(key);
5708
5727
  entries.push(entry);
@@ -5769,9 +5788,6 @@ function handleGoogleError(err) {
5769
5788
  });
5770
5789
  process.exit(1);
5771
5790
  }
5772
- function noopHints(data) {
5773
- return data.staged ? [] : [`NOT STAGED \u2014 ${data.reason}. Nothing to publish for this change; say so rather than reporting it as done.`];
5774
- }
5775
5791
  async function stageGoogleOp(raw, hints) {
5776
5792
  const preflight = googleDraftOpInputSchema.safeParse(raw);
5777
5793
  if (!preflight.success) {
@@ -5789,8 +5805,7 @@ async function stageGoogleOp(raw, hints) {
5789
5805
  try {
5790
5806
  const chatId = requireChatId();
5791
5807
  const response = await apiPost("/api/ads/google/draft/stage", { chatId, op: preflight.data });
5792
- const allHints = [...noopHints(response.data), ...hints ?? []];
5793
- writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5808
+ writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5794
5809
  } catch (err) {
5795
5810
  handleGoogleError(err);
5796
5811
  }
@@ -5826,14 +5841,7 @@ async function stageGoogleOps(rawOps, hints) {
5826
5841
  try {
5827
5842
  const chatId = requireChatId();
5828
5843
  const response = await apiPost("/api/ads/google/draft/stage-batch", { chatId, ops });
5829
- const skipped = response.data.skipped ?? [];
5830
- const allHints = [
5831
- ...skipped.length > 0 ? [
5832
- `${skipped.length} of ${ops.length} op(s) NOT STAGED \u2014 already in the requested state: ${skipped.map((s) => s.reason).join("; ")}`
5833
- ] : [],
5834
- ...hints ?? []
5835
- ];
5836
- writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5844
+ writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5837
5845
  } catch (err) {
5838
5846
  handleGoogleError(err);
5839
5847
  }
@@ -24492,6 +24500,31 @@ var ingestCommand = defineCommand119({
24492
24500
 
24493
24501
  // src/commands/images/library.ts
24494
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
24495
24528
  registerSchema({
24496
24529
  command: "images.library",
24497
24530
  description: "Search the company image library. Returns only ready images.",
@@ -24556,8 +24589,10 @@ var libraryCommand = defineCommand120({
24556
24589
  if (minScore !== void 0) {
24557
24590
  data = data.filter((r) => typeof r.score === "number" && r.score >= minScore);
24558
24591
  }
24592
+ const shaped = data.map(withLogoShape);
24593
+ const hints = buildLogoLibraryHints(query, shaped);
24559
24594
  writeOutput(
24560
- { ok: true, data },
24595
+ { ok: true, data: shaped, ...hints.length > 0 ? { hints } : {} },
24561
24596
  args.output || "json",
24562
24597
  args.fields ? args.fields.split(",") : void 0,
24563
24598
  args.full
@@ -24575,11 +24610,16 @@ var libraryCommand = defineCommand120({
24575
24610
 
24576
24611
  // src/commands/images/logo.ts
24577
24612
  import { defineCommand as defineCommand121 } from "citty";
24613
+ var MAX_DOMAINS = 20;
24578
24614
  registerSchema({
24579
24615
  command: "images.logo",
24580
24616
  description: "Brand logo lookup via Brandfetch CDN (fallback/404). Auto-ingests by default.",
24581
24617
  args: {
24582
- 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
+ },
24583
24623
  variant: { type: "string", description: "icon | logo | symbol", required: false },
24584
24624
  "auto-ingest": {
24585
24625
  type: "number",
@@ -24602,10 +24642,10 @@ registerSchema({
24602
24642
  var logoCommand = defineCommand121({
24603
24643
  meta: {
24604
24644
  name: "logo",
24605
- 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"
24606
24646
  },
24607
24647
  args: {
24608
- domain: { type: "positional", description: "Brand domain", required: false },
24648
+ domain: { type: "positional", description: "Brand domain, or a comma-separated list", required: false },
24609
24649
  variant: { type: "string", description: "icon|logo|symbol", required: false },
24610
24650
  "auto-ingest": { type: "string", description: "Ingest top N (0-20, default 1)", required: false },
24611
24651
  "no-auto-ingest": { type: "boolean", description: "Skip auto-ingest", required: false },
@@ -24613,18 +24653,50 @@ var logoCommand = defineCommand121({
24613
24653
  },
24614
24654
  run: async ({ args }) => {
24615
24655
  try {
24616
- const domain = args.domain;
24617
- 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) {
24618
24663
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Domain is required" } });
24619
24664
  process.exit(1);
24620
24665
  }
24621
- const body = { domain };
24622
- if (args.variant) body.variant = args.variant;
24623
- if (args["auto-ingest"] !== void 0) body.autoIngest = Number(args["auto-ingest"]);
24624
- else if (args["no-auto-ingest"]) body.autoIngest = 0;
24625
- if (args.context) body.descriptionContext = args.context;
24626
- const data = await apiPost("/api/images/logo", body);
24627
- 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 } : {} });
24628
24700
  } catch (err) {
24629
24701
  if (err instanceof ApiError) {
24630
24702
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
@@ -24668,6 +24740,7 @@ function getDominantEdgeColor(data, width, height) {
24668
24740
  const colorCount = {};
24669
24741
  function accumulateColor(i, j) {
24670
24742
  const idx = (i * width + j) * 4;
24743
+ if ((data[idx + 3] ?? 0) < 10) return;
24671
24744
  const colorKey = `${data[idx]},${data[idx + 1]},${data[idx + 2]}`;
24672
24745
  colorCount[colorKey] = (colorCount[colorKey] ?? 0) + 1;
24673
24746
  }
@@ -24680,7 +24753,7 @@ function getDominantEdgeColor(data, width, height) {
24680
24753
  accumulateColor(i, width - 1);
24681
24754
  }
24682
24755
  let maxCount = 0;
24683
- let dominantColor = { r: 0, g: 0, b: 0 };
24756
+ let dominantColor = null;
24684
24757
  for (const key in colorCount) {
24685
24758
  const count = colorCount[key];
24686
24759
  if (count > maxCount) {
@@ -24691,6 +24764,27 @@ function getDominantEdgeColor(data, width, height) {
24691
24764
  }
24692
24765
  return dominantColor;
24693
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;
24694
24788
  function hasTransparency(data, threshold = 0.02) {
24695
24789
  let transparentPixels = 0;
24696
24790
  let totalPixels = 0;
@@ -24755,6 +24849,9 @@ function removeBackground(data, width, height, colorRangeThreshold = COLOR_RANGE
24755
24849
  return data;
24756
24850
  }
24757
24851
  const dominantEdgeColor = getDominantEdgeColor(data, width, height);
24852
+ if (!dominantEdgeColor) {
24853
+ return data;
24854
+ }
24758
24855
  const isGradient = hasGradientColors(data);
24759
24856
  const result = Buffer.from(data);
24760
24857
  if (isGradient) {
@@ -25032,8 +25129,8 @@ async function processInternal(inputBuffer, isSVG, options) {
25032
25129
  const metadata = await sharp3(inputBuffer).metadata();
25033
25130
  let alreadyTransparent = false;
25034
25131
  if (metadata.hasAlpha) {
25035
- const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25036
- 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;
25037
25134
  }
25038
25135
  let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
25039
25136
  if (options.color) {