@koda-sl/baker-cli 0.244.0 → 0.246.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.
- package/README.md +49 -24
- package/dist/cli.js +626 -573
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9408,8 +9408,8 @@ function countStaccato(text2) {
|
|
|
9408
9408
|
let first = "";
|
|
9409
9409
|
let runStart = 0;
|
|
9410
9410
|
for (const [i, s] of sentences.entries()) {
|
|
9411
|
-
const
|
|
9412
|
-
if (
|
|
9411
|
+
const words2 = s.split(/\s+/).filter(Boolean).length;
|
|
9412
|
+
if (words2 > 0 && words2 <= STACCATO_MAX_WORDS) {
|
|
9413
9413
|
if (run === 0) runStart = i;
|
|
9414
9414
|
run++;
|
|
9415
9415
|
if (run === STACCATO_RUN) {
|
|
@@ -9427,10 +9427,10 @@ function countTitleCaseHeadings(text2) {
|
|
|
9427
9427
|
let first = "";
|
|
9428
9428
|
for (const m of text2.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
|
|
9429
9429
|
const heading = (m[1] ?? "").trim();
|
|
9430
|
-
const
|
|
9431
|
-
if (
|
|
9432
|
-
const capitalized =
|
|
9433
|
-
if (capitalized /
|
|
9430
|
+
const words2 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
|
|
9431
|
+
if (words2.length < 4) continue;
|
|
9432
|
+
const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
|
|
9433
|
+
if (capitalized / words2.length < 0.8) continue;
|
|
9434
9434
|
count++;
|
|
9435
9435
|
if (!first) first = heading;
|
|
9436
9436
|
}
|
|
@@ -9679,7 +9679,7 @@ function renderAdvisories(data, lines) {
|
|
|
9679
9679
|
return;
|
|
9680
9680
|
}
|
|
9681
9681
|
lines.push("");
|
|
9682
|
-
lines.push("Advisories (non-blocking \u2014
|
|
9682
|
+
lines.push("Advisories (non-blocking, checked across the whole draft \u2014 settle these before you publish)");
|
|
9683
9683
|
for (const advisory of advisories) {
|
|
9684
9684
|
lines.push(` \u2022 ${advisory.message}`);
|
|
9685
9685
|
}
|
|
@@ -9779,11 +9779,11 @@ function rawTextEntries(value) {
|
|
|
9779
9779
|
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
9780
9780
|
return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
9781
9781
|
}
|
|
9782
|
-
function rawFileEntries(
|
|
9783
|
-
if (typeof
|
|
9782
|
+
function rawFileEntries(path38) {
|
|
9783
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
9784
9784
|
return [];
|
|
9785
9785
|
}
|
|
9786
|
-
return readFileSync2(
|
|
9786
|
+
return readFileSync2(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
9787
9787
|
}
|
|
9788
9788
|
function keywordEntries(args) {
|
|
9789
9789
|
const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
|
|
@@ -9806,19 +9806,19 @@ function keywordEntries(args) {
|
|
|
9806
9806
|
}
|
|
9807
9807
|
return entries;
|
|
9808
9808
|
}
|
|
9809
|
-
function loadJsonFileArg(
|
|
9810
|
-
if (typeof
|
|
9809
|
+
function loadJsonFileArg(path38) {
|
|
9810
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
9811
9811
|
return {};
|
|
9812
9812
|
}
|
|
9813
9813
|
try {
|
|
9814
|
-
const parsed = JSON.parse(readFileSync2(
|
|
9814
|
+
const parsed = JSON.parse(readFileSync2(path38, "utf8"));
|
|
9815
9815
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9816
|
-
failWriteValidation(`${
|
|
9816
|
+
failWriteValidation(`${path38} must contain a JSON object`);
|
|
9817
9817
|
}
|
|
9818
9818
|
return parsed;
|
|
9819
9819
|
} catch (err) {
|
|
9820
9820
|
if (err instanceof SyntaxError) {
|
|
9821
|
-
failWriteValidation(`${
|
|
9821
|
+
failWriteValidation(`${path38} is not valid JSON: ${err.message}`);
|
|
9822
9822
|
}
|
|
9823
9823
|
throw err;
|
|
9824
9824
|
}
|
|
@@ -9948,10 +9948,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
|
|
|
9948
9948
|
async function stageTarget(kind, customerId, target, hints) {
|
|
9949
9949
|
await stageGoogleOp({ kind, customerId, target }, hints);
|
|
9950
9950
|
}
|
|
9951
|
-
async function draftAction(
|
|
9951
|
+
async function draftAction(path38, body, chat) {
|
|
9952
9952
|
try {
|
|
9953
9953
|
const chatId = resolveChatId(chat);
|
|
9954
|
-
const response = await apiPost(
|
|
9954
|
+
const response = await apiPost(path38, { chatId, ...body });
|
|
9955
9955
|
writeJsonEnvelope(response);
|
|
9956
9956
|
} catch (err) {
|
|
9957
9957
|
handleGoogleError(err);
|
|
@@ -10188,10 +10188,33 @@ function warnDefaults(ctx) {
|
|
|
10188
10188
|
}
|
|
10189
10189
|
}
|
|
10190
10190
|
|
|
10191
|
+
// src/commands/ads/google/keywords/provenance.ts
|
|
10192
|
+
var PLANNER_SOURCE = "keyword_planner_estimate";
|
|
10193
|
+
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["keyword"]);
|
|
10194
|
+
function stampPlannerRows(rows) {
|
|
10195
|
+
return rows.map((row) => {
|
|
10196
|
+
const stamped = { source: PLANNER_SOURCE };
|
|
10197
|
+
for (const [key, value] of Object.entries(row)) {
|
|
10198
|
+
stamped[IDENTITY_FIELDS.has(key) || key.startsWith("planner_") ? key : `planner_${key}`] = value;
|
|
10199
|
+
}
|
|
10200
|
+
return stamped;
|
|
10201
|
+
});
|
|
10202
|
+
}
|
|
10203
|
+
var PLANNER_FIELD_DESCRIPTIONS = {
|
|
10204
|
+
source: `Always "${PLANNER_SOURCE}". These rows are Keyword Planner ESTIMATES of total market search volume, NOT this account's data. Never report them as impressions, clicks or any account metric \u2014 read those from a query against the account.`,
|
|
10205
|
+
keyword: "The keyword this estimate is for",
|
|
10206
|
+
planner_avg_monthly_searches: "ESTIMATE: average monthly searches across the whole market (string), last 12 months",
|
|
10207
|
+
planner_competition: "ESTIMATE: competition level \u2014 LOW, MEDIUM, HIGH",
|
|
10208
|
+
planner_competition_index: "ESTIMATE: competition index 0-100 as string (higher = more competitive)",
|
|
10209
|
+
planner_low_top_of_page_bid_micros: "ESTIMATE: low-range top-of-page CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10210
|
+
planner_high_top_of_page_bid_micros: "ESTIMATE: high-range top-of-page CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10211
|
+
planner_monthly_search_volumes: "ESTIMATE: monthly market search volume breakdown (array of {year, month, monthly_searches})"
|
|
10212
|
+
};
|
|
10213
|
+
|
|
10191
10214
|
// src/commands/ads/google/keywords/discover.ts
|
|
10192
10215
|
registerSchema({
|
|
10193
10216
|
command: "ads.google.keywords.discover",
|
|
10194
|
-
description:
|
|
10217
|
+
description: 'Discover keyword ideas from seed keywords or URLs. Returns { source: "keyword_planner_estimate", keywords, total_results, next_page_token? }. THESE ARE ESTIMATES OF TOTAL MARKET SEARCH VOLUME, NOT THIS ACCOUNT\'S DATA: every row carries a source column and every metric is prefixed planner_ (planner_avg_monthly_searches, planner_competition, ...). Never report a planner_ number as impressions, clicks or any account metric \u2014 read those with `baker ads google query` against the account. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.',
|
|
10195
10218
|
args: {
|
|
10196
10219
|
"customer-id": {
|
|
10197
10220
|
type: "string",
|
|
@@ -10220,15 +10243,9 @@ registerSchema({
|
|
|
10220
10243
|
"no-cache": { type: "boolean", description: "Skip cache", required: false }
|
|
10221
10244
|
}
|
|
10222
10245
|
});
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
competition: "Competition level: LOW, MEDIUM, HIGH",
|
|
10227
|
-
competition_index: "Competition index 0-100 as string (higher = more competitive)",
|
|
10228
|
-
low_top_of_page_bid_micros: "Low-range CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10229
|
-
high_top_of_page_bid_micros: "High-range CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10230
|
-
monthly_search_volumes: "Monthly search volume breakdown (array of {year, month, monthly_searches})"
|
|
10231
|
-
};
|
|
10246
|
+
function stampDiscoverResponse(data) {
|
|
10247
|
+
return { ...data, source: PLANNER_SOURCE, keywords: stampPlannerRows(data.keywords ?? []) };
|
|
10248
|
+
}
|
|
10232
10249
|
function buildDiscoverBody(customerId, args) {
|
|
10233
10250
|
const body = {
|
|
10234
10251
|
customerId,
|
|
@@ -10269,6 +10286,8 @@ var discoverCommand = defineCommand23({
|
|
|
10269
10286
|
name: "discover",
|
|
10270
10287
|
description: `Discover new keyword ideas from seed keywords or competitor URLs.
|
|
10271
10288
|
|
|
10289
|
+
Returns Keyword Planner ESTIMATES of market search volume, not this account's numbers. Every row carries source=keyword_planner_estimate and planner_-prefixed metrics; keep those names when you quote a figure, and never present one as an impression or click count.
|
|
10290
|
+
|
|
10272
10291
|
Examples:
|
|
10273
10292
|
baker ads google keywords discover --customer-id 1234567890 --seeds "running shoes,athletic footwear"
|
|
10274
10293
|
baker ads google keywords discover --customer-id 1234567890 --url "https://competitor.com"
|
|
@@ -10303,7 +10322,7 @@ Examples:
|
|
|
10303
10322
|
const cached = cacheGet("keywords", cacheKey);
|
|
10304
10323
|
if (cached) {
|
|
10305
10324
|
warnDefaults(queryContext);
|
|
10306
|
-
writeAdsJson({ ok: true, data: cached.data, cached: true, query_context: queryContext });
|
|
10325
|
+
writeAdsJson({ ok: true, data: stampDiscoverResponse(cached.data), cached: true, query_context: queryContext });
|
|
10307
10326
|
return;
|
|
10308
10327
|
}
|
|
10309
10328
|
}
|
|
@@ -10313,12 +10332,13 @@ Examples:
|
|
|
10313
10332
|
cacheSet("keywords", cacheKey, data, 24 * 60 * 60 * 1e3);
|
|
10314
10333
|
}
|
|
10315
10334
|
warnDefaults(queryContext);
|
|
10335
|
+
const stamped = stampDiscoverResponse(data);
|
|
10316
10336
|
const format = args.output || "json";
|
|
10317
10337
|
if (format !== "json") {
|
|
10318
|
-
writeAdsOutput(
|
|
10338
|
+
writeAdsOutput(stamped.keywords, format);
|
|
10319
10339
|
return;
|
|
10320
10340
|
}
|
|
10321
|
-
writeAdsJson({ ok: true, data, fields:
|
|
10341
|
+
writeAdsJson({ ok: true, data: stamped, fields: PLANNER_FIELD_DESCRIPTIONS, query_context: queryContext });
|
|
10322
10342
|
} catch (err) {
|
|
10323
10343
|
handleKeywordError(err);
|
|
10324
10344
|
}
|
|
@@ -10371,7 +10391,7 @@ var locationsCommand = defineCommand25({
|
|
|
10371
10391
|
import { defineCommand as defineCommand26 } from "citty";
|
|
10372
10392
|
registerSchema({
|
|
10373
10393
|
command: "ads.google.keywords.metrics",
|
|
10374
|
-
description:
|
|
10394
|
+
description: 'Get historical market metrics for specific keywords. Returns { source: "keyword_planner_estimate", historical_metrics: [...] }. THESE ARE KEYWORD PLANNER ESTIMATES OF TOTAL MARKET SEARCH VOLUME, NOT THIS ACCOUNT\'S DATA: every row carries a source column and every metric is prefixed planner_ (planner_avg_monthly_searches, planner_competition, ...). Never report a planner_ number as impressions, clicks or any account metric \u2014 read those with `baker ads google query` against the account. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.',
|
|
10375
10395
|
args: {
|
|
10376
10396
|
"customer-id": {
|
|
10377
10397
|
type: "string",
|
|
@@ -10393,6 +10413,9 @@ registerSchema({
|
|
|
10393
10413
|
"no-cache": { type: "boolean", description: "Skip cache", required: false }
|
|
10394
10414
|
}
|
|
10395
10415
|
});
|
|
10416
|
+
function stampMetricsResponse(data) {
|
|
10417
|
+
return { ...data, source: PLANNER_SOURCE, historical_metrics: stampPlannerRows(data.historical_metrics ?? []) };
|
|
10418
|
+
}
|
|
10396
10419
|
function handleMetricsError(err) {
|
|
10397
10420
|
if (err instanceof ApiError) {
|
|
10398
10421
|
if (isNotConnectedError(err.code, err.message)) {
|
|
@@ -10418,6 +10441,8 @@ var metricsCommand = defineCommand26({
|
|
|
10418
10441
|
name: "metrics",
|
|
10419
10442
|
description: `Get historical search metrics for specific keywords.
|
|
10420
10443
|
|
|
10444
|
+
Returns Keyword Planner ESTIMATES of market search volume, not this account's numbers. Every row carries source=keyword_planner_estimate and planner_-prefixed metrics; keep those names when you quote a figure, and never present one as an impression or click count.
|
|
10445
|
+
|
|
10421
10446
|
Examples:
|
|
10422
10447
|
baker ads google keywords metrics --customer-id 1234567890 --keywords "running shoes,nike shoes,adidas shoes"
|
|
10423
10448
|
baker ads google keywords metrics --customer-id 1234567890 --keywords "seo tools" --location 2826`
|
|
@@ -10454,7 +10479,7 @@ Examples:
|
|
|
10454
10479
|
const cached = cacheGet("keywords", cacheKey);
|
|
10455
10480
|
if (cached) {
|
|
10456
10481
|
warnDefaults(queryContext);
|
|
10457
|
-
writeAdsJson({ ok: true, data: cached.data, cached: true, query_context: queryContext });
|
|
10482
|
+
writeAdsJson({ ok: true, data: stampMetricsResponse(cached.data), cached: true, query_context: queryContext });
|
|
10458
10483
|
return;
|
|
10459
10484
|
}
|
|
10460
10485
|
}
|
|
@@ -10464,21 +10489,13 @@ Examples:
|
|
|
10464
10489
|
cacheSet("keywords", cacheKey, data, 24 * 60 * 60 * 1e3);
|
|
10465
10490
|
}
|
|
10466
10491
|
warnDefaults(queryContext);
|
|
10492
|
+
const stamped = stampMetricsResponse(data);
|
|
10467
10493
|
const format = args.output || "json";
|
|
10468
10494
|
if (format !== "json") {
|
|
10469
|
-
writeAdsOutput(
|
|
10495
|
+
writeAdsOutput(stamped.historical_metrics, format);
|
|
10470
10496
|
return;
|
|
10471
10497
|
}
|
|
10472
|
-
|
|
10473
|
-
keyword: "The keyword analyzed",
|
|
10474
|
-
avg_monthly_searches: "Average monthly search volume as string (last 12 months)",
|
|
10475
|
-
competition: "Competition level: LOW, MEDIUM, HIGH",
|
|
10476
|
-
competition_index: "Competition index 0-100 as string (higher = more competitive)",
|
|
10477
|
-
low_top_of_page_bid_micros: "Low-range CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10478
|
-
high_top_of_page_bid_micros: "High-range CPC bid in micros (\xF7 1,000,000 for currency)",
|
|
10479
|
-
monthly_search_volumes: "Monthly search volume breakdown (array of {year, month, monthly_searches})"
|
|
10480
|
-
};
|
|
10481
|
-
writeAdsJson({ ok: true, data, fields, query_context: queryContext });
|
|
10498
|
+
writeAdsJson({ ok: true, data: stamped, fields: PLANNER_FIELD_DESCRIPTIONS, query_context: queryContext });
|
|
10482
10499
|
} catch (err) {
|
|
10483
10500
|
handleMetricsError(err);
|
|
10484
10501
|
}
|
|
@@ -10625,10 +10642,211 @@ function keywordServingWarnings(rows, options) {
|
|
|
10625
10642
|
return renderKeywordLimits(collectKeywordLimits(rows, options));
|
|
10626
10643
|
}
|
|
10627
10644
|
|
|
10645
|
+
// src/commands/ads/google/presets.ts
|
|
10646
|
+
var GAQL_DATE_RANGE_RE = /^(?:TODAY|YESTERDAY|LAST_7_DAYS|LAST_14_DAYS|LAST_30_DAYS|LAST_90_DAYS|THIS_MONTH|LAST_MONTH|THIS_QUARTER|LAST_QUARTER|THIS_YEAR|LAST_YEAR|ALL_TIME|BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}')$/i;
|
|
10647
|
+
function isValidDateRange(value) {
|
|
10648
|
+
return GAQL_DATE_RANGE_RE.test(value.trim());
|
|
10649
|
+
}
|
|
10650
|
+
var SERVING_CHAINS = {
|
|
10651
|
+
keyword_view: ["campaign.status", "ad_group.status", "ad_group_criterion.status"],
|
|
10652
|
+
ad_group_criterion: ["campaign.status", "ad_group.status", "ad_group_criterion.status"],
|
|
10653
|
+
ad_group_ad: ["campaign.status", "ad_group.status", "ad_group_ad.status"],
|
|
10654
|
+
search_term_view: ["campaign.status", "ad_group.status"],
|
|
10655
|
+
ad_group: ["campaign.status", "ad_group.status"],
|
|
10656
|
+
asset_group: ["campaign.status", "asset_group.status"],
|
|
10657
|
+
asset_group_asset: ["campaign.status", "asset_group.status"]
|
|
10658
|
+
};
|
|
10659
|
+
var PRESETS = [
|
|
10660
|
+
{
|
|
10661
|
+
name: "campaign-performance",
|
|
10662
|
+
description: "Campaign-level metrics overview",
|
|
10663
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
|
|
10664
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10665
|
+
defaultLimit: 200,
|
|
10666
|
+
statusFields: ["campaign.status"]
|
|
10667
|
+
},
|
|
10668
|
+
{
|
|
10669
|
+
name: "keyword-analysis",
|
|
10670
|
+
description: "Keyword performance with match type and quality",
|
|
10671
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.quality_info.quality_score, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10672
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10673
|
+
defaultLimit: 200,
|
|
10674
|
+
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10675
|
+
},
|
|
10676
|
+
{
|
|
10677
|
+
name: "keyword-serving",
|
|
10678
|
+
description: "Why a keyword is limited \u2014 Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score",
|
|
10679
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.primary_status, ad_group_criterion.primary_status_reasons, ad_group_criterion.system_serving_status, ad_group_criterion.approval_status, ad_group_criterion.effective_cpc_bid_micros, ad_group_criterion.effective_cpc_bid_source, ad_group_criterion.position_estimates.first_page_cpc_micros, ad_group_criterion.position_estimates.top_of_page_cpc_micros, ad_group_criterion.quality_info.quality_score, ad_group_criterion.quality_info.creative_quality_score, ad_group_criterion.quality_info.post_click_quality_score, ad_group_criterion.quality_info.search_predicted_ctr FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
|
|
10680
|
+
defaultDateRange: "ALL_TIME",
|
|
10681
|
+
defaultLimit: 500,
|
|
10682
|
+
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10683
|
+
},
|
|
10684
|
+
{
|
|
10685
|
+
name: "positive-keywords",
|
|
10686
|
+
description: "Positive (targeting) keywords only \u2014 excludes negatives",
|
|
10687
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
|
|
10688
|
+
defaultDateRange: "ALL_TIME",
|
|
10689
|
+
defaultLimit: 500,
|
|
10690
|
+
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10691
|
+
},
|
|
10692
|
+
{
|
|
10693
|
+
name: "negative-keywords",
|
|
10694
|
+
description: "Negative keywords only \u2014 campaign and ad group level",
|
|
10695
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE AND campaign.status != 'REMOVED' ORDER BY campaign.name LIMIT {limit}`,
|
|
10696
|
+
defaultDateRange: "ALL_TIME",
|
|
10697
|
+
defaultLimit: 500
|
|
10698
|
+
},
|
|
10699
|
+
{
|
|
10700
|
+
name: "negative-keyword-lists",
|
|
10701
|
+
description: "Shared negative keyword lists and the terms inside them (SharedSet members)",
|
|
10702
|
+
gaqlTemplate: `SELECT shared_set.id, shared_set.name, shared_criterion.keyword.text, shared_criterion.keyword.match_type FROM shared_criterion WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND shared_set.status = 'ENABLED' ORDER BY shared_set.name LIMIT {limit}`,
|
|
10703
|
+
defaultDateRange: "ALL_TIME",
|
|
10704
|
+
defaultLimit: 500
|
|
10705
|
+
},
|
|
10706
|
+
{
|
|
10707
|
+
name: "negative-list-attachments",
|
|
10708
|
+
description: "Which campaigns each shared negative keyword list is attached to",
|
|
10709
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, shared_set.id, shared_set.name FROM campaign_shared_set WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND campaign_shared_set.status = 'ENABLED' ORDER BY campaign.name LIMIT {limit}`,
|
|
10710
|
+
defaultDateRange: "ALL_TIME",
|
|
10711
|
+
defaultLimit: 500
|
|
10712
|
+
},
|
|
10713
|
+
{
|
|
10714
|
+
name: "search-terms",
|
|
10715
|
+
description: "Actual user search queries triggering ads",
|
|
10716
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10717
|
+
defaultDateRange: "LAST_7_DAYS",
|
|
10718
|
+
defaultLimit: 200,
|
|
10719
|
+
statusFields: ["campaign.status", "ad_group.status"]
|
|
10720
|
+
},
|
|
10721
|
+
{
|
|
10722
|
+
name: "ad-copy-performance",
|
|
10723
|
+
description: "Ad headline and description effectiveness",
|
|
10724
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.status, ad_group_ad.status, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.ad.final_urls, metrics.impressions, metrics.clicks, metrics.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10725
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10726
|
+
defaultLimit: 200,
|
|
10727
|
+
statusFields: ["campaign.status", "ad_group.status", "ad_group_ad.status"]
|
|
10728
|
+
},
|
|
10729
|
+
{
|
|
10730
|
+
name: "asset-performance",
|
|
10731
|
+
description: "Performance Max asset performance labels",
|
|
10732
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} LIMIT {limit}`,
|
|
10733
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10734
|
+
defaultLimit: 200,
|
|
10735
|
+
statusFields: ["campaign.status", "asset_group.status"]
|
|
10736
|
+
},
|
|
10737
|
+
{
|
|
10738
|
+
name: "shopping-products",
|
|
10739
|
+
description: "Product-level shopping performance metrics",
|
|
10740
|
+
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, segments.product_title, segments.product_item_id, segments.product_brand, segments.product_type_l1, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM shopping_performance_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
|
|
10741
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10742
|
+
defaultLimit: 200,
|
|
10743
|
+
statusFields: ["campaign.status"]
|
|
10744
|
+
},
|
|
10745
|
+
{
|
|
10746
|
+
name: "account-summary",
|
|
10747
|
+
description: "Account-level totals for a date range",
|
|
10748
|
+
gaqlTemplate: `SELECT customer.id, customer.descriptive_name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING {dateRange} LIMIT 1`,
|
|
10749
|
+
defaultDateRange: "LAST_30_DAYS",
|
|
10750
|
+
defaultLimit: 1
|
|
10751
|
+
}
|
|
10752
|
+
];
|
|
10753
|
+
function getPreset(name) {
|
|
10754
|
+
return PRESETS.find((p) => p.name === name);
|
|
10755
|
+
}
|
|
10756
|
+
function buildStatusScope(statusFields, includePaused) {
|
|
10757
|
+
if (!statusFields || statusFields.length === 0) return "";
|
|
10758
|
+
const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
|
|
10759
|
+
return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
|
|
10760
|
+
}
|
|
10761
|
+
function expandPreset(preset, params) {
|
|
10762
|
+
const dateRange = params.dateRange ?? preset.defaultDateRange;
|
|
10763
|
+
const limit = params.limit ?? preset.defaultLimit;
|
|
10764
|
+
if (!isValidDateRange(dateRange)) {
|
|
10765
|
+
return {
|
|
10766
|
+
query: "",
|
|
10767
|
+
dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
|
|
10768
|
+
};
|
|
10769
|
+
}
|
|
10770
|
+
const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
|
|
10771
|
+
const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
|
|
10772
|
+
return { query };
|
|
10773
|
+
}
|
|
10774
|
+
|
|
10628
10775
|
// src/commands/ads/google/preflight.ts
|
|
10629
10776
|
function buildCommand2(query, customerId) {
|
|
10630
10777
|
return `baker ads google query "${query}" --customer-id ${customerId}`;
|
|
10631
10778
|
}
|
|
10779
|
+
var RUNG_PIN_FIELDS = {
|
|
10780
|
+
"campaign.status": ["campaign.id", "campaign.resource_name"],
|
|
10781
|
+
"ad_group.status": ["ad_group.id", "ad_group.resource_name"],
|
|
10782
|
+
"ad_group_criterion.status": ["ad_group_criterion.criterion_id", "ad_group_criterion.resource_name"],
|
|
10783
|
+
"ad_group_ad.status": ["ad_group_ad.ad.id", "ad_group_ad.resource_name"],
|
|
10784
|
+
"asset_group.status": ["asset_group.id", "asset_group.resource_name"]
|
|
10785
|
+
};
|
|
10786
|
+
function whereClause(query) {
|
|
10787
|
+
const match = query.match(/\bWHERE\b([\s\S]*)$/i);
|
|
10788
|
+
if (!match?.[1]) return "";
|
|
10789
|
+
let rest = match[1];
|
|
10790
|
+
for (const trailing of [/\bORDER\s+BY\b/i, /\bLIMIT\b/i, /\bPARAMETERS\b/i]) {
|
|
10791
|
+
const idx = rest.search(trailing);
|
|
10792
|
+
if (idx !== -1) rest = rest.slice(0, idx);
|
|
10793
|
+
}
|
|
10794
|
+
return rest;
|
|
10795
|
+
}
|
|
10796
|
+
function selectClause(query) {
|
|
10797
|
+
return query.split(/\bFROM\b/i)[0] ?? "";
|
|
10798
|
+
}
|
|
10799
|
+
function mentions(clause, field) {
|
|
10800
|
+
return new RegExp(`(?<![\\w.])${field.replace(/\./g, "\\.")}\\b`, "i").test(clause);
|
|
10801
|
+
}
|
|
10802
|
+
function analyzeServingChain(query) {
|
|
10803
|
+
const resource = fromResource(query);
|
|
10804
|
+
const chain = resource ? SERVING_CHAINS[resource] : void 0;
|
|
10805
|
+
if (!chain) return null;
|
|
10806
|
+
const where = whereClause(query);
|
|
10807
|
+
if (/ad_group_criterion\.negative\s*=\s*TRUE/i.test(where)) return null;
|
|
10808
|
+
const unscoped = chain.filter((rung) => {
|
|
10809
|
+
if (mentions(where, rung)) return false;
|
|
10810
|
+
return !(RUNG_PIN_FIELDS[rung] ?? []).some((pin) => mentions(where, pin));
|
|
10811
|
+
});
|
|
10812
|
+
const assertsServing = chain.some(
|
|
10813
|
+
(rung) => new RegExp(`${rung.replace(/\./g, "\\.")}\\s*=\\s*'ENABLED'`, "i").test(where)
|
|
10814
|
+
);
|
|
10815
|
+
return { chain, unscoped, assertsServing };
|
|
10816
|
+
}
|
|
10817
|
+
function rejectPartialChain(query, customerId, analysis) {
|
|
10818
|
+
if (!analysis.assertsServing || analysis.unscoped.length === 0) return null;
|
|
10819
|
+
const added = analysis.unscoped.map((rung) => `${rung} = 'ENABLED'`).join(" AND ");
|
|
10820
|
+
const fixed = query.replace(/\bWHERE\b/i, `WHERE ${added} AND`);
|
|
10821
|
+
return {
|
|
10822
|
+
ok: false,
|
|
10823
|
+
error: {
|
|
10824
|
+
code: "INCOMPLETE_STATUS_CHAIN",
|
|
10825
|
+
message: `This query filters on serving status but leaves ${analysis.unscoped.join(", ")} unscoped. Google Ads has no single serving flag \u2014 a row is live only when its whole chain (${analysis.chain.join(" \u2192 ")}) is ENABLED, so rows under a paused parent will come back looking live.`,
|
|
10826
|
+
fix: {
|
|
10827
|
+
action: "retry_with_modified_query",
|
|
10828
|
+
correctedCommand: buildCommand2(fixed, customerId),
|
|
10829
|
+
explanation: `Add the missing rungs: ${added}. To look at paused entities on purpose, scope every rung explicitly instead (e.g. ${analysis.unscoped.map((rung) => `${rung} != 'REMOVED'`).join(" AND ")}), or use --preset ... --include-paused.`
|
|
10830
|
+
},
|
|
10831
|
+
retryable: false
|
|
10832
|
+
}
|
|
10833
|
+
};
|
|
10834
|
+
}
|
|
10835
|
+
function annotateUnscopedChain(query, analysis) {
|
|
10836
|
+
if (analysis.assertsServing || analysis.unscoped.length === 0) return { query, warnings: [] };
|
|
10837
|
+
const select = selectClause(query);
|
|
10838
|
+
const missing = analysis.unscoped.filter((rung) => !mentions(select, rung));
|
|
10839
|
+
if (missing.length === 0) return { query, warnings: [] };
|
|
10840
|
+
return {
|
|
10841
|
+
query: query.replace(/SELECT\s+/i, `SELECT ${missing.join(", ")}, `),
|
|
10842
|
+
warnings: [
|
|
10843
|
+
{
|
|
10844
|
+
code: "SERVING_STATUS_ADDED",
|
|
10845
|
+
message: `Added ${missing.join(", ")} to SELECT \u2014 this query does not scope ${analysis.unscoped.join(", ")}, so it returns paused-parent rows that are not serving. Read those columns before calling any row live; filter every rung = 'ENABLED' to get serving rows only.`
|
|
10846
|
+
}
|
|
10847
|
+
]
|
|
10848
|
+
};
|
|
10849
|
+
}
|
|
10632
10850
|
var SINGLE_ROW_RESOURCES = ["customer"];
|
|
10633
10851
|
function fromResource(query) {
|
|
10634
10852
|
const match = query.match(/\bFROM\s+([A-Za-z_][A-Za-z0-9_]*)/i);
|
|
@@ -10641,10 +10859,10 @@ function isSingleRowRead(query) {
|
|
|
10641
10859
|
}
|
|
10642
10860
|
function addRequiredCampaignFields(query) {
|
|
10643
10861
|
if (!/FROM\s+campaign_budget\b/i.test(query)) return { query, warnings: [] };
|
|
10644
|
-
const
|
|
10645
|
-
const
|
|
10646
|
-
const selectFields = new Set(
|
|
10647
|
-
const missing = [...new Set(
|
|
10862
|
+
const whereClause3 = query.split(/\bWHERE\b/i)[1] ?? "";
|
|
10863
|
+
const selectClause2 = query.split(/\bFROM\b/i)[0] ?? "";
|
|
10864
|
+
const selectFields = new Set(selectClause2.match(/campaign\.[\w.]+/g) ?? []);
|
|
10865
|
+
const missing = [...new Set(whereClause3.match(/campaign\.[\w.]+/g) ?? [])].filter(
|
|
10648
10866
|
(field) => !selectFields.has(field)
|
|
10649
10867
|
);
|
|
10650
10868
|
if (missing.length === 0) return { query, warnings: [] };
|
|
@@ -10794,128 +11012,13 @@ function validatePreflight(query, customerId, limit) {
|
|
|
10794
11012
|
}
|
|
10795
11013
|
const { query: autoFixed, warnings: autoFixWarnings } = applyAutoFixes(corrected, limit);
|
|
10796
11014
|
warnings.push(...autoFixWarnings);
|
|
10797
|
-
|
|
10798
|
-
}
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
return
|
|
10804
|
-
}
|
|
10805
|
-
var PRESETS = [
|
|
10806
|
-
{
|
|
10807
|
-
name: "campaign-performance",
|
|
10808
|
-
description: "Campaign-level metrics overview",
|
|
10809
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
|
|
10810
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10811
|
-
defaultLimit: 200,
|
|
10812
|
-
statusFields: ["campaign.status"]
|
|
10813
|
-
},
|
|
10814
|
-
{
|
|
10815
|
-
name: "keyword-analysis",
|
|
10816
|
-
description: "Keyword performance with match type and quality",
|
|
10817
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.quality_info.quality_score, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10818
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10819
|
-
defaultLimit: 200,
|
|
10820
|
-
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10821
|
-
},
|
|
10822
|
-
{
|
|
10823
|
-
name: "keyword-serving",
|
|
10824
|
-
description: "Why a keyword is limited \u2014 Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score",
|
|
10825
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.primary_status, ad_group_criterion.primary_status_reasons, ad_group_criterion.system_serving_status, ad_group_criterion.approval_status, ad_group_criterion.effective_cpc_bid_micros, ad_group_criterion.effective_cpc_bid_source, ad_group_criterion.position_estimates.first_page_cpc_micros, ad_group_criterion.position_estimates.top_of_page_cpc_micros, ad_group_criterion.quality_info.quality_score, ad_group_criterion.quality_info.creative_quality_score, ad_group_criterion.quality_info.post_click_quality_score, ad_group_criterion.quality_info.search_predicted_ctr FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
|
|
10826
|
-
defaultDateRange: "ALL_TIME",
|
|
10827
|
-
defaultLimit: 500,
|
|
10828
|
-
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10829
|
-
},
|
|
10830
|
-
{
|
|
10831
|
-
name: "positive-keywords",
|
|
10832
|
-
description: "Positive (targeting) keywords only \u2014 excludes negatives",
|
|
10833
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
|
|
10834
|
-
defaultDateRange: "ALL_TIME",
|
|
10835
|
-
defaultLimit: 500,
|
|
10836
|
-
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
10837
|
-
},
|
|
10838
|
-
{
|
|
10839
|
-
name: "negative-keywords",
|
|
10840
|
-
description: "Negative keywords only \u2014 campaign and ad group level",
|
|
10841
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE AND campaign.status != 'REMOVED' ORDER BY campaign.name LIMIT {limit}`,
|
|
10842
|
-
defaultDateRange: "ALL_TIME",
|
|
10843
|
-
defaultLimit: 500
|
|
10844
|
-
},
|
|
10845
|
-
{
|
|
10846
|
-
name: "negative-keyword-lists",
|
|
10847
|
-
description: "Shared negative keyword lists and the terms inside them (SharedSet members)",
|
|
10848
|
-
gaqlTemplate: `SELECT shared_set.id, shared_set.name, shared_criterion.keyword.text, shared_criterion.keyword.match_type FROM shared_criterion WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND shared_set.status = 'ENABLED' ORDER BY shared_set.name LIMIT {limit}`,
|
|
10849
|
-
defaultDateRange: "ALL_TIME",
|
|
10850
|
-
defaultLimit: 500
|
|
10851
|
-
},
|
|
10852
|
-
{
|
|
10853
|
-
name: "negative-list-attachments",
|
|
10854
|
-
description: "Which campaigns each shared negative keyword list is attached to",
|
|
10855
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, shared_set.id, shared_set.name FROM campaign_shared_set WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND campaign_shared_set.status = 'ENABLED' ORDER BY campaign.name LIMIT {limit}`,
|
|
10856
|
-
defaultDateRange: "ALL_TIME",
|
|
10857
|
-
defaultLimit: 500
|
|
10858
|
-
},
|
|
10859
|
-
{
|
|
10860
|
-
name: "search-terms",
|
|
10861
|
-
description: "Actual user search queries triggering ads",
|
|
10862
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10863
|
-
defaultDateRange: "LAST_7_DAYS",
|
|
10864
|
-
defaultLimit: 200,
|
|
10865
|
-
statusFields: ["campaign.status", "ad_group.status"]
|
|
10866
|
-
},
|
|
10867
|
-
{
|
|
10868
|
-
name: "ad-copy-performance",
|
|
10869
|
-
description: "Ad headline and description effectiveness",
|
|
10870
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.status, ad_group_ad.status, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.ad.final_urls, metrics.impressions, metrics.clicks, metrics.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
|
|
10871
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10872
|
-
defaultLimit: 200,
|
|
10873
|
-
statusFields: ["campaign.status", "ad_group.status", "ad_group_ad.status"]
|
|
10874
|
-
},
|
|
10875
|
-
{
|
|
10876
|
-
name: "asset-performance",
|
|
10877
|
-
description: "Performance Max asset performance labels",
|
|
10878
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} LIMIT {limit}`,
|
|
10879
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10880
|
-
defaultLimit: 200,
|
|
10881
|
-
statusFields: ["campaign.status", "asset_group.status"]
|
|
10882
|
-
},
|
|
10883
|
-
{
|
|
10884
|
-
name: "shopping-products",
|
|
10885
|
-
description: "Product-level shopping performance metrics",
|
|
10886
|
-
gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, segments.product_title, segments.product_item_id, segments.product_brand, segments.product_type_l1, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM shopping_performance_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
|
|
10887
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10888
|
-
defaultLimit: 200,
|
|
10889
|
-
statusFields: ["campaign.status"]
|
|
10890
|
-
},
|
|
10891
|
-
{
|
|
10892
|
-
name: "account-summary",
|
|
10893
|
-
description: "Account-level totals for a date range",
|
|
10894
|
-
gaqlTemplate: `SELECT customer.id, customer.descriptive_name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING {dateRange} LIMIT 1`,
|
|
10895
|
-
defaultDateRange: "LAST_30_DAYS",
|
|
10896
|
-
defaultLimit: 1
|
|
10897
|
-
}
|
|
10898
|
-
];
|
|
10899
|
-
function getPreset(name) {
|
|
10900
|
-
return PRESETS.find((p) => p.name === name);
|
|
10901
|
-
}
|
|
10902
|
-
function buildStatusScope(statusFields, includePaused) {
|
|
10903
|
-
if (!statusFields || statusFields.length === 0) return "";
|
|
10904
|
-
const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
|
|
10905
|
-
return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
|
|
10906
|
-
}
|
|
10907
|
-
function expandPreset(preset, params) {
|
|
10908
|
-
const dateRange = params.dateRange ?? preset.defaultDateRange;
|
|
10909
|
-
const limit = params.limit ?? preset.defaultLimit;
|
|
10910
|
-
if (!isValidDateRange(dateRange)) {
|
|
10911
|
-
return {
|
|
10912
|
-
query: "",
|
|
10913
|
-
dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
|
|
10914
|
-
};
|
|
10915
|
-
}
|
|
10916
|
-
const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
|
|
10917
|
-
const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
|
|
10918
|
-
return { query };
|
|
11015
|
+
const analysis = analyzeServingChain(autoFixed);
|
|
11016
|
+
if (!analysis) return { valid: true, correctedQuery: autoFixed, warnings };
|
|
11017
|
+
const rejection = rejectPartialChain(autoFixed, customerId, analysis);
|
|
11018
|
+
if (rejection) return { valid: false, warnings: [], error: rejection };
|
|
11019
|
+
const annotated = annotateUnscopedChain(autoFixed, analysis);
|
|
11020
|
+
warnings.push(...annotated.warnings);
|
|
11021
|
+
return { valid: true, correctedQuery: annotated.query, warnings };
|
|
10919
11022
|
}
|
|
10920
11023
|
|
|
10921
11024
|
// src/commands/ads/google/query.ts
|
|
@@ -10944,12 +11047,17 @@ registerSchema({
|
|
|
10944
11047
|
description: "Include paused entities in preset results. Off by default: presets return only actually-serving entities. Google Ads has no single serving flag \u2014 an entity serves only when its whole chain (campaign, ad group, ad) is ENABLED, so recommendations should target serving entities unless the user asks about paused ones.",
|
|
10945
11048
|
required: false
|
|
10946
11049
|
},
|
|
10947
|
-
limit: {
|
|
11050
|
+
limit: {
|
|
11051
|
+
type: "number",
|
|
11052
|
+
description: "Max rows in ONE page (default 200), AND the LIMIT added to a query that has none \u2014 so it caps the whole read with --all too. Values up to 10000 are accepted. Any read that fills this cap comes back flagged TRUNCATED_RESULTS \u2014 never count or total from one.",
|
|
11053
|
+
required: false,
|
|
11054
|
+
default: 200
|
|
11055
|
+
},
|
|
10948
11056
|
"list-presets": { type: "boolean", description: "List all available query presets", required: false },
|
|
10949
11057
|
cursor: { type: "string", description: "Pagination cursor from previous response", required: false },
|
|
10950
11058
|
all: {
|
|
10951
11059
|
type: "boolean",
|
|
10952
|
-
description: "
|
|
11060
|
+
description: "Follow every page until the data set is exhausted (use with --out for large datasets). Needed for a read you may count or total, but not sufficient: it exhausts the pages, not the statement's LIMIT, so pair it with a --limit above the row count you expect (or your own LIMIT) and check the read came back without TRUNCATED_RESULTS.",
|
|
10953
11061
|
required: false
|
|
10954
11062
|
},
|
|
10955
11063
|
out: {
|
|
@@ -11004,53 +11112,117 @@ function writeRowsToFile(filePath, rows, fields, append) {
|
|
|
11004
11112
|
writeFileSync(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
11005
11113
|
}
|
|
11006
11114
|
}
|
|
11115
|
+
function effectiveRowLimit(finalQuery, limit, all = false) {
|
|
11116
|
+
const match = finalQuery.match(/\bLIMIT\s+(\d+)/i);
|
|
11117
|
+
const statementLimit = match?.[1] ? Number(match[1]) : void 0;
|
|
11118
|
+
if (all) return statementLimit;
|
|
11119
|
+
return statementLimit === void 0 ? limit : Math.min(limit, statementLimit);
|
|
11120
|
+
}
|
|
11121
|
+
function warningLines(warnings, format) {
|
|
11122
|
+
return warnings.map((w) => {
|
|
11123
|
+
const message = w.message.replace(/\s*\n\s*/g, " ");
|
|
11124
|
+
if (format === "jsonl") return JSON.stringify({ _baker_warning: { code: w.code, message } });
|
|
11125
|
+
if (format === "md") return `> **${w.code}** \u2014 ${message}`;
|
|
11126
|
+
return `# baker ${w.code}: ${message}`;
|
|
11127
|
+
});
|
|
11128
|
+
}
|
|
11129
|
+
function completenessWarnings(read) {
|
|
11130
|
+
const cap3 = read.effectiveLimit;
|
|
11131
|
+
const filledTheCap = cap3 !== void 0 && cap3 > 0 && read.rowCount >= cap3;
|
|
11132
|
+
if (!read.hasMore && !filledTheCap) return [];
|
|
11133
|
+
const cause = read.hasMore ? `More rows are waiting: the account returned a next-page cursor this run did not follow.` : `The ${read.rowCount} rows returned exactly fill the row cap in force (${cap3}).`;
|
|
11134
|
+
const fix = read.all ? `Every page was followed, so the cap is the LIMIT in the statement itself \u2014 raise it above ${cap3}, or drop it and pass a larger --limit, then re-run.` : `Re-run with --all --out <file> to page through everything, or raise --limit above ${cap3}.`;
|
|
11135
|
+
return [
|
|
11136
|
+
{
|
|
11137
|
+
code: "TRUNCATED_RESULTS",
|
|
11138
|
+
message: `INCOMPLETE READ \u2014 ${cause} These rows are a page, not the data set. Do NOT count, total or describe them as the whole picture. ${fix}`
|
|
11139
|
+
}
|
|
11140
|
+
];
|
|
11141
|
+
}
|
|
11007
11142
|
function buildQueryEnvelope(rows, fieldDescs, warnings, options) {
|
|
11008
|
-
const
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
}
|
|
11018
|
-
if (options.cached) {
|
|
11019
|
-
envelope.cached = true;
|
|
11020
|
-
}
|
|
11021
|
-
return envelope;
|
|
11143
|
+
const notice = summariseWarnings(warnings);
|
|
11144
|
+
return {
|
|
11145
|
+
ok: true,
|
|
11146
|
+
...options.cached ? { cached: true } : {},
|
|
11147
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
11148
|
+
...options.nextCursor ? { pagination: { hasMore: true, cursor: options.nextCursor } } : {},
|
|
11149
|
+
data: rows,
|
|
11150
|
+
...options.full && Object.keys(fieldDescs).length > 0 ? { fields: fieldDescs } : {},
|
|
11151
|
+
...notice ? { notice } : {}
|
|
11152
|
+
};
|
|
11022
11153
|
}
|
|
11023
|
-
function
|
|
11024
|
-
|
|
11025
|
-
const
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11154
|
+
function summariseWarnings(warnings) {
|
|
11155
|
+
if (warnings.length === 0) return void 0;
|
|
11156
|
+
const codes = [...new Set(warnings.map((w) => w.code))].join(", ");
|
|
11157
|
+
return `Read the "warnings" above before using these rows: ${codes}.`;
|
|
11158
|
+
}
|
|
11159
|
+
function buildFileSummary(input) {
|
|
11160
|
+
const notice = summariseWarnings(input.warnings);
|
|
11161
|
+
return {
|
|
11162
|
+
ok: true,
|
|
11163
|
+
fields: {},
|
|
11164
|
+
file: input.file,
|
|
11165
|
+
rows: input.rows,
|
|
11166
|
+
complete: input.complete,
|
|
11167
|
+
...input.warnings.length > 0 ? { warnings: input.warnings } : {},
|
|
11168
|
+
...notice ? { notice } : {}
|
|
11169
|
+
};
|
|
11170
|
+
}
|
|
11171
|
+
var REPEAT_NOTICE_ABOVE_ROWS = 20;
|
|
11172
|
+
function emitToStderr(notices) {
|
|
11173
|
+
for (const line of notices) process.stderr.write(`${line}
|
|
11031
11174
|
`);
|
|
11032
|
-
|
|
11033
|
-
|
|
11034
|
-
}
|
|
11035
|
-
if (format === "jsonl") {
|
|
11036
|
-
for (const row of rows) {
|
|
11037
|
-
process.stdout.write(`${JSON.stringify(row)}
|
|
11175
|
+
}
|
|
11176
|
+
function writeLine(line) {
|
|
11177
|
+
process.stdout.write(`${line}
|
|
11038
11178
|
`);
|
|
11039
|
-
|
|
11040
|
-
|
|
11179
|
+
}
|
|
11180
|
+
var writeCsvRows = (rows, fields, notices) => {
|
|
11181
|
+
writeLine(toCsvRow(fields));
|
|
11182
|
+
for (const line of notices) writeLine(line);
|
|
11183
|
+
for (const row of rows) writeLine(toCsvRow(fields.map((f) => String(row[f] ?? ""))));
|
|
11184
|
+
};
|
|
11185
|
+
var writeJsonlRows = (rows, _fields, notices) => {
|
|
11186
|
+
for (const line of notices) writeLine(line);
|
|
11187
|
+
for (const row of rows) writeLine(JSON.stringify(row));
|
|
11188
|
+
};
|
|
11189
|
+
var writeMdRows = (rows, fields, notices) => {
|
|
11190
|
+
for (const line of notices) writeLine(line);
|
|
11191
|
+
writeLine(`| ${fields.join(" | ")} |`);
|
|
11192
|
+
writeLine(`| ${fields.map(() => "---").join(" | ")} |`);
|
|
11193
|
+
for (const row of rows) writeLine(`| ${fields.map((f) => String(row[f] ?? "")).join(" | ")} |`);
|
|
11194
|
+
};
|
|
11195
|
+
var ROW_WRITERS = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };
|
|
11196
|
+
function writeRows(format, rows, notices) {
|
|
11197
|
+
const write2 = ROW_WRITERS[format] ?? writeMdRows;
|
|
11198
|
+
write2(rows, Object.keys(rows[0] ?? {}), notices);
|
|
11199
|
+
if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {
|
|
11200
|
+
for (const line of notices) writeLine(line);
|
|
11041
11201
|
}
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11202
|
+
}
|
|
11203
|
+
function outputResults(rows, fieldDescs, warnings, args, cached, read) {
|
|
11204
|
+
const format = args.output || "json";
|
|
11205
|
+
const allWarnings = [
|
|
11206
|
+
...warnings,
|
|
11207
|
+
...completenessWarnings({
|
|
11208
|
+
rowCount: rows.length,
|
|
11209
|
+
effectiveLimit: read.effectiveLimit,
|
|
11210
|
+
all: read.all,
|
|
11211
|
+
hasMore: Boolean(read.nextCursor)
|
|
11212
|
+
})
|
|
11213
|
+
];
|
|
11214
|
+
emitToStderr(warningLines(allWarnings, "csv"));
|
|
11215
|
+
if (format === "json") {
|
|
11216
|
+
writeAdsJson(
|
|
11217
|
+
buildQueryEnvelope(rows, fieldDescs, allWarnings, {
|
|
11218
|
+
full: Boolean(args.full),
|
|
11219
|
+
cached,
|
|
11220
|
+
nextCursor: read.nextCursor
|
|
11221
|
+
})
|
|
11222
|
+
);
|
|
11051
11223
|
return;
|
|
11052
11224
|
}
|
|
11053
|
-
|
|
11225
|
+
writeRows(format, rows, warningLines(allWarnings, format));
|
|
11054
11226
|
}
|
|
11055
11227
|
function resolveGaql(args, limit) {
|
|
11056
11228
|
if (args.preset) {
|
|
@@ -11099,8 +11271,8 @@ var SERVING_HIERARCHY_RESOURCES = [
|
|
|
11099
11271
|
"search_term_view",
|
|
11100
11272
|
"shopping_performance_view",
|
|
11101
11273
|
"asset_group_asset"
|
|
11102
|
-
];
|
|
11103
|
-
function
|
|
11274
|
+
].filter((resource) => !(resource in SERVING_CHAINS));
|
|
11275
|
+
function whereClause2(upperQuery) {
|
|
11104
11276
|
const whereIdx = upperQuery.indexOf(" WHERE ");
|
|
11105
11277
|
if (whereIdx === -1) return "";
|
|
11106
11278
|
let rest = upperQuery.slice(whereIdx + 7);
|
|
@@ -11124,7 +11296,7 @@ function servingScopeWarnings(args, finalQuery) {
|
|
|
11124
11296
|
const fromMatch = upper2.match(/\bFROM\s+([A-Z_]+)/);
|
|
11125
11297
|
const resource = fromMatch?.[1]?.toLowerCase();
|
|
11126
11298
|
const hierarchical = resource ? SERVING_HIERARCHY_RESOURCES.includes(resource) : false;
|
|
11127
|
-
const filtersStatus =
|
|
11299
|
+
const filtersStatus = whereClause2(upper2).includes(".STATUS");
|
|
11128
11300
|
if (hierarchical && !filtersStatus) {
|
|
11129
11301
|
return [
|
|
11130
11302
|
{
|
|
@@ -11141,11 +11313,12 @@ function parseSelectedFields(gaql) {
|
|
|
11141
11313
|
if (!list) return [];
|
|
11142
11314
|
return list.split(",").map((field) => field.trim()).filter((field) => /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*$/.test(field));
|
|
11143
11315
|
}
|
|
11144
|
-
function
|
|
11145
|
-
const observed = /* @__PURE__ */ new Set();
|
|
11316
|
+
function foldObservedFields(rows, observed) {
|
|
11146
11317
|
for (const row of rows) {
|
|
11147
11318
|
for (const key of Object.keys(row)) observed.add(key);
|
|
11148
11319
|
}
|
|
11320
|
+
}
|
|
11321
|
+
function unobservedFields(gaql, observed) {
|
|
11149
11322
|
return parseSelectedFields(gaql).filter((field) => {
|
|
11150
11323
|
if (field.endsWith(".resource_name")) return false;
|
|
11151
11324
|
if (observed.has(field)) return false;
|
|
@@ -11153,8 +11326,13 @@ function unobservedSelectedFields(gaql, rows) {
|
|
|
11153
11326
|
});
|
|
11154
11327
|
}
|
|
11155
11328
|
function missingFieldWarnings(gaql, rows) {
|
|
11156
|
-
|
|
11157
|
-
|
|
11329
|
+
const observed = /* @__PURE__ */ new Set();
|
|
11330
|
+
foldObservedFields(rows, observed);
|
|
11331
|
+
return missingFieldWarningsFor(gaql, observed, rows.length);
|
|
11332
|
+
}
|
|
11333
|
+
function missingFieldWarningsFor(gaql, observed, rowCount2) {
|
|
11334
|
+
if (rowCount2 === 0) return [];
|
|
11335
|
+
const missing = unobservedFields(gaql, observed);
|
|
11158
11336
|
if (missing.length === 0) return [];
|
|
11159
11337
|
return [
|
|
11160
11338
|
{
|
|
@@ -11171,6 +11349,7 @@ async function fetchAllPages(body, args, servingOptions) {
|
|
|
11171
11349
|
let pageToken = args.cursor;
|
|
11172
11350
|
let totalRowsWritten = 0;
|
|
11173
11351
|
let keywordLimits;
|
|
11352
|
+
const observedFields = /* @__PURE__ */ new Set();
|
|
11174
11353
|
do {
|
|
11175
11354
|
const requestBody2 = { ...body };
|
|
11176
11355
|
if (pageToken) requestBody2.pageToken = pageToken;
|
|
@@ -11178,11 +11357,11 @@ async function fetchAllPages(body, args, servingOptions) {
|
|
|
11178
11357
|
const rows = response.rows ?? [];
|
|
11179
11358
|
pageToken = response.pageToken;
|
|
11180
11359
|
if (args.out) {
|
|
11181
|
-
const filePath = resolve(args.out);
|
|
11182
11360
|
const shouldAppend = args.append || totalRowsWritten > 0;
|
|
11183
|
-
writeRowsToFile(
|
|
11361
|
+
writeRowsToFile(resolve(args.out), rows, extractFields(rows), shouldAppend);
|
|
11184
11362
|
totalRowsWritten += rows.length;
|
|
11185
11363
|
keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
|
|
11364
|
+
foldObservedFields(rows, observedFields);
|
|
11186
11365
|
} else {
|
|
11187
11366
|
allRows = allRows.concat(rows);
|
|
11188
11367
|
}
|
|
@@ -11192,7 +11371,8 @@ async function fetchAllPages(body, args, servingOptions) {
|
|
|
11192
11371
|
allRows,
|
|
11193
11372
|
lastPageToken: pageToken,
|
|
11194
11373
|
totalRowsWritten,
|
|
11195
|
-
keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions)
|
|
11374
|
+
keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),
|
|
11375
|
+
observedFields
|
|
11196
11376
|
};
|
|
11197
11377
|
}
|
|
11198
11378
|
async function executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey) {
|
|
@@ -11202,12 +11382,33 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
|
|
|
11202
11382
|
if (args.cursor) body.pageToken = args.cursor;
|
|
11203
11383
|
if (!useCache) body.skipCache = true;
|
|
11204
11384
|
const servingOptions = keywordServingOptions(finalQuery);
|
|
11205
|
-
const { allRows, lastPageToken, totalRowsWritten, keywordLimits } = await fetchAllPages(
|
|
11385
|
+
const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields } = await fetchAllPages(
|
|
11386
|
+
body,
|
|
11387
|
+
args,
|
|
11388
|
+
servingOptions
|
|
11389
|
+
);
|
|
11206
11390
|
if (args.out) {
|
|
11207
|
-
const
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11391
|
+
const truncation = completenessWarnings({
|
|
11392
|
+
rowCount: totalRowsWritten,
|
|
11393
|
+
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
|
|
11394
|
+
all: Boolean(args.all),
|
|
11395
|
+
hasMore: Boolean(lastPageToken)
|
|
11396
|
+
});
|
|
11397
|
+
const fileWarnings = [
|
|
11398
|
+
...warnings,
|
|
11399
|
+
...renderKeywordLimits(keywordLimits),
|
|
11400
|
+
...missingFieldWarningsFor(finalQuery, observedFields, totalRowsWritten),
|
|
11401
|
+
...truncation
|
|
11402
|
+
];
|
|
11403
|
+
emitToStderr(warningLines(fileWarnings, "csv"));
|
|
11404
|
+
writeAdsJson(
|
|
11405
|
+
buildFileSummary({
|
|
11406
|
+
file: resolve(args.out),
|
|
11407
|
+
rows: totalRowsWritten,
|
|
11408
|
+
warnings: fileWarnings,
|
|
11409
|
+
complete: truncation.length === 0
|
|
11410
|
+
})
|
|
11411
|
+
);
|
|
11211
11412
|
return;
|
|
11212
11413
|
}
|
|
11213
11414
|
if (useCache && allRows.length > 0) {
|
|
@@ -11221,9 +11422,16 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
|
|
|
11221
11422
|
[...warnings, ...missingFieldWarnings(finalQuery, allRows), ...keywordServingWarnings(allRows, servingOptions)],
|
|
11222
11423
|
args,
|
|
11223
11424
|
false,
|
|
11224
|
-
|
|
11425
|
+
{
|
|
11426
|
+
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
|
|
11427
|
+
all: Boolean(args.all),
|
|
11428
|
+
nextCursor: lastPageToken
|
|
11429
|
+
}
|
|
11225
11430
|
);
|
|
11226
11431
|
}
|
|
11432
|
+
function shouldReadCache(args) {
|
|
11433
|
+
return !args["no-cache"] && !args.out;
|
|
11434
|
+
}
|
|
11227
11435
|
async function validateArgs(args) {
|
|
11228
11436
|
const customerId = await resolveCustomerId(args);
|
|
11229
11437
|
const limit = args.limit ? Number(args.limit) : 200;
|
|
@@ -11286,10 +11494,14 @@ Examples:
|
|
|
11286
11494
|
description: "Include paused entities in preset results (default: serving entities only)",
|
|
11287
11495
|
required: false
|
|
11288
11496
|
},
|
|
11289
|
-
limit: {
|
|
11497
|
+
limit: {
|
|
11498
|
+
type: "string",
|
|
11499
|
+
description: "Max rows in one page (default 200); without --all the read stops there",
|
|
11500
|
+
required: false
|
|
11501
|
+
},
|
|
11290
11502
|
"list-presets": { type: "boolean", description: "List available presets", required: false },
|
|
11291
11503
|
cursor: { type: "string", description: "Pagination cursor", required: false },
|
|
11292
|
-
all: { type: "boolean", description: "
|
|
11504
|
+
all: { type: "boolean", description: "Follow every page (use with --out for large datasets)", required: false },
|
|
11293
11505
|
out: { type: "string", description: "File path for output", required: false },
|
|
11294
11506
|
append: { type: "boolean", description: "Append to file", required: false },
|
|
11295
11507
|
output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" },
|
|
@@ -11313,7 +11525,7 @@ Examples:
|
|
|
11313
11525
|
const validated = await validateArgs(args);
|
|
11314
11526
|
if (!validated) return;
|
|
11315
11527
|
const { customerId, limit, finalQuery, warnings, useCache, cacheKey } = validated;
|
|
11316
|
-
if (
|
|
11528
|
+
if (shouldReadCache(args)) {
|
|
11317
11529
|
const cached = cacheGet("queries", cacheKey);
|
|
11318
11530
|
if (cached) {
|
|
11319
11531
|
const fields = extractFields(cached.data);
|
|
@@ -11326,7 +11538,8 @@ Examples:
|
|
|
11326
11538
|
...keywordServingWarnings(cached.data, keywordServingOptions(finalQuery))
|
|
11327
11539
|
],
|
|
11328
11540
|
args,
|
|
11329
|
-
true
|
|
11541
|
+
true,
|
|
11542
|
+
{ effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)), all: Boolean(args.all) }
|
|
11330
11543
|
);
|
|
11331
11544
|
return;
|
|
11332
11545
|
}
|
|
@@ -13402,9 +13615,16 @@ var customerIdArg = { "customer-id": { type: "string", description: "10-digit Go
|
|
|
13402
13615
|
var fileArg = {
|
|
13403
13616
|
file: { type: "string", description: "JSON file with the full op payload (flags override)" }
|
|
13404
13617
|
};
|
|
13618
|
+
var BUDGET_DEMAND_HINTS = [
|
|
13619
|
+
"Check this daily budget against the search volume you measured for these keywords, not against the plan you had before you measured it. A budget many times larger than the traffic the target pool can absorb never spends, and reads to the client as a number nobody costed.",
|
|
13620
|
+
"No volume measured yet? Pull it first \u2014 `baker ads google query --preset search-terms` for what the account already captures, `baker ads google keywords metrics` for the market estimate (those are planner figures, never account performance)."
|
|
13621
|
+
];
|
|
13405
13622
|
var budgetsCommand = defineCommand30({
|
|
13406
13623
|
meta: { name: "budgets", description: "Stage campaign budget create/update" },
|
|
13407
13624
|
subCommands: {
|
|
13625
|
+
// Fires where the budget is actually set, rather than on whatever the chat happened to stage
|
|
13626
|
+
// first. A budget several times what the target pool can absorb never spends, and reads to the
|
|
13627
|
+
// client as a plan nobody costed.
|
|
13408
13628
|
create: defineCommand30({
|
|
13409
13629
|
meta: { name: "create", description: "Stage a campaign budget" },
|
|
13410
13630
|
args: {
|
|
@@ -13423,7 +13643,7 @@ var budgetsCommand = defineCommand30({
|
|
|
13423
13643
|
deliveryMethod: args.delivery,
|
|
13424
13644
|
explicitlyShared: args.shared || void 0
|
|
13425
13645
|
});
|
|
13426
|
-
await stageCreate("google.budget.create", customerId, payload);
|
|
13646
|
+
await stageCreate("google.budget.create", customerId, payload, BUDGET_DEMAND_HINTS);
|
|
13427
13647
|
}
|
|
13428
13648
|
}),
|
|
13429
13649
|
update: defineCommand30({
|
|
@@ -13443,7 +13663,7 @@ var budgetsCommand = defineCommand30({
|
|
|
13443
13663
|
amountMicros: microsFlag(args.amount, "--amount"),
|
|
13444
13664
|
deliveryMethod: args.delivery
|
|
13445
13665
|
});
|
|
13446
|
-
await stageUpdate("google.budget.update", customerId, target, payload);
|
|
13666
|
+
await stageUpdate("google.budget.update", customerId, target, payload, BUDGET_DEMAND_HINTS);
|
|
13447
13667
|
}
|
|
13448
13668
|
})
|
|
13449
13669
|
}
|
|
@@ -14916,19 +15136,19 @@ function failWriteValidation2(message) {
|
|
|
14916
15136
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
14917
15137
|
process.exit(1);
|
|
14918
15138
|
}
|
|
14919
|
-
function loadJsonFileArg2(
|
|
14920
|
-
if (typeof
|
|
15139
|
+
function loadJsonFileArg2(path38) {
|
|
15140
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
14921
15141
|
return {};
|
|
14922
15142
|
}
|
|
14923
15143
|
try {
|
|
14924
|
-
const parsed = JSON.parse(readFileSync4(
|
|
15144
|
+
const parsed = JSON.parse(readFileSync4(path38, "utf8"));
|
|
14925
15145
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14926
|
-
failWriteValidation2(`${
|
|
15146
|
+
failWriteValidation2(`${path38} must contain a JSON object`);
|
|
14927
15147
|
}
|
|
14928
15148
|
return parsed;
|
|
14929
15149
|
} catch (err) {
|
|
14930
15150
|
if (err instanceof SyntaxError) {
|
|
14931
|
-
failWriteValidation2(`${
|
|
15151
|
+
failWriteValidation2(`${path38} is not valid JSON: ${err.message}`);
|
|
14932
15152
|
}
|
|
14933
15153
|
throw err;
|
|
14934
15154
|
}
|
|
@@ -15013,15 +15233,15 @@ function parseLocaleFlag(value) {
|
|
|
15013
15233
|
}
|
|
15014
15234
|
return { language: match[1], country: match[2].toUpperCase() };
|
|
15015
15235
|
}
|
|
15016
|
-
function loadTargetingFileArg(
|
|
15017
|
-
if (typeof
|
|
15236
|
+
function loadTargetingFileArg(path38) {
|
|
15237
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
15018
15238
|
return void 0;
|
|
15019
15239
|
}
|
|
15020
|
-
const parsed = loadJsonFileArg2(
|
|
15240
|
+
const parsed = loadJsonFileArg2(path38);
|
|
15021
15241
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
15022
15242
|
if (!criteria.include) {
|
|
15023
15243
|
failWriteValidation2(
|
|
15024
|
-
`${
|
|
15244
|
+
`${path38} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
15025
15245
|
);
|
|
15026
15246
|
}
|
|
15027
15247
|
return criteria;
|
|
@@ -15056,14 +15276,14 @@ function parseCsvLine(line) {
|
|
|
15056
15276
|
cells.push(current);
|
|
15057
15277
|
return cells.map((cell2) => cell2.trim());
|
|
15058
15278
|
}
|
|
15059
|
-
function parseListFileArg(
|
|
15060
|
-
if (typeof
|
|
15279
|
+
function parseListFileArg(path38, maxRows) {
|
|
15280
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
15061
15281
|
return void 0;
|
|
15062
15282
|
}
|
|
15063
|
-
const raw = readFileSync4(
|
|
15283
|
+
const raw = readFileSync4(path38, "utf8");
|
|
15064
15284
|
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
15065
15285
|
if (lines.length < 2) {
|
|
15066
|
-
failWriteValidation2(`${
|
|
15286
|
+
failWriteValidation2(`${path38} needs a header row and at least one data row`);
|
|
15067
15287
|
}
|
|
15068
15288
|
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
15069
15289
|
const rows = [];
|
|
@@ -15082,7 +15302,7 @@ function parseListFileArg(path40, maxRows) {
|
|
|
15082
15302
|
}
|
|
15083
15303
|
}
|
|
15084
15304
|
if (rows.length > maxRows) {
|
|
15085
|
-
failWriteValidation2(`${
|
|
15305
|
+
failWriteValidation2(`${path38} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
15086
15306
|
}
|
|
15087
15307
|
return { columns, rows };
|
|
15088
15308
|
}
|
|
@@ -15178,11 +15398,11 @@ function readPositionals(args) {
|
|
|
15178
15398
|
function splitIdList(raw) {
|
|
15179
15399
|
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
15180
15400
|
}
|
|
15181
|
-
function idsFileEntries(
|
|
15182
|
-
if (typeof
|
|
15401
|
+
function idsFileEntries(path38) {
|
|
15402
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
15183
15403
|
return [];
|
|
15184
15404
|
}
|
|
15185
|
-
return readFileSync4(
|
|
15405
|
+
return readFileSync4(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
|
|
15186
15406
|
}
|
|
15187
15407
|
function requireTargets(args, entity) {
|
|
15188
15408
|
const positionals = readPositionals(args);
|
|
@@ -17803,9 +18023,9 @@ function compactRow(row) {
|
|
|
17803
18023
|
...destination.postUrn ? { postUrn: destination.postUrn } : {}
|
|
17804
18024
|
};
|
|
17805
18025
|
}
|
|
17806
|
-
function readPath(row,
|
|
18026
|
+
function readPath(row, path38) {
|
|
17807
18027
|
let current = row;
|
|
17808
|
-
for (const segment of
|
|
18028
|
+
for (const segment of path38.split(".")) {
|
|
17809
18029
|
const record = asRecord2(current);
|
|
17810
18030
|
if (!record) return void 0;
|
|
17811
18031
|
current = record[segment];
|
|
@@ -17815,10 +18035,10 @@ function readPath(row, path40) {
|
|
|
17815
18035
|
function projectFields(rows, paths) {
|
|
17816
18036
|
return rows.map((row) => {
|
|
17817
18037
|
const projected = {};
|
|
17818
|
-
for (const
|
|
17819
|
-
const value = readPath(row,
|
|
18038
|
+
for (const path38 of paths) {
|
|
18039
|
+
const value = readPath(row, path38);
|
|
17820
18040
|
if (value !== void 0) {
|
|
17821
|
-
projected[
|
|
18041
|
+
projected[path38] = value;
|
|
17822
18042
|
}
|
|
17823
18043
|
}
|
|
17824
18044
|
return projected;
|
|
@@ -19118,11 +19338,11 @@ var updateStatusSchema = z25.enum(UPDATE_STATUSES);
|
|
|
19118
19338
|
function currencyMinimums2(currencyCode) {
|
|
19119
19339
|
return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
|
|
19120
19340
|
}
|
|
19121
|
-
function validateDailyBudgetFloor(money, ctx,
|
|
19341
|
+
function validateDailyBudgetFloor(money, ctx, path38) {
|
|
19122
19342
|
if (money?.currencyCode) {
|
|
19123
19343
|
const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
|
|
19124
19344
|
if (Number(money.amount) < min) {
|
|
19125
|
-
ctx.addIssue({ code: "custom", path:
|
|
19345
|
+
ctx.addIssue({ code: "custom", path: path38, message: `below the ${min} ${money.currencyCode} daily minimum` });
|
|
19126
19346
|
}
|
|
19127
19347
|
}
|
|
19128
19348
|
}
|
|
@@ -19791,19 +20011,19 @@ function failWriteValidation3(message) {
|
|
|
19791
20011
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
19792
20012
|
process.exit(1);
|
|
19793
20013
|
}
|
|
19794
|
-
function loadJsonFileArg3(
|
|
19795
|
-
if (typeof
|
|
20014
|
+
function loadJsonFileArg3(path38) {
|
|
20015
|
+
if (typeof path38 !== "string" || path38.length === 0) {
|
|
19796
20016
|
return {};
|
|
19797
20017
|
}
|
|
19798
20018
|
try {
|
|
19799
|
-
const parsed = JSON.parse(readFileSync8(
|
|
20019
|
+
const parsed = JSON.parse(readFileSync8(path38, "utf8"));
|
|
19800
20020
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19801
|
-
failWriteValidation3(`${
|
|
20021
|
+
failWriteValidation3(`${path38} must contain a JSON object`);
|
|
19802
20022
|
}
|
|
19803
20023
|
return parsed;
|
|
19804
20024
|
} catch (err) {
|
|
19805
20025
|
if (err instanceof SyntaxError) {
|
|
19806
|
-
failWriteValidation3(`${
|
|
20026
|
+
failWriteValidation3(`${path38} is not valid JSON: ${err.message}`);
|
|
19807
20027
|
}
|
|
19808
20028
|
throw err;
|
|
19809
20029
|
}
|
|
@@ -24401,11 +24621,11 @@ function unwrap(response) {
|
|
|
24401
24621
|
}
|
|
24402
24622
|
return response.data;
|
|
24403
24623
|
}
|
|
24404
|
-
async function readAvatars(
|
|
24405
|
-
return unwrap(await apiGet(
|
|
24624
|
+
async function readAvatars(path38, params) {
|
|
24625
|
+
return unwrap(await apiGet(path38, params));
|
|
24406
24626
|
}
|
|
24407
|
-
async function writeAvatars(
|
|
24408
|
-
return unwrap(await apiPost(
|
|
24627
|
+
async function writeAvatars(path38, body) {
|
|
24628
|
+
return unwrap(await apiPost(path38, body));
|
|
24409
24629
|
}
|
|
24410
24630
|
|
|
24411
24631
|
// src/commands/avatars/create.ts
|
|
@@ -25151,14 +25371,14 @@ function suggestFamilies(wanted, catalogue, limit = 5) {
|
|
|
25151
25371
|
const key = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
25152
25372
|
const target = key(wanted);
|
|
25153
25373
|
if (!target) return [];
|
|
25154
|
-
const
|
|
25374
|
+
const words2 = target.split(" ");
|
|
25155
25375
|
const scored = catalogue.map((family) => {
|
|
25156
25376
|
const candidate = key(family);
|
|
25157
25377
|
if (candidate === target) return { family, score: 0 };
|
|
25158
25378
|
if (candidate.startsWith(target)) return { family, score: 1 };
|
|
25159
25379
|
if (candidate.includes(target)) return { family, score: 2 };
|
|
25160
|
-
const shared =
|
|
25161
|
-
return { family, score: shared > 0 ? 3 + (
|
|
25380
|
+
const shared = words2.filter((w) => w.length > 2 && candidate.includes(w)).length;
|
|
25381
|
+
return { family, score: shared > 0 ? 3 + (words2.length - shared) : Number.POSITIVE_INFINITY };
|
|
25162
25382
|
}).filter((c) => Number.isFinite(c.score)).sort((a, b) => a.score - b.score || a.family.localeCompare(b.family));
|
|
25163
25383
|
return scored.slice(0, limit).map((c) => c.family);
|
|
25164
25384
|
}
|
|
@@ -25239,12 +25459,12 @@ function missingFontFiles(urls, available) {
|
|
|
25239
25459
|
function planFontAdoption(sources, families) {
|
|
25240
25460
|
const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
|
|
25241
25461
|
const byFamily = /* @__PURE__ */ new Map();
|
|
25242
|
-
for (const { path:
|
|
25243
|
-
const dir = posix.dirname(
|
|
25462
|
+
for (const { path: path38, source } of sources) {
|
|
25463
|
+
const dir = posix.dirname(path38);
|
|
25244
25464
|
for (const face of declaredFontFaces(source)) {
|
|
25245
25465
|
if (!wanted.has(face.family)) continue;
|
|
25246
25466
|
const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
|
|
25247
|
-
perFile.set(
|
|
25467
|
+
perFile.set(path38, [...perFile.get(path38) ?? [], rebaseFontFaceSrc(face.block, dir)]);
|
|
25248
25468
|
byFamily.set(face.family, perFile);
|
|
25249
25469
|
}
|
|
25250
25470
|
}
|
|
@@ -26652,10 +26872,10 @@ function estSpeechS(text2) {
|
|
|
26652
26872
|
var OBSERVED_WPS_MIN = 1;
|
|
26653
26873
|
var OBSERVED_WPS_MAX = 6;
|
|
26654
26874
|
function estSpeechWindowS(text2, startS, endS) {
|
|
26655
|
-
const
|
|
26875
|
+
const words2 = wordCount(text2);
|
|
26656
26876
|
const window2 = (endS ?? 0) - (startS ?? 0);
|
|
26657
|
-
if (
|
|
26658
|
-
const wps =
|
|
26877
|
+
if (words2 > 0 && window2 > 0.3) {
|
|
26878
|
+
const wps = words2 / window2;
|
|
26659
26879
|
if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
|
|
26660
26880
|
}
|
|
26661
26881
|
return estSpeechS(text2);
|
|
@@ -27511,10 +27731,10 @@ function scrubFloatSentences(text2, floatDescs) {
|
|
|
27511
27731
|
if (floatDescs.length === 0 || !text2) return text2;
|
|
27512
27732
|
const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
|
|
27513
27733
|
const kept = text2.split(/(?<=[.!?])\s+/).filter((sentence) => {
|
|
27514
|
-
const
|
|
27734
|
+
const words2 = new Set(floatTokens(sentence));
|
|
27515
27735
|
return !tokenSets.some((ts) => {
|
|
27516
27736
|
let hits = 0;
|
|
27517
|
-
for (const w of
|
|
27737
|
+
for (const w of words2) if (ts.has(w)) hits++;
|
|
27518
27738
|
return hits >= 2;
|
|
27519
27739
|
});
|
|
27520
27740
|
}).join(" ").trim();
|
|
@@ -33439,12 +33659,12 @@ function collectSideEffects(tree) {
|
|
|
33439
33659
|
);
|
|
33440
33660
|
}
|
|
33441
33661
|
function readFlowTree(slug) {
|
|
33442
|
-
const
|
|
33443
|
-
if (!existsSync5(
|
|
33662
|
+
const path38 = join3(flowsDir(), slug, "_data.json");
|
|
33663
|
+
if (!existsSync5(path38)) {
|
|
33444
33664
|
failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
|
|
33445
33665
|
}
|
|
33446
33666
|
try {
|
|
33447
|
-
return JSON.parse(readFileSync9(
|
|
33667
|
+
return JSON.parse(readFileSync9(path38, "utf-8"));
|
|
33448
33668
|
} catch (error) {
|
|
33449
33669
|
failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
33450
33670
|
}
|
|
@@ -33836,10 +34056,10 @@ function parseValueExpression(raw) {
|
|
|
33836
34056
|
return parts.map(parsePart);
|
|
33837
34057
|
}
|
|
33838
34058
|
function trackingFieldIds() {
|
|
33839
|
-
const
|
|
33840
|
-
if (!existsSync6(
|
|
34059
|
+
const path38 = join4(flowsDir(), "..", "tracking.ts");
|
|
34060
|
+
if (!existsSync6(path38)) return null;
|
|
33841
34061
|
try {
|
|
33842
|
-
const source = readFileSync10(
|
|
34062
|
+
const source = readFileSync10(path38, "utf-8");
|
|
33843
34063
|
const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
|
|
33844
34064
|
if (!block2) return null;
|
|
33845
34065
|
const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
|
|
@@ -34391,13 +34611,13 @@ function specsFromFile(parsed) {
|
|
|
34391
34611
|
return `${destField}${type}=${entry?.value ?? ""}`;
|
|
34392
34612
|
});
|
|
34393
34613
|
}
|
|
34394
|
-
function readSpecFile(
|
|
34614
|
+
function readSpecFile(path38) {
|
|
34395
34615
|
let raw;
|
|
34396
34616
|
try {
|
|
34397
|
-
raw =
|
|
34617
|
+
raw = path38 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path38, "utf-8");
|
|
34398
34618
|
} catch (error) {
|
|
34399
34619
|
refuse(
|
|
34400
|
-
`Could not read ${
|
|
34620
|
+
`Could not read ${path38 === "-" ? "the mapping from stdin" : `"${path38}"`}: ${error instanceof Error ? error.message : String(error)}`
|
|
34401
34621
|
);
|
|
34402
34622
|
}
|
|
34403
34623
|
let parsed;
|
|
@@ -34405,7 +34625,7 @@ function readSpecFile(path40) {
|
|
|
34405
34625
|
parsed = JSON.parse(raw);
|
|
34406
34626
|
} catch (error) {
|
|
34407
34627
|
refuse(
|
|
34408
|
-
`${
|
|
34628
|
+
`${path38 === "-" ? "stdin" : `"${path38}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
34409
34629
|
'Expected { "map": { "<destField>": "<value>", \u2026 } }'
|
|
34410
34630
|
);
|
|
34411
34631
|
}
|
|
@@ -34659,18 +34879,18 @@ var ARRAY_FIELDS = [
|
|
|
34659
34879
|
"tagIds"
|
|
34660
34880
|
];
|
|
34661
34881
|
var ARRAY_OWNERS = ["", "body"];
|
|
34662
|
-
function dropUnsetOptionals(sideEffect,
|
|
34882
|
+
function dropUnsetOptionals(sideEffect, path38) {
|
|
34663
34883
|
return OPTIONAL_STRINGS.flatMap((key) => {
|
|
34664
34884
|
if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
|
|
34665
34885
|
delete sideEffect[key];
|
|
34666
|
-
return [{ path:
|
|
34886
|
+
return [{ path: path38, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
|
|
34667
34887
|
});
|
|
34668
34888
|
}
|
|
34669
|
-
function fillNulledArrays(target, prefix,
|
|
34889
|
+
function fillNulledArrays(target, prefix, path38) {
|
|
34670
34890
|
return ARRAY_FIELDS.flatMap((key) => {
|
|
34671
34891
|
if (!(key in target) || target[key] !== null) return [];
|
|
34672
34892
|
target[key] = [];
|
|
34673
|
-
return [{ path:
|
|
34893
|
+
return [{ path: path38, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
|
|
34674
34894
|
});
|
|
34675
34895
|
}
|
|
34676
34896
|
function sideEffectsOf(node) {
|
|
@@ -34680,13 +34900,13 @@ function sideEffectsOf(node) {
|
|
|
34680
34900
|
);
|
|
34681
34901
|
}
|
|
34682
34902
|
function normalizeSideEffect(sideEffect, where) {
|
|
34683
|
-
const
|
|
34903
|
+
const path38 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
|
|
34684
34904
|
const arrays = ARRAY_OWNERS.flatMap((owner) => {
|
|
34685
34905
|
const target = owner ? sideEffect[owner] : sideEffect;
|
|
34686
34906
|
if (!target || typeof target !== "object") return [];
|
|
34687
|
-
return fillNulledArrays(target, owner ? `${owner}.` : "",
|
|
34907
|
+
return fillNulledArrays(target, owner ? `${owner}.` : "", path38);
|
|
34688
34908
|
});
|
|
34689
|
-
return [...dropUnsetOptionals(sideEffect,
|
|
34909
|
+
return [...dropUnsetOptionals(sideEffect, path38), ...arrays];
|
|
34690
34910
|
}
|
|
34691
34911
|
function normalizeFlowTree(tree) {
|
|
34692
34912
|
const changes = [];
|
|
@@ -35224,10 +35444,10 @@ async function stageOps(ops) {
|
|
|
35224
35444
|
handleError2(err);
|
|
35225
35445
|
}
|
|
35226
35446
|
}
|
|
35227
|
-
async function draftAction2(
|
|
35447
|
+
async function draftAction2(path38, body, chat) {
|
|
35228
35448
|
const chatId = resolveChatId(chat);
|
|
35229
35449
|
try {
|
|
35230
|
-
const data = await apiPost(
|
|
35450
|
+
const data = await apiPost(path38, { chatId, ...body });
|
|
35231
35451
|
writeJsonEnvelope({ ok: true, data });
|
|
35232
35452
|
return data;
|
|
35233
35453
|
} catch (err) {
|
|
@@ -37659,9 +37879,9 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
37659
37879
|
}
|
|
37660
37880
|
return readFile20(pathOrUrl);
|
|
37661
37881
|
}
|
|
37662
|
-
async function isDirectory(
|
|
37882
|
+
async function isDirectory(path38) {
|
|
37663
37883
|
try {
|
|
37664
|
-
const s = await stat4(
|
|
37884
|
+
const s = await stat4(path38);
|
|
37665
37885
|
return s.isDirectory();
|
|
37666
37886
|
} catch {
|
|
37667
37887
|
return false;
|
|
@@ -37963,13 +38183,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
|
|
|
37963
38183
|
}
|
|
37964
38184
|
function disambiguate(paths) {
|
|
37965
38185
|
const taken = /* @__PURE__ */ new Set();
|
|
37966
|
-
return paths.map((
|
|
37967
|
-
if (!taken.has(
|
|
37968
|
-
taken.add(
|
|
37969
|
-
return
|
|
38186
|
+
return paths.map((path38) => {
|
|
38187
|
+
if (!taken.has(path38)) {
|
|
38188
|
+
taken.add(path38);
|
|
38189
|
+
return path38;
|
|
37970
38190
|
}
|
|
37971
|
-
const ext = extname3(
|
|
37972
|
-
const stem =
|
|
38191
|
+
const ext = extname3(path38);
|
|
38192
|
+
const stem = path38.slice(0, path38.length - ext.length);
|
|
37973
38193
|
let n = 2;
|
|
37974
38194
|
while (taken.has(`${stem}-${n}${ext}`)) n += 1;
|
|
37975
38195
|
const unique = `${stem}-${n}${ext}`;
|
|
@@ -38096,10 +38316,10 @@ async function runDownloads(plan) {
|
|
|
38096
38316
|
const paths = disambiguate(fetched.map((item) => item.path));
|
|
38097
38317
|
const downloaded = [];
|
|
38098
38318
|
for (const [index, item] of fetched.entries()) {
|
|
38099
|
-
const
|
|
38319
|
+
const path38 = paths[index] ?? item.path;
|
|
38100
38320
|
try {
|
|
38101
|
-
await atomicWrite(
|
|
38102
|
-
downloaded.push({ input: item.input, output:
|
|
38321
|
+
await atomicWrite(path38, item.buffer);
|
|
38322
|
+
downloaded.push({ input: item.input, output: path38, bytes: item.buffer.length, contentType: item.contentType });
|
|
38103
38323
|
} catch (err) {
|
|
38104
38324
|
failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
|
|
38105
38325
|
}
|
|
@@ -41059,7 +41279,7 @@ import { defineCommand as defineCommand166 } from "citty";
|
|
|
41059
41279
|
|
|
41060
41280
|
// src/commands/landing/critique.ts
|
|
41061
41281
|
import { readdir as readdir9, stat as stat6 } from "fs/promises";
|
|
41062
|
-
import
|
|
41282
|
+
import path28 from "path";
|
|
41063
41283
|
import { defineCommand as defineCommand156 } from "citty";
|
|
41064
41284
|
|
|
41065
41285
|
// src/engine/landing/lib/constants.ts
|
|
@@ -41262,11 +41482,6 @@ var RULE_META = {
|
|
|
41262
41482
|
severity: "block",
|
|
41263
41483
|
note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
|
|
41264
41484
|
},
|
|
41265
|
-
"copied-reference-copy": {
|
|
41266
|
-
family: "originality",
|
|
41267
|
-
severity: "block",
|
|
41268
|
-
note: "This line is lifted from a section you consulted in the inspiration library. Reference sections are for structure and mechanism, never words \u2014 a visitor who has seen the original reads this as a clone, and the claim is not yours to make. Rewrite it from the client's own offer."
|
|
41269
|
-
},
|
|
41270
41485
|
"broken-image": {
|
|
41271
41486
|
family: "integrity",
|
|
41272
41487
|
severity: "block",
|
|
@@ -41447,64 +41662,6 @@ var SEVERITY_WEIGHT = {
|
|
|
41447
41662
|
advisory: 0.05
|
|
41448
41663
|
};
|
|
41449
41664
|
|
|
41450
|
-
// src/engine/landing/lib/originality.ts
|
|
41451
|
-
var MIN_COMPARABLE_LENGTH = 12;
|
|
41452
|
-
var NEAR_MATCH_RATIO = 0.8;
|
|
41453
|
-
function normalize(value) {
|
|
41454
|
-
return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
|
|
41455
|
-
}
|
|
41456
|
-
function words2(value) {
|
|
41457
|
-
return normalize(value).split(" ").filter(Boolean);
|
|
41458
|
-
}
|
|
41459
|
-
function copyOverlapRatio(candidate, reference) {
|
|
41460
|
-
const referenceWords = words2(reference);
|
|
41461
|
-
if (referenceWords.length === 0) return 0;
|
|
41462
|
-
const candidateWords = new Set(words2(candidate));
|
|
41463
|
-
const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
|
|
41464
|
-
return shared / referenceWords.length;
|
|
41465
|
-
}
|
|
41466
|
-
function isVerbatimReuse(candidate, reference) {
|
|
41467
|
-
const normalizedCandidate = normalize(candidate);
|
|
41468
|
-
const normalizedReference = normalize(reference);
|
|
41469
|
-
if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
|
|
41470
|
-
if (normalizedCandidate.includes(normalizedReference)) return true;
|
|
41471
|
-
return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
|
|
41472
|
-
}
|
|
41473
|
-
function extractVisibleStrings(text2) {
|
|
41474
|
-
const found = [];
|
|
41475
|
-
const lines = text2.split("\n");
|
|
41476
|
-
for (const [index, line] of lines.entries()) {
|
|
41477
|
-
for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
|
|
41478
|
-
const value = match[1]?.trim();
|
|
41479
|
-
if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
|
|
41480
|
-
}
|
|
41481
|
-
}
|
|
41482
|
-
return found;
|
|
41483
|
-
}
|
|
41484
|
-
function detectOriginality(sources, references) {
|
|
41485
|
-
if (references.length === 0) return [];
|
|
41486
|
-
const findings = [];
|
|
41487
|
-
const seen = /* @__PURE__ */ new Set();
|
|
41488
|
-
for (const source of sources) {
|
|
41489
|
-
for (const { value, line } of extractVisibleStrings(source.text)) {
|
|
41490
|
-
for (const reference of references) {
|
|
41491
|
-
const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
|
|
41492
|
-
if (!hit) continue;
|
|
41493
|
-
const key = `${source.path}:${line}:${normalize(hit)}`;
|
|
41494
|
-
if (seen.has(key)) continue;
|
|
41495
|
-
seen.add(key);
|
|
41496
|
-
findings.push({
|
|
41497
|
-
id: "copied-reference-copy",
|
|
41498
|
-
snippet: value.slice(0, 120),
|
|
41499
|
-
file: source.path,
|
|
41500
|
-
line
|
|
41501
|
-
});
|
|
41502
|
-
}
|
|
41503
|
-
}
|
|
41504
|
-
}
|
|
41505
|
-
return findings;
|
|
41506
|
-
}
|
|
41507
|
-
|
|
41508
41665
|
// src/engine/landing/lib/rules.ts
|
|
41509
41666
|
var cap2 = (m, i) => m[i] ?? "";
|
|
41510
41667
|
var num2 = (m, i) => Number(m[i] ?? 0);
|
|
@@ -42032,16 +42189,7 @@ function dedupe(findings) {
|
|
|
42032
42189
|
}
|
|
42033
42190
|
|
|
42034
42191
|
// src/engine/landing/lib/critique.ts
|
|
42035
|
-
var FAMILIES = [
|
|
42036
|
-
"typography",
|
|
42037
|
-
"color",
|
|
42038
|
-
"borders_depth",
|
|
42039
|
-
"motion",
|
|
42040
|
-
"spacing",
|
|
42041
|
-
"copy",
|
|
42042
|
-
"integrity",
|
|
42043
|
-
"originality"
|
|
42044
|
-
];
|
|
42192
|
+
var FAMILIES = ["typography", "color", "borders_depth", "motion", "spacing", "copy", "integrity"];
|
|
42045
42193
|
function round4(n) {
|
|
42046
42194
|
return Math.round(n * 100) / 100;
|
|
42047
42195
|
}
|
|
@@ -42060,7 +42208,6 @@ function critiqueLanding(input) {
|
|
|
42060
42208
|
const raws = [];
|
|
42061
42209
|
for (const source of sources) raws.push(...detectSource(source));
|
|
42062
42210
|
raws.push(...detectPage(sources));
|
|
42063
|
-
raws.push(...detectOriginality(sources, input.references ?? []));
|
|
42064
42211
|
for (const raw of raws) {
|
|
42065
42212
|
const meta = RULE_META[raw.id];
|
|
42066
42213
|
if (!meta) continue;
|
|
@@ -42098,82 +42245,41 @@ function describeCounts(findings) {
|
|
|
42098
42245
|
return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
|
|
42099
42246
|
}
|
|
42100
42247
|
|
|
42101
|
-
// src/engine/landing/lib/referenceStore.ts
|
|
42102
|
-
import { mkdir as mkdir8, readFile as readFile21, writeFile as writeFile12 } from "fs/promises";
|
|
42103
|
-
import path26 from "path";
|
|
42104
|
-
var REFERENCES_FILE = ".cache/inspiration-refs.json";
|
|
42105
|
-
var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
42106
|
-
async function readReferences(projectRoot) {
|
|
42107
|
-
try {
|
|
42108
|
-
const raw = await readFile21(path26.join(projectRoot, REFERENCES_FILE), "utf8");
|
|
42109
|
-
const parsed = JSON.parse(raw);
|
|
42110
|
-
if (!Array.isArray(parsed)) return [];
|
|
42111
|
-
const cutoff = Date.now() - REFERENCE_TTL_MS;
|
|
42112
|
-
return parsed.filter((entry) => {
|
|
42113
|
-
if (typeof entry !== "object" || entry === null) return false;
|
|
42114
|
-
const candidate = entry;
|
|
42115
|
-
if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
|
|
42116
|
-
const at = Date.parse(candidate.consultedAt ?? "");
|
|
42117
|
-
return Number.isNaN(at) ? true : at >= cutoff;
|
|
42118
|
-
});
|
|
42119
|
-
} catch {
|
|
42120
|
-
return [];
|
|
42121
|
-
}
|
|
42122
|
-
}
|
|
42123
|
-
async function recordReference(projectRoot, reference) {
|
|
42124
|
-
return await recordReferences(projectRoot, [reference]);
|
|
42125
|
-
}
|
|
42126
|
-
async function recordReferences(projectRoot, references) {
|
|
42127
|
-
if (references.length === 0) return true;
|
|
42128
|
-
try {
|
|
42129
|
-
const existing = await readReferences(projectRoot);
|
|
42130
|
-
const replaced = new Set(references.map((reference) => reference.sectionId));
|
|
42131
|
-
const merged = [...existing.filter((entry) => !replaced.has(entry.sectionId)), ...references];
|
|
42132
|
-
const file = path26.join(projectRoot, REFERENCES_FILE);
|
|
42133
|
-
await mkdir8(path26.dirname(file), { recursive: true });
|
|
42134
|
-
await writeFile12(file, `${JSON.stringify(merged, null, 2)}
|
|
42135
|
-
`);
|
|
42136
|
-
return true;
|
|
42137
|
-
} catch {
|
|
42138
|
-
return false;
|
|
42139
|
-
}
|
|
42140
|
-
}
|
|
42141
|
-
|
|
42142
42248
|
// src/commands/landing/snapshot.ts
|
|
42143
|
-
import { mkdir as
|
|
42144
|
-
import
|
|
42249
|
+
import { mkdir as mkdir8, rename as rename2, writeFile as writeFile12 } from "fs/promises";
|
|
42250
|
+
import path26 from "path";
|
|
42145
42251
|
var CRITIC_VERSION = "2";
|
|
42146
42252
|
function critiqueCacheDir(projectRoot) {
|
|
42147
|
-
return
|
|
42253
|
+
return path26.join(projectRoot, ".cache", "landing-critique");
|
|
42148
42254
|
}
|
|
42149
42255
|
function snapshotPath(projectRoot, slug) {
|
|
42150
|
-
return
|
|
42256
|
+
return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
|
|
42151
42257
|
}
|
|
42152
42258
|
async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
42153
|
-
await
|
|
42259
|
+
await mkdir8(critiqueCacheDir(projectRoot), { recursive: true });
|
|
42154
42260
|
const dest = snapshotPath(projectRoot, snapshot.slug);
|
|
42155
42261
|
const tmp = `${dest}.tmp`;
|
|
42156
|
-
await
|
|
42262
|
+
await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
|
|
42157
42263
|
`, "utf8");
|
|
42158
42264
|
await rename2(tmp, dest);
|
|
42159
42265
|
}
|
|
42160
42266
|
|
|
42161
42267
|
// src/commands/landing/source-version.ts
|
|
42162
|
-
import { readdir as readdir8, readFile as
|
|
42163
|
-
import
|
|
42268
|
+
import { readdir as readdir8, readFile as readFile21, stat as stat5 } from "fs/promises";
|
|
42269
|
+
import path27 from "path";
|
|
42164
42270
|
async function landingSourceRelPaths(landingDir) {
|
|
42165
42271
|
const rel = [];
|
|
42166
|
-
if (await isFile(
|
|
42167
|
-
const componentsDir =
|
|
42272
|
+
if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
|
|
42273
|
+
const componentsDir = path27.join(landingDir, "_components");
|
|
42168
42274
|
for (const abs of await walkAstro(componentsDir)) {
|
|
42169
|
-
rel.push(
|
|
42275
|
+
rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
|
|
42170
42276
|
}
|
|
42171
42277
|
return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
42172
42278
|
}
|
|
42173
42279
|
async function readLandingSources(landingDir) {
|
|
42174
42280
|
const rel = await landingSourceRelPaths(landingDir);
|
|
42175
42281
|
const out = [];
|
|
42176
|
-
for (const r of rel) out.push({ path: r, text: await
|
|
42282
|
+
for (const r of rel) out.push({ path: r, text: await readFile21(path27.join(landingDir, r), "utf8") });
|
|
42177
42283
|
return out;
|
|
42178
42284
|
}
|
|
42179
42285
|
async function computeLandingSourceSha(landingDir) {
|
|
@@ -42182,7 +42288,7 @@ async function computeLandingSourceSha(landingDir) {
|
|
|
42182
42288
|
for (const r of rel) {
|
|
42183
42289
|
let bytes;
|
|
42184
42290
|
try {
|
|
42185
|
-
bytes = await
|
|
42291
|
+
bytes = await readFile21(path27.join(landingDir, r));
|
|
42186
42292
|
} catch {
|
|
42187
42293
|
bytes = Buffer.alloc(0);
|
|
42188
42294
|
}
|
|
@@ -42206,7 +42312,7 @@ async function walkAstro(dir) {
|
|
|
42206
42312
|
}
|
|
42207
42313
|
const out = [];
|
|
42208
42314
|
for (const entry of entries) {
|
|
42209
|
-
const abs =
|
|
42315
|
+
const abs = path27.join(dir, entry.name);
|
|
42210
42316
|
if (entry.isDirectory()) out.push(...await walkAstro(abs));
|
|
42211
42317
|
else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
|
|
42212
42318
|
}
|
|
@@ -42267,14 +42373,14 @@ var critiqueCommand2 = defineCommand156({
|
|
|
42267
42373
|
{ availableSlugs: await listLandingSlugs(projectRoot) }
|
|
42268
42374
|
);
|
|
42269
42375
|
}
|
|
42270
|
-
if (!await isDir(
|
|
42376
|
+
if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
|
|
42271
42377
|
fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
|
|
42272
42378
|
availableSlugs: await listLandingSlugs(projectRoot)
|
|
42273
42379
|
});
|
|
42274
42380
|
}
|
|
42275
42381
|
}
|
|
42276
|
-
const
|
|
42277
|
-
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand
|
|
42382
|
+
const brand = await loadBrandTokens(projectRoot);
|
|
42383
|
+
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
|
|
42278
42384
|
const landings = results.map(({ slug, report }) => ({
|
|
42279
42385
|
slug,
|
|
42280
42386
|
overall: report.overall,
|
|
@@ -42305,10 +42411,10 @@ var critiqueCommand2 = defineCommand156({
|
|
|
42305
42411
|
);
|
|
42306
42412
|
}
|
|
42307
42413
|
});
|
|
42308
|
-
async function critiqueOne(projectRoot, slug, brand
|
|
42309
|
-
const landingDir =
|
|
42414
|
+
async function critiqueOne(projectRoot, slug, brand) {
|
|
42415
|
+
const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
|
|
42310
42416
|
const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
|
|
42311
|
-
const report = critiqueLanding({ slug, sources, brand
|
|
42417
|
+
const report = critiqueLanding({ slug, sources, brand });
|
|
42312
42418
|
let snapshotFailed = false;
|
|
42313
42419
|
try {
|
|
42314
42420
|
await writeCritiqueSnapshot(projectRoot, {
|
|
@@ -42326,7 +42432,7 @@ async function critiqueOne(projectRoot, slug, brand, references) {
|
|
|
42326
42432
|
}
|
|
42327
42433
|
async function listLandingSlugs(projectRoot) {
|
|
42328
42434
|
try {
|
|
42329
|
-
const entries = await readdir9(
|
|
42435
|
+
const entries = await readdir9(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
42330
42436
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
42331
42437
|
} catch {
|
|
42332
42438
|
return [];
|
|
@@ -42359,7 +42465,7 @@ import { defineCommand as defineCommand157 } from "citty";
|
|
|
42359
42465
|
|
|
42360
42466
|
// src/commands/landing/inspiration/shared.ts
|
|
42361
42467
|
var INSPIRATION_HINTS = {
|
|
42362
|
-
adapt: "Reference only. Re-express this in the client's BRAND.md palette, type and imagery register. Reusing a headline, subhead or CTA verbatim is a Tier 0 message-match failure (references/gotchas.md)
|
|
42468
|
+
adapt: "Reference only. Re-express this in the client's BRAND.md palette, type and imagery register. Reusing a headline, subhead or CTA verbatim is a Tier 0 message-match failure (references/gotchas.md).",
|
|
42363
42469
|
structureNotCopy: "Take the structural decision, not the furniture: what the eye hits first, the grid ratio, what was deliberately left out. Your copy must come from the client's own offer."
|
|
42364
42470
|
};
|
|
42365
42471
|
function favoritesScopeHints(health, resultCount) {
|
|
@@ -42477,12 +42583,12 @@ var addCommand = defineCommand157({
|
|
|
42477
42583
|
});
|
|
42478
42584
|
|
|
42479
42585
|
// src/commands/landing/inspiration/code.ts
|
|
42480
|
-
import { mkdir as
|
|
42481
|
-
import
|
|
42586
|
+
import { mkdir as mkdir9, writeFile as writeFile13 } from "fs/promises";
|
|
42587
|
+
import path29 from "path";
|
|
42482
42588
|
import { defineCommand as defineCommand158 } from "citty";
|
|
42483
42589
|
registerSchema({
|
|
42484
42590
|
command: "landing.inspiration.code",
|
|
42485
|
-
description: "Write a reference section's standalone HTML+CSS to .baker/inspiration/<id>/ so you can read how it is built. Reference only \u2014 the structure is the lesson, the words are not yours to reuse.
|
|
42591
|
+
description: "Write a reference section's standalone HTML+CSS to .baker/inspiration/<id>/ so you can read how it is built. Reference only \u2014 the structure is the lesson, the words are not yours to reuse.",
|
|
42486
42592
|
args: {
|
|
42487
42593
|
id: { type: "string", description: "Section id from search", required: true },
|
|
42488
42594
|
full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
|
|
@@ -42501,30 +42607,19 @@ var codeCommand = defineCommand158({
|
|
|
42501
42607
|
try {
|
|
42502
42608
|
const id = args.id;
|
|
42503
42609
|
const data = await apiGet("/api/landing-inspiration/section-code", { id });
|
|
42504
|
-
const dir =
|
|
42505
|
-
await
|
|
42506
|
-
const file =
|
|
42507
|
-
await
|
|
42508
|
-
const recorded = await recordReference(process.cwd(), {
|
|
42509
|
-
sectionId: id,
|
|
42510
|
-
sourceUrl: data.sourceUrl,
|
|
42511
|
-
copyStrings: data.copyStrings,
|
|
42512
|
-
consultedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
42513
|
-
});
|
|
42610
|
+
const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
|
|
42611
|
+
await mkdir9(dir, { recursive: true });
|
|
42612
|
+
const file = path29.join(dir, "section.html");
|
|
42613
|
+
await writeFile13(file, data.html);
|
|
42514
42614
|
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
42515
42615
|
const fidelity = fidelityHint(data.fidelity);
|
|
42516
42616
|
if (fidelity) hints.push(fidelity);
|
|
42517
42617
|
if (data.fidelityNote) hints.push(data.fidelityNote);
|
|
42518
|
-
if (!recorded) {
|
|
42519
|
-
hints.push(
|
|
42520
|
-
"Could not record this reference (.cache/ not writable?) \u2014 the originality check at publish will not be able to see it, so be especially careful not to reuse its copy."
|
|
42521
|
-
);
|
|
42522
|
-
}
|
|
42523
42618
|
writeJson({
|
|
42524
42619
|
ok: true,
|
|
42525
42620
|
data: {
|
|
42526
42621
|
id,
|
|
42527
|
-
file:
|
|
42622
|
+
file: path29.relative(process.cwd(), file),
|
|
42528
42623
|
bytes: data.html.length,
|
|
42529
42624
|
fidelity: data.fidelity,
|
|
42530
42625
|
reproduction_notes: data.reproductionNotes,
|
|
@@ -42779,25 +42874,8 @@ var pageCommand2 = defineCommand160({
|
|
|
42779
42874
|
});
|
|
42780
42875
|
|
|
42781
42876
|
// src/commands/landing/inspiration/scrape.ts
|
|
42782
|
-
import { readFile as readFile23 } from "fs/promises";
|
|
42783
|
-
import path34 from "path";
|
|
42784
42877
|
import { defineCommand as defineCommand161 } from "citty";
|
|
42785
42878
|
|
|
42786
|
-
// src/engine/landing/lib/capturedReferences.ts
|
|
42787
|
-
var MAX_STRINGS_PER_SECTION = 40;
|
|
42788
|
-
function bodyOf(markup) {
|
|
42789
|
-
const body = markup.indexOf("<body");
|
|
42790
|
-
return body === -1 ? markup : markup.slice(body);
|
|
42791
|
-
}
|
|
42792
|
-
function capturedCopyStrings(markup) {
|
|
42793
|
-
const seen = /* @__PURE__ */ new Set();
|
|
42794
|
-
for (const { value } of extractVisibleStrings(bodyOf(markup))) {
|
|
42795
|
-
seen.add(value);
|
|
42796
|
-
if (seen.size >= MAX_STRINGS_PER_SECTION) break;
|
|
42797
|
-
}
|
|
42798
|
-
return [...seen];
|
|
42799
|
-
}
|
|
42800
|
-
|
|
42801
42879
|
// src/engine/landing-library/proxyFailure.ts
|
|
42802
42880
|
var PROXY_STATUS = 407;
|
|
42803
42881
|
var PROXY_NET_ERRORS = [
|
|
@@ -42972,8 +43050,8 @@ function classifyCaptureFailure(error) {
|
|
|
42972
43050
|
}
|
|
42973
43051
|
|
|
42974
43052
|
// src/engine/landing-library/run.ts
|
|
42975
|
-
import { mkdir as
|
|
42976
|
-
import
|
|
43053
|
+
import { mkdir as mkdir10, writeFile as writeFile15 } from "fs/promises";
|
|
43054
|
+
import path31 from "path";
|
|
42977
43055
|
|
|
42978
43056
|
// ../proxy/src/preflight.ts
|
|
42979
43057
|
import http from "http";
|
|
@@ -44388,11 +44466,11 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
|
|
|
44388
44466
|
}
|
|
44389
44467
|
|
|
44390
44468
|
// src/engine/landing-library/report.ts
|
|
44391
|
-
import { writeFile as
|
|
44392
|
-
import
|
|
44469
|
+
import { writeFile as writeFile14 } from "fs/promises";
|
|
44470
|
+
import path30 from "path";
|
|
44393
44471
|
async function writeCaptureReport(manifest, outDir) {
|
|
44394
|
-
const file =
|
|
44395
|
-
await
|
|
44472
|
+
const file = path30.join(outDir, "report.html");
|
|
44473
|
+
await writeFile14(file, renderReport(manifest));
|
|
44396
44474
|
return file;
|
|
44397
44475
|
}
|
|
44398
44476
|
function escapeHtml3(value) {
|
|
@@ -44570,32 +44648,32 @@ async function reproducePage(args) {
|
|
|
44570
44648
|
const { browser, page, outDir, pageUrl, livePageShot } = args;
|
|
44571
44649
|
const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
|
|
44572
44650
|
if (!built) return { bundle: null, fidelity: null };
|
|
44573
|
-
await
|
|
44651
|
+
await writeFile15(path31.join(outDir, "page.html"), built.html);
|
|
44574
44652
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
|
|
44575
44653
|
wholePage: true,
|
|
44576
44654
|
timeoutMs: 6e4
|
|
44577
44655
|
});
|
|
44578
44656
|
if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
|
|
44579
|
-
await
|
|
44657
|
+
await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
|
|
44580
44658
|
const { score, note } = await scoreFidelity(livePageShot, rendered);
|
|
44581
44659
|
return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
|
|
44582
44660
|
}
|
|
44583
44661
|
async function captureOneSection(args) {
|
|
44584
44662
|
const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
|
|
44585
|
-
const dir =
|
|
44586
|
-
await
|
|
44663
|
+
const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
|
|
44664
|
+
await mkdir10(dir, { recursive: true });
|
|
44587
44665
|
const desktop = await captureSection(page, candidate);
|
|
44588
|
-
if (desktop) await
|
|
44666
|
+
if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
|
|
44589
44667
|
const visualHash = desktop ? await perceptualHash(desktop) : null;
|
|
44590
44668
|
const motion = await collectMotion(page, candidate.selector);
|
|
44591
44669
|
const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
|
|
44592
44670
|
let fidelity = null;
|
|
44593
44671
|
let fidelityNote;
|
|
44594
44672
|
if (built) {
|
|
44595
|
-
await
|
|
44673
|
+
await writeFile15(path31.join(dir, "section.html"), built.html);
|
|
44596
44674
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
|
|
44597
44675
|
if (rendered && desktop) {
|
|
44598
|
-
await
|
|
44676
|
+
await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
|
|
44599
44677
|
const result = await scoreFidelity(desktop, rendered);
|
|
44600
44678
|
fidelity = result.score;
|
|
44601
44679
|
fidelityNote = result.note;
|
|
@@ -44603,9 +44681,9 @@ async function captureOneSection(args) {
|
|
|
44603
44681
|
}
|
|
44604
44682
|
return {
|
|
44605
44683
|
...candidate,
|
|
44606
|
-
desktopShot: desktop ?
|
|
44684
|
+
desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
|
|
44607
44685
|
mobileShot: null,
|
|
44608
|
-
bundle: built ?
|
|
44686
|
+
bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
|
|
44609
44687
|
fidelity,
|
|
44610
44688
|
...fidelityNote ? { fidelityNote } : {},
|
|
44611
44689
|
...built ? { cssStats: built.stats } : {},
|
|
@@ -44624,9 +44702,9 @@ async function captureMobileShots(args) {
|
|
|
44624
44702
|
for (const section of sections) {
|
|
44625
44703
|
const shot = await captureSectionOnMobile(mobile.page, section);
|
|
44626
44704
|
if (!shot) continue;
|
|
44627
|
-
const file =
|
|
44628
|
-
await
|
|
44629
|
-
section.mobileShot =
|
|
44705
|
+
const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
|
|
44706
|
+
await writeFile15(file, shot);
|
|
44707
|
+
section.mobileShot = path31.relative(outDir, file);
|
|
44630
44708
|
}
|
|
44631
44709
|
} finally {
|
|
44632
44710
|
await mobile.context.close();
|
|
@@ -44641,10 +44719,10 @@ async function captureMotionTakes(args) {
|
|
|
44641
44719
|
const filmOne = async (section) => {
|
|
44642
44720
|
const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
|
|
44643
44721
|
if (!take) return;
|
|
44644
|
-
const dir =
|
|
44645
|
-
const file =
|
|
44646
|
-
await
|
|
44647
|
-
section.motionFilmstrip =
|
|
44722
|
+
const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
|
|
44723
|
+
const file = path31.join(dir, "motion-filmstrip.png");
|
|
44724
|
+
await writeFile15(file, take.filmstrip);
|
|
44725
|
+
section.motionFilmstrip = path31.relative(outDir, file);
|
|
44648
44726
|
log(` [${section.index}] ${section.motion.summary}`);
|
|
44649
44727
|
};
|
|
44650
44728
|
const queue = [...moving];
|
|
@@ -44701,7 +44779,7 @@ async function captureAlternateViews(args) {
|
|
|
44701
44779
|
async function reproduceWholePage(args) {
|
|
44702
44780
|
const { browser, page, outDir, pageUrl, withCode, log } = args;
|
|
44703
44781
|
const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
44704
|
-
if (fullPage) await
|
|
44782
|
+
if (fullPage) await writeFile15(path31.join(outDir, "full-page.png"), fullPage);
|
|
44705
44783
|
if (!withCode) return { bundle: null, fidelity: null };
|
|
44706
44784
|
const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
|
|
44707
44785
|
log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
|
|
@@ -44772,7 +44850,7 @@ async function openViaLadder(args) {
|
|
|
44772
44850
|
async function scrapeLanding(options) {
|
|
44773
44851
|
const timeoutMs = options.timeoutMs ?? 45e3;
|
|
44774
44852
|
const log = options.onProgress ?? (() => void 0);
|
|
44775
|
-
const sectionsDir =
|
|
44853
|
+
const sectionsDir = path31.join(options.outDir, "sections");
|
|
44776
44854
|
const nonPublic = refuseNonPublicUrl(options.url);
|
|
44777
44855
|
if (nonPublic) {
|
|
44778
44856
|
throw new BlockedPageError({
|
|
@@ -44792,7 +44870,7 @@ async function scrapeLanding(options) {
|
|
|
44792
44870
|
const withMotion = options.motion !== false && !escalated;
|
|
44793
44871
|
const renderBrowser = options.code === false ? null : await launchBrowser();
|
|
44794
44872
|
try {
|
|
44795
|
-
await
|
|
44873
|
+
await mkdir10(sectionsDir, { recursive: true });
|
|
44796
44874
|
const sections = await captureSections({
|
|
44797
44875
|
browser: renderBrowser ?? browser,
|
|
44798
44876
|
page,
|
|
@@ -44834,7 +44912,7 @@ async function scrapeLanding(options) {
|
|
|
44834
44912
|
security: prepared.security,
|
|
44835
44913
|
captureTier: tier
|
|
44836
44914
|
};
|
|
44837
|
-
await
|
|
44915
|
+
await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
44838
44916
|
`);
|
|
44839
44917
|
if (options.report !== false) {
|
|
44840
44918
|
const reportPath = await writeCaptureReport(manifest, options.outDir);
|
|
@@ -44849,28 +44927,28 @@ async function scrapeLanding(options) {
|
|
|
44849
44927
|
|
|
44850
44928
|
// src/commands/landing/inspiration/captureOut.ts
|
|
44851
44929
|
import { existsSync as existsSync9 } from "fs";
|
|
44852
|
-
import
|
|
44930
|
+
import path32 from "path";
|
|
44853
44931
|
var SCRATCH_DIR = ".baker";
|
|
44854
44932
|
function isWithin(parent, target) {
|
|
44855
|
-
const relative =
|
|
44856
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
44933
|
+
const relative = path32.relative(parent, target);
|
|
44934
|
+
return relative === "" || !relative.startsWith("..") && !path32.isAbsolute(relative);
|
|
44857
44935
|
}
|
|
44858
44936
|
function findRepoRoot(from) {
|
|
44859
|
-
let dir =
|
|
44937
|
+
let dir = path32.resolve(from);
|
|
44860
44938
|
for (; ; ) {
|
|
44861
|
-
if (existsSync9(
|
|
44862
|
-
const parent =
|
|
44939
|
+
if (existsSync9(path32.join(dir, ".git"))) return dir;
|
|
44940
|
+
const parent = path32.dirname(dir);
|
|
44863
44941
|
if (parent === dir) return null;
|
|
44864
44942
|
dir = parent;
|
|
44865
44943
|
}
|
|
44866
44944
|
}
|
|
44867
44945
|
function checkCaptureOut(out, options) {
|
|
44868
44946
|
const { cwd, repoRoot } = options;
|
|
44869
|
-
const resolved =
|
|
44947
|
+
const resolved = path32.resolve(cwd, out);
|
|
44870
44948
|
if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
|
|
44871
|
-
const scratch =
|
|
44949
|
+
const scratch = path32.join(repoRoot, SCRATCH_DIR);
|
|
44872
44950
|
if (isWithin(scratch, resolved)) return { ok: true };
|
|
44873
|
-
const suggestion =
|
|
44951
|
+
const suggestion = path32.posix.join(SCRATCH_DIR, "teardowns", path32.basename(resolved) || "capture");
|
|
44874
44952
|
return {
|
|
44875
44953
|
ok: false,
|
|
44876
44954
|
error: {
|
|
@@ -44893,29 +44971,8 @@ var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
|
|
|
44893
44971
|
// would blacklist a page that was never actually judged.
|
|
44894
44972
|
"PROXY_UNAVAILABLE"
|
|
44895
44973
|
]);
|
|
44896
|
-
async function recordCapture(manifest, outDir) {
|
|
44897
|
-
const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
44898
|
-
const references = [];
|
|
44899
|
-
for (const section of manifest.sections) {
|
|
44900
|
-
if (!section.bundle) continue;
|
|
44901
|
-
try {
|
|
44902
|
-
const markup = await readFile23(path34.join(outDir, section.bundle), "utf8");
|
|
44903
|
-
const copyStrings = capturedCopyStrings(markup);
|
|
44904
|
-
if (copyStrings.length === 0) continue;
|
|
44905
|
-
references.push({
|
|
44906
|
-
sectionId: `local:${manifest.finalUrl}#${section.index}`,
|
|
44907
|
-
sourceUrl: manifest.finalUrl,
|
|
44908
|
-
copyStrings,
|
|
44909
|
-
consultedAt
|
|
44910
|
-
});
|
|
44911
|
-
} catch {
|
|
44912
|
-
}
|
|
44913
|
-
}
|
|
44914
|
-
const written = await recordReferences(process.cwd(), references);
|
|
44915
|
-
return written ? references.length : 0;
|
|
44916
|
-
}
|
|
44917
44974
|
function captureHints(args) {
|
|
44918
|
-
const { manifest, outDir, report
|
|
44975
|
+
const { manifest, outDir, report } = args;
|
|
44919
44976
|
const shots = manifest.sections.filter((section) => section.desktopShot !== null).length;
|
|
44920
44977
|
const anyScored = manifest.sections.some((section) => section.fidelity !== null);
|
|
44921
44978
|
const hints = [];
|
|
@@ -44935,14 +44992,11 @@ function captureHints(args) {
|
|
|
44935
44992
|
);
|
|
44936
44993
|
}
|
|
44937
44994
|
hints.push(INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt);
|
|
44938
|
-
hints.push(
|
|
44939
|
-
recorded > 0 ? `${recorded} sections are recorded for the originality check \u2014 \`baker landing critique\` will block a publish that ships their copy.` : "Nothing from this capture is recorded for the originality check, so it cannot see this page. Be especially careful not to reuse its copy."
|
|
44940
|
-
);
|
|
44941
44995
|
return hints;
|
|
44942
44996
|
}
|
|
44943
44997
|
registerSchema({
|
|
44944
44998
|
command: "landing.inspiration.scrape",
|
|
44945
|
-
description: "Start here when the user points at a specific page and wants it now: capture one landing page into a directory of section screenshots, standalone HTML bundles, a whole-page reproduction and motion filmstrips. Returns when the capture is done, unlike `add`. Read the screenshots.
|
|
44999
|
+
description: "Start here when the user points at a specific page and wants it now: capture one landing page into a directory of section screenshots, standalone HTML bundles, a whole-page reproduction and motion filmstrips. Returns when the capture is done, unlike `add`. Read the screenshots.",
|
|
44946
45000
|
args: {
|
|
44947
45001
|
url: { type: "string", description: "Page to capture", required: true },
|
|
44948
45002
|
out: { type: "string", description: "Output directory under .baker/", required: true },
|
|
@@ -45010,7 +45064,6 @@ var scrapeCommand = defineCommand161({
|
|
|
45010
45064
|
`)
|
|
45011
45065
|
});
|
|
45012
45066
|
const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
|
|
45013
|
-
const recorded = await recordCapture(manifest, args.out);
|
|
45014
45067
|
writeJson({
|
|
45015
45068
|
ok: true,
|
|
45016
45069
|
data: {
|
|
@@ -45031,7 +45084,7 @@ var scrapeCommand = defineCommand161({
|
|
|
45031
45084
|
certificate_verified: false,
|
|
45032
45085
|
...manifest.security.certificateNotes.length > 0 ? { certificate_notes: manifest.security.certificateNotes } : {}
|
|
45033
45086
|
},
|
|
45034
|
-
hints: captureHints({ manifest, outDir: args.out, report: args.report
|
|
45087
|
+
hints: captureHints({ manifest, outDir: args.out, report: args.report })
|
|
45035
45088
|
});
|
|
45036
45089
|
} catch (error) {
|
|
45037
45090
|
const failure = classifyCaptureFailure(error);
|
|
@@ -45069,12 +45122,12 @@ var scrapeCommand = defineCommand161({
|
|
|
45069
45122
|
});
|
|
45070
45123
|
|
|
45071
45124
|
// src/commands/landing/inspiration/search.ts
|
|
45072
|
-
import
|
|
45125
|
+
import path34 from "path";
|
|
45073
45126
|
import { defineCommand as defineCommand162 } from "citty";
|
|
45074
45127
|
|
|
45075
45128
|
// src/commands/landing/inspiration/shot.ts
|
|
45076
|
-
import { mkdir as
|
|
45077
|
-
import
|
|
45129
|
+
import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
|
|
45130
|
+
import path33 from "path";
|
|
45078
45131
|
import sharp6 from "sharp";
|
|
45079
45132
|
var READABLE_SHOT = {
|
|
45080
45133
|
maxWidth: 1440,
|
|
@@ -45100,9 +45153,9 @@ async function downloadReadableShot(url, file) {
|
|
|
45100
45153
|
const response = await fetch(url);
|
|
45101
45154
|
if (!response.ok) return null;
|
|
45102
45155
|
const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
|
|
45103
|
-
await
|
|
45104
|
-
await
|
|
45105
|
-
return
|
|
45156
|
+
await mkdir11(path33.dirname(file), { recursive: true });
|
|
45157
|
+
await writeFile16(file, shot);
|
|
45158
|
+
return path33.relative(process.cwd(), file);
|
|
45106
45159
|
} catch {
|
|
45107
45160
|
return null;
|
|
45108
45161
|
}
|
|
@@ -45194,13 +45247,13 @@ function buildSearchBody(args) {
|
|
|
45194
45247
|
return body;
|
|
45195
45248
|
}
|
|
45196
45249
|
async function downloadShots(results) {
|
|
45197
|
-
const dir =
|
|
45250
|
+
const dir = path34.join(process.cwd(), ".baker", "inspiration");
|
|
45198
45251
|
const saved = /* @__PURE__ */ new Map();
|
|
45199
45252
|
await Promise.all(
|
|
45200
45253
|
results.map(async (result) => {
|
|
45201
45254
|
const file = await downloadReadableShot(
|
|
45202
45255
|
result.desktopShotUrl,
|
|
45203
|
-
|
|
45256
|
+
path34.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
|
|
45204
45257
|
);
|
|
45205
45258
|
if (file) saved.set(result.id, file);
|
|
45206
45259
|
})
|
|
@@ -45428,7 +45481,7 @@ var sequencesCommand = defineCommand163({
|
|
|
45428
45481
|
});
|
|
45429
45482
|
|
|
45430
45483
|
// src/commands/landing/inspiration/view.ts
|
|
45431
|
-
import
|
|
45484
|
+
import path35 from "path";
|
|
45432
45485
|
import { defineCommand as defineCommand164 } from "citty";
|
|
45433
45486
|
registerSchema({
|
|
45434
45487
|
command: "landing.inspiration.view",
|
|
@@ -45461,12 +45514,12 @@ var viewCommand2 = defineCommand164({
|
|
|
45461
45514
|
const id = args.id;
|
|
45462
45515
|
const data = await apiGet("/api/landing-inspiration/section", { id });
|
|
45463
45516
|
const section = data.section;
|
|
45464
|
-
const dir =
|
|
45517
|
+
const dir = path35.join(process.cwd(), ".baker", "inspiration", id);
|
|
45465
45518
|
const ext = READABLE_SHOT.extension;
|
|
45466
45519
|
const [desktop, mobile, filmstrip] = await Promise.all([
|
|
45467
|
-
downloadReadableShot(section.desktopShotUrl,
|
|
45468
|
-
downloadReadableShot(section.mobileShotUrl,
|
|
45469
|
-
downloadReadableShot(section.motionFilmstripUrl,
|
|
45520
|
+
downloadReadableShot(section.desktopShotUrl, path35.join(dir, `desktop.${ext}`)),
|
|
45521
|
+
downloadReadableShot(section.mobileShotUrl, path35.join(dir, `mobile.${ext}`)),
|
|
45522
|
+
downloadReadableShot(section.motionFilmstripUrl, path35.join(dir, `motion-filmstrip.${ext}`))
|
|
45470
45523
|
]);
|
|
45471
45524
|
const full = args.full;
|
|
45472
45525
|
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
@@ -45533,7 +45586,7 @@ var inspirationCommand = defineCommand165({
|
|
|
45533
45586
|
|
|
45534
45587
|
Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
|
|
45535
45588
|
|
|
45536
|
-
This is inspiration, never a clipboard. Take the mechanism \u2014 what the eye hits first, what proof arrives before the ask, how the grid is split. The words are never yours to reuse: shipping a reference's headline is a Tier 0 message-match failure
|
|
45589
|
+
This is inspiration, never a clipboard. Take the mechanism \u2014 what the eye hits first, what proof arrives before the ask, how the grid is split. The words are never yours to reuse: shipping a reference's headline is a Tier 0 message-match failure.
|
|
45537
45590
|
|
|
45538
45591
|
Subcommands:
|
|
45539
45592
|
baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
|
|
@@ -47315,8 +47368,8 @@ var listCommand15 = defineCommand183({
|
|
|
47315
47368
|
});
|
|
47316
47369
|
|
|
47317
47370
|
// src/commands/scheduled-actions/templates.ts
|
|
47318
|
-
import { readFile as
|
|
47319
|
-
import
|
|
47371
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
47372
|
+
import path36 from "path";
|
|
47320
47373
|
import { defineCommand as defineCommand184 } from "citty";
|
|
47321
47374
|
registerSchema({
|
|
47322
47375
|
command: "scheduled-actions.templates",
|
|
@@ -47419,7 +47472,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
47419
47472
|
}
|
|
47420
47473
|
if (save.length > 0) {
|
|
47421
47474
|
const briefFile = flag("brief-file");
|
|
47422
|
-
const brief = briefFile.length > 0 ? await
|
|
47475
|
+
const brief = briefFile.length > 0 ? await readFile22(path36.resolve(briefFile), "utf8") : flag("brief");
|
|
47423
47476
|
if (brief.trim().length === 0) {
|
|
47424
47477
|
failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
|
|
47425
47478
|
}
|
|
@@ -47890,7 +47943,7 @@ function parseImageRefs(spec) {
|
|
|
47890
47943
|
}
|
|
47891
47944
|
var defaultDeps = {
|
|
47892
47945
|
ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
|
|
47893
|
-
upload: (
|
|
47946
|
+
upload: (path38) => uploadLocalImage({ file: path38, contentType: detectImageContentType(path38), source: "uploaded" })
|
|
47894
47947
|
};
|
|
47895
47948
|
async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
47896
47949
|
const refs = parseImageRefs(spec);
|
|
@@ -47910,10 +47963,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
|
47910
47963
|
}
|
|
47911
47964
|
return { imageIds, added };
|
|
47912
47965
|
}
|
|
47913
|
-
function uploadFailure(
|
|
47966
|
+
function uploadFailure(path38) {
|
|
47914
47967
|
return (error) => {
|
|
47915
47968
|
if (error instanceof ApiError) throw error;
|
|
47916
|
-
throw new ApiError("VALIDATION_ERROR", `Could not read "${
|
|
47969
|
+
throw new ApiError("VALIDATION_ERROR", `Could not read "${path38}" as an image.`);
|
|
47917
47970
|
};
|
|
47918
47971
|
}
|
|
47919
47972
|
|
|
@@ -48975,10 +49028,10 @@ async function stageOp4(op) {
|
|
|
48975
49028
|
handleError5(err);
|
|
48976
49029
|
}
|
|
48977
49030
|
}
|
|
48978
|
-
async function draftAction3(
|
|
49031
|
+
async function draftAction3(path38, body, chat) {
|
|
48979
49032
|
const chatId = resolveChatId(chat);
|
|
48980
49033
|
try {
|
|
48981
|
-
const data = await apiPost(
|
|
49034
|
+
const data = await apiPost(path38, { chatId, ...body });
|
|
48982
49035
|
writeJsonEnvelope({ ok: true, data });
|
|
48983
49036
|
return data;
|
|
48984
49037
|
} catch (err) {
|
|
@@ -50085,7 +50138,7 @@ var groupCommand2 = defineCommand209({
|
|
|
50085
50138
|
// src/commands/videos/ingest.ts
|
|
50086
50139
|
import { mkdtemp as mkdtemp2, rm as rm7, stat as stat7 } from "fs/promises";
|
|
50087
50140
|
import { tmpdir as tmpdir3 } from "os";
|
|
50088
|
-
import
|
|
50141
|
+
import path37 from "path";
|
|
50089
50142
|
import { defineCommand as defineCommand210 } from "citty";
|
|
50090
50143
|
|
|
50091
50144
|
// src/lib/streamUpload.ts
|
|
@@ -50436,7 +50489,7 @@ function ingestUrl(args) {
|
|
|
50436
50489
|
}
|
|
50437
50490
|
async function downloadThenIngest(args, country) {
|
|
50438
50491
|
const vimeoCookie = captureVimeoCookie();
|
|
50439
|
-
const workDir = await mkdtemp2(
|
|
50492
|
+
const workDir = await mkdtemp2(path37.join(tmpdir3(), "videos-ingest-"));
|
|
50440
50493
|
try {
|
|
50441
50494
|
const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
|
|
50442
50495
|
if (isAudioOnly(probe.info)) {
|
|
@@ -50572,7 +50625,7 @@ var searchCommand4 = defineCommand211({
|
|
|
50572
50625
|
var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
50573
50626
|
|
|
50574
50627
|
// src/commands/videos/upload.ts
|
|
50575
|
-
import { readFile as
|
|
50628
|
+
import { readFile as readFile23, stat as stat8 } from "fs/promises";
|
|
50576
50629
|
import { basename as basename3, extname as extname4 } from "path";
|
|
50577
50630
|
import { defineCommand as defineCommand212 } from "citty";
|
|
50578
50631
|
var MIME_MAP = {
|
|
@@ -50669,7 +50722,7 @@ var uploadCommand2 = defineCommand212({
|
|
|
50669
50722
|
originalFilename,
|
|
50670
50723
|
descriptionContext
|
|
50671
50724
|
});
|
|
50672
|
-
const fileBuffer = await
|
|
50725
|
+
const fileBuffer = await readFile23(filePath);
|
|
50673
50726
|
const uploadResponse = await fetch(uploadUrl, {
|
|
50674
50727
|
method: "PUT",
|
|
50675
50728
|
headers: { "Content-Type": contentType },
|
|
@@ -52056,7 +52109,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
|
|
|
52056
52109
|
};
|
|
52057
52110
|
}
|
|
52058
52111
|
function commandPathOf(root, argv) {
|
|
52059
|
-
const
|
|
52112
|
+
const path38 = [];
|
|
52060
52113
|
let command = root;
|
|
52061
52114
|
for (const token of argv) {
|
|
52062
52115
|
if (token === "--" || token.startsWith("-")) {
|
|
@@ -52067,10 +52120,10 @@ function commandPathOf(root, argv) {
|
|
|
52067
52120
|
if (next === void 0 || typeof next !== "object") {
|
|
52068
52121
|
break;
|
|
52069
52122
|
}
|
|
52070
|
-
|
|
52123
|
+
path38.push(token);
|
|
52071
52124
|
command = next;
|
|
52072
52125
|
}
|
|
52073
|
-
return
|
|
52126
|
+
return path38.join(" ");
|
|
52074
52127
|
}
|
|
52075
52128
|
function refuseUnknownFlags(root, argv) {
|
|
52076
52129
|
const unknown = findUnknownFlags(root, argv);
|