@koda-sl/baker-cli 0.150.0-dev.a970fa118 → 0.151.0-dev.1a051172a
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 +15 -2
- package/dist/cli.js +226 -55
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -300,11 +300,22 @@ When a query fails, the error includes a `fix` object with the exact corrected c
|
|
|
300
300
|
|
|
301
301
|
### `baker ads google changes`
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
Read the change history of an account.
|
|
304
|
+
|
|
305
|
+
Google keeps account history in two layers, and neither reaches past 90 days:
|
|
306
|
+
|
|
307
|
+
| Scope | Reach | What you get |
|
|
308
|
+
|-------------|---------|---------------------------------------------------------------------|
|
|
309
|
+
| `detail` | 30 days | What changed, old → new value, and who changed it |
|
|
310
|
+
| `summary` | 90 days | Which resources were added, changed or removed — no values, no user |
|
|
311
|
+
|
|
312
|
+
Beyond 90 days there is no API. Infer the change from a monthly spend trend per campaign
|
|
313
|
+
instead, or ask the user to check the change history in the Google Ads web UI.
|
|
304
314
|
|
|
305
315
|
```bash
|
|
306
316
|
baker ads google changes --customer-id 1234567890
|
|
307
317
|
baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN
|
|
318
|
+
baker ads google changes --customer-id 1234567890 --days 60 --scope summary
|
|
308
319
|
```
|
|
309
320
|
|
|
310
321
|
**Flags:**
|
|
@@ -312,7 +323,8 @@ baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMP
|
|
|
312
323
|
| Flag | Description |
|
|
313
324
|
|-------------------|---------------------------------------------------------------|
|
|
314
325
|
| `--customer-id` | Google Ads customer ID. Falls back to `BAKER_GOOGLE_ADS_CUSTOMER_ID` env var. |
|
|
315
|
-
| `--
|
|
326
|
+
| `--scope` | `detail` (default, 30 days) or `summary` (90 days) |
|
|
327
|
+
| `--days` | Lookback days (default 7). Max 30 with `detail`, 90 with `summary` |
|
|
316
328
|
| `--resource-type` | Filter: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION |
|
|
317
329
|
| `--limit` | Max results (default 50) |
|
|
318
330
|
| `--output` | Format: `json` \| `csv` \| `jsonl` \| `md` |
|
|
@@ -4671,6 +4683,7 @@ baker brand fonts fetch "Inter" --subsets latin,latin-ext # add accented-glyph
|
|
|
4671
4683
|
```
|
|
4672
4684
|
|
|
4673
4685
|
- **`check`** asks Google Fonts what it *actually* serves for every family and weight `src/styles/global.css` requests, and reports `requested` / `served` / `missing` plus the available unicode subsets. It exits 2 with `FONT_NOT_SERVED` when the brand asks for something Google won't serve, and distinguishes an unknown family name from a family that exists at other weights — the fix differs.
|
|
4686
|
+
- **Failures carry the numbers, so nothing needs a second call.** `check`'s error keeps the full per-family breakdown at `error.fix.families` (healthy families included, so a multi-family brand shows what not to touch), and both commands probe the standard weights to report the ones that *do* exist at `error.fix.served` — the remediation is "retry with one of these" rather than "run `check`", which would be the command that just failed.
|
|
4674
4687
|
- **`fetch`** downloads the family into `src/brand/fonts/` and prints the `@font-face` block to paste above `@theme`. Defaults to the `latin` subset; `--subsets latin,latin-ext` adds accented-glyph coverage for Spanish and Portuguese copy. One file per weight *and* subset, since Google ships them separately and `unicode-range` is carried through verbatim.
|
|
4675
4688
|
- **It does not edit `global.css` for you.** Paste the block, then drop that family from the Google `@import` — otherwise the page self-hosts *and* still calls Google. Record the weights in `src/brand/BRAND.md` in the same change.
|
|
4676
4689
|
- **Licensing:** Google Fonts are open-licensed and safe to self-host. A client's own commercial typeface is not — confirm before re-hosting one.
|
package/dist/cli.js
CHANGED
|
@@ -4042,11 +4042,21 @@ var FIELD_DESCRIPTIONS = {
|
|
|
4042
4042
|
"asset_group.status": "ENABLED, PAUSED, or REMOVED",
|
|
4043
4043
|
"asset_group_asset.field_type": "Where the asset is used (HEADLINE, DESCRIPTION, LOGO, etc.)",
|
|
4044
4044
|
"asset_group_asset.performance_label": "Performance rating: BEST, GOOD, LOW, LEARNING",
|
|
4045
|
-
// Change events
|
|
4045
|
+
// Change events (field-level detail, last 30 days)
|
|
4046
4046
|
"change_event.change_date_time": "When the change occurred (ISO 8601)",
|
|
4047
4047
|
"change_event.change_resource_type": "What was changed: CAMPAIGN, AD_GROUP, AD, etc.",
|
|
4048
4048
|
"change_event.resource_change_operation": "CREATE, UPDATE, or REMOVE",
|
|
4049
4049
|
"change_event.user_email": "Email of user who made the change",
|
|
4050
|
+
// Change status (which resources changed, last 90 days — no values, no user)
|
|
4051
|
+
"change_status.last_change_date_time": "When this resource last changed (ISO 8601)",
|
|
4052
|
+
"change_status.resource_type": "What changed: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION, etc.",
|
|
4053
|
+
"change_status.resource_status": "ADDED, CHANGED, or REMOVED \u2014 not the old or new value",
|
|
4054
|
+
"change_status.resource_name": "Identifier of the changed resource; query it to read its current values",
|
|
4055
|
+
"change_status.campaign": "Resource name of the campaign the change belongs to",
|
|
4056
|
+
"change_status.ad_group": "Resource name of the ad group the change belongs to",
|
|
4057
|
+
"change_status.ad_group_ad": "Resource name of the changed ad",
|
|
4058
|
+
"change_status.ad_group_criterion": "Resource name of the changed keyword or criterion",
|
|
4059
|
+
"change_status.campaign_criterion": "Resource name of the changed campaign-level criterion",
|
|
4050
4060
|
// Landing pages
|
|
4051
4061
|
"landing_page_view.unexpanded_final_url": "Landing page URL",
|
|
4052
4062
|
// Geo
|
|
@@ -4064,6 +4074,89 @@ function getFieldDescriptions(fields) {
|
|
|
4064
4074
|
return result;
|
|
4065
4075
|
}
|
|
4066
4076
|
|
|
4077
|
+
// src/commands/ads/google/changes-window.ts
|
|
4078
|
+
var CHANGE_SCOPE_MAX_DAYS = {
|
|
4079
|
+
detail: 30,
|
|
4080
|
+
summary: 90
|
|
4081
|
+
};
|
|
4082
|
+
var DEFAULT_CHANGE_DAYS = 7;
|
|
4083
|
+
function isChangeScope(value) {
|
|
4084
|
+
return value === "detail" || value === "summary";
|
|
4085
|
+
}
|
|
4086
|
+
function resolveChangesWindow(input) {
|
|
4087
|
+
const scope = input.scope ?? "detail";
|
|
4088
|
+
if (!isChangeScope(scope)) {
|
|
4089
|
+
return {
|
|
4090
|
+
ok: false,
|
|
4091
|
+
error: {
|
|
4092
|
+
code: "INVALID_SCOPE",
|
|
4093
|
+
message: `Unknown scope '${scope}'. Use 'detail' (field-level changes, 30 days) or 'summary' (which resources changed, 90 days).`,
|
|
4094
|
+
fix: {
|
|
4095
|
+
action: "retry_with_flag",
|
|
4096
|
+
explanation: "Pass --scope detail or --scope summary."
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
const days = input.days ?? DEFAULT_CHANGE_DAYS;
|
|
4102
|
+
if (!Number.isInteger(days) || days < 1) {
|
|
4103
|
+
return {
|
|
4104
|
+
ok: false,
|
|
4105
|
+
error: {
|
|
4106
|
+
code: "INVALID_LOOKBACK",
|
|
4107
|
+
message: `--days must be a whole number of days, got '${days}'.`,
|
|
4108
|
+
fix: {
|
|
4109
|
+
action: "retry_with_flag",
|
|
4110
|
+
explanation: `Pass --days as a positive integer, e.g. --days ${DEFAULT_CHANGE_DAYS}.`
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
const max = CHANGE_SCOPE_MAX_DAYS[scope];
|
|
4116
|
+
if (days > max) {
|
|
4117
|
+
return { ok: false, error: lookbackTooLong(scope, days, max) };
|
|
4118
|
+
}
|
|
4119
|
+
return { ok: true, scope, days, hints: hintsFor(scope, days) };
|
|
4120
|
+
}
|
|
4121
|
+
function lookbackTooLong(scope, days, max) {
|
|
4122
|
+
if (scope === "detail" && days <= CHANGE_SCOPE_MAX_DAYS.summary) {
|
|
4123
|
+
return {
|
|
4124
|
+
code: "LOOKBACK_TOO_LONG",
|
|
4125
|
+
message: `Google keeps field-level change detail for ${max} days only; ${days} days is outside that window.`,
|
|
4126
|
+
fix: {
|
|
4127
|
+
action: "narrow_date_range",
|
|
4128
|
+
explanation: `Re-run with --scope summary --days ${days} to reach back ${CHANGE_SCOPE_MAX_DAYS.summary} days. Summary tells you which campaigns, ad groups, ads and criteria changed and when, but not the old and new values \u2014 read the resource itself for current values.`
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
return {
|
|
4133
|
+
code: "LOOKBACK_TOO_LONG",
|
|
4134
|
+
message: `Google's account history API reaches back ${CHANGE_SCOPE_MAX_DAYS.summary} days at most; ${days} days is outside that window.`,
|
|
4135
|
+
fix: {
|
|
4136
|
+
action: "use_different_resource",
|
|
4137
|
+
explanation: "Do not abort \u2014 infer the change from performance instead. Query monthly metrics per campaign (SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS) to see which campaigns stopped or started spending. That shows what changed and when, though not who changed it. Older history exists only in the Google Ads web UI, which has no API \u2014 ask the user to check it if the 'who' matters."
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
function hintsFor(scope, days) {
|
|
4142
|
+
const hints = [];
|
|
4143
|
+
if (scope === "detail") {
|
|
4144
|
+
hints.push(
|
|
4145
|
+
`Detailed history covers the last ${CHANGE_SCOPE_MAX_DAYS.detail} days. For anything older use --scope summary (reaches ${CHANGE_SCOPE_MAX_DAYS.summary} days, reports which resources changed but not their values).`
|
|
4146
|
+
);
|
|
4147
|
+
} else {
|
|
4148
|
+
hints.push(
|
|
4149
|
+
"Summary history reports which resources changed and when, not old and new values. To see what a changed resource looks like now, query it by resource_name."
|
|
4150
|
+
);
|
|
4151
|
+
if (days > CHANGE_SCOPE_MAX_DAYS.detail) {
|
|
4152
|
+
hints.push(
|
|
4153
|
+
`Changes inside the last ${CHANGE_SCOPE_MAX_DAYS.detail} days also have field-level detail \u2014 re-run with --scope detail for those.`
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
return hints;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4067
4160
|
// src/commands/ads/google/correction-table.ts
|
|
4068
4161
|
function buildCommand(query, ctx) {
|
|
4069
4162
|
return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
|
|
@@ -4392,6 +4485,35 @@ var CORRECTION_RULES = [
|
|
|
4392
4485
|
};
|
|
4393
4486
|
}
|
|
4394
4487
|
},
|
|
4488
|
+
// 20a. change_event past its 30-day reach → change_status, which reaches 90 days.
|
|
4489
|
+
// The enum prefix in the API message is what separates this from the change_status
|
|
4490
|
+
// case below; a bare START_DATE_TOO_OLD falls through to the query-pattern pass,
|
|
4491
|
+
// which routes on the resource the caller actually queried.
|
|
4492
|
+
{
|
|
4493
|
+
matchQuery: /FROM\s+change_event\b/i,
|
|
4494
|
+
matchApiError: /ChangeEventError\.START_DATE_TOO_OLD/i,
|
|
4495
|
+
fix: (ctx) => {
|
|
4496
|
+
const limit = ctx.originalQuery.match(/LIMIT\s+(\d+)/i)?.[1] ?? "50";
|
|
4497
|
+
const requested = ctx.originalQuery.match(/change_date_time\s*>=\s*'([^']+)'/i)?.[1]?.slice(0, 10);
|
|
4498
|
+
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
4499
|
+
const start = requested && requested > ninetyDaysAgo ? requested : ninetyDaysAgo;
|
|
4500
|
+
const corrected = `SELECT change_status.last_change_date_time, change_status.resource_type, change_status.resource_status, change_status.resource_name, change_status.campaign, change_status.ad_group FROM change_status WHERE change_status.last_change_date_time >= '${start}' ORDER BY change_status.last_change_date_time DESC LIMIT ${limit}`;
|
|
4501
|
+
return {
|
|
4502
|
+
action: "use_different_resource",
|
|
4503
|
+
correctedCommand: buildCommand(corrected, ctx),
|
|
4504
|
+
explanation: `change_event only reaches back 30 days. change_status reaches 90 (from ${start}) but reports only which resources changed and whether they were added, changed or removed \u2014 no old/new values and no user email. Read a changed resource by its resource_name to see its current values.`
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
},
|
|
4508
|
+
// 20b. Past 90 days no history resource reaches — infer the change from spend instead.
|
|
4509
|
+
{
|
|
4510
|
+
matchQuery: /FROM\s+change_status\b/i,
|
|
4511
|
+
matchApiError: /ChangeStatusError\.START_DATE_TOO_OLD/i,
|
|
4512
|
+
fix: () => ({
|
|
4513
|
+
action: "use_different_resource",
|
|
4514
|
+
explanation: "Google's account history reaches back 90 days at most, and this query asks for more. Do not abort \u2014 infer the change from performance: SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS shows which campaigns started or stopped spending and when, though not who changed them. Anything older with attribution exists only in the Google Ads web UI, which has no API."
|
|
4515
|
+
})
|
|
4516
|
+
},
|
|
4395
4517
|
// 20. change_event without date constraint
|
|
4396
4518
|
{
|
|
4397
4519
|
matchQuery: /FROM\s+change_event\b(?!.*change_date_time)/i,
|
|
@@ -4545,14 +4667,25 @@ function parseApiError(errorMessage, originalQuery, customerId) {
|
|
|
4545
4667
|
// src/commands/ads/google/changes.ts
|
|
4546
4668
|
registerSchema({
|
|
4547
4669
|
command: "ads.google.changes",
|
|
4548
|
-
description: "
|
|
4670
|
+
description: "Read the account's change history. --scope detail (default) shows what changed and to what value, back 30 days; --scope summary shows which resources changed, back 90 days.",
|
|
4549
4671
|
args: {
|
|
4550
4672
|
"customer-id": {
|
|
4551
4673
|
type: "string",
|
|
4552
4674
|
description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
|
|
4553
4675
|
required: false
|
|
4554
4676
|
},
|
|
4555
|
-
days: {
|
|
4677
|
+
days: {
|
|
4678
|
+
type: "string",
|
|
4679
|
+
description: `Lookback days (default: ${DEFAULT_CHANGE_DAYS}). Max ${CHANGE_SCOPE_MAX_DAYS.detail} with --scope detail, ${CHANGE_SCOPE_MAX_DAYS.summary} with --scope summary. Google keeps nothing older behind any API.`,
|
|
4680
|
+
required: false,
|
|
4681
|
+
default: DEFAULT_CHANGE_DAYS
|
|
4682
|
+
},
|
|
4683
|
+
scope: {
|
|
4684
|
+
type: "string",
|
|
4685
|
+
description: `detail|summary (default: detail). detail = old/new values plus who made the change, ${CHANGE_SCOPE_MAX_DAYS.detail} days. summary = which resources were added, changed or removed, ${CHANGE_SCOPE_MAX_DAYS.summary} days, without values.`,
|
|
4686
|
+
required: false,
|
|
4687
|
+
default: "detail"
|
|
4688
|
+
},
|
|
4556
4689
|
"resource-type": {
|
|
4557
4690
|
type: "string",
|
|
4558
4691
|
description: "Filter by type: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION",
|
|
@@ -4565,15 +4698,24 @@ registerSchema({
|
|
|
4565
4698
|
var changesCommand = defineCommand20({
|
|
4566
4699
|
meta: {
|
|
4567
4700
|
name: "changes",
|
|
4568
|
-
description: `
|
|
4701
|
+
description: `Read the change history of a Google Ads account.
|
|
4702
|
+
|
|
4703
|
+
Google keeps account history in two layers, and neither reaches past 90 days:
|
|
4704
|
+
--scope detail (default) what changed, old \u2192 new value, who changed it \u2014 last ${CHANGE_SCOPE_MAX_DAYS.detail} days
|
|
4705
|
+
--scope summary which resources were added, changed or removed \u2014 last ${CHANGE_SCOPE_MAX_DAYS.summary} days
|
|
4706
|
+
|
|
4707
|
+
For anything older, infer it from performance instead \u2014 a monthly spend trend per
|
|
4708
|
+
campaign shows what started or stopped, just not who did it.
|
|
4569
4709
|
|
|
4570
4710
|
Examples:
|
|
4571
4711
|
baker ads google changes --customer-id 1234567890
|
|
4572
|
-
baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN
|
|
4712
|
+
baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN
|
|
4713
|
+
baker ads google changes --customer-id 1234567890 --days 60 --scope summary`
|
|
4573
4714
|
},
|
|
4574
4715
|
args: {
|
|
4575
4716
|
"customer-id": { type: "string", description: "Google Ads customer ID", required: false },
|
|
4576
|
-
days: { type: "string", description:
|
|
4717
|
+
days: { type: "string", description: `Lookback days (default ${DEFAULT_CHANGE_DAYS})`, required: false },
|
|
4718
|
+
scope: { type: "string", description: "detail (30 days) or summary (90 days)", required: false },
|
|
4577
4719
|
"resource-type": { type: "string", description: "Filter by resource type", required: false },
|
|
4578
4720
|
limit: { type: "string", description: "Max results (default 50)", required: false },
|
|
4579
4721
|
"no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false },
|
|
@@ -4581,9 +4723,19 @@ Examples:
|
|
|
4581
4723
|
},
|
|
4582
4724
|
run: async ({ args }) => {
|
|
4583
4725
|
const customerId = await resolveCustomerId(args);
|
|
4726
|
+
const window = resolveChangesWindow({
|
|
4727
|
+
days: args.days ? Number(args.days) : void 0,
|
|
4728
|
+
scope: args.scope
|
|
4729
|
+
});
|
|
4730
|
+
if (!window.ok) {
|
|
4731
|
+
writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
|
|
4732
|
+
process.exit(1);
|
|
4733
|
+
return;
|
|
4734
|
+
}
|
|
4584
4735
|
const body = {
|
|
4585
4736
|
customerId,
|
|
4586
|
-
days:
|
|
4737
|
+
days: window.days,
|
|
4738
|
+
scope: window.scope,
|
|
4587
4739
|
limit: args.limit ? Number(args.limit) : 50
|
|
4588
4740
|
};
|
|
4589
4741
|
const managerId = getManagerIdForCustomer(customerId);
|
|
@@ -4600,7 +4752,7 @@ Examples:
|
|
|
4600
4752
|
const first = data[0];
|
|
4601
4753
|
const fields = first ? Object.keys(first) : [];
|
|
4602
4754
|
const fieldDescs = getFieldDescriptions(fields);
|
|
4603
|
-
|
|
4755
|
+
writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
|
|
4604
4756
|
} catch (err) {
|
|
4605
4757
|
if (err instanceof ApiError) {
|
|
4606
4758
|
writeAdsJson(parseApiError(err.message, "", customerId));
|
|
@@ -15369,11 +15521,46 @@ function resolveFetchWeights(flag, declared) {
|
|
|
15369
15521
|
return declared?.length ? declared : [400];
|
|
15370
15522
|
}
|
|
15371
15523
|
function googleFontCssUrl(family, weights) {
|
|
15372
|
-
const name = family.trim().
|
|
15524
|
+
const name = family.trim().split(/\s+/).map(encodeURIComponent).join("+");
|
|
15373
15525
|
const axis = [...new Set(weights)].sort((a, b) => a - b).join(";");
|
|
15374
15526
|
const spec = axis ? `${name}:wght@${axis}` : name;
|
|
15375
15527
|
return `${GOOGLE_CSS2}?family=${spec}&display=swap`;
|
|
15376
15528
|
}
|
|
15529
|
+
var STANDARD_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
15530
|
+
async function probeServedWeights(family, fetchCss) {
|
|
15531
|
+
const probes = await Promise.all(
|
|
15532
|
+
STANDARD_WEIGHTS.map(async (weight) => await fetchCss(googleFontCssUrl(family, [weight])) ? weight : null)
|
|
15533
|
+
);
|
|
15534
|
+
return probes.filter((w) => w !== null);
|
|
15535
|
+
}
|
|
15536
|
+
async function checkFontRequests(requests, fetchCss) {
|
|
15537
|
+
const results = [];
|
|
15538
|
+
for (const request of requests) {
|
|
15539
|
+
const css = await fetchCss(googleFontCssUrl(request.family, request.weights));
|
|
15540
|
+
if (css === null) {
|
|
15541
|
+
const served2 = await probeServedWeights(request.family, fetchCss);
|
|
15542
|
+
results.push({
|
|
15543
|
+
family: request.family,
|
|
15544
|
+
requested: request.weights,
|
|
15545
|
+
served: served2,
|
|
15546
|
+
missing: request.weights.filter((w) => !served2.includes(w)),
|
|
15547
|
+
subsets: [],
|
|
15548
|
+
cause: served2.length === 0 ? "unknown-family" : "unavailable-weight"
|
|
15549
|
+
});
|
|
15550
|
+
continue;
|
|
15551
|
+
}
|
|
15552
|
+
const faces = parseGoogleFontFaces(css);
|
|
15553
|
+
const served = [...new Set(faces.map((f) => f.weight))].sort((a, b) => a - b);
|
|
15554
|
+
results.push({
|
|
15555
|
+
family: request.family,
|
|
15556
|
+
requested: request.weights,
|
|
15557
|
+
served,
|
|
15558
|
+
missing: request.weights.filter((w) => !served.includes(w)),
|
|
15559
|
+
subsets: [...new Set(faces.map((f) => f.subset))]
|
|
15560
|
+
});
|
|
15561
|
+
}
|
|
15562
|
+
return results;
|
|
15563
|
+
}
|
|
15377
15564
|
|
|
15378
15565
|
// src/commands/brand/fonts.ts
|
|
15379
15566
|
var GLOBAL_CSS = path.join("src", "styles", "global.css");
|
|
@@ -15406,19 +15593,19 @@ function fail(code, message, fix) {
|
|
|
15406
15593
|
writeJson({ ok: false, error: { code, message, ...fix ? { fix } : {} } });
|
|
15407
15594
|
process.exit(2);
|
|
15408
15595
|
}
|
|
15596
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
15409
15597
|
async function fetchGoogleCss(url) {
|
|
15410
15598
|
try {
|
|
15411
|
-
const res = await fetch(url, {
|
|
15599
|
+
const res = await fetch(url, {
|
|
15600
|
+
headers: { "User-Agent": BROWSER_UA },
|
|
15601
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
15602
|
+
});
|
|
15412
15603
|
if (!res.ok) return null;
|
|
15413
15604
|
return await res.text();
|
|
15414
15605
|
} catch {
|
|
15415
15606
|
return null;
|
|
15416
15607
|
}
|
|
15417
15608
|
}
|
|
15418
|
-
async function diagnoseUnserved(family) {
|
|
15419
|
-
const bare = await fetchGoogleCss(googleFontCssUrl(family, []));
|
|
15420
|
-
return bare === null ? "unknown-family" : "unavailable-weight";
|
|
15421
|
-
}
|
|
15422
15609
|
async function readGlobalCss() {
|
|
15423
15610
|
try {
|
|
15424
15611
|
return await readFile(path.resolve(process.cwd(), GLOBAL_CSS), "utf8");
|
|
@@ -15444,41 +15631,20 @@ var fontsCheckCommand = defineCommand85({
|
|
|
15444
15631
|
});
|
|
15445
15632
|
return;
|
|
15446
15633
|
}
|
|
15447
|
-
const families =
|
|
15448
|
-
const
|
|
15449
|
-
|
|
15450
|
-
const
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
15455
|
-
|
|
15456
|
-
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
missing: request.weights,
|
|
15462
|
-
cause
|
|
15463
|
-
});
|
|
15464
|
-
continue;
|
|
15465
|
-
}
|
|
15466
|
-
const faces = parseGoogleFontFaces(served);
|
|
15467
|
-
const servedWeights = [...new Set(faces.map((f) => f.weight))].sort((a, b) => a - b);
|
|
15468
|
-
const missing = request.weights.filter((w) => !servedWeights.includes(w));
|
|
15469
|
-
if (missing.length > 0) unavailable.push(`${request.family} ${missing.join("/")}`);
|
|
15470
|
-
families.push({
|
|
15471
|
-
family: request.family,
|
|
15472
|
-
requested: request.weights,
|
|
15473
|
-
served: servedWeights,
|
|
15474
|
-
missing,
|
|
15475
|
-
subsets: [...new Set(faces.map((f) => f.subset))]
|
|
15476
|
-
});
|
|
15477
|
-
}
|
|
15478
|
-
if (unavailable.length > 0) {
|
|
15479
|
-
fail("FONT_NOT_SERVED", `Google Fonts does not serve: ${unavailable.join("; ")}`, {
|
|
15480
|
-
action: `Correct the family name or weight in ${GLOBAL_CSS} and src/brand/BRAND.md, or run \`baker brand fonts fetch\` for a weight that does exist.`,
|
|
15481
|
-
explanation: "A requested weight Google does not serve is silently synthesized by the browser, so the page looks subtly wrong with nothing failing."
|
|
15634
|
+
const families = await checkFontRequests(requests, fetchGoogleCss);
|
|
15635
|
+
const broken = families.filter((f) => f.missing.length > 0);
|
|
15636
|
+
if (broken.length > 0) {
|
|
15637
|
+
const summary = broken.map(
|
|
15638
|
+
(f) => f.cause === "unknown-family" ? `${f.family} (no such family on Google Fonts \u2014 check the exact spelling)` : `${f.family} ${f.missing.join("/")} (served: ${f.served.join("/") || "none"})`
|
|
15639
|
+
).join("; ");
|
|
15640
|
+
fail("FONT_NOT_SERVED", `Google Fonts does not serve: ${summary}`, {
|
|
15641
|
+
action: `Correct the family name or weight in ${GLOBAL_CSS} and src/brand/BRAND.md, picking from the \`served\` weights below.`,
|
|
15642
|
+
explanation: "A requested weight Google does not serve is silently synthesized by the browser, so the page looks subtly wrong with nothing failing.",
|
|
15643
|
+
// The per-family breakdown is the point of the command, so it ships on
|
|
15644
|
+
// the failure path too — otherwise the only remedy is re-running the
|
|
15645
|
+
// command that just failed. Healthy families are included so a
|
|
15646
|
+
// multi-family brand shows what NOT to touch.
|
|
15647
|
+
families
|
|
15482
15648
|
});
|
|
15483
15649
|
}
|
|
15484
15650
|
writeJson({
|
|
@@ -15515,16 +15681,18 @@ var fontsFetchCommand = defineCommand85({
|
|
|
15515
15681
|
);
|
|
15516
15682
|
const servedCss = await fetchGoogleCss(googleFontCssUrl(family, weights));
|
|
15517
15683
|
if (servedCss === null) {
|
|
15518
|
-
const
|
|
15684
|
+
const [diagnosis] = await checkFontRequests([{ family, weights }], fetchGoogleCss);
|
|
15685
|
+
const served = diagnosis?.served ?? [];
|
|
15519
15686
|
fail(
|
|
15520
15687
|
"FONT_NOT_SERVED",
|
|
15521
|
-
|
|
15522
|
-
|
|
15688
|
+
served.length === 0 ? `Google Fonts has no family called "${family}".` : `"${family}" exists on Google Fonts but not at weight ${weights.join("/")}.`,
|
|
15689
|
+
served.length === 0 ? {
|
|
15523
15690
|
action: "Check the exact family name at fonts.google.com, then retry.",
|
|
15524
15691
|
explanation: "Family names are case- and space-sensitive in the Google Fonts API."
|
|
15525
15692
|
} : {
|
|
15526
|
-
action: `
|
|
15527
|
-
explanation: "Google rejects the whole request when one requested weight does not exist."
|
|
15693
|
+
action: `Retry with --weights set to one of these: ${served.join(",")}.`,
|
|
15694
|
+
explanation: "Google rejects the whole request when one requested weight does not exist.",
|
|
15695
|
+
served
|
|
15528
15696
|
}
|
|
15529
15697
|
);
|
|
15530
15698
|
}
|
|
@@ -15539,7 +15707,10 @@ var fontsFetchCommand = defineCommand85({
|
|
|
15539
15707
|
await mkdir(fontsDir, { recursive: true });
|
|
15540
15708
|
const downloaded = [];
|
|
15541
15709
|
for (const face of faces) {
|
|
15542
|
-
const res = await fetch(face.url, {
|
|
15710
|
+
const res = await fetch(face.url, {
|
|
15711
|
+
headers: { "User-Agent": BROWSER_UA },
|
|
15712
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
15713
|
+
});
|
|
15543
15714
|
if (!res.ok) continue;
|
|
15544
15715
|
await writeFile(path.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
|
|
15545
15716
|
downloaded.push(face);
|