@koda-sl/baker-cli 0.188.0 → 0.190.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 +38 -1
- package/dist/cli.js +3083 -289
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
} from "./chunk-YL3HDEIJ.js";
|
|
91
91
|
|
|
92
92
|
// src/cli.ts
|
|
93
|
-
import { defineCommand as
|
|
93
|
+
import { defineCommand as defineCommand209, runMain } from "citty";
|
|
94
94
|
|
|
95
95
|
// src/commands/actions/index.ts
|
|
96
96
|
import { defineCommand as defineCommand18 } from "citty";
|
|
@@ -2635,6 +2635,8 @@ var chatChangeTypeSchema = z7.enum([
|
|
|
2635
2635
|
"image",
|
|
2636
2636
|
"video",
|
|
2637
2637
|
"followed-advertiser",
|
|
2638
|
+
// A landing page saved to the reference library, keyed on the page.
|
|
2639
|
+
"landing-reference",
|
|
2638
2640
|
// One Studio batch — the takes of one brief, as a set. See convex/lib/validators.ts.
|
|
2639
2641
|
"studio-generation",
|
|
2640
2642
|
"briefs"
|
|
@@ -3420,8 +3422,7 @@ var imageHitSchema = z12.object({
|
|
|
3420
3422
|
tags: z12.array(z12.string()).optional(),
|
|
3421
3423
|
score: z12.number().optional(),
|
|
3422
3424
|
alsoInLibrary: z12.string().optional(),
|
|
3423
|
-
prefetchedBytes: z12.string().optional()
|
|
3424
|
-
prefetchedContentType: z12.string().optional()
|
|
3425
|
+
prefetchedBytes: z12.string().optional()
|
|
3425
3426
|
});
|
|
3426
3427
|
var ingestedImageHitSchema = imageHitSchema.extend({
|
|
3427
3428
|
imageId: z12.string().optional(),
|
|
@@ -5099,6 +5100,20 @@ var FIELD_DESCRIPTIONS = {
|
|
|
5099
5100
|
"ad_group_criterion.keyword.match_type": "EXACT, PHRASE, or BROAD",
|
|
5100
5101
|
"ad_group_criterion.status": "ENABLED, PAUSED, or REMOVED",
|
|
5101
5102
|
"ad_group_criterion.quality_info.quality_score": "Quality score 1-10 (higher is better)",
|
|
5103
|
+
"ad_group_criterion.quality_info.creative_quality_score": "Ad relevance bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
|
|
5104
|
+
"ad_group_criterion.quality_info.post_click_quality_score": "Landing page experience bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
|
|
5105
|
+
"ad_group_criterion.quality_info.search_predicted_ctr": "Expected click-through rate bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
|
|
5106
|
+
"ad_group_criterion.primary_status": "Whether the keyword can serve: ELIGIBLE, PAUSED, REMOVED, PENDING, NOT_ELIGIBLE. There is no LIMITED value \u2014 Google Ads' 'Eligible (Limited)' is ELIGIBLE plus a limiting entry in primary_status_reasons, so read both.",
|
|
5107
|
+
"ad_group_criterion.primary_status_reasons": "Why the keyword is in that status (array). AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID and AD_GROUP_CRITERION_LOW_QUALITY are the two that render as '(Limited)'; the CAMPAIGN_*/AD_GROUP_* entries just restate a paused or removed parent.",
|
|
5108
|
+
"ad_group_criterion.system_serving_status": "ELIGIBLE or RARELY_SERVED (too few searches to show)",
|
|
5109
|
+
"ad_group_criterion.approval_status": "Ad-approval state: APPROVED, DISAPPROVED, PENDING_REVIEW, UNDER_REVIEW",
|
|
5110
|
+
"ad_group_criterion.disapproval_reasons": "Policy reasons the keyword was disapproved (array)",
|
|
5111
|
+
"ad_group_criterion.cpc_bid_micros": "Max CPC bid set on this keyword, in micros (\xF7 1,000,000)",
|
|
5112
|
+
"ad_group_criterion.effective_cpc_bid_micros": "Max CPC actually in force, in micros (\xF7 1,000,000) \u2014 the keyword's own bid, or the ad group's when the keyword has none",
|
|
5113
|
+
"ad_group_criterion.effective_cpc_bid_source": "Where the effective max CPC comes from: ADGROUP, CRITERION, or CAMPAIGN_BIDDING_STRATEGY",
|
|
5114
|
+
"ad_group_criterion.position_estimates.first_page_cpc_micros": "Bid estimated to reach the first page of results, in micros (\xF7 1,000,000). The number in Google Ads' 'Below first page bid (\u20AC3.63)' label \u2014 compare it against effective_cpc_bid_micros.",
|
|
5115
|
+
"ad_group_criterion.position_estimates.top_of_page_cpc_micros": "Bid estimated to reach the top of the first page, in micros (\xF7 1,000,000)",
|
|
5116
|
+
"ad_group_criterion.position_estimates.first_position_cpc_micros": "Bid estimated to reach the first ad position, in micros (\xF7 1,000,000)",
|
|
5102
5117
|
// Search terms
|
|
5103
5118
|
"search_term_view.search_term": "Actual user search query that triggered the ad",
|
|
5104
5119
|
"search_term_view.status": "Whether the search term is added, excluded, or none",
|
|
@@ -7758,19 +7773,19 @@ Examples:
|
|
|
7758
7773
|
},
|
|
7759
7774
|
run: async ({ args }) => {
|
|
7760
7775
|
const customerId = await resolveCustomerId(args);
|
|
7761
|
-
const
|
|
7776
|
+
const window2 = resolveChangesWindow({
|
|
7762
7777
|
days: args.days ? Number(args.days) : void 0,
|
|
7763
7778
|
scope: args.scope
|
|
7764
7779
|
});
|
|
7765
|
-
if (!
|
|
7766
|
-
writeJsonEnvelope({ ok: false, error: { ...
|
|
7780
|
+
if (!window2.ok) {
|
|
7781
|
+
writeJsonEnvelope({ ok: false, error: { ...window2.error, retryable: false } });
|
|
7767
7782
|
process.exit(1);
|
|
7768
7783
|
return;
|
|
7769
7784
|
}
|
|
7770
7785
|
const body = {
|
|
7771
7786
|
customerId,
|
|
7772
|
-
days:
|
|
7773
|
-
scope:
|
|
7787
|
+
days: window2.days,
|
|
7788
|
+
scope: window2.scope,
|
|
7774
7789
|
limit: args.limit ? Number(args.limit) : 50
|
|
7775
7790
|
};
|
|
7776
7791
|
const managerId = getManagerIdForCustomer(customerId);
|
|
@@ -7787,7 +7802,7 @@ Examples:
|
|
|
7787
7802
|
const first = data[0];
|
|
7788
7803
|
const fields = first ? Object.keys(first) : [];
|
|
7789
7804
|
const fieldDescs = getFieldDescriptions(fields);
|
|
7790
|
-
writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints:
|
|
7805
|
+
writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window2.hints });
|
|
7791
7806
|
} catch (err) {
|
|
7792
7807
|
if (err instanceof ApiError) {
|
|
7793
7808
|
writeAdsJson(parseApiError(err.message, "", customerId, err.code));
|
|
@@ -8144,8 +8159,8 @@ function countStaccato(text) {
|
|
|
8144
8159
|
let first = "";
|
|
8145
8160
|
let runStart = 0;
|
|
8146
8161
|
for (const [i, s] of sentences.entries()) {
|
|
8147
|
-
const
|
|
8148
|
-
if (
|
|
8162
|
+
const words3 = s.split(/\s+/).filter(Boolean).length;
|
|
8163
|
+
if (words3 > 0 && words3 <= STACCATO_MAX_WORDS) {
|
|
8149
8164
|
if (run === 0) runStart = i;
|
|
8150
8165
|
run++;
|
|
8151
8166
|
if (run === STACCATO_RUN) {
|
|
@@ -8163,10 +8178,10 @@ function countTitleCaseHeadings(text) {
|
|
|
8163
8178
|
let first = "";
|
|
8164
8179
|
for (const m of text.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
|
|
8165
8180
|
const heading = (m[1] ?? "").trim();
|
|
8166
|
-
const
|
|
8167
|
-
if (
|
|
8168
|
-
const capitalized =
|
|
8169
|
-
if (capitalized /
|
|
8181
|
+
const words3 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
|
|
8182
|
+
if (words3.length < 4) continue;
|
|
8183
|
+
const capitalized = words3.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
|
|
8184
|
+
if (capitalized / words3.length < 0.8) continue;
|
|
8170
8185
|
count++;
|
|
8171
8186
|
if (!first) first = heading;
|
|
8172
8187
|
}
|
|
@@ -8498,11 +8513,11 @@ function rawTextEntries(value) {
|
|
|
8498
8513
|
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
8499
8514
|
return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
8500
8515
|
}
|
|
8501
|
-
function rawFileEntries(
|
|
8502
|
-
if (typeof
|
|
8516
|
+
function rawFileEntries(path35) {
|
|
8517
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
8503
8518
|
return [];
|
|
8504
8519
|
}
|
|
8505
|
-
return readFileSync2(
|
|
8520
|
+
return readFileSync2(path35, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
8506
8521
|
}
|
|
8507
8522
|
function keywordEntries(args) {
|
|
8508
8523
|
const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
|
|
@@ -8525,19 +8540,19 @@ function keywordEntries(args) {
|
|
|
8525
8540
|
}
|
|
8526
8541
|
return entries;
|
|
8527
8542
|
}
|
|
8528
|
-
function loadJsonFileArg(
|
|
8529
|
-
if (typeof
|
|
8543
|
+
function loadJsonFileArg(path35) {
|
|
8544
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
8530
8545
|
return {};
|
|
8531
8546
|
}
|
|
8532
8547
|
try {
|
|
8533
|
-
const parsed = JSON.parse(readFileSync2(
|
|
8548
|
+
const parsed = JSON.parse(readFileSync2(path35, "utf8"));
|
|
8534
8549
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8535
|
-
failWriteValidation(`${
|
|
8550
|
+
failWriteValidation(`${path35} must contain a JSON object`);
|
|
8536
8551
|
}
|
|
8537
8552
|
return parsed;
|
|
8538
8553
|
} catch (err) {
|
|
8539
8554
|
if (err instanceof SyntaxError) {
|
|
8540
|
-
failWriteValidation(`${
|
|
8555
|
+
failWriteValidation(`${path35} is not valid JSON: ${err.message}`);
|
|
8541
8556
|
}
|
|
8542
8557
|
throw err;
|
|
8543
8558
|
}
|
|
@@ -8667,10 +8682,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
|
|
|
8667
8682
|
async function stageTarget(kind, customerId, target, hints) {
|
|
8668
8683
|
await stageGoogleOp({ kind, customerId, target }, hints);
|
|
8669
8684
|
}
|
|
8670
|
-
async function draftAction(
|
|
8685
|
+
async function draftAction(path35, body, chat) {
|
|
8671
8686
|
try {
|
|
8672
8687
|
const chatId = resolveChatId(chat);
|
|
8673
|
-
const response = await apiPost(
|
|
8688
|
+
const response = await apiPost(path35, { chatId, ...body });
|
|
8674
8689
|
writeJsonEnvelope(response);
|
|
8675
8690
|
} catch (err) {
|
|
8676
8691
|
handleGoogleError(err);
|
|
@@ -9229,6 +9244,121 @@ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync
|
|
|
9229
9244
|
import { resolve } from "path";
|
|
9230
9245
|
import { defineCommand as defineCommand28 } from "citty";
|
|
9231
9246
|
|
|
9247
|
+
// src/commands/ads/google/keyword-serving.ts
|
|
9248
|
+
var BELOW_FIRST_PAGE_BID = "AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID";
|
|
9249
|
+
var ACTIONABLE_REASONS = {
|
|
9250
|
+
[BELOW_FIRST_PAGE_BID]: {
|
|
9251
|
+
label: "Below first page bid",
|
|
9252
|
+
advice: "raise the max CPC (keywords update --cpc-bid) to at least the first-page estimate, or move the ad group onto a smart bidding strategy that sets the bid itself"
|
|
9253
|
+
},
|
|
9254
|
+
AD_GROUP_CRITERION_LOW_QUALITY: {
|
|
9255
|
+
label: "Low quality score",
|
|
9256
|
+
advice: "a bid raise does not lift quality \u2014 tighten ad relevance to the keyword and the landing page it points at (ad_group_criterion.quality_info.* breaks the score into its three parts)"
|
|
9257
|
+
},
|
|
9258
|
+
AD_GROUP_CRITERION_RARELY_SERVED: {
|
|
9259
|
+
label: "Rarely served",
|
|
9260
|
+
advice: "too few people search this term \u2014 broaden the match type or drop it, don't bid it up"
|
|
9261
|
+
},
|
|
9262
|
+
AD_GROUP_CRITERION_DISAPPROVED: {
|
|
9263
|
+
label: "Disapproved",
|
|
9264
|
+
advice: "select ad_group_criterion.disapproval_reasons to see which policy blocked it"
|
|
9265
|
+
},
|
|
9266
|
+
AD_GROUP_CRITERION_RESTRICTED: {
|
|
9267
|
+
label: "Restricted",
|
|
9268
|
+
advice: "policy limits where and to whom this keyword may show"
|
|
9269
|
+
},
|
|
9270
|
+
AD_GROUP_CRITERION_UNDER_REVIEW: {
|
|
9271
|
+
label: "Under review",
|
|
9272
|
+
advice: "waiting on Google's review \u2014 nothing to change yet"
|
|
9273
|
+
},
|
|
9274
|
+
AD_GROUP_CRITERION_PENDING_REVIEW: {
|
|
9275
|
+
label: "Pending review",
|
|
9276
|
+
advice: "waiting on Google's review \u2014 nothing to change yet"
|
|
9277
|
+
},
|
|
9278
|
+
AD_GROUP_CRITERION_PAUSED_DUE_TO_LOW_ACTIVITY: {
|
|
9279
|
+
label: "Auto-paused for low activity",
|
|
9280
|
+
advice: "Google paused it after prolonged low serving \u2014 resume it only alongside a change that gives it traffic"
|
|
9281
|
+
}
|
|
9282
|
+
};
|
|
9283
|
+
var REASON_ORDER = Object.keys(ACTIONABLE_REASONS);
|
|
9284
|
+
var MAX_NAMED_KEYWORDS = 5;
|
|
9285
|
+
function toMicros2(value) {
|
|
9286
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9287
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
|
|
9288
|
+
return null;
|
|
9289
|
+
}
|
|
9290
|
+
function formatMoney(micros) {
|
|
9291
|
+
return (micros / 1e6).toFixed(2);
|
|
9292
|
+
}
|
|
9293
|
+
function keywordLabel(row) {
|
|
9294
|
+
const text = row["ad_group_criterion.keyword.text"];
|
|
9295
|
+
const id = row["ad_group_criterion.criterion_id"];
|
|
9296
|
+
const name = typeof text === "string" && text !== "" ? text : id == null ? "(unnamed keyword)" : `criterion ${id}`;
|
|
9297
|
+
const adGroup = row["ad_group.name"];
|
|
9298
|
+
return typeof adGroup === "string" && adGroup !== "" ? `${name} (ad group ${adGroup})` : name;
|
|
9299
|
+
}
|
|
9300
|
+
function keywordKey(row) {
|
|
9301
|
+
const adGroup = row["ad_group.id"] ?? row["ad_group.name"] ?? "";
|
|
9302
|
+
const criterion = row["ad_group_criterion.criterion_id"] ?? row["ad_group_criterion.keyword.text"] ?? "";
|
|
9303
|
+
return `${String(adGroup)}~${String(criterion)}` === "~" ? keywordLabel(row) : `${adGroup}~${criterion}`;
|
|
9304
|
+
}
|
|
9305
|
+
function reasonsOn(row) {
|
|
9306
|
+
const raw = row["ad_group_criterion.primary_status_reasons"];
|
|
9307
|
+
if (!Array.isArray(raw)) return null;
|
|
9308
|
+
return raw.filter((r) => typeof r === "string");
|
|
9309
|
+
}
|
|
9310
|
+
function bidShortfall(row) {
|
|
9311
|
+
const bid = toMicros2(row["ad_group_criterion.effective_cpc_bid_micros"] ?? row["ad_group_criterion.cpc_bid_micros"]);
|
|
9312
|
+
const firstPage = toMicros2(row["ad_group_criterion.position_estimates.first_page_cpc_micros"]);
|
|
9313
|
+
if (bid == null || firstPage == null || bid >= firstPage) return null;
|
|
9314
|
+
return `bids ${formatMoney(bid)} against a ${formatMoney(firstPage)} first-page estimate`;
|
|
9315
|
+
}
|
|
9316
|
+
function limitsOn(row, options) {
|
|
9317
|
+
const reasons = reasonsOn(row);
|
|
9318
|
+
const shortfall = bidShortfall(row);
|
|
9319
|
+
const label = keywordLabel(row);
|
|
9320
|
+
const applicable = reasons ? reasons.filter((reason) => reason in ACTIONABLE_REASONS) : options.statusReasonsSelected || shortfall === null ? [] : [BELOW_FIRST_PAGE_BID];
|
|
9321
|
+
return applicable.map((reason) => ({
|
|
9322
|
+
reason,
|
|
9323
|
+
detail: reason === BELOW_FIRST_PAGE_BID && shortfall ? `"${label}" ${shortfall}` : `"${label}"`
|
|
9324
|
+
}));
|
|
9325
|
+
}
|
|
9326
|
+
function describeLimit(reason, details) {
|
|
9327
|
+
const named = details.slice(0, MAX_NAMED_KEYWORDS).join("; ");
|
|
9328
|
+
const rest = details.length > MAX_NAMED_KEYWORDS ? ` (+${details.length - MAX_NAMED_KEYWORDS} more)` : "";
|
|
9329
|
+
const { label, advice } = ACTIONABLE_REASONS[reason];
|
|
9330
|
+
return `${label} \u2014 ${details.length}: ${named}${rest}. Fix: ${advice}.`;
|
|
9331
|
+
}
|
|
9332
|
+
function collectKeywordLimits(rows, options, acc = { detailsByReason: /* @__PURE__ */ new Map(), keywords: /* @__PURE__ */ new Set() }) {
|
|
9333
|
+
for (const row of rows) {
|
|
9334
|
+
const limits = limitsOn(row, options);
|
|
9335
|
+
if (limits.length === 0) continue;
|
|
9336
|
+
acc.keywords.add(keywordKey(row));
|
|
9337
|
+
for (const { reason, detail } of limits) {
|
|
9338
|
+
const bucket = acc.detailsByReason.get(reason);
|
|
9339
|
+
if (bucket) bucket.push(detail);
|
|
9340
|
+
else acc.detailsByReason.set(reason, [detail]);
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9343
|
+
return acc;
|
|
9344
|
+
}
|
|
9345
|
+
function renderKeywordLimits(limits) {
|
|
9346
|
+
if (limits.detailsByReason.size === 0) return [];
|
|
9347
|
+
const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
|
|
9348
|
+
(reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
|
|
9349
|
+
);
|
|
9350
|
+
const count = limits.keywords.size;
|
|
9351
|
+
return [
|
|
9352
|
+
{
|
|
9353
|
+
code: "KEYWORD_SERVING_LIMITED",
|
|
9354
|
+
message: `${count} ${count === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
|
|
9355
|
+
}
|
|
9356
|
+
];
|
|
9357
|
+
}
|
|
9358
|
+
function keywordServingWarnings(rows, options) {
|
|
9359
|
+
return renderKeywordLimits(collectKeywordLimits(rows, options));
|
|
9360
|
+
}
|
|
9361
|
+
|
|
9232
9362
|
// src/commands/ads/google/preflight.ts
|
|
9233
9363
|
function buildCommand2(query, customerId) {
|
|
9234
9364
|
return `baker ads google query "${query}" --customer-id ${customerId}`;
|
|
@@ -9418,11 +9548,19 @@ var PRESETS = [
|
|
|
9418
9548
|
{
|
|
9419
9549
|
name: "keyword-analysis",
|
|
9420
9550
|
description: "Keyword performance with match type and quality",
|
|
9421
|
-
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, 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}`,
|
|
9551
|
+
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}`,
|
|
9422
9552
|
defaultDateRange: "LAST_30_DAYS",
|
|
9423
9553
|
defaultLimit: 200,
|
|
9424
9554
|
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
9425
9555
|
},
|
|
9556
|
+
{
|
|
9557
|
+
name: "keyword-serving",
|
|
9558
|
+
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",
|
|
9559
|
+
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}`,
|
|
9560
|
+
defaultDateRange: "ALL_TIME",
|
|
9561
|
+
defaultLimit: 500,
|
|
9562
|
+
statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
|
|
9563
|
+
},
|
|
9426
9564
|
{
|
|
9427
9565
|
name: "positive-keywords",
|
|
9428
9566
|
description: "Positive (targeting) keywords only \u2014 excludes negatives",
|
|
@@ -9527,7 +9665,7 @@ registerSchema({
|
|
|
9527
9665
|
},
|
|
9528
9666
|
preset: {
|
|
9529
9667
|
type: "string",
|
|
9530
|
-
description: "Named query preset. Reads (campaign-performance, keyword-analysis, positive-keywords, negative-keywords, negative-keyword-lists, negative-list-attachments, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary). Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default \u2014 pass --include-paused to include paused ones.",
|
|
9668
|
+
description: "Named query preset. Reads (campaign-performance, keyword-analysis, keyword-serving, positive-keywords, negative-keywords, negative-keyword-lists, negative-list-attachments, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary). Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default \u2014 pass --include-paused to include paused ones.",
|
|
9531
9669
|
required: false
|
|
9532
9670
|
},
|
|
9533
9671
|
"date-range": {
|
|
@@ -9759,10 +9897,14 @@ function missingFieldWarnings(gaql, rows) {
|
|
|
9759
9897
|
}
|
|
9760
9898
|
];
|
|
9761
9899
|
}
|
|
9762
|
-
|
|
9900
|
+
function keywordServingOptions(gaql) {
|
|
9901
|
+
return { statusReasonsSelected: parseSelectedFields(gaql).includes("ad_group_criterion.primary_status_reasons") };
|
|
9902
|
+
}
|
|
9903
|
+
async function fetchAllPages(body, args, servingOptions) {
|
|
9763
9904
|
let allRows = [];
|
|
9764
9905
|
let pageToken = args.cursor;
|
|
9765
9906
|
let totalRowsWritten = 0;
|
|
9907
|
+
let keywordLimits;
|
|
9766
9908
|
do {
|
|
9767
9909
|
const requestBody = { ...body };
|
|
9768
9910
|
if (pageToken) requestBody.pageToken = pageToken;
|
|
@@ -9774,12 +9916,18 @@ async function fetchAllPages(body, args) {
|
|
|
9774
9916
|
const shouldAppend = args.append || totalRowsWritten > 0;
|
|
9775
9917
|
writeRowsToFile(filePath, rows, extractFields(rows), shouldAppend);
|
|
9776
9918
|
totalRowsWritten += rows.length;
|
|
9919
|
+
keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
|
|
9777
9920
|
} else {
|
|
9778
9921
|
allRows = allRows.concat(rows);
|
|
9779
9922
|
}
|
|
9780
9923
|
if (!args.all) break;
|
|
9781
9924
|
} while (pageToken);
|
|
9782
|
-
return {
|
|
9925
|
+
return {
|
|
9926
|
+
allRows,
|
|
9927
|
+
lastPageToken: pageToken,
|
|
9928
|
+
totalRowsWritten,
|
|
9929
|
+
keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions)
|
|
9930
|
+
};
|
|
9783
9931
|
}
|
|
9784
9932
|
async function executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey) {
|
|
9785
9933
|
const body = { customerId, query: finalQuery, pageSize: limit };
|
|
@@ -9787,9 +9935,12 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
|
|
|
9787
9935
|
if (managerId) body.managerId = managerId;
|
|
9788
9936
|
if (args.cursor) body.pageToken = args.cursor;
|
|
9789
9937
|
if (!useCache) body.skipCache = true;
|
|
9790
|
-
const
|
|
9938
|
+
const servingOptions = keywordServingOptions(finalQuery);
|
|
9939
|
+
const { allRows, lastPageToken, totalRowsWritten, keywordLimits } = await fetchAllPages(body, args, servingOptions);
|
|
9791
9940
|
if (args.out) {
|
|
9792
9941
|
const summary = { ok: true, fields: {}, file: resolve(args.out), rows: totalRowsWritten };
|
|
9942
|
+
const limitWarnings = renderKeywordLimits(keywordLimits);
|
|
9943
|
+
if (limitWarnings.length > 0) summary.warnings = limitWarnings;
|
|
9793
9944
|
writeAdsJson(summary);
|
|
9794
9945
|
return;
|
|
9795
9946
|
}
|
|
@@ -9801,7 +9952,7 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
|
|
|
9801
9952
|
outputResults(
|
|
9802
9953
|
allRows,
|
|
9803
9954
|
getFieldDescriptions(fields),
|
|
9804
|
-
[...warnings, ...missingFieldWarnings(finalQuery, allRows)],
|
|
9955
|
+
[...warnings, ...missingFieldWarnings(finalQuery, allRows), ...keywordServingWarnings(allRows, servingOptions)],
|
|
9805
9956
|
args,
|
|
9806
9957
|
false,
|
|
9807
9958
|
lastPageToken
|
|
@@ -9903,7 +10054,11 @@ Examples:
|
|
|
9903
10054
|
outputResults(
|
|
9904
10055
|
cached.data,
|
|
9905
10056
|
cached.fields ?? getFieldDescriptions(fields),
|
|
9906
|
-
[
|
|
10057
|
+
[
|
|
10058
|
+
...warnings,
|
|
10059
|
+
...missingFieldWarnings(finalQuery, cached.data),
|
|
10060
|
+
...keywordServingWarnings(cached.data, keywordServingOptions(finalQuery))
|
|
10061
|
+
],
|
|
9907
10062
|
args,
|
|
9908
10063
|
true
|
|
9909
10064
|
);
|
|
@@ -11388,19 +11543,19 @@ function failWriteValidation2(message) {
|
|
|
11388
11543
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
11389
11544
|
process.exit(1);
|
|
11390
11545
|
}
|
|
11391
|
-
function loadJsonFileArg2(
|
|
11392
|
-
if (typeof
|
|
11546
|
+
function loadJsonFileArg2(path35) {
|
|
11547
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
11393
11548
|
return {};
|
|
11394
11549
|
}
|
|
11395
11550
|
try {
|
|
11396
|
-
const parsed = JSON.parse(readFileSync4(
|
|
11551
|
+
const parsed = JSON.parse(readFileSync4(path35, "utf8"));
|
|
11397
11552
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11398
|
-
failWriteValidation2(`${
|
|
11553
|
+
failWriteValidation2(`${path35} must contain a JSON object`);
|
|
11399
11554
|
}
|
|
11400
11555
|
return parsed;
|
|
11401
11556
|
} catch (err) {
|
|
11402
11557
|
if (err instanceof SyntaxError) {
|
|
11403
|
-
failWriteValidation2(`${
|
|
11558
|
+
failWriteValidation2(`${path35} is not valid JSON: ${err.message}`);
|
|
11404
11559
|
}
|
|
11405
11560
|
throw err;
|
|
11406
11561
|
}
|
|
@@ -11485,15 +11640,15 @@ function parseLocaleFlag(value) {
|
|
|
11485
11640
|
}
|
|
11486
11641
|
return { language: match[1], country: match[2].toUpperCase() };
|
|
11487
11642
|
}
|
|
11488
|
-
function loadTargetingFileArg(
|
|
11489
|
-
if (typeof
|
|
11643
|
+
function loadTargetingFileArg(path35) {
|
|
11644
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
11490
11645
|
return void 0;
|
|
11491
11646
|
}
|
|
11492
|
-
const parsed = loadJsonFileArg2(
|
|
11647
|
+
const parsed = loadJsonFileArg2(path35);
|
|
11493
11648
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
11494
11649
|
if (!criteria.include) {
|
|
11495
11650
|
failWriteValidation2(
|
|
11496
|
-
`${
|
|
11651
|
+
`${path35} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
11497
11652
|
);
|
|
11498
11653
|
}
|
|
11499
11654
|
return criteria;
|
|
@@ -11528,14 +11683,14 @@ function parseCsvLine(line) {
|
|
|
11528
11683
|
cells.push(current);
|
|
11529
11684
|
return cells.map((cell) => cell.trim());
|
|
11530
11685
|
}
|
|
11531
|
-
function parseListFileArg(
|
|
11532
|
-
if (typeof
|
|
11686
|
+
function parseListFileArg(path35, maxRows) {
|
|
11687
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
11533
11688
|
return void 0;
|
|
11534
11689
|
}
|
|
11535
|
-
const raw = readFileSync4(
|
|
11690
|
+
const raw = readFileSync4(path35, "utf8");
|
|
11536
11691
|
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
11537
11692
|
if (lines.length < 2) {
|
|
11538
|
-
failWriteValidation2(`${
|
|
11693
|
+
failWriteValidation2(`${path35} needs a header row and at least one data row`);
|
|
11539
11694
|
}
|
|
11540
11695
|
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
11541
11696
|
const rows = [];
|
|
@@ -11554,7 +11709,7 @@ function parseListFileArg(path28, maxRows) {
|
|
|
11554
11709
|
}
|
|
11555
11710
|
}
|
|
11556
11711
|
if (rows.length > maxRows) {
|
|
11557
|
-
failWriteValidation2(`${
|
|
11712
|
+
failWriteValidation2(`${path35} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
11558
11713
|
}
|
|
11559
11714
|
return { columns, rows };
|
|
11560
11715
|
}
|
|
@@ -11650,11 +11805,11 @@ function readPositionals(args) {
|
|
|
11650
11805
|
function splitIdList(raw) {
|
|
11651
11806
|
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
11652
11807
|
}
|
|
11653
|
-
function idsFileEntries(
|
|
11654
|
-
if (typeof
|
|
11808
|
+
function idsFileEntries(path35) {
|
|
11809
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
11655
11810
|
return [];
|
|
11656
11811
|
}
|
|
11657
|
-
return readFileSync4(
|
|
11812
|
+
return readFileSync4(path35, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
|
|
11658
11813
|
}
|
|
11659
11814
|
function requireTargets(args, entity) {
|
|
11660
11815
|
const positionals = readPositionals(args);
|
|
@@ -14277,9 +14432,9 @@ function compactRow(row) {
|
|
|
14277
14432
|
...destination.postUrn ? { postUrn: destination.postUrn } : {}
|
|
14278
14433
|
};
|
|
14279
14434
|
}
|
|
14280
|
-
function readPath(row,
|
|
14435
|
+
function readPath(row, path35) {
|
|
14281
14436
|
let current = row;
|
|
14282
|
-
for (const segment of
|
|
14437
|
+
for (const segment of path35.split(".")) {
|
|
14283
14438
|
const record = asRecord2(current);
|
|
14284
14439
|
if (!record) return void 0;
|
|
14285
14440
|
current = record[segment];
|
|
@@ -14289,10 +14444,10 @@ function readPath(row, path28) {
|
|
|
14289
14444
|
function projectFields(rows, paths) {
|
|
14290
14445
|
return rows.map((row) => {
|
|
14291
14446
|
const projected = {};
|
|
14292
|
-
for (const
|
|
14293
|
-
const value = readPath(row,
|
|
14447
|
+
for (const path35 of paths) {
|
|
14448
|
+
const value = readPath(row, path35);
|
|
14294
14449
|
if (value !== void 0) {
|
|
14295
|
-
projected[
|
|
14450
|
+
projected[path35] = value;
|
|
14296
14451
|
}
|
|
14297
14452
|
}
|
|
14298
14453
|
return projected;
|
|
@@ -15499,11 +15654,11 @@ var updateStatusSchema = z20.enum(UPDATE_STATUSES);
|
|
|
15499
15654
|
function currencyMinimums2(currencyCode) {
|
|
15500
15655
|
return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
|
|
15501
15656
|
}
|
|
15502
|
-
function validateDailyBudgetFloor(money, ctx,
|
|
15657
|
+
function validateDailyBudgetFloor(money, ctx, path35) {
|
|
15503
15658
|
if (money?.currencyCode) {
|
|
15504
15659
|
const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
|
|
15505
15660
|
if (Number(money.amount) < min) {
|
|
15506
|
-
ctx.addIssue({ code: "custom", path:
|
|
15661
|
+
ctx.addIssue({ code: "custom", path: path35, message: `below the ${min} ${money.currencyCode} daily minimum` });
|
|
15507
15662
|
}
|
|
15508
15663
|
}
|
|
15509
15664
|
}
|
|
@@ -16146,19 +16301,19 @@ function failWriteValidation3(message) {
|
|
|
16146
16301
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
16147
16302
|
process.exit(1);
|
|
16148
16303
|
}
|
|
16149
|
-
function loadJsonFileArg3(
|
|
16150
|
-
if (typeof
|
|
16304
|
+
function loadJsonFileArg3(path35) {
|
|
16305
|
+
if (typeof path35 !== "string" || path35.length === 0) {
|
|
16151
16306
|
return {};
|
|
16152
16307
|
}
|
|
16153
16308
|
try {
|
|
16154
|
-
const parsed = JSON.parse(readFileSync8(
|
|
16309
|
+
const parsed = JSON.parse(readFileSync8(path35, "utf8"));
|
|
16155
16310
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16156
|
-
failWriteValidation3(`${
|
|
16311
|
+
failWriteValidation3(`${path35} must contain a JSON object`);
|
|
16157
16312
|
}
|
|
16158
16313
|
return parsed;
|
|
16159
16314
|
} catch (err) {
|
|
16160
16315
|
if (err instanceof SyntaxError) {
|
|
16161
|
-
failWriteValidation3(`${
|
|
16316
|
+
failWriteValidation3(`${path35} is not valid JSON: ${err.message}`);
|
|
16162
16317
|
}
|
|
16163
16318
|
throw err;
|
|
16164
16319
|
}
|
|
@@ -20220,11 +20375,11 @@ function estSpeechS(text) {
|
|
|
20220
20375
|
var OBSERVED_WPS_MIN = 1;
|
|
20221
20376
|
var OBSERVED_WPS_MAX = 6;
|
|
20222
20377
|
function estSpeechWindowS(text, startS, endS) {
|
|
20223
|
-
const
|
|
20224
|
-
const
|
|
20225
|
-
if (
|
|
20226
|
-
const wps =
|
|
20227
|
-
if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return
|
|
20378
|
+
const words3 = wordCount(text);
|
|
20379
|
+
const window2 = (endS ?? 0) - (startS ?? 0);
|
|
20380
|
+
if (words3 > 0 && window2 > 0.3) {
|
|
20381
|
+
const wps = words3 / window2;
|
|
20382
|
+
if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
|
|
20228
20383
|
}
|
|
20229
20384
|
return estSpeechS(text);
|
|
20230
20385
|
}
|
|
@@ -21079,10 +21234,10 @@ function scrubFloatSentences(text, floatDescs) {
|
|
|
21079
21234
|
if (floatDescs.length === 0 || !text) return text;
|
|
21080
21235
|
const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
|
|
21081
21236
|
const kept = text.split(/(?<=[.!?])\s+/).filter((sentence) => {
|
|
21082
|
-
const
|
|
21237
|
+
const words3 = new Set(floatTokens(sentence));
|
|
21083
21238
|
return !tokenSets.some((ts) => {
|
|
21084
21239
|
let hits = 0;
|
|
21085
|
-
for (const w of
|
|
21240
|
+
for (const w of words3) if (ts.has(w)) hits++;
|
|
21086
21241
|
return hits >= 2;
|
|
21087
21242
|
});
|
|
21088
21243
|
}).join(" ").trim();
|
|
@@ -23273,15 +23428,15 @@ function collectClipAdvisories(scene, i, out) {
|
|
|
23273
23428
|
const round22 = (n) => Math.round(n * 100) / 100;
|
|
23274
23429
|
const original = scene.duration_s ?? 5;
|
|
23275
23430
|
if (original > 15) out.clamped.push({ scene: i, original_s: original, clip_s: snapToSeedance(original) });
|
|
23276
|
-
const
|
|
23277
|
-
if (
|
|
23278
|
-
out.oversize.push({ scene: i, scene_s: round22(
|
|
23431
|
+
const window2 = sceneDurationS(scene);
|
|
23432
|
+
if (window2 > SEEDANCE_SAFE_MAX_S)
|
|
23433
|
+
out.oversize.push({ scene: i, scene_s: round22(window2), clip_s: ceilToSeedance(window2) });
|
|
23279
23434
|
const speech = (scene.dialogue ?? []).reduce(
|
|
23280
23435
|
(s, line) => s + (line.line ? estSpeechWindowS(line.line, line.start_s, line.end_s) : 0),
|
|
23281
23436
|
0
|
|
23282
23437
|
);
|
|
23283
|
-
if (speech >
|
|
23284
|
-
out.overstuffed.push({ scene: i, scene_s: round22(
|
|
23438
|
+
if (speech > window2 * OVERSTUFF_RATIO)
|
|
23439
|
+
out.overstuffed.push({ scene: i, scene_s: round22(window2), est_speech_s: round22(speech) });
|
|
23285
23440
|
}
|
|
23286
23441
|
function videoReport(input, elementsInput) {
|
|
23287
23442
|
const blueprint = VideoBlueprint.parse(input);
|
|
@@ -26957,12 +27112,12 @@ function listFlowSlugs() {
|
|
|
26957
27112
|
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
|
|
26958
27113
|
}
|
|
26959
27114
|
function readFlowTree(slug) {
|
|
26960
|
-
const
|
|
26961
|
-
if (!existsSync4(
|
|
27115
|
+
const path35 = join3(flowsDir(), slug, "_data.json");
|
|
27116
|
+
if (!existsSync4(path35)) {
|
|
26962
27117
|
failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
|
|
26963
27118
|
}
|
|
26964
27119
|
try {
|
|
26965
|
-
return JSON.parse(readFileSync9(
|
|
27120
|
+
return JSON.parse(readFileSync9(path35, "utf-8"));
|
|
26966
27121
|
} catch (error) {
|
|
26967
27122
|
failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
26968
27123
|
}
|
|
@@ -27375,10 +27530,10 @@ async function stageOps(ops) {
|
|
|
27375
27530
|
handleError(err);
|
|
27376
27531
|
}
|
|
27377
27532
|
}
|
|
27378
|
-
async function draftAction2(
|
|
27533
|
+
async function draftAction2(path35, body, chat) {
|
|
27379
27534
|
const chatId = resolveChatId(chat);
|
|
27380
27535
|
try {
|
|
27381
|
-
const data = await apiPost(
|
|
27536
|
+
const data = await apiPost(path35, { chatId, ...body });
|
|
27382
27537
|
writeJsonEnvelope({ ok: true, data });
|
|
27383
27538
|
return data;
|
|
27384
27539
|
} catch (err) {
|
|
@@ -29402,9 +29557,9 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
29402
29557
|
}
|
|
29403
29558
|
return readFile19(pathOrUrl);
|
|
29404
29559
|
}
|
|
29405
|
-
async function isDirectory(
|
|
29560
|
+
async function isDirectory(path35) {
|
|
29406
29561
|
try {
|
|
29407
|
-
const s = await stat4(
|
|
29562
|
+
const s = await stat4(path35);
|
|
29408
29563
|
return s.isDirectory();
|
|
29409
29564
|
} catch {
|
|
29410
29565
|
return false;
|
|
@@ -31942,11 +32097,11 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
31942
32097
|
});
|
|
31943
32098
|
|
|
31944
32099
|
// src/commands/landing/index.ts
|
|
31945
|
-
import { defineCommand as
|
|
32100
|
+
import { defineCommand as defineCommand151 } from "citty";
|
|
31946
32101
|
|
|
31947
32102
|
// src/commands/landing/critique.ts
|
|
31948
32103
|
import { readdir as readdir8, stat as stat6 } from "fs/promises";
|
|
31949
|
-
import
|
|
32104
|
+
import path28 from "path";
|
|
31950
32105
|
import { defineCommand as defineCommand142 } from "citty";
|
|
31951
32106
|
|
|
31952
32107
|
// src/engine/landing/lib/brand-tokens.ts
|
|
@@ -32355,6 +32510,11 @@ var RULE_META = {
|
|
|
32355
32510
|
severity: "block",
|
|
32356
32511
|
note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
|
|
32357
32512
|
},
|
|
32513
|
+
"copied-reference-copy": {
|
|
32514
|
+
family: "originality",
|
|
32515
|
+
severity: "block",
|
|
32516
|
+
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."
|
|
32517
|
+
},
|
|
32358
32518
|
"broken-image": {
|
|
32359
32519
|
family: "integrity",
|
|
32360
32520
|
severity: "block",
|
|
@@ -32535,6 +32695,64 @@ var SEVERITY_WEIGHT = {
|
|
|
32535
32695
|
advisory: 0.05
|
|
32536
32696
|
};
|
|
32537
32697
|
|
|
32698
|
+
// src/engine/landing/lib/originality.ts
|
|
32699
|
+
var MIN_COMPARABLE_LENGTH = 12;
|
|
32700
|
+
var NEAR_MATCH_RATIO = 0.8;
|
|
32701
|
+
function normalize(value) {
|
|
32702
|
+
return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
|
|
32703
|
+
}
|
|
32704
|
+
function words2(value) {
|
|
32705
|
+
return normalize(value).split(" ").filter(Boolean);
|
|
32706
|
+
}
|
|
32707
|
+
function copyOverlapRatio(candidate, reference) {
|
|
32708
|
+
const referenceWords = words2(reference);
|
|
32709
|
+
if (referenceWords.length === 0) return 0;
|
|
32710
|
+
const candidateWords = new Set(words2(candidate));
|
|
32711
|
+
const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
|
|
32712
|
+
return shared / referenceWords.length;
|
|
32713
|
+
}
|
|
32714
|
+
function isVerbatimReuse(candidate, reference) {
|
|
32715
|
+
const normalizedCandidate = normalize(candidate);
|
|
32716
|
+
const normalizedReference = normalize(reference);
|
|
32717
|
+
if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
|
|
32718
|
+
if (normalizedCandidate.includes(normalizedReference)) return true;
|
|
32719
|
+
return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
|
|
32720
|
+
}
|
|
32721
|
+
function extractVisibleStrings(text) {
|
|
32722
|
+
const found = [];
|
|
32723
|
+
const lines = text.split("\n");
|
|
32724
|
+
for (const [index, line] of lines.entries()) {
|
|
32725
|
+
for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
|
|
32726
|
+
const value = match[1]?.trim();
|
|
32727
|
+
if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
|
|
32728
|
+
}
|
|
32729
|
+
}
|
|
32730
|
+
return found;
|
|
32731
|
+
}
|
|
32732
|
+
function detectOriginality(sources, references) {
|
|
32733
|
+
if (references.length === 0) return [];
|
|
32734
|
+
const findings = [];
|
|
32735
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32736
|
+
for (const source of sources) {
|
|
32737
|
+
for (const { value, line } of extractVisibleStrings(source.text)) {
|
|
32738
|
+
for (const reference of references) {
|
|
32739
|
+
const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
|
|
32740
|
+
if (!hit) continue;
|
|
32741
|
+
const key = `${source.path}:${line}:${normalize(hit)}`;
|
|
32742
|
+
if (seen.has(key)) continue;
|
|
32743
|
+
seen.add(key);
|
|
32744
|
+
findings.push({
|
|
32745
|
+
id: "copied-reference-copy",
|
|
32746
|
+
snippet: value.slice(0, 120),
|
|
32747
|
+
file: source.path,
|
|
32748
|
+
line
|
|
32749
|
+
});
|
|
32750
|
+
}
|
|
32751
|
+
}
|
|
32752
|
+
}
|
|
32753
|
+
return findings;
|
|
32754
|
+
}
|
|
32755
|
+
|
|
32538
32756
|
// src/engine/landing/lib/rules.ts
|
|
32539
32757
|
var cap2 = (m, i) => m[i] ?? "";
|
|
32540
32758
|
var num = (m, i) => Number(m[i] ?? 0);
|
|
@@ -32939,8 +33157,8 @@ var ANALYZERS = [
|
|
|
32939
33157
|
const lines = text.split("\n");
|
|
32940
33158
|
for (const m of text.matchAll(/(?:-webkit-)?background-clip\s*:\s*text/gi)) {
|
|
32941
33159
|
const line = lineOf(text, m.index ?? 0);
|
|
32942
|
-
const
|
|
32943
|
-
if (/gradient\(/i.test(
|
|
33160
|
+
const window2 = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
|
|
33161
|
+
if (/gradient\(/i.test(window2)) {
|
|
32944
33162
|
out.push({ id: "gradient-text", snippet: "background-clip: text + gradient", file, line });
|
|
32945
33163
|
}
|
|
32946
33164
|
}
|
|
@@ -33062,7 +33280,16 @@ function dedupe(findings) {
|
|
|
33062
33280
|
}
|
|
33063
33281
|
|
|
33064
33282
|
// src/engine/landing/lib/critique.ts
|
|
33065
|
-
var FAMILIES = [
|
|
33283
|
+
var FAMILIES = [
|
|
33284
|
+
"typography",
|
|
33285
|
+
"color",
|
|
33286
|
+
"borders_depth",
|
|
33287
|
+
"motion",
|
|
33288
|
+
"spacing",
|
|
33289
|
+
"copy",
|
|
33290
|
+
"integrity",
|
|
33291
|
+
"originality"
|
|
33292
|
+
];
|
|
33066
33293
|
function round4(n) {
|
|
33067
33294
|
return Math.round(n * 100) / 100;
|
|
33068
33295
|
}
|
|
@@ -33081,6 +33308,7 @@ function critiqueLanding(input) {
|
|
|
33081
33308
|
const raws = [];
|
|
33082
33309
|
for (const source of sources) raws.push(...detectSource(source));
|
|
33083
33310
|
raws.push(...detectPage(sources));
|
|
33311
|
+
raws.push(...detectOriginality(sources, input.references ?? []));
|
|
33084
33312
|
for (const raw of raws) {
|
|
33085
33313
|
const meta = RULE_META[raw.id];
|
|
33086
33314
|
if (!meta) continue;
|
|
@@ -33118,41 +33346,82 @@ function describeCounts(findings) {
|
|
|
33118
33346
|
return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
|
|
33119
33347
|
}
|
|
33120
33348
|
|
|
33121
|
-
// src/
|
|
33122
|
-
import { mkdir as mkdir8,
|
|
33349
|
+
// src/engine/landing/lib/referenceStore.ts
|
|
33350
|
+
import { mkdir as mkdir8, readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
|
|
33123
33351
|
import path25 from "path";
|
|
33352
|
+
var REFERENCES_FILE = ".cache/inspiration-refs.json";
|
|
33353
|
+
var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
33354
|
+
async function readReferences(projectRoot) {
|
|
33355
|
+
try {
|
|
33356
|
+
const raw = await readFile21(path25.join(projectRoot, REFERENCES_FILE), "utf8");
|
|
33357
|
+
const parsed = JSON.parse(raw);
|
|
33358
|
+
if (!Array.isArray(parsed)) return [];
|
|
33359
|
+
const cutoff = Date.now() - REFERENCE_TTL_MS;
|
|
33360
|
+
return parsed.filter((entry) => {
|
|
33361
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
33362
|
+
const candidate = entry;
|
|
33363
|
+
if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
|
|
33364
|
+
const at = Date.parse(candidate.consultedAt ?? "");
|
|
33365
|
+
return Number.isNaN(at) ? true : at >= cutoff;
|
|
33366
|
+
});
|
|
33367
|
+
} catch {
|
|
33368
|
+
return [];
|
|
33369
|
+
}
|
|
33370
|
+
}
|
|
33371
|
+
async function recordReference(projectRoot, reference) {
|
|
33372
|
+
return await recordReferences(projectRoot, [reference]);
|
|
33373
|
+
}
|
|
33374
|
+
async function recordReferences(projectRoot, references) {
|
|
33375
|
+
if (references.length === 0) return true;
|
|
33376
|
+
try {
|
|
33377
|
+
const existing = await readReferences(projectRoot);
|
|
33378
|
+
const replaced = new Set(references.map((reference) => reference.sectionId));
|
|
33379
|
+
const merged = [...existing.filter((entry) => !replaced.has(entry.sectionId)), ...references];
|
|
33380
|
+
const file = path25.join(projectRoot, REFERENCES_FILE);
|
|
33381
|
+
await mkdir8(path25.dirname(file), { recursive: true });
|
|
33382
|
+
await writeFile11(file, `${JSON.stringify(merged, null, 2)}
|
|
33383
|
+
`);
|
|
33384
|
+
return true;
|
|
33385
|
+
} catch {
|
|
33386
|
+
return false;
|
|
33387
|
+
}
|
|
33388
|
+
}
|
|
33389
|
+
|
|
33390
|
+
// src/commands/landing/snapshot.ts
|
|
33391
|
+
import { mkdir as mkdir9, rename as rename2, writeFile as writeFile12 } from "fs/promises";
|
|
33392
|
+
import path26 from "path";
|
|
33124
33393
|
var CRITIC_VERSION = "2";
|
|
33125
33394
|
function critiqueCacheDir(projectRoot) {
|
|
33126
|
-
return
|
|
33395
|
+
return path26.join(projectRoot, ".cache", "landing-critique");
|
|
33127
33396
|
}
|
|
33128
33397
|
function snapshotPath(projectRoot, slug) {
|
|
33129
|
-
return
|
|
33398
|
+
return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
|
|
33130
33399
|
}
|
|
33131
33400
|
async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
33132
|
-
await
|
|
33401
|
+
await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
|
|
33133
33402
|
const dest = snapshotPath(projectRoot, snapshot.slug);
|
|
33134
33403
|
const tmp = `${dest}.tmp`;
|
|
33135
|
-
await
|
|
33404
|
+
await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
|
|
33136
33405
|
`, "utf8");
|
|
33137
33406
|
await rename2(tmp, dest);
|
|
33138
33407
|
}
|
|
33139
33408
|
|
|
33140
33409
|
// src/commands/landing/source-version.ts
|
|
33141
|
-
import { readdir as readdir7, readFile as
|
|
33142
|
-
import
|
|
33410
|
+
import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
|
|
33411
|
+
import path27 from "path";
|
|
33143
33412
|
async function landingSourceRelPaths(landingDir) {
|
|
33144
33413
|
const rel = [];
|
|
33145
|
-
if (await isFile(
|
|
33146
|
-
const componentsDir =
|
|
33414
|
+
if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
|
|
33415
|
+
const componentsDir = path27.join(landingDir, "_components");
|
|
33147
33416
|
for (const abs of await walkAstro(componentsDir)) {
|
|
33148
|
-
rel.push(
|
|
33417
|
+
rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
|
|
33149
33418
|
}
|
|
33150
33419
|
return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
33151
33420
|
}
|
|
33152
33421
|
async function readLandingSources(landingDir) {
|
|
33153
33422
|
const rel = await landingSourceRelPaths(landingDir);
|
|
33154
33423
|
const out = [];
|
|
33155
|
-
for (const r of rel) out.push({ path: r, text: await
|
|
33424
|
+
for (const r of rel) out.push({ path: r, text: await readFile22(path27.join(landingDir, r), "utf8") });
|
|
33156
33425
|
return out;
|
|
33157
33426
|
}
|
|
33158
33427
|
async function computeLandingSourceSha(landingDir) {
|
|
@@ -33161,7 +33430,7 @@ async function computeLandingSourceSha(landingDir) {
|
|
|
33161
33430
|
for (const r of rel) {
|
|
33162
33431
|
let bytes;
|
|
33163
33432
|
try {
|
|
33164
|
-
bytes = await
|
|
33433
|
+
bytes = await readFile22(path27.join(landingDir, r));
|
|
33165
33434
|
} catch {
|
|
33166
33435
|
bytes = Buffer.alloc(0);
|
|
33167
33436
|
}
|
|
@@ -33185,7 +33454,7 @@ async function walkAstro(dir) {
|
|
|
33185
33454
|
}
|
|
33186
33455
|
const out = [];
|
|
33187
33456
|
for (const entry of entries) {
|
|
33188
|
-
const abs =
|
|
33457
|
+
const abs = path27.join(dir, entry.name);
|
|
33189
33458
|
if (entry.isDirectory()) out.push(...await walkAstro(abs));
|
|
33190
33459
|
else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
|
|
33191
33460
|
}
|
|
@@ -33246,14 +33515,14 @@ var critiqueCommand2 = defineCommand142({
|
|
|
33246
33515
|
{ availableSlugs: await listLandingSlugs(projectRoot) }
|
|
33247
33516
|
);
|
|
33248
33517
|
}
|
|
33249
|
-
if (!await isDir(
|
|
33518
|
+
if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
|
|
33250
33519
|
fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
|
|
33251
33520
|
availableSlugs: await listLandingSlugs(projectRoot)
|
|
33252
33521
|
});
|
|
33253
33522
|
}
|
|
33254
33523
|
}
|
|
33255
|
-
const brand = await loadBrandTokens(projectRoot);
|
|
33256
|
-
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
|
|
33524
|
+
const [brand, references] = await Promise.all([loadBrandTokens(projectRoot), readReferences(projectRoot)]);
|
|
33525
|
+
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, references)));
|
|
33257
33526
|
const landings = results.map(({ slug, report }) => ({
|
|
33258
33527
|
slug,
|
|
33259
33528
|
overall: report.overall,
|
|
@@ -33284,10 +33553,10 @@ var critiqueCommand2 = defineCommand142({
|
|
|
33284
33553
|
);
|
|
33285
33554
|
}
|
|
33286
33555
|
});
|
|
33287
|
-
async function critiqueOne(projectRoot, slug, brand) {
|
|
33288
|
-
const landingDir =
|
|
33556
|
+
async function critiqueOne(projectRoot, slug, brand, references) {
|
|
33557
|
+
const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
|
|
33289
33558
|
const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
|
|
33290
|
-
const report = critiqueLanding({ slug, sources, brand });
|
|
33559
|
+
const report = critiqueLanding({ slug, sources, brand, references });
|
|
33291
33560
|
let snapshotFailed = false;
|
|
33292
33561
|
try {
|
|
33293
33562
|
await writeCritiqueSnapshot(projectRoot, {
|
|
@@ -33305,7 +33574,7 @@ async function critiqueOne(projectRoot, slug, brand) {
|
|
|
33305
33574
|
}
|
|
33306
33575
|
async function listLandingSlugs(projectRoot) {
|
|
33307
33576
|
try {
|
|
33308
|
-
const entries = await readdir8(
|
|
33577
|
+
const entries = await readdir8(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
33309
33578
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
33310
33579
|
} catch {
|
|
33311
33580
|
return [];
|
|
@@ -33330,8 +33599,2531 @@ async function isDir(p) {
|
|
|
33330
33599
|
}
|
|
33331
33600
|
}
|
|
33332
33601
|
|
|
33602
|
+
// src/commands/landing/inspiration/index.ts
|
|
33603
|
+
import { defineCommand as defineCommand150 } from "citty";
|
|
33604
|
+
|
|
33605
|
+
// src/commands/landing/inspiration/add.ts
|
|
33606
|
+
import { defineCommand as defineCommand143 } from "citty";
|
|
33607
|
+
|
|
33608
|
+
// src/commands/landing/inspiration/shared.ts
|
|
33609
|
+
var INSPIRATION_HINTS = {
|
|
33610
|
+
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) and `baker landing critique` will block the publish.",
|
|
33611
|
+
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."
|
|
33612
|
+
};
|
|
33613
|
+
function fidelityHint(fidelity) {
|
|
33614
|
+
if (fidelity == null) return null;
|
|
33615
|
+
if (fidelity >= 0.9) return null;
|
|
33616
|
+
if (fidelity >= 0.75) {
|
|
33617
|
+
return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 the markup is close but not exact. Trust the screenshot over the code.`;
|
|
33618
|
+
}
|
|
33619
|
+
return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 this section is scroll- or JS-driven and the extracted markup does NOT render like the original. Use the screenshot and the notes; treat the code as a hint only.`;
|
|
33620
|
+
}
|
|
33621
|
+
function reportError(error) {
|
|
33622
|
+
if (error instanceof ApiError) {
|
|
33623
|
+
writeJson({ ok: false, error: { code: error.code, message: error.message } });
|
|
33624
|
+
} else {
|
|
33625
|
+
writeJson({
|
|
33626
|
+
ok: false,
|
|
33627
|
+
error: { code: "UNKNOWN", message: error instanceof Error ? error.message : String(error) }
|
|
33628
|
+
});
|
|
33629
|
+
}
|
|
33630
|
+
process.exit(1);
|
|
33631
|
+
}
|
|
33632
|
+
function splitList(value) {
|
|
33633
|
+
if (!value) return void 0;
|
|
33634
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
33635
|
+
return parts.length > 0 ? parts : void 0;
|
|
33636
|
+
}
|
|
33637
|
+
function parseNumber(value) {
|
|
33638
|
+
if (value === void 0 || value === "") return void 0;
|
|
33639
|
+
const parsed = Number(value);
|
|
33640
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
33641
|
+
}
|
|
33642
|
+
|
|
33643
|
+
// src/commands/landing/inspiration/add.ts
|
|
33644
|
+
registerSchema({
|
|
33645
|
+
command: "landing.inspiration.add",
|
|
33646
|
+
description: "Add someone else's landing page to the reference library and save it to this company. Returns immediately \u2014 studying a page takes a few minutes, so never wait on it. Use it when the client names a site they admire, or when you find a competitor page worth learning from. Never point it at a page we built: it is refused, because the source is already in the project and reading a screenshot of it tells you less than opening the file.",
|
|
33647
|
+
args: {
|
|
33648
|
+
url: { type: "string", description: "Landing page address", required: true },
|
|
33649
|
+
note: { type: "string", description: "Why this page is worth keeping", required: false }
|
|
33650
|
+
}
|
|
33651
|
+
});
|
|
33652
|
+
var addCommand = defineCommand143({
|
|
33653
|
+
meta: {
|
|
33654
|
+
name: "add",
|
|
33655
|
+
description: "Add someone else's landing page to the reference library. Example: baker landing inspiration add https://linear.app --note 'the client likes this density'"
|
|
33656
|
+
},
|
|
33657
|
+
args: {
|
|
33658
|
+
url: { type: "positional", description: "Landing page address", required: true },
|
|
33659
|
+
note: { type: "string", description: "Why this page is worth keeping", required: false }
|
|
33660
|
+
},
|
|
33661
|
+
run: async ({ args }) => {
|
|
33662
|
+
try {
|
|
33663
|
+
const data = await apiPost("/api/landing-inspiration/add", {
|
|
33664
|
+
url: args.url,
|
|
33665
|
+
note: args.note,
|
|
33666
|
+
favorite: true
|
|
33667
|
+
});
|
|
33668
|
+
const hints = data.alreadyKnown ? [
|
|
33669
|
+
"This page was already in the library, so nothing was re-studied \u2014 its sections are searchable now.",
|
|
33670
|
+
`Search it with: baker landing inspiration search --domain ${new URL(data.canonicalUrl).hostname}`
|
|
33671
|
+
] : [
|
|
33672
|
+
"Studying this page takes a few minutes. Do NOT wait on it \u2014 carry on, and search for it later in the turn or in a later one.",
|
|
33673
|
+
"Meanwhile, search what is already in the library: baker landing inspiration search '<what you want to see>' --scope all",
|
|
33674
|
+
"This is saved and being studied now, so it is listed as already applied \u2014 tell the user it is kept whatever they do with this chat."
|
|
33675
|
+
];
|
|
33676
|
+
writeJson({
|
|
33677
|
+
ok: true,
|
|
33678
|
+
data: { id: data.sourceId, url: data.canonicalUrl, status: data.status, already_known: data.alreadyKnown },
|
|
33679
|
+
hints
|
|
33680
|
+
});
|
|
33681
|
+
} catch (error) {
|
|
33682
|
+
reportError(error);
|
|
33683
|
+
}
|
|
33684
|
+
}
|
|
33685
|
+
});
|
|
33686
|
+
|
|
33687
|
+
// src/commands/landing/inspiration/code.ts
|
|
33688
|
+
import { mkdir as mkdir10, writeFile as writeFile13 } from "fs/promises";
|
|
33689
|
+
import path29 from "path";
|
|
33690
|
+
import { defineCommand as defineCommand144 } from "citty";
|
|
33691
|
+
registerSchema({
|
|
33692
|
+
command: "landing.inspiration.code",
|
|
33693
|
+
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. Consulting a section records it, and `baker landing critique` blocks a publish that ships its copy verbatim.",
|
|
33694
|
+
args: {
|
|
33695
|
+
id: { type: "string", description: "Section id from search", required: true },
|
|
33696
|
+
full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
|
|
33697
|
+
}
|
|
33698
|
+
});
|
|
33699
|
+
var codeCommand = defineCommand144({
|
|
33700
|
+
meta: {
|
|
33701
|
+
name: "code",
|
|
33702
|
+
description: "Write one reference section's standalone markup to disk. Example: baker landing inspiration code k57abc\u2026 \u2014 read it for structure, then build your own."
|
|
33703
|
+
},
|
|
33704
|
+
args: {
|
|
33705
|
+
id: { type: "positional", description: "Section id from search", required: true },
|
|
33706
|
+
full: { type: "boolean", description: "Also print the markup inline", required: false, default: false }
|
|
33707
|
+
},
|
|
33708
|
+
run: async ({ args }) => {
|
|
33709
|
+
try {
|
|
33710
|
+
const id = args.id;
|
|
33711
|
+
const data = await apiGet("/api/landing-inspiration/section-code", { id });
|
|
33712
|
+
const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
|
|
33713
|
+
await mkdir10(dir, { recursive: true });
|
|
33714
|
+
const file = path29.join(dir, "section.html");
|
|
33715
|
+
await writeFile13(file, data.html);
|
|
33716
|
+
const recorded = await recordReference(process.cwd(), {
|
|
33717
|
+
sectionId: id,
|
|
33718
|
+
sourceUrl: data.sourceUrl,
|
|
33719
|
+
copyStrings: data.copyStrings,
|
|
33720
|
+
consultedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33721
|
+
});
|
|
33722
|
+
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
33723
|
+
const fidelity = fidelityHint(data.fidelity);
|
|
33724
|
+
if (fidelity) hints.push(fidelity);
|
|
33725
|
+
if (data.fidelityNote) hints.push(data.fidelityNote);
|
|
33726
|
+
if (!recorded) {
|
|
33727
|
+
hints.push(
|
|
33728
|
+
"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."
|
|
33729
|
+
);
|
|
33730
|
+
}
|
|
33731
|
+
writeJson({
|
|
33732
|
+
ok: true,
|
|
33733
|
+
data: {
|
|
33734
|
+
id,
|
|
33735
|
+
file: path29.relative(process.cwd(), file),
|
|
33736
|
+
bytes: data.html.length,
|
|
33737
|
+
fidelity: data.fidelity,
|
|
33738
|
+
reproduction_notes: data.reproductionNotes,
|
|
33739
|
+
adaptation_notes: data.adaptationNotes,
|
|
33740
|
+
source_url: data.sourceUrl,
|
|
33741
|
+
// Behind --full with the markup it belongs to. Up to 60 custom-property
|
|
33742
|
+
// *names*, one per line, was over half the default response — and the
|
|
33743
|
+
// file on disk already has them, next to the values that give them
|
|
33744
|
+
// meaning. A list of variable names on its own decides nothing.
|
|
33745
|
+
...args.full ? { css_custom_properties: data.cssCustomProperties, html: data.html } : {}
|
|
33746
|
+
},
|
|
33747
|
+
hints
|
|
33748
|
+
});
|
|
33749
|
+
} catch (error) {
|
|
33750
|
+
reportError(error);
|
|
33751
|
+
}
|
|
33752
|
+
}
|
|
33753
|
+
});
|
|
33754
|
+
|
|
33755
|
+
// src/commands/landing/inspiration/favorites.ts
|
|
33756
|
+
import { defineCommand as defineCommand145 } from "citty";
|
|
33757
|
+
registerSchema({
|
|
33758
|
+
command: "landing.inspiration.favorites",
|
|
33759
|
+
description: "List the reference sections this company has saved. This is what `search` looks at by default, so it is the client's own taste profile \u2014 read it before proposing a direction.",
|
|
33760
|
+
args: {
|
|
33761
|
+
type: { type: "string", description: "Comma list of section types to filter by", required: false },
|
|
33762
|
+
limit: { type: "number", description: "Max results (default 30, capped at 100)", required: false },
|
|
33763
|
+
full: {
|
|
33764
|
+
type: "boolean",
|
|
33765
|
+
description: "Add the transferable idea behind each saved section. Costs roughly 60% more; read the list first",
|
|
33766
|
+
required: false
|
|
33767
|
+
}
|
|
33768
|
+
}
|
|
33769
|
+
});
|
|
33770
|
+
var favoritesCommand = defineCommand145({
|
|
33771
|
+
meta: {
|
|
33772
|
+
name: "favorites",
|
|
33773
|
+
description: "List this company's saved reference sections. Example: baker landing inspiration favorites --type hero,pricing"
|
|
33774
|
+
},
|
|
33775
|
+
args: {
|
|
33776
|
+
type: { type: "string", description: "Comma list of section types", required: false },
|
|
33777
|
+
limit: { type: "string", description: "Max results (default 30, capped at 100)", required: false },
|
|
33778
|
+
full: { type: "boolean", description: "Include why_it_works per section", required: false, default: false }
|
|
33779
|
+
},
|
|
33780
|
+
run: async ({ args }) => {
|
|
33781
|
+
try {
|
|
33782
|
+
const params = {};
|
|
33783
|
+
const types = splitList(args.type);
|
|
33784
|
+
if (types) params.type = types.join(",");
|
|
33785
|
+
const limit = parseNumber(args.limit);
|
|
33786
|
+
if (limit !== void 0) params.limit = String(limit);
|
|
33787
|
+
const data = await apiGet("/api/landing-inspiration/favorites", params);
|
|
33788
|
+
const sections = Array.isArray(data?.sections) ? data.sections : [];
|
|
33789
|
+
const full = args.full;
|
|
33790
|
+
writeJson({
|
|
33791
|
+
ok: true,
|
|
33792
|
+
data: {
|
|
33793
|
+
sections: sections.map((section) => ({
|
|
33794
|
+
id: section.id,
|
|
33795
|
+
page_id: section.sourceId,
|
|
33796
|
+
section: section.sectionType,
|
|
33797
|
+
composition: section.composition,
|
|
33798
|
+
look: section.visualRegister,
|
|
33799
|
+
domain: section.domain,
|
|
33800
|
+
// Behind --full: this is a 600-character paragraph, and at the
|
|
33801
|
+
// default 30 rows it was about 60% of the response. Reading a taste
|
|
33802
|
+
// profile is a question about *what* the client keeps saving —
|
|
33803
|
+
// types, compositions, which sites — and the essay per row answers
|
|
33804
|
+
// a question you only have once you have picked a row.
|
|
33805
|
+
...full ? { why_it_works: section.whyItWorks } : {}
|
|
33806
|
+
}))
|
|
33807
|
+
},
|
|
33808
|
+
hints: sections.length === 0 ? [
|
|
33809
|
+
"Nothing saved yet. Add a page the client admires with `baker landing inspiration add <url>`, or browse the whole library with `baker landing inspiration search --scope all`."
|
|
33810
|
+
] : [
|
|
33811
|
+
"These are the client's taste signals \u2014 read them as direction, not as a component library.",
|
|
33812
|
+
...full ? [] : ["Add --full for the transferable idea behind each one."],
|
|
33813
|
+
INSPIRATION_HINTS.adapt
|
|
33814
|
+
]
|
|
33815
|
+
});
|
|
33816
|
+
} catch (error) {
|
|
33817
|
+
reportError(error);
|
|
33818
|
+
}
|
|
33819
|
+
}
|
|
33820
|
+
});
|
|
33821
|
+
registerSchema({
|
|
33822
|
+
command: "landing.inspiration.favorite",
|
|
33823
|
+
description: "Save a reference section (or a whole page) to this company, so it shows up in the default search scope.",
|
|
33824
|
+
args: {
|
|
33825
|
+
id: { type: "string", description: "Section id, or page id with --page", required: true },
|
|
33826
|
+
page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false },
|
|
33827
|
+
note: { type: "string", description: "Why this is worth keeping", required: false }
|
|
33828
|
+
}
|
|
33829
|
+
});
|
|
33830
|
+
var favoriteCommand = defineCommand145({
|
|
33831
|
+
meta: {
|
|
33832
|
+
name: "favorite",
|
|
33833
|
+
description: "Save a reference section to this company. Example: baker landing inspiration favorite k57abc\u2026"
|
|
33834
|
+
},
|
|
33835
|
+
args: {
|
|
33836
|
+
id: { type: "positional", description: "Section id (or page id with --page)", required: true },
|
|
33837
|
+
page: { type: "boolean", description: "Treat the id as a page", required: false, default: false },
|
|
33838
|
+
note: { type: "string", description: "Why this is worth keeping", required: false }
|
|
33839
|
+
},
|
|
33840
|
+
run: async ({ args }) => {
|
|
33841
|
+
try {
|
|
33842
|
+
const id = args.id;
|
|
33843
|
+
const body = args.page ? { sourceId: id } : { sectionId: id };
|
|
33844
|
+
const data = await apiPost("/api/landing-inspiration/favorite", {
|
|
33845
|
+
...body,
|
|
33846
|
+
note: args.note
|
|
33847
|
+
});
|
|
33848
|
+
writeJson({
|
|
33849
|
+
ok: true,
|
|
33850
|
+
data: { id, favorited: data.favorited },
|
|
33851
|
+
hints: [
|
|
33852
|
+
"Saved. `search` looks at this company's saved sections by default, so this now shows up without --scope all."
|
|
33853
|
+
]
|
|
33854
|
+
});
|
|
33855
|
+
} catch (error) {
|
|
33856
|
+
reportError(error);
|
|
33857
|
+
}
|
|
33858
|
+
}
|
|
33859
|
+
});
|
|
33860
|
+
registerSchema({
|
|
33861
|
+
command: "landing.inspiration.unfavorite",
|
|
33862
|
+
description: "Remove a reference section (or page) from this company's saved set.",
|
|
33863
|
+
args: {
|
|
33864
|
+
id: { type: "string", description: "Section id, or page id with --page", required: true },
|
|
33865
|
+
page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false }
|
|
33866
|
+
}
|
|
33867
|
+
});
|
|
33868
|
+
var unfavoriteCommand = defineCommand145({
|
|
33869
|
+
meta: {
|
|
33870
|
+
name: "unfavorite",
|
|
33871
|
+
description: "Remove a reference section from this company's saved set. Example: baker landing inspiration unfavorite k57abc\u2026"
|
|
33872
|
+
},
|
|
33873
|
+
args: {
|
|
33874
|
+
id: { type: "positional", description: "Section id (or page id with --page)", required: true },
|
|
33875
|
+
page: { type: "boolean", description: "Treat the id as a page", required: false, default: false }
|
|
33876
|
+
},
|
|
33877
|
+
run: async ({ args }) => {
|
|
33878
|
+
try {
|
|
33879
|
+
const id = args.id;
|
|
33880
|
+
const body = args.page ? { sourceId: id } : { sectionId: id };
|
|
33881
|
+
const data = await apiPost("/api/landing-inspiration/unfavorite", body);
|
|
33882
|
+
writeJson({ ok: true, data: { id, favorited: data.favorited } });
|
|
33883
|
+
} catch (error) {
|
|
33884
|
+
reportError(error);
|
|
33885
|
+
}
|
|
33886
|
+
}
|
|
33887
|
+
});
|
|
33888
|
+
|
|
33889
|
+
// src/commands/landing/inspiration/page.ts
|
|
33890
|
+
import { defineCommand as defineCommand146 } from "citty";
|
|
33891
|
+
registerSchema({
|
|
33892
|
+
command: "landing.inspiration.page",
|
|
33893
|
+
description: "Show a whole reference page as a sequence: every section top to bottom with its type and the idea behind it. This is the view to use when the question is how a good page is ORDERED rather than what one section looks like.",
|
|
33894
|
+
args: {
|
|
33895
|
+
id: {
|
|
33896
|
+
type: "string",
|
|
33897
|
+
description: "Page id \u2014 the `page_id` on any search result or favorites row",
|
|
33898
|
+
required: true
|
|
33899
|
+
},
|
|
33900
|
+
full: {
|
|
33901
|
+
type: "boolean",
|
|
33902
|
+
description: "Add the transferable idea behind every section. A long page roughly triples the response",
|
|
33903
|
+
required: false
|
|
33904
|
+
}
|
|
33905
|
+
}
|
|
33906
|
+
});
|
|
33907
|
+
var pageCommand = defineCommand146({
|
|
33908
|
+
meta: {
|
|
33909
|
+
name: "page",
|
|
33910
|
+
description: "Show how a reference page sequences its sections. Example: baker landing inspiration page j91xyz\u2026 \u2014 the blueprint, not the pixels."
|
|
33911
|
+
},
|
|
33912
|
+
args: {
|
|
33913
|
+
id: { type: "positional", description: "Page id (the `page_id` on a search result)", required: true },
|
|
33914
|
+
full: { type: "boolean", description: "Include why_it_works per section", required: false, default: false }
|
|
33915
|
+
},
|
|
33916
|
+
run: async ({ args }) => {
|
|
33917
|
+
try {
|
|
33918
|
+
const data = await apiGet("/api/landing-inspiration/page", { id: args.id });
|
|
33919
|
+
if (data.status !== "indexed") {
|
|
33920
|
+
writeJson({
|
|
33921
|
+
ok: true,
|
|
33922
|
+
data: { id: data.id, url: data.url, status: data.status, sections: [] },
|
|
33923
|
+
hints: [
|
|
33924
|
+
data.status === "error" ? "We couldn't read this page. Try a different address, or a different page on the same site." : "Still being studied \u2014 check back later in the turn or in a later one."
|
|
33925
|
+
]
|
|
33926
|
+
});
|
|
33927
|
+
return;
|
|
33928
|
+
}
|
|
33929
|
+
const full = args.full;
|
|
33930
|
+
writeJson({
|
|
33931
|
+
ok: true,
|
|
33932
|
+
data: {
|
|
33933
|
+
id: data.id,
|
|
33934
|
+
url: data.url,
|
|
33935
|
+
domain: data.domain,
|
|
33936
|
+
title: data.title,
|
|
33937
|
+
archetype: data.archetype,
|
|
33938
|
+
stack: data.detectedStack,
|
|
33939
|
+
page_fidelity: data.pageFidelity,
|
|
33940
|
+
blueprint: data.sectionOrder,
|
|
33941
|
+
sections: data.sections.map((section) => ({
|
|
33942
|
+
id: section.id,
|
|
33943
|
+
position: section.index,
|
|
33944
|
+
section: section.sectionType,
|
|
33945
|
+
composition: section.composition,
|
|
33946
|
+
look: section.visualRegister,
|
|
33947
|
+
motion: section.motionSummary,
|
|
33948
|
+
craft: section.craftScore,
|
|
33949
|
+
// Behind --full. This command exists to answer an ordering
|
|
33950
|
+
// question, and `blueprint` answers it in a line; a 600-character
|
|
33951
|
+
// paragraph per section turned a 40-section page into ~5,900 tokens
|
|
33952
|
+
// of material about individual sections nobody had chosen yet.
|
|
33953
|
+
...full ? { why_it_works: section.whyItWorks } : {}
|
|
33954
|
+
}))
|
|
33955
|
+
},
|
|
33956
|
+
hints: [
|
|
33957
|
+
"The blueprint is the section order top to bottom \u2014 the answer to 'how do good pages in this category sequence themselves'.",
|
|
33958
|
+
"Use `baker landing inspiration view <section id>` for any section worth a closer look.",
|
|
33959
|
+
...full ? [] : ["Add --full for the idea behind every section, once you know which page you care about."],
|
|
33960
|
+
INSPIRATION_HINTS.adapt
|
|
33961
|
+
]
|
|
33962
|
+
});
|
|
33963
|
+
} catch (error) {
|
|
33964
|
+
reportError(error);
|
|
33965
|
+
}
|
|
33966
|
+
}
|
|
33967
|
+
});
|
|
33968
|
+
|
|
33969
|
+
// src/commands/landing/inspiration/scrape.ts
|
|
33970
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
33971
|
+
import path32 from "path";
|
|
33972
|
+
import { defineCommand as defineCommand147 } from "citty";
|
|
33973
|
+
|
|
33974
|
+
// src/engine/landing/lib/capturedReferences.ts
|
|
33975
|
+
var MAX_STRINGS_PER_SECTION = 40;
|
|
33976
|
+
function bodyOf(markup) {
|
|
33977
|
+
const body = markup.indexOf("<body");
|
|
33978
|
+
return body === -1 ? markup : markup.slice(body);
|
|
33979
|
+
}
|
|
33980
|
+
function capturedCopyStrings(markup) {
|
|
33981
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33982
|
+
for (const { value } of extractVisibleStrings(bodyOf(markup))) {
|
|
33983
|
+
seen.add(value);
|
|
33984
|
+
if (seen.size >= MAX_STRINGS_PER_SECTION) break;
|
|
33985
|
+
}
|
|
33986
|
+
return [...seen];
|
|
33987
|
+
}
|
|
33988
|
+
|
|
33989
|
+
// src/engine/landing-library/blocked.ts
|
|
33990
|
+
var CHALLENGE_PHRASES = [
|
|
33991
|
+
"just a moment",
|
|
33992
|
+
"attention required",
|
|
33993
|
+
"verify you are human",
|
|
33994
|
+
"checking your browser",
|
|
33995
|
+
"enable javascript and cookies to continue",
|
|
33996
|
+
"unusual traffic",
|
|
33997
|
+
"access denied",
|
|
33998
|
+
"you have been blocked",
|
|
33999
|
+
"request unsuccessful",
|
|
34000
|
+
"are you a robot",
|
|
34001
|
+
"security check",
|
|
34002
|
+
"ddos protection",
|
|
34003
|
+
"captcha"
|
|
34004
|
+
];
|
|
34005
|
+
var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
|
|
34006
|
+
function detectBlockedPage(page) {
|
|
34007
|
+
if (page.status !== null && page.status >= 400) {
|
|
34008
|
+
return {
|
|
34009
|
+
code: "HTTP_ERROR",
|
|
34010
|
+
message: `The site returned ${page.status} instead of the page \u2014 it may be blocking automated visits.`
|
|
34011
|
+
};
|
|
34012
|
+
}
|
|
34013
|
+
const haystack = `${page.title}
|
|
34014
|
+
${page.bodyText.slice(0, 2e3)}`.toLowerCase();
|
|
34015
|
+
const phrase = CHALLENGE_PHRASES.find((candidate) => haystack.includes(candidate));
|
|
34016
|
+
if (phrase) {
|
|
34017
|
+
return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
|
|
34018
|
+
}
|
|
34019
|
+
const html = page.html?.toLowerCase() ?? "";
|
|
34020
|
+
if (CHALLENGE_MARKERS.some((marker) => html.includes(marker))) {
|
|
34021
|
+
return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
|
|
34022
|
+
}
|
|
34023
|
+
return null;
|
|
34024
|
+
}
|
|
34025
|
+
var BlockedPageError = class extends Error {
|
|
34026
|
+
code;
|
|
34027
|
+
constructor(blocked) {
|
|
34028
|
+
super(blocked.message);
|
|
34029
|
+
this.name = "BlockedPageError";
|
|
34030
|
+
this.code = blocked.code;
|
|
34031
|
+
}
|
|
34032
|
+
};
|
|
34033
|
+
|
|
34034
|
+
// src/engine/landing-library/run.ts
|
|
34035
|
+
import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
|
|
34036
|
+
import path31 from "path";
|
|
34037
|
+
|
|
34038
|
+
// src/engine/landing-library/browser.ts
|
|
34039
|
+
import { createRequire } from "module";
|
|
34040
|
+
var require_ = createRequire(import.meta.url);
|
|
34041
|
+
var pwSpecifier = ["play", "wright"].join("");
|
|
34042
|
+
var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
|
|
34043
|
+
var MOBILE_VIEWPORT = { width: 390, height: 844 };
|
|
34044
|
+
var DEVICE_SCALE_FACTOR = 2;
|
|
34045
|
+
async function launchBrowser() {
|
|
34046
|
+
const playwright = require_(pwSpecifier);
|
|
34047
|
+
return await playwright.chromium.launch({
|
|
34048
|
+
headless: true,
|
|
34049
|
+
args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
|
|
34050
|
+
});
|
|
34051
|
+
}
|
|
34052
|
+
async function newPage(browser, viewport, opts = { motion: false }) {
|
|
34053
|
+
const context = await browser.newContext({
|
|
34054
|
+
viewport,
|
|
34055
|
+
deviceScaleFactor: DEVICE_SCALE_FACTOR,
|
|
34056
|
+
// Still captures freeze motion so screenshots are reproducible; the motion
|
|
34057
|
+
// pass re-opens a context with animation enabled.
|
|
34058
|
+
reducedMotion: opts.motion ? "no-preference" : "reduce",
|
|
34059
|
+
userAgent: viewport.width < 500 ? "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
|
34060
|
+
});
|
|
34061
|
+
const page = await context.newPage();
|
|
34062
|
+
return { context, page };
|
|
34063
|
+
}
|
|
34064
|
+
|
|
34065
|
+
// src/engine/landing-library/usedCss.ts
|
|
34066
|
+
async function collectUsedCss(page, selector) {
|
|
34067
|
+
const collected = await page.evaluate(inPageCollectUsedCss, selector);
|
|
34068
|
+
const extra = [];
|
|
34069
|
+
for (const href of collected.unreadableHrefs) {
|
|
34070
|
+
const text = await fetchStylesheet(page, href);
|
|
34071
|
+
if (text) extra.push(`/* ${href} */
|
|
34072
|
+
${text}`);
|
|
34073
|
+
}
|
|
34074
|
+
return { ...collected, css: [collected.css, ...extra].filter(Boolean).join("\n\n") };
|
|
34075
|
+
}
|
|
34076
|
+
async function fetchStylesheet(page, href) {
|
|
34077
|
+
try {
|
|
34078
|
+
const response = await page.request.get(href, { timeout: 1e4 });
|
|
34079
|
+
if (!response.ok()) return null;
|
|
34080
|
+
return await response.text();
|
|
34081
|
+
} catch {
|
|
34082
|
+
return null;
|
|
34083
|
+
}
|
|
34084
|
+
}
|
|
34085
|
+
var inPageCollectUsedCss = (selector) => {
|
|
34086
|
+
const root = document.querySelector(selector);
|
|
34087
|
+
const empty = {
|
|
34088
|
+
css: "",
|
|
34089
|
+
variables: "",
|
|
34090
|
+
inheritedSeed: "",
|
|
34091
|
+
unreadableHrefs: [],
|
|
34092
|
+
stats: { totalRules: 0, keptRules: 0, fontFaces: 0, keyframes: 0 }
|
|
34093
|
+
};
|
|
34094
|
+
if (!root) return empty;
|
|
34095
|
+
const INHERITED = [
|
|
34096
|
+
"color",
|
|
34097
|
+
"font-family",
|
|
34098
|
+
"font-size",
|
|
34099
|
+
"font-weight",
|
|
34100
|
+
"line-height",
|
|
34101
|
+
"letter-spacing",
|
|
34102
|
+
"text-align",
|
|
34103
|
+
"background-color",
|
|
34104
|
+
"-webkit-font-smoothing"
|
|
34105
|
+
];
|
|
34106
|
+
const unreadableHrefs = [];
|
|
34107
|
+
const kept = [];
|
|
34108
|
+
const fontFaces = [];
|
|
34109
|
+
const keyframesByName = /* @__PURE__ */ new Map();
|
|
34110
|
+
let totalRules = 0;
|
|
34111
|
+
let keptStyleRules = 0;
|
|
34112
|
+
const matchesInSection = (selectorText) => {
|
|
34113
|
+
for (const part of selectorText.split(",")) {
|
|
34114
|
+
const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
|
|
34115
|
+
if (!base) continue;
|
|
34116
|
+
try {
|
|
34117
|
+
if (root.matches(base) || root.querySelector(base)) return true;
|
|
34118
|
+
} catch {
|
|
34119
|
+
return true;
|
|
34120
|
+
}
|
|
34121
|
+
}
|
|
34122
|
+
return false;
|
|
34123
|
+
};
|
|
34124
|
+
const walk = (rules, sink) => {
|
|
34125
|
+
for (const rule of Array.from(rules)) {
|
|
34126
|
+
totalRules++;
|
|
34127
|
+
if (rule instanceof CSSStyleRule) {
|
|
34128
|
+
if (matchesInSection(rule.selectorText)) {
|
|
34129
|
+
sink.push(rule.cssText);
|
|
34130
|
+
keptStyleRules++;
|
|
34131
|
+
}
|
|
34132
|
+
continue;
|
|
34133
|
+
}
|
|
34134
|
+
if (rule instanceof CSSFontFaceRule) {
|
|
34135
|
+
fontFaces.push(rule.cssText);
|
|
34136
|
+
continue;
|
|
34137
|
+
}
|
|
34138
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
34139
|
+
keyframesByName.set(rule.name, rule.cssText);
|
|
34140
|
+
continue;
|
|
34141
|
+
}
|
|
34142
|
+
const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
|
|
34143
|
+
if (grouping) {
|
|
34144
|
+
const inner = [];
|
|
34145
|
+
walk(rule.cssRules, inner);
|
|
34146
|
+
if (inner.length === 0) continue;
|
|
34147
|
+
const condition = rule.conditionText ?? "";
|
|
34148
|
+
const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
|
|
34149
|
+
sink.push(`${prelude} {
|
|
34150
|
+
${inner.join("\n")}
|
|
34151
|
+
}`);
|
|
34152
|
+
}
|
|
34153
|
+
}
|
|
34154
|
+
};
|
|
34155
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
34156
|
+
const owner = sheet.ownerNode;
|
|
34157
|
+
if (owner?.hasAttribute?.("data-baker-freeze")) continue;
|
|
34158
|
+
try {
|
|
34159
|
+
walk(sheet.cssRules, kept);
|
|
34160
|
+
} catch {
|
|
34161
|
+
if (sheet.href) unreadableHrefs.push(sheet.href);
|
|
34162
|
+
}
|
|
34163
|
+
}
|
|
34164
|
+
const keptText = kept.join("\n");
|
|
34165
|
+
const usedKeyframes = [];
|
|
34166
|
+
for (const [name, text] of keyframesByName) {
|
|
34167
|
+
if (new RegExp(`(^|[\\s:,])${name}([\\s;,}]|$)`).test(keptText)) usedKeyframes.push(text);
|
|
34168
|
+
}
|
|
34169
|
+
const rootStyle = getComputedStyle(root);
|
|
34170
|
+
const variableDeclarations = [];
|
|
34171
|
+
for (const property of Array.from(rootStyle)) {
|
|
34172
|
+
if (!property.startsWith("--")) continue;
|
|
34173
|
+
const value = rootStyle.getPropertyValue(property).trim();
|
|
34174
|
+
if (value) variableDeclarations.push(` ${property}: ${value};`);
|
|
34175
|
+
}
|
|
34176
|
+
const seed = INHERITED.map((property) => ` ${property}: ${rootStyle.getPropertyValue(property)};`).join("\n");
|
|
34177
|
+
return {
|
|
34178
|
+
css: [...fontFaces, ...usedKeyframes, ...kept].join("\n"),
|
|
34179
|
+
variables: variableDeclarations.length ? `:root {
|
|
34180
|
+
${variableDeclarations.join("\n")}
|
|
34181
|
+
}` : "",
|
|
34182
|
+
inheritedSeed: seed,
|
|
34183
|
+
unreadableHrefs,
|
|
34184
|
+
stats: {
|
|
34185
|
+
totalRules,
|
|
34186
|
+
keptRules: keptStyleRules,
|
|
34187
|
+
fontFaces: fontFaces.length,
|
|
34188
|
+
keyframes: usedKeyframes.length
|
|
34189
|
+
}
|
|
34190
|
+
};
|
|
34191
|
+
};
|
|
34192
|
+
|
|
34193
|
+
// src/engine/landing-library/bundle.ts
|
|
34194
|
+
async function buildSectionBundle(page, selector, pageUrl) {
|
|
34195
|
+
const used = await collectUsedCss(page, selector);
|
|
34196
|
+
const extracted = await page.evaluate(inPageExtractMarkup, {
|
|
34197
|
+
selector,
|
|
34198
|
+
pageUrl,
|
|
34199
|
+
rootAttribute: SECTION_ROOT_ATTRIBUTE
|
|
34200
|
+
});
|
|
34201
|
+
const css = [
|
|
34202
|
+
"*, *::before, *::after { box-sizing: border-box; }",
|
|
34203
|
+
"html, body { margin: 0; padding: 0; }",
|
|
34204
|
+
used.variables,
|
|
34205
|
+
`body {
|
|
34206
|
+
${used.inheritedSeed}
|
|
34207
|
+
}`,
|
|
34208
|
+
used.css
|
|
34209
|
+
].filter(Boolean).join("\n\n");
|
|
34210
|
+
const html = `<!doctype html>
|
|
34211
|
+
<html ${extracted.rootAttributes}>
|
|
34212
|
+
<head>
|
|
34213
|
+
<meta charset="utf-8">
|
|
34214
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
34215
|
+
<title>Section from ${escapeHtml2(extracted.host)}</title>
|
|
34216
|
+
<style>
|
|
34217
|
+
${css}
|
|
34218
|
+
</style>
|
|
34219
|
+
</head>
|
|
34220
|
+
<body ${extracted.bodyAttributes}>
|
|
34221
|
+
${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
|
|
34222
|
+
</body>
|
|
34223
|
+
</html>
|
|
34224
|
+
`;
|
|
34225
|
+
return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
|
|
34226
|
+
}
|
|
34227
|
+
function escapeHtml2(value) {
|
|
34228
|
+
return value.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c] ?? c);
|
|
34229
|
+
}
|
|
34230
|
+
var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
|
|
34231
|
+
function wrapInLayoutContext(html, context) {
|
|
34232
|
+
let wrapped = context.width > 0 && context.width < context.viewportWidth ? `<div style="width:${context.width}px;margin:0 auto;">
|
|
34233
|
+
${html}
|
|
34234
|
+
</div>` : html;
|
|
34235
|
+
for (const ancestor of [...context.ancestors].reverse()) {
|
|
34236
|
+
const attributes = ancestor.attributes ? ` ${ancestor.attributes}` : "";
|
|
34237
|
+
const container = ancestor.containerType && ancestor.containerType !== "normal" ? `container-type:${ancestor.containerType};${ancestor.containerName && ancestor.containerName !== "none" ? `container-name:${ancestor.containerName};` : ""}` : "";
|
|
34238
|
+
wrapped = `<${ancestor.tag}${attributes} style="${NEUTRALIZED_ANCESTOR_STYLE}${container}">
|
|
34239
|
+
${wrapped}
|
|
34240
|
+
</${ancestor.tag}>`;
|
|
34241
|
+
}
|
|
34242
|
+
return wrapped;
|
|
34243
|
+
}
|
|
34244
|
+
var NEUTRALIZED_ANCESTOR_STYLE = [
|
|
34245
|
+
"display:block !important",
|
|
34246
|
+
"position:static !important",
|
|
34247
|
+
"margin:0 !important",
|
|
34248
|
+
"padding:0 !important",
|
|
34249
|
+
"border:0 !important",
|
|
34250
|
+
"width:auto !important",
|
|
34251
|
+
"min-width:0 !important",
|
|
34252
|
+
"max-width:none !important",
|
|
34253
|
+
"height:auto !important",
|
|
34254
|
+
"min-height:0 !important",
|
|
34255
|
+
"max-height:none !important",
|
|
34256
|
+
"transform:none !important",
|
|
34257
|
+
"overflow:visible !important",
|
|
34258
|
+
""
|
|
34259
|
+
].join(";");
|
|
34260
|
+
var inPageExtractMarkup = ({
|
|
34261
|
+
selector,
|
|
34262
|
+
pageUrl,
|
|
34263
|
+
rootAttribute
|
|
34264
|
+
}) => {
|
|
34265
|
+
const root = document.querySelector(selector);
|
|
34266
|
+
const emptyContext = { ancestors: [], width: 0, viewportWidth: 0 };
|
|
34267
|
+
const serializeAttributes = (element) => {
|
|
34268
|
+
if (!element) return "";
|
|
34269
|
+
return Array.from(element.attributes).filter((attribute) => attribute.name !== "style").map((attribute) => `${attribute.name}="${attribute.value.replace(/"/g, """)}"`).join(" ");
|
|
34270
|
+
};
|
|
34271
|
+
const rootAttributes = serializeAttributes(document.documentElement) || 'lang="en"';
|
|
34272
|
+
const bodyAttributes = serializeAttributes(document.body);
|
|
34273
|
+
if (!root) {
|
|
34274
|
+
return {
|
|
34275
|
+
html: "",
|
|
34276
|
+
assetUrls: [],
|
|
34277
|
+
host: "",
|
|
34278
|
+
layoutContext: emptyContext,
|
|
34279
|
+
rootAttributes,
|
|
34280
|
+
bodyAttributes
|
|
34281
|
+
};
|
|
34282
|
+
}
|
|
34283
|
+
const ancestors = [];
|
|
34284
|
+
for (let ancestor = root.parentElement; ancestor && ancestor !== document.body; ) {
|
|
34285
|
+
const style = getComputedStyle(ancestor);
|
|
34286
|
+
ancestors.unshift({
|
|
34287
|
+
tag: /^[a-zA-Z][a-zA-Z0-9-]*$/.test(ancestor.tagName) ? ancestor.tagName.toLowerCase() : "div",
|
|
34288
|
+
attributes: Array.from(ancestor.attributes).filter((a) => a.name === "class" || a.name === "id" || a.name.startsWith("data-")).map((a) => `${a.name}="${a.value.replace(/"/g, """)}"`).join(" "),
|
|
34289
|
+
containerType: style.containerType,
|
|
34290
|
+
containerName: style.containerName
|
|
34291
|
+
});
|
|
34292
|
+
ancestor = ancestor.parentElement;
|
|
34293
|
+
}
|
|
34294
|
+
const layoutContext = {
|
|
34295
|
+
ancestors,
|
|
34296
|
+
width: Math.round(root.getBoundingClientRect().width),
|
|
34297
|
+
viewportWidth: window.innerWidth
|
|
34298
|
+
};
|
|
34299
|
+
const absolute = (value) => {
|
|
34300
|
+
try {
|
|
34301
|
+
return new URL(value, pageUrl).href;
|
|
34302
|
+
} catch {
|
|
34303
|
+
return value;
|
|
34304
|
+
}
|
|
34305
|
+
};
|
|
34306
|
+
const clone = root.cloneNode(true);
|
|
34307
|
+
clone.setAttribute(rootAttribute, "");
|
|
34308
|
+
const assetUrls = /* @__PURE__ */ new Set();
|
|
34309
|
+
for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
|
|
34310
|
+
for (const attribute of ["src", "href", "poster"]) {
|
|
34311
|
+
const value = element.getAttribute(attribute);
|
|
34312
|
+
if (!value || value.startsWith("data:") || value.startsWith("#")) continue;
|
|
34313
|
+
const url = absolute(value);
|
|
34314
|
+
element.setAttribute(attribute, url);
|
|
34315
|
+
if (attribute !== "href" || element.tagName === "LINK") assetUrls.add(url);
|
|
34316
|
+
}
|
|
34317
|
+
const srcset = element.getAttribute("srcset");
|
|
34318
|
+
if (srcset) {
|
|
34319
|
+
element.setAttribute(
|
|
34320
|
+
"srcset",
|
|
34321
|
+
srcset.split(",").map((candidate) => {
|
|
34322
|
+
const [url, descriptor] = candidate.trim().split(/\s+/, 2);
|
|
34323
|
+
if (!url) return candidate;
|
|
34324
|
+
const resolved = absolute(url);
|
|
34325
|
+
assetUrls.add(resolved);
|
|
34326
|
+
return descriptor ? `${resolved} ${descriptor}` : resolved;
|
|
34327
|
+
}).join(", ")
|
|
34328
|
+
);
|
|
34329
|
+
}
|
|
34330
|
+
const style = element.getAttribute("style");
|
|
34331
|
+
if (style?.includes("url(")) {
|
|
34332
|
+
element.setAttribute(
|
|
34333
|
+
"style",
|
|
34334
|
+
style.replace(/url\((['"]?)([^'")]+)\1\)/g, (_match, quote, url) => {
|
|
34335
|
+
if (url.startsWith("data:")) return `url(${quote}${url}${quote})`;
|
|
34336
|
+
const resolved = absolute(url);
|
|
34337
|
+
assetUrls.add(resolved);
|
|
34338
|
+
return `url(${quote}${resolved}${quote})`;
|
|
34339
|
+
})
|
|
34340
|
+
);
|
|
34341
|
+
}
|
|
34342
|
+
}
|
|
34343
|
+
return {
|
|
34344
|
+
html: clone.outerHTML,
|
|
34345
|
+
assetUrls: Array.from(assetUrls),
|
|
34346
|
+
host: location.host,
|
|
34347
|
+
layoutContext,
|
|
34348
|
+
rootAttributes,
|
|
34349
|
+
bodyAttributes
|
|
34350
|
+
};
|
|
34351
|
+
};
|
|
34352
|
+
|
|
34353
|
+
// src/engine/landing-library/capture.ts
|
|
34354
|
+
async function captureSection(page, section) {
|
|
34355
|
+
const locator = page.locator(section.selector).first();
|
|
34356
|
+
if (await locator.count().catch(() => 0)) {
|
|
34357
|
+
const shot = await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
|
|
34358
|
+
if (shot) return shot;
|
|
34359
|
+
}
|
|
34360
|
+
const doc = await page.evaluate(() => ({
|
|
34361
|
+
width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
34362
|
+
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
|
|
34363
|
+
}));
|
|
34364
|
+
const width = Math.min(section.rect.width, doc.width - section.rect.left);
|
|
34365
|
+
const height = Math.min(section.rect.height, doc.height - section.rect.top);
|
|
34366
|
+
if (width <= 0 || height <= 0) return null;
|
|
34367
|
+
return await page.screenshot({
|
|
34368
|
+
type: "png",
|
|
34369
|
+
clip: { x: section.rect.left, y: section.rect.top, width, height },
|
|
34370
|
+
timeout: 15e3
|
|
34371
|
+
}).catch(() => null);
|
|
34372
|
+
}
|
|
34373
|
+
async function captureSectionOnMobile(page, section) {
|
|
34374
|
+
const locator = page.locator(section.selector).first();
|
|
34375
|
+
if (!await locator.count().catch(() => 0)) return null;
|
|
34376
|
+
if (!await locator.isVisible().catch(() => false)) return null;
|
|
34377
|
+
return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
|
|
34378
|
+
}
|
|
34379
|
+
|
|
34380
|
+
// src/engine/landing-library/fidelity.ts
|
|
34381
|
+
import sharp3 from "sharp";
|
|
34382
|
+
var COMPARISON_SIZE = 32;
|
|
34383
|
+
function pixelSimilarity(a, b) {
|
|
34384
|
+
const length = Math.min(a.length, b.length);
|
|
34385
|
+
if (length === 0) return 0;
|
|
34386
|
+
let total = 0;
|
|
34387
|
+
for (let i = 0; i < length; i++) {
|
|
34388
|
+
total += Math.abs((a[i] ?? 0) - (b[i] ?? 0));
|
|
34389
|
+
}
|
|
34390
|
+
return 1 - total / (length * 255);
|
|
34391
|
+
}
|
|
34392
|
+
async function scoreFidelity(live, rendered) {
|
|
34393
|
+
const [liveMeta, renderedMeta] = await Promise.all([sharp3(live).metadata(), sharp3(rendered).metadata()]);
|
|
34394
|
+
const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
|
|
34395
|
+
const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
|
|
34396
|
+
const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
|
|
34397
|
+
const score = pixelSimilarity(livePixels, renderedPixels);
|
|
34398
|
+
const heightRatio = liveSize.height > 0 && renderedSize.height > 0 ? Math.min(liveSize.height, renderedSize.height) / Math.max(liveSize.height, renderedSize.height) : 0;
|
|
34399
|
+
return {
|
|
34400
|
+
score,
|
|
34401
|
+
liveSize,
|
|
34402
|
+
renderedSize,
|
|
34403
|
+
...heightRatio < 0.8 ? { note: `height differs by ${Math.round((1 - heightRatio) * 100)}% \u2014 the bundle reflowed` } : {}
|
|
34404
|
+
};
|
|
34405
|
+
}
|
|
34406
|
+
async function toGreyGrid(image) {
|
|
34407
|
+
const raw = await sharp3(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
|
|
34408
|
+
return new Uint8Array(raw);
|
|
34409
|
+
}
|
|
34410
|
+
|
|
34411
|
+
// src/engine/landing-library/motion.ts
|
|
34412
|
+
var EMPTY_MOTION = {
|
|
34413
|
+
hasMotion: false,
|
|
34414
|
+
// `none` rather than "no motion": it is the `MOTION_KINDS` member for this,
|
|
34415
|
+
// so a section with no movement is findable by the same facet as everything
|
|
34416
|
+
// else instead of being a special case only prose describes.
|
|
34417
|
+
summary: "none",
|
|
34418
|
+
entrance: [],
|
|
34419
|
+
hover: [],
|
|
34420
|
+
scroll: [],
|
|
34421
|
+
loop: [],
|
|
34422
|
+
libraries: [],
|
|
34423
|
+
respectsReducedMotion: false
|
|
34424
|
+
};
|
|
34425
|
+
function isWorthFilming(motion) {
|
|
34426
|
+
return motion.entrance.length + motion.scroll.length + motion.loop.length > 0;
|
|
34427
|
+
}
|
|
34428
|
+
async function collectMotion(page, selector) {
|
|
34429
|
+
const raw = await page.evaluate(inPageCollectMotion, selector).catch(() => null);
|
|
34430
|
+
if (!raw) return EMPTY_MOTION;
|
|
34431
|
+
return { ...raw, summary: summarizeMotion(raw) };
|
|
34432
|
+
}
|
|
34433
|
+
var HOVER_TRANSFORMS = /* @__PURE__ */ new Set(["lift", "grow", "tilt", "shift", "spin", "shadow", "animate"]);
|
|
34434
|
+
var KEYFRAME_PATTERNS = [
|
|
34435
|
+
[/marquee|ticker|scroll(ing)?-?(x|left|right)/, "marquee"],
|
|
34436
|
+
[/parallax/, "parallax"],
|
|
34437
|
+
[/sticky|pin(ned)?/, "sticky-pin"],
|
|
34438
|
+
[/count(er|up)/, "counter"],
|
|
34439
|
+
[/zoom|scale|pulse|grow/, "zoom"],
|
|
34440
|
+
[/fade|opacity|appear/, "fade"],
|
|
34441
|
+
[/slide|translate|in-?(left|right|up|down)/, "slide-in"]
|
|
34442
|
+
];
|
|
34443
|
+
function toMotionKinds(kind, channel) {
|
|
34444
|
+
if (channel === "hover") return [HOVER_TRANSFORMS.has(kind) ? "hover-lift" : "fade"];
|
|
34445
|
+
const name = kind.toLowerCase();
|
|
34446
|
+
const matched = KEYFRAME_PATTERNS.filter(([pattern]) => pattern.test(name)).map(([, motionKind]) => motionKind);
|
|
34447
|
+
if (matched.length > 0) return matched;
|
|
34448
|
+
return [channel === "loop" ? "marquee" : "scroll-reveal"];
|
|
34449
|
+
}
|
|
34450
|
+
function motionKindsOf(motion) {
|
|
34451
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
34452
|
+
const collect = (effects, channel) => {
|
|
34453
|
+
for (const effect of effects) {
|
|
34454
|
+
for (const kind of toMotionKinds(effect.kind, channel)) kinds.add(kind);
|
|
34455
|
+
}
|
|
34456
|
+
};
|
|
34457
|
+
collect(motion.entrance, "entrance");
|
|
34458
|
+
collect(motion.hover, "hover");
|
|
34459
|
+
collect(motion.scroll, "scroll");
|
|
34460
|
+
collect(motion.loop, "loop");
|
|
34461
|
+
const delays = new Set(motion.entrance.map((effect) => effect.delayMs ?? 0).filter((ms) => ms > 0));
|
|
34462
|
+
if (delays.size > 1) kinds.add("stagger");
|
|
34463
|
+
return [...kinds];
|
|
34464
|
+
}
|
|
34465
|
+
function summarizeMotion(motion) {
|
|
34466
|
+
const kinds = motionKindsOf(motion);
|
|
34467
|
+
if (kinds.length === 0 && motion.libraries.length === 0) return "none";
|
|
34468
|
+
const parts = [...kinds];
|
|
34469
|
+
if (motion.libraries.length > 0) parts.push(`via ${motion.libraries.join("/")}`);
|
|
34470
|
+
return parts.join(", ") + (motion.respectsReducedMotion ? "" : " (ignores reduced-motion)");
|
|
34471
|
+
}
|
|
34472
|
+
var inPageCollectMotion = (selector) => {
|
|
34473
|
+
const root = document.querySelector(selector);
|
|
34474
|
+
const empty = {
|
|
34475
|
+
hasMotion: false,
|
|
34476
|
+
entrance: [],
|
|
34477
|
+
hover: [],
|
|
34478
|
+
scroll: [],
|
|
34479
|
+
loop: [],
|
|
34480
|
+
libraries: [],
|
|
34481
|
+
respectsReducedMotion: false
|
|
34482
|
+
};
|
|
34483
|
+
if (!root) return empty;
|
|
34484
|
+
const entrance = [];
|
|
34485
|
+
const hover = [];
|
|
34486
|
+
const scroll = [];
|
|
34487
|
+
const loop = [];
|
|
34488
|
+
const keyframesByName = /* @__PURE__ */ new Map();
|
|
34489
|
+
let respectsReducedMotion = false;
|
|
34490
|
+
const inSection = (selectorText) => {
|
|
34491
|
+
for (const part of selectorText.split(",")) {
|
|
34492
|
+
const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
|
|
34493
|
+
if (!base) continue;
|
|
34494
|
+
try {
|
|
34495
|
+
if (root.matches(base) || root.querySelector(base)) return true;
|
|
34496
|
+
} catch {
|
|
34497
|
+
return true;
|
|
34498
|
+
}
|
|
34499
|
+
}
|
|
34500
|
+
return false;
|
|
34501
|
+
};
|
|
34502
|
+
const toMs = (value) => {
|
|
34503
|
+
const first = value.split(",")[0]?.trim() ?? "";
|
|
34504
|
+
if (first.endsWith("ms")) return Number.parseFloat(first);
|
|
34505
|
+
if (first.endsWith("s")) return Number.parseFloat(first) * 1e3;
|
|
34506
|
+
return void 0;
|
|
34507
|
+
};
|
|
34508
|
+
const classifyKeyframes = (body) => {
|
|
34509
|
+
const text = body.toLowerCase();
|
|
34510
|
+
const fades = /opacity\s*:\s*0(\.0+)?\s*[;}]/.test(text);
|
|
34511
|
+
if (/translatey\(\s*-?\d/.test(text)) {
|
|
34512
|
+
const upward = /translatey\(\s*(\d|\.)/.test(text);
|
|
34513
|
+
return fades ? upward ? "fade-up" : "fade-down" : upward ? "slide-up" : "slide-down";
|
|
34514
|
+
}
|
|
34515
|
+
if (/translatex\(\s*-?\d/.test(text)) return fades ? "fade-in-x" : "slide-in-x";
|
|
34516
|
+
if (/scale\(/.test(text)) return fades ? "fade-zoom" : "zoom";
|
|
34517
|
+
if (/rotate\(/.test(text)) return "spin";
|
|
34518
|
+
if (fades) return "fade";
|
|
34519
|
+
return "animate";
|
|
34520
|
+
};
|
|
34521
|
+
const classifyHover = (style) => {
|
|
34522
|
+
const transform = style.transform ?? "";
|
|
34523
|
+
if (/translatey\(\s*-/i.test(transform)) return "lift";
|
|
34524
|
+
if (/scale\(\s*(1\.\d|[2-9])/i.test(transform)) return "grow";
|
|
34525
|
+
if (/rotate\(/i.test(transform)) return "tilt";
|
|
34526
|
+
if (transform && transform !== "none") return "shift";
|
|
34527
|
+
if (style.boxShadow) return "shadow";
|
|
34528
|
+
if (style.opacity) return "dim";
|
|
34529
|
+
if (style.backgroundColor || style.color) return "recolor";
|
|
34530
|
+
return null;
|
|
34531
|
+
};
|
|
34532
|
+
const readStyleRule = (rule, insideScrollTimeline) => {
|
|
34533
|
+
const style = rule.style;
|
|
34534
|
+
const isHover = /:hover\b/.test(rule.selectorText);
|
|
34535
|
+
if (isHover) {
|
|
34536
|
+
const kind = classifyHover(style);
|
|
34537
|
+
if (!kind || !inSection(rule.selectorText)) return;
|
|
34538
|
+
hover.push({
|
|
34539
|
+
kind,
|
|
34540
|
+
selector: rule.selectorText.slice(0, 120),
|
|
34541
|
+
durationMs: toMs(style.transitionDuration ?? ""),
|
|
34542
|
+
easing: style.transitionTimingFunction || void 0
|
|
34543
|
+
});
|
|
34544
|
+
return;
|
|
34545
|
+
}
|
|
34546
|
+
const animationName = style.animationName;
|
|
34547
|
+
if (!animationName || animationName === "none") return;
|
|
34548
|
+
if (!inSection(rule.selectorText)) return;
|
|
34549
|
+
const effect = {
|
|
34550
|
+
kind: animationName,
|
|
34551
|
+
selector: rule.selectorText.slice(0, 120),
|
|
34552
|
+
durationMs: toMs(style.animationDuration ?? ""),
|
|
34553
|
+
delayMs: toMs(style.animationDelay ?? ""),
|
|
34554
|
+
easing: style.animationTimingFunction || void 0
|
|
34555
|
+
};
|
|
34556
|
+
const infinite = (style.animationIterationCount ?? "").includes("infinite");
|
|
34557
|
+
const scrollDriven = insideScrollTimeline || Boolean(style.getPropertyValue("animation-timeline"));
|
|
34558
|
+
if (scrollDriven) scroll.push(effect);
|
|
34559
|
+
else if (infinite) loop.push(effect);
|
|
34560
|
+
else entrance.push(effect);
|
|
34561
|
+
};
|
|
34562
|
+
const walk = (rules, insideScrollTimeline) => {
|
|
34563
|
+
for (const rule of Array.from(rules)) {
|
|
34564
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
34565
|
+
keyframesByName.set(rule.name, rule.cssText);
|
|
34566
|
+
continue;
|
|
34567
|
+
}
|
|
34568
|
+
if (rule instanceof CSSStyleRule) {
|
|
34569
|
+
try {
|
|
34570
|
+
readStyleRule(rule, insideScrollTimeline);
|
|
34571
|
+
} catch {
|
|
34572
|
+
}
|
|
34573
|
+
continue;
|
|
34574
|
+
}
|
|
34575
|
+
if (rule instanceof CSSMediaRule) {
|
|
34576
|
+
if (rule.conditionText.includes("prefers-reduced-motion")) {
|
|
34577
|
+
respectsReducedMotion = true;
|
|
34578
|
+
continue;
|
|
34579
|
+
}
|
|
34580
|
+
walk(rule.cssRules, insideScrollTimeline);
|
|
34581
|
+
continue;
|
|
34582
|
+
}
|
|
34583
|
+
const grouping = rule;
|
|
34584
|
+
if (grouping.cssRules) walk(grouping.cssRules, insideScrollTimeline);
|
|
34585
|
+
}
|
|
34586
|
+
};
|
|
34587
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
34588
|
+
if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
|
|
34589
|
+
try {
|
|
34590
|
+
walk(sheet.cssRules, false);
|
|
34591
|
+
} catch {
|
|
34592
|
+
}
|
|
34593
|
+
}
|
|
34594
|
+
for (const effect of [...entrance, ...loop, ...scroll]) {
|
|
34595
|
+
const body = keyframesByName.get(effect.kind);
|
|
34596
|
+
if (!body) continue;
|
|
34597
|
+
const classified = classifyKeyframes(body);
|
|
34598
|
+
effect.kind = loop.includes(effect) && classified.startsWith("slide") ? "marquee" : classified;
|
|
34599
|
+
}
|
|
34600
|
+
const libraries = [];
|
|
34601
|
+
const scoped = window ?? {};
|
|
34602
|
+
if (scoped.gsap || document.querySelector("[data-gsap]")) libraries.push("gsap");
|
|
34603
|
+
if (document.querySelector("[data-framer-name], [data-projection-id]")) libraries.push("framer-motion");
|
|
34604
|
+
if (document.querySelector("[data-aos]")) libraries.push("aos");
|
|
34605
|
+
if (scoped.Lenis || document.querySelector("[data-lenis]")) libraries.push("lenis");
|
|
34606
|
+
if (document.querySelector("[data-scroll], [data-scroll-container]")) libraries.push("locomotive");
|
|
34607
|
+
return {
|
|
34608
|
+
hasMotion: entrance.length + hover.length + scroll.length + loop.length + libraries.length > 0,
|
|
34609
|
+
entrance: entrance.slice(0, 12),
|
|
34610
|
+
hover: hover.slice(0, 12),
|
|
34611
|
+
scroll: scroll.slice(0, 12),
|
|
34612
|
+
loop: loop.slice(0, 12),
|
|
34613
|
+
libraries,
|
|
34614
|
+
respectsReducedMotion
|
|
34615
|
+
};
|
|
34616
|
+
};
|
|
34617
|
+
|
|
34618
|
+
// src/engine/landing-library/motionTake.ts
|
|
34619
|
+
import sharp4 from "sharp";
|
|
34620
|
+
|
|
34621
|
+
// src/engine/landing-library/prepare.ts
|
|
34622
|
+
function isNavigationTeardown(error) {
|
|
34623
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
34624
|
+
return message.includes("Execution context was destroyed") || message.includes("Cannot find context with specified id") || message.includes("frame was detached") || message.includes("has been closed");
|
|
34625
|
+
}
|
|
34626
|
+
var CONSENT_SELECTORS = [
|
|
34627
|
+
"#onetrust-accept-btn-handler",
|
|
34628
|
+
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
|
|
34629
|
+
"button#didomi-notice-agree-button",
|
|
34630
|
+
"[aria-label='Accept all']",
|
|
34631
|
+
"[data-testid='uc-accept-all-button']",
|
|
34632
|
+
".cc-allow",
|
|
34633
|
+
".cookie-accept"
|
|
34634
|
+
];
|
|
34635
|
+
var CONSENT_TEXTS = ["Accept all", "Accept All", "Allow all", "I agree", "Got it", "Aceptar todo"];
|
|
34636
|
+
var CONSENT_HOSTS = [
|
|
34637
|
+
"transcend-cdn.com",
|
|
34638
|
+
"cookielaw.org",
|
|
34639
|
+
"onetrust.com",
|
|
34640
|
+
"cookiebot.com",
|
|
34641
|
+
"osano.com",
|
|
34642
|
+
"trustarc.com",
|
|
34643
|
+
"truste.com",
|
|
34644
|
+
"usercentrics.eu",
|
|
34645
|
+
"didomi.io",
|
|
34646
|
+
"privacy-center.org",
|
|
34647
|
+
"iubenda.com",
|
|
34648
|
+
"termly.io",
|
|
34649
|
+
"cookieyes.com",
|
|
34650
|
+
"sp-prod.net",
|
|
34651
|
+
"quantcast.com",
|
|
34652
|
+
"consensu.org",
|
|
34653
|
+
"ketch.com",
|
|
34654
|
+
"secureprivacy.ai",
|
|
34655
|
+
"civicuk.com"
|
|
34656
|
+
];
|
|
34657
|
+
async function blockConsentManagers(page) {
|
|
34658
|
+
await page.route("**/*", (route) => {
|
|
34659
|
+
let host = "";
|
|
34660
|
+
try {
|
|
34661
|
+
host = new URL(route.request().url()).host;
|
|
34662
|
+
} catch {
|
|
34663
|
+
return route.continue();
|
|
34664
|
+
}
|
|
34665
|
+
const isConsentVendor = CONSENT_HOSTS.some((vendor) => host === vendor || host.endsWith(`.${vendor}`));
|
|
34666
|
+
return isConsentVendor ? route.abort() : route.continue();
|
|
34667
|
+
});
|
|
34668
|
+
}
|
|
34669
|
+
var ignore = () => void 0;
|
|
34670
|
+
async function preparePage(page, url, timeoutMs) {
|
|
34671
|
+
const documentStatus = trackMainDocumentStatus(page);
|
|
34672
|
+
try {
|
|
34673
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
|
|
34674
|
+
await settleNavigation(page);
|
|
34675
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(ignore);
|
|
34676
|
+
await dismissConsent(page, 4e3);
|
|
34677
|
+
await scrollThroughPage(page);
|
|
34678
|
+
await dismissConsent(page, 1e3);
|
|
34679
|
+
await acrossNavigation(
|
|
34680
|
+
page,
|
|
34681
|
+
() => page.evaluate(async () => {
|
|
34682
|
+
await document.fonts.ready;
|
|
34683
|
+
})
|
|
34684
|
+
);
|
|
34685
|
+
await freezeMotion(page);
|
|
34686
|
+
await unpinOverlays(page);
|
|
34687
|
+
const measured = await acrossNavigation(
|
|
34688
|
+
page,
|
|
34689
|
+
() => page.evaluate(() => ({
|
|
34690
|
+
finalUrl: location.href,
|
|
34691
|
+
title: document.title,
|
|
34692
|
+
documentHeight: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
34693
|
+
bodyText: (document.body?.innerText ?? "").slice(0, 2e3)
|
|
34694
|
+
}))
|
|
34695
|
+
);
|
|
34696
|
+
return { ...measured, status: documentStatus.last() ?? response?.status() ?? null };
|
|
34697
|
+
} finally {
|
|
34698
|
+
documentStatus.stop();
|
|
34699
|
+
}
|
|
34700
|
+
}
|
|
34701
|
+
function trackMainDocumentStatus(page) {
|
|
34702
|
+
let status = null;
|
|
34703
|
+
const onResponse = (response) => {
|
|
34704
|
+
const request = response.request();
|
|
34705
|
+
if (request.resourceType() !== "document" || request.frame() !== page.mainFrame()) return;
|
|
34706
|
+
const code = response.status();
|
|
34707
|
+
if (code >= 300 && code < 400) return;
|
|
34708
|
+
status = code;
|
|
34709
|
+
};
|
|
34710
|
+
page.on("response", onResponse);
|
|
34711
|
+
return { last: () => status, stop: () => page.off("response", onResponse) };
|
|
34712
|
+
}
|
|
34713
|
+
var NAVIGATION_QUIET_MS = 1500;
|
|
34714
|
+
var NAVIGATION_SETTLE_BUDGET_MS = 2e4;
|
|
34715
|
+
async function settleNavigation(page, budgetMs = NAVIGATION_SETTLE_BUDGET_MS) {
|
|
34716
|
+
let lastNavigatedAt = Date.now();
|
|
34717
|
+
const onNavigated = (frame) => {
|
|
34718
|
+
if (frame === page.mainFrame()) lastNavigatedAt = Date.now();
|
|
34719
|
+
};
|
|
34720
|
+
page.on("framenavigated", onNavigated);
|
|
34721
|
+
try {
|
|
34722
|
+
const deadline = Date.now() + budgetMs;
|
|
34723
|
+
while (Date.now() < deadline && Date.now() - lastNavigatedAt < NAVIGATION_QUIET_MS) {
|
|
34724
|
+
await page.waitForTimeout(250);
|
|
34725
|
+
}
|
|
34726
|
+
} finally {
|
|
34727
|
+
page.off("framenavigated", onNavigated);
|
|
34728
|
+
}
|
|
34729
|
+
}
|
|
34730
|
+
async function acrossNavigation(page, run) {
|
|
34731
|
+
try {
|
|
34732
|
+
return await run();
|
|
34733
|
+
} catch (error) {
|
|
34734
|
+
if (!isNavigationTeardown(error)) throw error;
|
|
34735
|
+
await settleNavigation(page);
|
|
34736
|
+
return await run();
|
|
34737
|
+
}
|
|
34738
|
+
}
|
|
34739
|
+
async function settleConsent(page) {
|
|
34740
|
+
await dismissConsent(page, 4e3);
|
|
34741
|
+
await page.mouse.wheel(0, 400).catch(ignore);
|
|
34742
|
+
await page.waitForTimeout(1500);
|
|
34743
|
+
await dismissConsent(page, 2e3);
|
|
34744
|
+
await page.evaluate(() => window.scrollTo(0, 0)).catch(ignore);
|
|
34745
|
+
}
|
|
34746
|
+
async function dismissConsent(page, waitForBannerMs) {
|
|
34747
|
+
await page.locator(CONSENT_SELECTORS.join(", ")).first().waitFor({ state: "attached", timeout: waitForBannerMs }).catch(ignore);
|
|
34748
|
+
for (const selector of CONSENT_SELECTORS) {
|
|
34749
|
+
const found = page.locator(selector).first();
|
|
34750
|
+
if (!await found.count().catch(() => 0)) continue;
|
|
34751
|
+
await found.click({ timeout: 2e3, force: true }).catch(ignore);
|
|
34752
|
+
await page.waitForTimeout(400);
|
|
34753
|
+
return;
|
|
34754
|
+
}
|
|
34755
|
+
for (const text of CONSENT_TEXTS) {
|
|
34756
|
+
const button = page.getByRole("button", { name: text, exact: false }).first();
|
|
34757
|
+
if (!await button.count().catch(() => 0)) continue;
|
|
34758
|
+
if (!await button.isVisible().catch(() => false)) continue;
|
|
34759
|
+
await button.click({ timeout: 2e3, force: true }).catch(ignore);
|
|
34760
|
+
await page.waitForTimeout(400);
|
|
34761
|
+
return;
|
|
34762
|
+
}
|
|
34763
|
+
}
|
|
34764
|
+
async function scrollThroughPage(page) {
|
|
34765
|
+
await acrossNavigation(
|
|
34766
|
+
page,
|
|
34767
|
+
() => page.evaluate(async () => {
|
|
34768
|
+
const step = 600;
|
|
34769
|
+
const pause = () => new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
34770
|
+
for (let i = 0; i < 40; i++) {
|
|
34771
|
+
window.scrollBy(0, step);
|
|
34772
|
+
await pause();
|
|
34773
|
+
const reachedBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
34774
|
+
if (reachedBottom) break;
|
|
34775
|
+
}
|
|
34776
|
+
window.scrollTo(0, 0);
|
|
34777
|
+
await pause();
|
|
34778
|
+
})
|
|
34779
|
+
);
|
|
34780
|
+
await page.waitForTimeout(500);
|
|
34781
|
+
}
|
|
34782
|
+
var MAX_HEADER_HEIGHT = 220;
|
|
34783
|
+
function inPageUnpinOverlays(maxHeaderHeight) {
|
|
34784
|
+
window.scrollTo(0, 0);
|
|
34785
|
+
for (const element of Array.from(document.querySelectorAll("*"))) {
|
|
34786
|
+
const position = getComputedStyle(element).position;
|
|
34787
|
+
if (position === "sticky") {
|
|
34788
|
+
element.style.setProperty("position", "static", "important");
|
|
34789
|
+
continue;
|
|
34790
|
+
}
|
|
34791
|
+
if (position !== "fixed") continue;
|
|
34792
|
+
const rect = element.getBoundingClientRect();
|
|
34793
|
+
const isTopAnchoredHeader = rect.top <= 8 && rect.height > 0 && rect.height <= maxHeaderHeight;
|
|
34794
|
+
if (isTopAnchoredHeader) {
|
|
34795
|
+
element.style.setProperty("position", "absolute", "important");
|
|
34796
|
+
element.style.setProperty("bottom", "auto", "important");
|
|
34797
|
+
} else {
|
|
34798
|
+
element.style.setProperty("display", "none", "important");
|
|
34799
|
+
}
|
|
34800
|
+
}
|
|
34801
|
+
}
|
|
34802
|
+
async function unpinOverlays(page) {
|
|
34803
|
+
await acrossNavigation(page, () => page.evaluate(inPageUnpinOverlays, MAX_HEADER_HEIGHT));
|
|
34804
|
+
await page.waitForTimeout(250);
|
|
34805
|
+
}
|
|
34806
|
+
async function freezeMotion(page) {
|
|
34807
|
+
await acrossNavigation(
|
|
34808
|
+
page,
|
|
34809
|
+
() => page.evaluate(() => {
|
|
34810
|
+
const highestTimer = window.setTimeout(() => void 0, 0);
|
|
34811
|
+
for (let id = 1; id <= highestTimer; id++) window.clearInterval(id);
|
|
34812
|
+
const style = document.createElement("style");
|
|
34813
|
+
style.setAttribute("data-baker-freeze", "true");
|
|
34814
|
+
style.textContent = `*, *::before, *::after {
|
|
34815
|
+
animation-play-state: paused !important;
|
|
34816
|
+
animation-delay: 0s !important;
|
|
34817
|
+
transition: none !important;
|
|
34818
|
+
}`;
|
|
34819
|
+
document.head.appendChild(style);
|
|
34820
|
+
for (const video of Array.from(document.querySelectorAll("video"))) {
|
|
34821
|
+
video.pause();
|
|
34822
|
+
}
|
|
34823
|
+
})
|
|
34824
|
+
);
|
|
34825
|
+
await page.waitForTimeout(250);
|
|
34826
|
+
}
|
|
34827
|
+
|
|
34828
|
+
// src/engine/landing-library/geometry.ts
|
|
34829
|
+
function adaptiveGapThreshold(bands) {
|
|
34830
|
+
const sorted = [...bands].sort((a, b) => a.top - b.top);
|
|
34831
|
+
const gaps = [];
|
|
34832
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
34833
|
+
const current = sorted[i];
|
|
34834
|
+
const next = sorted[i + 1];
|
|
34835
|
+
if (!current || !next) continue;
|
|
34836
|
+
if (next.top > current.bottom) {
|
|
34837
|
+
const gap = next.top - current.bottom;
|
|
34838
|
+
if (gap < 200) gaps.push(gap);
|
|
34839
|
+
}
|
|
34840
|
+
}
|
|
34841
|
+
gaps.sort((a, b) => a - b);
|
|
34842
|
+
const p75 = gaps[Math.floor(gaps.length * 0.75)];
|
|
34843
|
+
if (p75 === void 0) return 15;
|
|
34844
|
+
return Math.max(10, Math.min(50, p75));
|
|
34845
|
+
}
|
|
34846
|
+
function bandsCollide(a, b, margin, maxGap) {
|
|
34847
|
+
const shrunkA = { top: a.top + margin, bottom: a.bottom - margin };
|
|
34848
|
+
const shrunkB = { top: b.top + margin, bottom: b.bottom - margin };
|
|
34849
|
+
const overlaps = shrunkA.top <= shrunkB.bottom && shrunkA.bottom >= shrunkB.top || shrunkB.top <= shrunkA.bottom && shrunkB.bottom >= shrunkA.top;
|
|
34850
|
+
if (overlaps) return true;
|
|
34851
|
+
const gap = Math.min(Math.abs(shrunkA.bottom - shrunkB.top), Math.abs(shrunkB.bottom - shrunkA.top));
|
|
34852
|
+
return gap <= maxGap;
|
|
34853
|
+
}
|
|
34854
|
+
function shouldPromoteToParent(group) {
|
|
34855
|
+
const isRunt = group.childCount < 3 || group.height < 80;
|
|
34856
|
+
return isRunt && group.parentHeight < 1600;
|
|
34857
|
+
}
|
|
34858
|
+
|
|
34859
|
+
// src/engine/landing-library/segment.ts
|
|
34860
|
+
async function installPageRuntime(page) {
|
|
34861
|
+
await page.addInitScript({
|
|
34862
|
+
content: `
|
|
34863
|
+
window.__name = window.__name || function (target) { return target; };
|
|
34864
|
+
window.__bakerGeom = {
|
|
34865
|
+
adaptiveGapThreshold: ${adaptiveGapThreshold.toString()},
|
|
34866
|
+
bandsCollide: ${bandsCollide.toString()},
|
|
34867
|
+
shouldPromoteToParent: ${shouldPromoteToParent.toString()},
|
|
34868
|
+
};`
|
|
34869
|
+
});
|
|
34870
|
+
}
|
|
34871
|
+
async function segmentPage(page, options) {
|
|
34872
|
+
return await page.evaluate(inPageSegment, options);
|
|
34873
|
+
}
|
|
34874
|
+
var inPageSegment = (options) => {
|
|
34875
|
+
const geom = window.__bakerGeom;
|
|
34876
|
+
const scrollX = window.scrollX;
|
|
34877
|
+
const scrollY = window.scrollY;
|
|
34878
|
+
const docWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0);
|
|
34879
|
+
const docHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0);
|
|
34880
|
+
const rectOf = (element) => {
|
|
34881
|
+
const r = element.getBoundingClientRect();
|
|
34882
|
+
const top = Math.max(0, Math.round(r.top + scrollY));
|
|
34883
|
+
const left = Math.max(0, Math.round(r.left + scrollX));
|
|
34884
|
+
return {
|
|
34885
|
+
top,
|
|
34886
|
+
left,
|
|
34887
|
+
bottom: Math.min(docHeight, Math.round(r.bottom + scrollY)),
|
|
34888
|
+
right: Math.min(docWidth, Math.round(r.right + scrollX))
|
|
34889
|
+
};
|
|
34890
|
+
};
|
|
34891
|
+
const syntheticRect = (element) => {
|
|
34892
|
+
const children = Array.from(element.children);
|
|
34893
|
+
if (children.length === 0) return null;
|
|
34894
|
+
let top = Number.POSITIVE_INFINITY;
|
|
34895
|
+
let left = Number.POSITIVE_INFINITY;
|
|
34896
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
34897
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
34898
|
+
for (const child of children) {
|
|
34899
|
+
const r = rectOf(child);
|
|
34900
|
+
if (r.bottom - r.top <= 0 && r.right - r.left <= 0) continue;
|
|
34901
|
+
top = Math.min(top, r.top);
|
|
34902
|
+
left = Math.min(left, r.left);
|
|
34903
|
+
bottom = Math.max(bottom, r.bottom);
|
|
34904
|
+
right = Math.max(right, r.right);
|
|
34905
|
+
}
|
|
34906
|
+
if (!Number.isFinite(top) || !Number.isFinite(bottom)) return null;
|
|
34907
|
+
return { top, left, bottom, right };
|
|
34908
|
+
};
|
|
34909
|
+
const boxOf = (element) => {
|
|
34910
|
+
const style = getComputedStyle(element);
|
|
34911
|
+
const r = style.display === "contents" ? syntheticRect(element) : rectOf(element);
|
|
34912
|
+
if (!r) return null;
|
|
34913
|
+
return { ...r, height: r.bottom - r.top, width: r.right - r.left };
|
|
34914
|
+
};
|
|
34915
|
+
const hasHiddenAncestor = (element) => {
|
|
34916
|
+
let current = element;
|
|
34917
|
+
while (current && current !== document.documentElement) {
|
|
34918
|
+
const style = getComputedStyle(current);
|
|
34919
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return true;
|
|
34920
|
+
current = current.parentElement;
|
|
34921
|
+
}
|
|
34922
|
+
return false;
|
|
34923
|
+
};
|
|
34924
|
+
const isClippedByAncestor = (element, box) => {
|
|
34925
|
+
let parent = element.parentElement;
|
|
34926
|
+
while (parent && parent !== document.documentElement) {
|
|
34927
|
+
const style = getComputedStyle(parent);
|
|
34928
|
+
const clips = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden" || style.overflow === "clip";
|
|
34929
|
+
if (clips) {
|
|
34930
|
+
const p = rectOf(parent);
|
|
34931
|
+
const intersects = box.left < p.right && box.right > p.left && box.top < p.bottom && box.bottom > p.top;
|
|
34932
|
+
if (!intersects) return true;
|
|
34933
|
+
}
|
|
34934
|
+
parent = parent.parentElement;
|
|
34935
|
+
}
|
|
34936
|
+
return false;
|
|
34937
|
+
};
|
|
34938
|
+
const directText = (element) => {
|
|
34939
|
+
let text = "";
|
|
34940
|
+
for (const node of Array.from(element.childNodes)) {
|
|
34941
|
+
if (node.nodeType === 3) text += node.textContent ?? "";
|
|
34942
|
+
}
|
|
34943
|
+
return text.trim();
|
|
34944
|
+
};
|
|
34945
|
+
const NON_COPY_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
34946
|
+
const visibleText = (root) => {
|
|
34947
|
+
let text = "";
|
|
34948
|
+
const hiddenCache = /* @__PURE__ */ new Map();
|
|
34949
|
+
const isHidden = (element) => {
|
|
34950
|
+
const cached = hiddenCache.get(element);
|
|
34951
|
+
if (cached !== void 0) return cached;
|
|
34952
|
+
const style = getComputedStyle(element);
|
|
34953
|
+
const hidden = style.display === "none" || style.visibility === "hidden";
|
|
34954
|
+
hiddenCache.set(element, hidden);
|
|
34955
|
+
return hidden;
|
|
34956
|
+
};
|
|
34957
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
34958
|
+
acceptNode: (node) => {
|
|
34959
|
+
let parent = node.parentElement;
|
|
34960
|
+
while (parent) {
|
|
34961
|
+
if (NON_COPY_TAGS.has(parent.tagName) || isHidden(parent)) return NodeFilter.FILTER_REJECT;
|
|
34962
|
+
if (parent === root) break;
|
|
34963
|
+
parent = parent.parentElement;
|
|
34964
|
+
}
|
|
34965
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
34966
|
+
}
|
|
34967
|
+
});
|
|
34968
|
+
while (walker.nextNode() && text.length < 400) {
|
|
34969
|
+
text += ` ${walker.currentNode.textContent ?? ""}`;
|
|
34970
|
+
}
|
|
34971
|
+
return text.replace(/\s+/g, " ").trim();
|
|
34972
|
+
};
|
|
34973
|
+
const GRAPHIC_TAGS = /* @__PURE__ */ new Set(["img", "svg", "video", "picture", "canvas", "iframe"]);
|
|
34974
|
+
const hasBackgroundImage = (element) => {
|
|
34975
|
+
const bg = getComputedStyle(element).backgroundImage;
|
|
34976
|
+
return Boolean(bg) && bg !== "none" && bg.includes("url(");
|
|
34977
|
+
};
|
|
34978
|
+
const isContentLeaf = (element) => {
|
|
34979
|
+
const tag = element.tagName.toLowerCase();
|
|
34980
|
+
if (GRAPHIC_TAGS.has(tag)) return true;
|
|
34981
|
+
if (directText(element).length > 0) return true;
|
|
34982
|
+
return hasBackgroundImage(element);
|
|
34983
|
+
};
|
|
34984
|
+
const selectorFor = (element) => {
|
|
34985
|
+
const parts = [];
|
|
34986
|
+
let current = element;
|
|
34987
|
+
while (current && current !== document.documentElement) {
|
|
34988
|
+
const tag = current.tagName.toLowerCase();
|
|
34989
|
+
if (tag === "body") {
|
|
34990
|
+
parts.unshift("body");
|
|
34991
|
+
break;
|
|
34992
|
+
}
|
|
34993
|
+
const parent = current.parentElement;
|
|
34994
|
+
if (!parent) {
|
|
34995
|
+
parts.unshift(tag);
|
|
34996
|
+
break;
|
|
34997
|
+
}
|
|
34998
|
+
const sameTag = Array.from(parent.children).filter((c) => c.tagName === current?.tagName);
|
|
34999
|
+
const position = sameTag.indexOf(current) + 1;
|
|
35000
|
+
parts.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${position})` : tag);
|
|
35001
|
+
current = parent;
|
|
35002
|
+
}
|
|
35003
|
+
return parts.join(" > ");
|
|
35004
|
+
};
|
|
35005
|
+
const leaves = [];
|
|
35006
|
+
const all = document.querySelectorAll("*");
|
|
35007
|
+
const limit = Math.min(all.length, options.maxElements);
|
|
35008
|
+
for (let i = 0; i < limit; i++) {
|
|
35009
|
+
const element = all[i];
|
|
35010
|
+
if (!element) continue;
|
|
35011
|
+
const tag = element.tagName.toLowerCase();
|
|
35012
|
+
if (tag === "script" || tag === "style" || tag === "noscript" || tag === "link" || tag === "head") continue;
|
|
35013
|
+
if (!isContentLeaf(element)) continue;
|
|
35014
|
+
const box = boxOf(element);
|
|
35015
|
+
if (!box || box.height <= 0 || box.width <= 0) continue;
|
|
35016
|
+
if (box.height > options.maxSectionHeight) continue;
|
|
35017
|
+
if (hasHiddenAncestor(element)) continue;
|
|
35018
|
+
if (isClippedByAncestor(element, box)) continue;
|
|
35019
|
+
leaves.push({
|
|
35020
|
+
element,
|
|
35021
|
+
top: box.top,
|
|
35022
|
+
bottom: box.bottom,
|
|
35023
|
+
left: box.left,
|
|
35024
|
+
right: box.right,
|
|
35025
|
+
height: box.height
|
|
35026
|
+
});
|
|
35027
|
+
}
|
|
35028
|
+
if (leaves.length === 0) return [];
|
|
35029
|
+
const gapThreshold = geom.adaptiveGapThreshold(leaves.map((l) => ({ top: l.top, bottom: l.bottom })));
|
|
35030
|
+
const commonAncestor = (elements) => {
|
|
35031
|
+
let ancestor = elements[0] ?? null;
|
|
35032
|
+
while (ancestor && !elements.every((e) => ancestor?.contains(e))) {
|
|
35033
|
+
ancestor = ancestor.parentElement;
|
|
35034
|
+
}
|
|
35035
|
+
return ancestor;
|
|
35036
|
+
};
|
|
35037
|
+
const abandon = (members) => members.map((m) => ({ ...m, sealed: true }));
|
|
35038
|
+
const findCoveringAncestor = (start, bounds) => {
|
|
35039
|
+
let element = start;
|
|
35040
|
+
while (element) {
|
|
35041
|
+
const box = boxOf(element);
|
|
35042
|
+
if (!box) return null;
|
|
35043
|
+
if (box.height > options.maxSectionHeight) return null;
|
|
35044
|
+
const covers = box.top <= bounds.top && box.bottom >= bounds.bottom && box.left <= bounds.left && box.right >= bounds.right;
|
|
35045
|
+
if (covers) return { element, box };
|
|
35046
|
+
if (!element.parentElement || element.parentElement === document.documentElement) return null;
|
|
35047
|
+
element = element.parentElement;
|
|
35048
|
+
}
|
|
35049
|
+
return null;
|
|
35050
|
+
};
|
|
35051
|
+
const mergeBucket = (members) => {
|
|
35052
|
+
const bounds = {
|
|
35053
|
+
top: Math.min(...members.map((m) => m.top)),
|
|
35054
|
+
bottom: Math.max(...members.map((m) => m.bottom)),
|
|
35055
|
+
left: Math.min(...members.map((m) => m.left)),
|
|
35056
|
+
right: Math.max(...members.map((m) => m.right))
|
|
35057
|
+
};
|
|
35058
|
+
const covering = findCoveringAncestor(commonAncestor(members.map((m) => m.root)), bounds);
|
|
35059
|
+
if (!covering) return abandon(members);
|
|
35060
|
+
return [
|
|
35061
|
+
{
|
|
35062
|
+
root: covering.element,
|
|
35063
|
+
top: covering.box.top,
|
|
35064
|
+
bottom: covering.box.bottom,
|
|
35065
|
+
left: covering.box.left,
|
|
35066
|
+
right: covering.box.right,
|
|
35067
|
+
height: covering.box.height,
|
|
35068
|
+
leaves: members.flatMap((m) => m.leaves),
|
|
35069
|
+
sealed: false
|
|
35070
|
+
}
|
|
35071
|
+
];
|
|
35072
|
+
};
|
|
35073
|
+
const clusterOnce = (groups2) => {
|
|
35074
|
+
const buckets = [];
|
|
35075
|
+
for (const group of groups2) {
|
|
35076
|
+
if (group.sealed) {
|
|
35077
|
+
buckets.push([group]);
|
|
35078
|
+
continue;
|
|
35079
|
+
}
|
|
35080
|
+
const target = buckets.find(
|
|
35081
|
+
(bucket) => bucket.some(
|
|
35082
|
+
(member) => !member.sealed && geom.bandsCollide(
|
|
35083
|
+
{ top: member.top, bottom: member.bottom },
|
|
35084
|
+
{ top: group.top, bottom: group.bottom },
|
|
35085
|
+
options.collisionMargin,
|
|
35086
|
+
gapThreshold
|
|
35087
|
+
)
|
|
35088
|
+
)
|
|
35089
|
+
);
|
|
35090
|
+
if (target) target.push(group);
|
|
35091
|
+
else buckets.push([group]);
|
|
35092
|
+
}
|
|
35093
|
+
if (buckets.length === groups2.length) return groups2;
|
|
35094
|
+
return buckets.flatMap((bucket) => bucket.length === 1 ? [bucket[0]] : mergeBucket(bucket));
|
|
35095
|
+
};
|
|
35096
|
+
let groups = leaves.map((leaf) => ({
|
|
35097
|
+
root: leaf.element,
|
|
35098
|
+
top: leaf.top,
|
|
35099
|
+
bottom: leaf.bottom,
|
|
35100
|
+
left: leaf.left,
|
|
35101
|
+
right: leaf.right,
|
|
35102
|
+
height: leaf.height,
|
|
35103
|
+
leaves: [leaf],
|
|
35104
|
+
sealed: false
|
|
35105
|
+
}));
|
|
35106
|
+
for (let pass = 0; pass < 40; pass++) {
|
|
35107
|
+
const next = clusterOnce(groups);
|
|
35108
|
+
if (next.length === groups.length) break;
|
|
35109
|
+
groups = next;
|
|
35110
|
+
}
|
|
35111
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
35112
|
+
let promoted = false;
|
|
35113
|
+
groups = groups.map((group) => {
|
|
35114
|
+
const parent = group.root.parentElement;
|
|
35115
|
+
if (!parent || parent === document.documentElement || parent === document.body) return group;
|
|
35116
|
+
const parentBox = boxOf(parent);
|
|
35117
|
+
if (!parentBox) return group;
|
|
35118
|
+
if (!geom.shouldPromoteToParent({
|
|
35119
|
+
childCount: group.leaves.length,
|
|
35120
|
+
height: group.height,
|
|
35121
|
+
parentHeight: parentBox.height
|
|
35122
|
+
})) {
|
|
35123
|
+
return group;
|
|
35124
|
+
}
|
|
35125
|
+
promoted = true;
|
|
35126
|
+
return {
|
|
35127
|
+
...group,
|
|
35128
|
+
root: parent,
|
|
35129
|
+
top: parentBox.top,
|
|
35130
|
+
bottom: parentBox.bottom,
|
|
35131
|
+
left: parentBox.left,
|
|
35132
|
+
right: parentBox.right,
|
|
35133
|
+
height: parentBox.height
|
|
35134
|
+
};
|
|
35135
|
+
});
|
|
35136
|
+
if (!promoted) break;
|
|
35137
|
+
groups = clusterOnce(groups);
|
|
35138
|
+
}
|
|
35139
|
+
const sameBox = (a, b) => Math.abs(a.top - b.top) <= 4 && Math.abs(a.bottom - b.bottom) <= 4 && Math.abs(a.left - b.left) <= 4 && Math.abs(a.right - b.right) <= 4;
|
|
35140
|
+
const containsBox = (outer, inner) => outer.top - 4 <= inner.top && outer.bottom + 4 >= inner.bottom && outer.left - 4 <= inner.left && outer.right + 4 >= inner.right;
|
|
35141
|
+
const deduped = [];
|
|
35142
|
+
const area = (g) => (g.right - g.left) * g.height;
|
|
35143
|
+
for (const group of groups.slice().sort((a, b) => area(b) - area(a) || b.leaves.length - a.leaves.length)) {
|
|
35144
|
+
const duplicate = deduped.some(
|
|
35145
|
+
(kept) => kept.root === group.root || kept.root.contains(group.root) || sameBox(kept, group) || containsBox(kept, group)
|
|
35146
|
+
);
|
|
35147
|
+
if (!duplicate) deduped.push(group);
|
|
35148
|
+
}
|
|
35149
|
+
return deduped.filter((group) => group.height > 0 && group.right - group.left > 0).sort((a, b) => a.top - b.top).map((group, index) => ({
|
|
35150
|
+
index,
|
|
35151
|
+
selector: selectorFor(group.root),
|
|
35152
|
+
rect: {
|
|
35153
|
+
top: group.top,
|
|
35154
|
+
left: group.left,
|
|
35155
|
+
width: group.right - group.left,
|
|
35156
|
+
height: group.height
|
|
35157
|
+
},
|
|
35158
|
+
leafCount: group.leaves.length,
|
|
35159
|
+
textPreview: visibleText(group.root).slice(0, 200)
|
|
35160
|
+
}));
|
|
35161
|
+
};
|
|
35162
|
+
|
|
35163
|
+
// src/engine/landing-library/motionTake.ts
|
|
35164
|
+
var FRAME_TIMES_MS = [0, 120, 260, 450, 800, 1400];
|
|
35165
|
+
var FRAME_WIDTH = 460;
|
|
35166
|
+
var GRID_COLUMNS = 3;
|
|
35167
|
+
var LABEL_HEIGHT = 22;
|
|
35168
|
+
var FRAME_MARGIN_PX = 16;
|
|
35169
|
+
function clampToViewport(box, viewport) {
|
|
35170
|
+
const left = Math.max(0, Math.min(box.left, viewport.width));
|
|
35171
|
+
const top = Math.max(0, Math.min(box.top, viewport.height));
|
|
35172
|
+
const width = Math.min(box.width - (left - box.left), viewport.width - left);
|
|
35173
|
+
const height = Math.min(box.height - (top - box.top), viewport.height - top);
|
|
35174
|
+
if (width < 1 || height < 1) return null;
|
|
35175
|
+
return { left, top, width, height };
|
|
35176
|
+
}
|
|
35177
|
+
async function captureMotionTake(browser, url, selector, timeoutMs = 45e3) {
|
|
35178
|
+
const { context, page } = await newPage(browser, DESKTOP_VIEWPORT, { motion: true });
|
|
35179
|
+
try {
|
|
35180
|
+
await blockConsentManagers(page);
|
|
35181
|
+
await installPageRuntime(page);
|
|
35182
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
|
|
35183
|
+
await settleNavigation(page);
|
|
35184
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => void 0);
|
|
35185
|
+
await settleConsent(page);
|
|
35186
|
+
const target = page.locator(selector).first();
|
|
35187
|
+
if (!await target.count().catch(() => 0)) return null;
|
|
35188
|
+
await page.evaluate((sectionSelector) => {
|
|
35189
|
+
const element = document.querySelector(sectionSelector);
|
|
35190
|
+
if (!element) return;
|
|
35191
|
+
const top = element.getBoundingClientRect().top + window.scrollY;
|
|
35192
|
+
window.scrollTo({ top: Math.max(0, top - window.innerHeight - 200), behavior: "instant" });
|
|
35193
|
+
}, selector);
|
|
35194
|
+
await page.waitForTimeout(600);
|
|
35195
|
+
const clip = await aimAtSection(page, selector);
|
|
35196
|
+
if (!clip) return null;
|
|
35197
|
+
await hideFloatingFurniture(page, selector);
|
|
35198
|
+
const frames = [];
|
|
35199
|
+
let previous = 0;
|
|
35200
|
+
for (const time of FRAME_TIMES_MS) {
|
|
35201
|
+
await page.waitForTimeout(Math.max(0, time - previous));
|
|
35202
|
+
previous = time;
|
|
35203
|
+
const shot = await page.screenshot({
|
|
35204
|
+
type: "png",
|
|
35205
|
+
timeout: 1e4,
|
|
35206
|
+
clip: { x: clip.left, y: clip.top, width: clip.width, height: clip.height }
|
|
35207
|
+
}).catch(() => null);
|
|
35208
|
+
if (shot) frames.push(shot);
|
|
35209
|
+
}
|
|
35210
|
+
if (frames.length === 0) return null;
|
|
35211
|
+
return { filmstrip: await composeFilmstrip(frames), frameCount: frames.length };
|
|
35212
|
+
} catch {
|
|
35213
|
+
return null;
|
|
35214
|
+
} finally {
|
|
35215
|
+
await context.close();
|
|
35216
|
+
}
|
|
35217
|
+
}
|
|
35218
|
+
async function aimAtSection(page, selector) {
|
|
35219
|
+
const box = await page.evaluate(
|
|
35220
|
+
([sectionSelector, margin]) => {
|
|
35221
|
+
const element = document.querySelector(sectionSelector);
|
|
35222
|
+
if (!element) return null;
|
|
35223
|
+
const before = element.getBoundingClientRect();
|
|
35224
|
+
window.scrollTo({ top: Math.max(0, before.top + window.scrollY - margin), behavior: "instant" });
|
|
35225
|
+
const after = element.getBoundingClientRect();
|
|
35226
|
+
return {
|
|
35227
|
+
left: after.left,
|
|
35228
|
+
top: after.top - margin,
|
|
35229
|
+
width: after.width,
|
|
35230
|
+
height: after.height + margin,
|
|
35231
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
35232
|
+
};
|
|
35233
|
+
},
|
|
35234
|
+
[selector, FRAME_MARGIN_PX]
|
|
35235
|
+
);
|
|
35236
|
+
if (!box) return null;
|
|
35237
|
+
return clampToViewport(box, box.viewport);
|
|
35238
|
+
}
|
|
35239
|
+
async function hideFloatingFurniture(page, selector) {
|
|
35240
|
+
await page.evaluate((sectionSelector) => {
|
|
35241
|
+
const target = document.querySelector(sectionSelector);
|
|
35242
|
+
if (!target) return;
|
|
35243
|
+
for (const element of Array.from(document.querySelectorAll("*"))) {
|
|
35244
|
+
const position = getComputedStyle(element).position;
|
|
35245
|
+
if (position !== "fixed" && position !== "sticky") continue;
|
|
35246
|
+
if (element.contains(target) || target.contains(element) || element === target) continue;
|
|
35247
|
+
element.style.setProperty("visibility", "hidden", "important");
|
|
35248
|
+
}
|
|
35249
|
+
}, selector);
|
|
35250
|
+
}
|
|
35251
|
+
async function composeFilmstrip(frames) {
|
|
35252
|
+
const scaled = await Promise.all(frames.map((frame) => sharp4(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
|
|
35253
|
+
const first = await sharp4(scaled[0]).metadata();
|
|
35254
|
+
const frameHeight = first.height ?? 300;
|
|
35255
|
+
const cellHeight = frameHeight + LABEL_HEIGHT;
|
|
35256
|
+
const rows = Math.ceil(scaled.length / GRID_COLUMNS);
|
|
35257
|
+
const width = FRAME_WIDTH * Math.min(GRID_COLUMNS, scaled.length);
|
|
35258
|
+
const composites = scaled.flatMap((frame, index) => {
|
|
35259
|
+
const column = index % GRID_COLUMNS;
|
|
35260
|
+
const row = Math.floor(index / GRID_COLUMNS);
|
|
35261
|
+
const label = Buffer.from(
|
|
35262
|
+
`<svg width="${FRAME_WIDTH}" height="${LABEL_HEIGHT}">
|
|
35263
|
+
<rect width="100%" height="100%" fill="#111827"/>
|
|
35264
|
+
<text x="8" y="15" font-family="monospace" font-size="12" fill="#f9fafb">+${FRAME_TIMES_MS[index] ?? 0}ms</text>
|
|
35265
|
+
</svg>`
|
|
35266
|
+
);
|
|
35267
|
+
return [
|
|
35268
|
+
{ input: label, left: column * FRAME_WIDTH, top: row * cellHeight },
|
|
35269
|
+
{ input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
|
|
35270
|
+
];
|
|
35271
|
+
});
|
|
35272
|
+
return await sharp4({
|
|
35273
|
+
create: {
|
|
35274
|
+
width,
|
|
35275
|
+
height: cellHeight * rows,
|
|
35276
|
+
channels: 3,
|
|
35277
|
+
background: { r: 17, g: 24, b: 39 }
|
|
35278
|
+
}
|
|
35279
|
+
}).composite(composites).png().toBuffer();
|
|
35280
|
+
}
|
|
35281
|
+
|
|
35282
|
+
// src/engine/landing-library/renderBundle.ts
|
|
35283
|
+
var SETTLE_ANIMATIONS_CSS = `*, *::before, *::after {
|
|
35284
|
+
animation-play-state: running !important;
|
|
35285
|
+
animation-delay: 0s !important;
|
|
35286
|
+
animation-duration: 1ms !important;
|
|
35287
|
+
animation-iteration-count: 1 !important;
|
|
35288
|
+
animation-fill-mode: forwards !important;
|
|
35289
|
+
transition: none !important;
|
|
35290
|
+
}`;
|
|
35291
|
+
async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
|
|
35292
|
+
const { wholePage = false, timeoutMs = 2e4 } = options;
|
|
35293
|
+
const { context, page } = await newPage(browser, { width: viewportWidth, height: 900 });
|
|
35294
|
+
try {
|
|
35295
|
+
await page.setContent(html, { waitUntil: "load", timeout: timeoutMs });
|
|
35296
|
+
await page.addStyleTag({ content: SETTLE_ANIMATIONS_CSS });
|
|
35297
|
+
await page.evaluate(async () => {
|
|
35298
|
+
await document.fonts.ready;
|
|
35299
|
+
});
|
|
35300
|
+
await page.waitForTimeout(300);
|
|
35301
|
+
if (!wholePage) {
|
|
35302
|
+
for (const selector of [`[${SECTION_ROOT_ATTRIBUTE}]`, "body > *"]) {
|
|
35303
|
+
const target = page.locator(selector).first();
|
|
35304
|
+
if (!await target.count()) continue;
|
|
35305
|
+
const shot = await target.screenshot({ type: "png", timeout: timeoutMs }).catch(() => null);
|
|
35306
|
+
if (shot) return shot;
|
|
35307
|
+
}
|
|
35308
|
+
}
|
|
35309
|
+
return await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
35310
|
+
} catch {
|
|
35311
|
+
return null;
|
|
35312
|
+
} finally {
|
|
35313
|
+
await context.close();
|
|
35314
|
+
}
|
|
35315
|
+
}
|
|
35316
|
+
|
|
35317
|
+
// src/engine/landing-library/report.ts
|
|
35318
|
+
import { writeFile as writeFile14 } from "fs/promises";
|
|
35319
|
+
import path30 from "path";
|
|
35320
|
+
async function writeCaptureReport(manifest, outDir) {
|
|
35321
|
+
const file = path30.join(outDir, "report.html");
|
|
35322
|
+
await writeFile14(file, renderReport(manifest));
|
|
35323
|
+
return file;
|
|
35324
|
+
}
|
|
35325
|
+
function escapeHtml3(value) {
|
|
35326
|
+
return value.replace(
|
|
35327
|
+
/[&<>"']/g,
|
|
35328
|
+
(character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character] ?? character
|
|
35329
|
+
);
|
|
35330
|
+
}
|
|
35331
|
+
function fidelityTone(fidelity) {
|
|
35332
|
+
if (fidelity === null) return "unknown";
|
|
35333
|
+
if (fidelity >= 0.95) return "good";
|
|
35334
|
+
if (fidelity >= 0.85) return "fair";
|
|
35335
|
+
return "poor";
|
|
35336
|
+
}
|
|
35337
|
+
function medianFidelity(sections) {
|
|
35338
|
+
const scored = sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
|
|
35339
|
+
return scored.length === 0 ? null : scored[Math.floor(scored.length / 2)] ?? null;
|
|
35340
|
+
}
|
|
35341
|
+
function formatFidelity(fidelity) {
|
|
35342
|
+
return fidelity === null ? "\u2014" : fidelity.toFixed(2);
|
|
35343
|
+
}
|
|
35344
|
+
function renderSection3(section) {
|
|
35345
|
+
const tone = fidelityTone(section.fidelity);
|
|
35346
|
+
const rendered = section.bundle ? section.bundle.replace(/section\.html$/, "section-rendered.png") : null;
|
|
35347
|
+
const shot = (label, src, note) => {
|
|
35348
|
+
if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
|
|
35349
|
+
return `<figure class="shot">
|
|
35350
|
+
<figcaption>${label}${note ? ` <span class="note">${escapeHtml3(note)}</span>` : ""}</figcaption>
|
|
35351
|
+
<a href="${escapeHtml3(src)}" target="_blank" rel="noopener"><img src="${escapeHtml3(src)}" alt="" loading="lazy"></a>
|
|
35352
|
+
</figure>`;
|
|
35353
|
+
};
|
|
35354
|
+
return `<section class="card">
|
|
35355
|
+
<header>
|
|
35356
|
+
<h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
|
|
35357
|
+
<span class="badge ${tone}">fidelity ${formatFidelity(section.fidelity)}</span>
|
|
35358
|
+
${section.fidelityNote ? `<span class="badge warn">${escapeHtml3(section.fidelityNote)}</span>` : ""}
|
|
35359
|
+
${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml3(section.motion.summary)}</span>` : ""}
|
|
35360
|
+
</header>
|
|
35361
|
+
<p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
|
|
35362
|
+
<div class="shots">
|
|
35363
|
+
${shot("Live", section.desktopShot)}
|
|
35364
|
+
${shot("Reproduction", rendered, "rendered from the extracted bundle")}
|
|
35365
|
+
${shot("Mobile", section.mobileShot)}
|
|
35366
|
+
</div>
|
|
35367
|
+
${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
|
|
35368
|
+
<footer>
|
|
35369
|
+
<code>${escapeHtml3(section.selector)}</code>
|
|
35370
|
+
${section.bundle ? `<a href="${escapeHtml3(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
|
|
35371
|
+
</footer>
|
|
35372
|
+
</section>`;
|
|
35373
|
+
}
|
|
35374
|
+
function renderReport(manifest) {
|
|
35375
|
+
const median = medianFidelity(manifest.sections);
|
|
35376
|
+
const moving = manifest.sections.filter((section) => section.motionFilmstrip !== null).length;
|
|
35377
|
+
return `<!doctype html>
|
|
35378
|
+
<html lang="en">
|
|
35379
|
+
<head>
|
|
35380
|
+
<meta charset="utf-8">
|
|
35381
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
35382
|
+
<title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
|
|
35383
|
+
<style>
|
|
35384
|
+
:root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
|
|
35385
|
+
@media (prefers-color-scheme: dark) {
|
|
35386
|
+
:root { --line: #27272a; --muted: #a1a1aa; --bg: #09090b; --card: #131316; }
|
|
35387
|
+
}
|
|
35388
|
+
* { box-sizing: border-box; }
|
|
35389
|
+
body { margin: 0; padding: 24px; background: var(--bg);
|
|
35390
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
35391
|
+
h1 { margin: 0 0 4px; font-size: 20px; }
|
|
35392
|
+
a { color: inherit; }
|
|
35393
|
+
.sub { color: var(--muted); margin: 0 0 20px; }
|
|
35394
|
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
|
|
35395
|
+
.stat { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; background: var(--card); }
|
|
35396
|
+
.stat b { display: block; font-size: 18px; }
|
|
35397
|
+
.stat span { color: var(--muted); font-size: 12px; }
|
|
35398
|
+
.card { border: 1px solid var(--line); border-radius: 12px; background: var(--card);
|
|
35399
|
+
padding: 16px; margin-bottom: 16px; }
|
|
35400
|
+
.card header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
|
|
35401
|
+
.card h2 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
35402
|
+
.index { display: inline-block; min-width: 26px; color: var(--muted); }
|
|
35403
|
+
.badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
|
|
35404
|
+
.badge.good { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
|
|
35405
|
+
.badge.fair { background: #fef3c7; color: #92400e; border-color: #fde68a; }
|
|
35406
|
+
.badge.poor { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
|
|
35407
|
+
.badge.warn { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
|
|
35408
|
+
.badge.motion { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
|
|
35409
|
+
.preview { color: var(--muted); margin: 0 0 12px; font-size: 13px; }
|
|
35410
|
+
.shots { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; align-items: start; }
|
|
35411
|
+
.filmstrip { margin-top: 12px; }
|
|
35412
|
+
figure { margin: 0; }
|
|
35413
|
+
figcaption { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
35414
|
+
figcaption .note { opacity: .75; }
|
|
35415
|
+
.shot img { width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px;
|
|
35416
|
+
background: #fff; display: block; }
|
|
35417
|
+
.shot.empty { border: 1px dashed var(--line); border-radius: 8px; padding: 20px; text-align: center; }
|
|
35418
|
+
.card footer { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px;
|
|
35419
|
+
font-size: 12px; color: var(--muted); }
|
|
35420
|
+
code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
|
|
35421
|
+
</style>
|
|
35422
|
+
</head>
|
|
35423
|
+
<body>
|
|
35424
|
+
<h1>${escapeHtml3(manifest.title || "Capture report")}</h1>
|
|
35425
|
+
<p class="sub"><a href="${escapeHtml3(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml3(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml3(manifest.capturedAt)}</p>
|
|
35426
|
+
|
|
35427
|
+
<div class="stats">
|
|
35428
|
+
<div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
|
|
35429
|
+
<div class="stat"><b>${formatFidelity(median)}</b><span>median fidelity</span></div>
|
|
35430
|
+
<div class="stat"><b>${formatFidelity(manifest.page.fidelity)}</b><span>whole page</span></div>
|
|
35431
|
+
<div class="stat"><b>${moving}</b><span>filmed moving</span></div>
|
|
35432
|
+
<div class="stat"><b>${manifest.documentHeight}px</b><span>page height</span></div>
|
|
35433
|
+
</div>
|
|
35434
|
+
|
|
35435
|
+
<section class="card">
|
|
35436
|
+
<header>
|
|
35437
|
+
<h2>Whole page</h2>
|
|
35438
|
+
<span class="badge ${fidelityTone(manifest.page.fidelity)}">fidelity ${formatFidelity(manifest.page.fidelity)}</span>
|
|
35439
|
+
</header>
|
|
35440
|
+
<p class="preview">The same extractor rooted at <body> \u2014 one standalone file that should render like the original.</p>
|
|
35441
|
+
<div class="shots">
|
|
35442
|
+
<figure class="shot"><figcaption>Live</figcaption><a href="full-page.png" target="_blank" rel="noopener"><img src="full-page.png" alt="" loading="lazy"></a></figure>
|
|
35443
|
+
${manifest.page.bundle ? `<figure class="shot"><figcaption>Reproduction <span class="note">from page.html</span></figcaption><a href="page-rendered.png" target="_blank" rel="noopener"><img src="page-rendered.png" alt="" loading="lazy"></a></figure>` : `<figure class="shot empty"><figcaption>Reproduction \u2014 none</figcaption></figure>`}
|
|
35444
|
+
</div>
|
|
35445
|
+
</section>
|
|
35446
|
+
|
|
35447
|
+
${manifest.sections.map(renderSection3).join("\n")}
|
|
35448
|
+
</body>
|
|
35449
|
+
</html>
|
|
35450
|
+
`;
|
|
35451
|
+
}
|
|
35452
|
+
|
|
35453
|
+
// src/engine/landing-library/types.ts
|
|
35454
|
+
var DEFAULT_SEGMENT_OPTIONS = {
|
|
35455
|
+
maxSectionHeight: 2160,
|
|
35456
|
+
collisionMargin: 2,
|
|
35457
|
+
maxElements: 15e3
|
|
35458
|
+
};
|
|
35459
|
+
|
|
35460
|
+
// src/engine/landing-library/visualHash.ts
|
|
35461
|
+
import sharp5 from "sharp";
|
|
35462
|
+
var HASH_WIDTH = 9;
|
|
35463
|
+
var HASH_HEIGHT = 8;
|
|
35464
|
+
async function perceptualHash(image) {
|
|
35465
|
+
try {
|
|
35466
|
+
const raw = await sharp5(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
|
|
35467
|
+
return bitsToHex(rowGradientBits(new Uint8Array(raw)));
|
|
35468
|
+
} catch {
|
|
35469
|
+
return null;
|
|
35470
|
+
}
|
|
35471
|
+
}
|
|
35472
|
+
function rowGradientBits(pixels) {
|
|
35473
|
+
const bits = [];
|
|
35474
|
+
for (let y = 0; y < HASH_HEIGHT; y++) {
|
|
35475
|
+
for (let x = 0; x < HASH_WIDTH - 1; x++) {
|
|
35476
|
+
const left = pixels[y * HASH_WIDTH + x] ?? 0;
|
|
35477
|
+
const right = pixels[y * HASH_WIDTH + x + 1] ?? 0;
|
|
35478
|
+
bits.push(left > right);
|
|
35479
|
+
}
|
|
35480
|
+
}
|
|
35481
|
+
return bits;
|
|
35482
|
+
}
|
|
35483
|
+
function bitsToHex(bits) {
|
|
35484
|
+
let hex = "";
|
|
35485
|
+
for (let index = 0; index < bits.length; index += 4) {
|
|
35486
|
+
let nibble = 0;
|
|
35487
|
+
for (let offset = 0; offset < 4; offset++) {
|
|
35488
|
+
if (bits[index + offset]) nibble |= 1 << 3 - offset;
|
|
35489
|
+
}
|
|
35490
|
+
hex += nibble.toString(16);
|
|
35491
|
+
}
|
|
35492
|
+
return hex;
|
|
35493
|
+
}
|
|
35494
|
+
|
|
35495
|
+
// src/engine/landing-library/run.ts
|
|
35496
|
+
async function reproducePage(args) {
|
|
35497
|
+
const { browser, page, outDir, pageUrl, livePageShot } = args;
|
|
35498
|
+
const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
|
|
35499
|
+
if (!built) return { bundle: null, fidelity: null };
|
|
35500
|
+
await writeFile15(path31.join(outDir, "page.html"), built.html);
|
|
35501
|
+
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
|
|
35502
|
+
wholePage: true,
|
|
35503
|
+
timeoutMs: 6e4
|
|
35504
|
+
});
|
|
35505
|
+
if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
|
|
35506
|
+
await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
|
|
35507
|
+
const { score, note } = await scoreFidelity(livePageShot, rendered);
|
|
35508
|
+
return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
|
|
35509
|
+
}
|
|
35510
|
+
async function captureOneSection(args) {
|
|
35511
|
+
const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
|
|
35512
|
+
const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
|
|
35513
|
+
await mkdir11(dir, { recursive: true });
|
|
35514
|
+
const desktop = await captureSection(page, candidate);
|
|
35515
|
+
if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
|
|
35516
|
+
const visualHash = desktop ? await perceptualHash(desktop) : null;
|
|
35517
|
+
const motion = await collectMotion(page, candidate.selector);
|
|
35518
|
+
const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
|
|
35519
|
+
let fidelity = null;
|
|
35520
|
+
let fidelityNote;
|
|
35521
|
+
if (built) {
|
|
35522
|
+
await writeFile15(path31.join(dir, "section.html"), built.html);
|
|
35523
|
+
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
|
|
35524
|
+
if (rendered && desktop) {
|
|
35525
|
+
await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
|
|
35526
|
+
const result = await scoreFidelity(desktop, rendered);
|
|
35527
|
+
fidelity = result.score;
|
|
35528
|
+
fidelityNote = result.note;
|
|
35529
|
+
}
|
|
35530
|
+
}
|
|
35531
|
+
return {
|
|
35532
|
+
...candidate,
|
|
35533
|
+
desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
|
|
35534
|
+
mobileShot: null,
|
|
35535
|
+
bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
|
|
35536
|
+
fidelity,
|
|
35537
|
+
...fidelityNote ? { fidelityNote } : {},
|
|
35538
|
+
...built ? { cssStats: built.stats } : {},
|
|
35539
|
+
motion,
|
|
35540
|
+
motionFilmstrip: null,
|
|
35541
|
+
visualHash
|
|
35542
|
+
};
|
|
35543
|
+
}
|
|
35544
|
+
async function captureMobileShots(args) {
|
|
35545
|
+
const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs } = args;
|
|
35546
|
+
const mobile = await newPage(browser, MOBILE_VIEWPORT);
|
|
35547
|
+
try {
|
|
35548
|
+
await blockConsentManagers(mobile.page);
|
|
35549
|
+
await installPageRuntime(mobile.page);
|
|
35550
|
+
await preparePage(mobile.page, pageUrl, timeoutMs);
|
|
35551
|
+
for (const section of sections) {
|
|
35552
|
+
const shot = await captureSectionOnMobile(mobile.page, section);
|
|
35553
|
+
if (!shot) continue;
|
|
35554
|
+
const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
|
|
35555
|
+
await writeFile15(file, shot);
|
|
35556
|
+
section.mobileShot = path31.relative(outDir, file);
|
|
35557
|
+
}
|
|
35558
|
+
} finally {
|
|
35559
|
+
await mobile.context.close();
|
|
35560
|
+
}
|
|
35561
|
+
}
|
|
35562
|
+
async function captureMotionTakes(args) {
|
|
35563
|
+
const { browser, sections, sectionsDir, outDir, pageUrl, log } = args;
|
|
35564
|
+
const moving = sections.filter((section) => isWorthFilming(section.motion));
|
|
35565
|
+
if (moving.length === 0) return;
|
|
35566
|
+
log(`filming ${moving.length} moving sections`);
|
|
35567
|
+
for (const section of moving) {
|
|
35568
|
+
const take = await captureMotionTake(browser, pageUrl, section.selector);
|
|
35569
|
+
if (!take) continue;
|
|
35570
|
+
const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
|
|
35571
|
+
const file = path31.join(dir, "motion-filmstrip.png");
|
|
35572
|
+
await writeFile15(file, take.filmstrip);
|
|
35573
|
+
section.motionFilmstrip = path31.relative(outDir, file);
|
|
35574
|
+
log(` [${section.index}] ${section.motion.summary}`);
|
|
35575
|
+
}
|
|
35576
|
+
}
|
|
35577
|
+
async function scrapeLanding(options) {
|
|
35578
|
+
const timeoutMs = options.timeoutMs ?? 45e3;
|
|
35579
|
+
const log = options.onProgress ?? (() => void 0);
|
|
35580
|
+
const sectionsDir = path31.join(options.outDir, "sections");
|
|
35581
|
+
const browser = await launchBrowser();
|
|
35582
|
+
try {
|
|
35583
|
+
const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
|
|
35584
|
+
await blockConsentManagers(page);
|
|
35585
|
+
await installPageRuntime(page);
|
|
35586
|
+
log(`loading ${options.url}`);
|
|
35587
|
+
const prepared = await preparePage(page, options.url, timeoutMs);
|
|
35588
|
+
const blocked = detectBlockedPage({
|
|
35589
|
+
status: prepared.status,
|
|
35590
|
+
title: prepared.title,
|
|
35591
|
+
bodyText: prepared.bodyText,
|
|
35592
|
+
html: await page.content().catch(() => "")
|
|
35593
|
+
});
|
|
35594
|
+
if (blocked) throw new BlockedPageError(blocked);
|
|
35595
|
+
await mkdir11(sectionsDir, { recursive: true });
|
|
35596
|
+
log("segmenting");
|
|
35597
|
+
const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
|
|
35598
|
+
log(`found ${candidates.length} sections`);
|
|
35599
|
+
const sections = [];
|
|
35600
|
+
for (const candidate of candidates) {
|
|
35601
|
+
const section = await captureOneSection({
|
|
35602
|
+
browser,
|
|
35603
|
+
page,
|
|
35604
|
+
candidate,
|
|
35605
|
+
sectionsDir,
|
|
35606
|
+
outDir: options.outDir,
|
|
35607
|
+
pageUrl: prepared.finalUrl,
|
|
35608
|
+
withCode: options.code !== false
|
|
35609
|
+
});
|
|
35610
|
+
sections.push(section);
|
|
35611
|
+
log(
|
|
35612
|
+
` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
|
|
35613
|
+
);
|
|
35614
|
+
}
|
|
35615
|
+
const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
35616
|
+
if (fullPage) await writeFile15(path31.join(options.outDir, "full-page.png"), fullPage);
|
|
35617
|
+
const reproduction = options.code === false ? { bundle: null, fidelity: null } : await reproducePage({
|
|
35618
|
+
browser,
|
|
35619
|
+
page,
|
|
35620
|
+
outDir: options.outDir,
|
|
35621
|
+
pageUrl: prepared.finalUrl,
|
|
35622
|
+
livePageShot: fullPage
|
|
35623
|
+
});
|
|
35624
|
+
log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
|
|
35625
|
+
await context.close();
|
|
35626
|
+
if (options.mobile !== false) {
|
|
35627
|
+
log("capturing mobile");
|
|
35628
|
+
await captureMobileShots({
|
|
35629
|
+
browser,
|
|
35630
|
+
sections,
|
|
35631
|
+
sectionsDir,
|
|
35632
|
+
outDir: options.outDir,
|
|
35633
|
+
pageUrl: prepared.finalUrl,
|
|
35634
|
+
timeoutMs
|
|
35635
|
+
});
|
|
35636
|
+
}
|
|
35637
|
+
if (options.motion !== false) {
|
|
35638
|
+
await captureMotionTakes({
|
|
35639
|
+
browser,
|
|
35640
|
+
sections,
|
|
35641
|
+
sectionsDir,
|
|
35642
|
+
outDir: options.outDir,
|
|
35643
|
+
pageUrl: prepared.finalUrl,
|
|
35644
|
+
log
|
|
35645
|
+
});
|
|
35646
|
+
}
|
|
35647
|
+
const manifest = {
|
|
35648
|
+
url: options.url,
|
|
35649
|
+
finalUrl: prepared.finalUrl,
|
|
35650
|
+
title: prepared.title,
|
|
35651
|
+
documentHeight: prepared.documentHeight,
|
|
35652
|
+
viewport: DESKTOP_VIEWPORT,
|
|
35653
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35654
|
+
sections,
|
|
35655
|
+
page: reproduction
|
|
35656
|
+
};
|
|
35657
|
+
await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
35658
|
+
`);
|
|
35659
|
+
if (options.report !== false) {
|
|
35660
|
+
const reportPath = await writeCaptureReport(manifest, options.outDir);
|
|
35661
|
+
log(`report: ${reportPath}`);
|
|
35662
|
+
}
|
|
35663
|
+
return manifest;
|
|
35664
|
+
} finally {
|
|
35665
|
+
await browser.close();
|
|
35666
|
+
}
|
|
35667
|
+
}
|
|
35668
|
+
|
|
35669
|
+
// src/commands/landing/inspiration/scrape.ts
|
|
35670
|
+
async function recordCapture(manifest, outDir) {
|
|
35671
|
+
const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35672
|
+
const references = [];
|
|
35673
|
+
for (const section of manifest.sections) {
|
|
35674
|
+
if (!section.bundle) continue;
|
|
35675
|
+
try {
|
|
35676
|
+
const markup = await readFile23(path32.join(outDir, section.bundle), "utf8");
|
|
35677
|
+
const copyStrings = capturedCopyStrings(markup);
|
|
35678
|
+
if (copyStrings.length === 0) continue;
|
|
35679
|
+
references.push({
|
|
35680
|
+
sectionId: `local:${manifest.finalUrl}#${section.index}`,
|
|
35681
|
+
sourceUrl: manifest.finalUrl,
|
|
35682
|
+
copyStrings,
|
|
35683
|
+
consultedAt
|
|
35684
|
+
});
|
|
35685
|
+
} catch {
|
|
35686
|
+
}
|
|
35687
|
+
}
|
|
35688
|
+
return await recordReferences(process.cwd(), references);
|
|
35689
|
+
}
|
|
35690
|
+
registerSchema({
|
|
35691
|
+
command: "landing.inspiration.scrape",
|
|
35692
|
+
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. Also records every captured section for the originality check.",
|
|
35693
|
+
args: {
|
|
35694
|
+
url: { type: "string", description: "Page to capture", required: true },
|
|
35695
|
+
out: { type: "string", description: "Output directory", required: true },
|
|
35696
|
+
mobile: { type: "boolean", description: "Phone-viewport pass. `--no-mobile` to skip", required: false },
|
|
35697
|
+
code: {
|
|
35698
|
+
type: "boolean",
|
|
35699
|
+
description: "Standalone HTML+CSS bundles and fidelity scores. `--no-code` to segment and screenshot only",
|
|
35700
|
+
required: false
|
|
35701
|
+
},
|
|
35702
|
+
motion: {
|
|
35703
|
+
type: "boolean",
|
|
35704
|
+
description: "Film sections that move. `--no-motion` to skip \u2014 roughly halves runtime",
|
|
35705
|
+
required: false
|
|
35706
|
+
},
|
|
35707
|
+
report: { type: "boolean", description: "Write report.html. `--no-report` to skip", required: false }
|
|
35708
|
+
}
|
|
35709
|
+
});
|
|
35710
|
+
var scrapeCommand = defineCommand147({
|
|
35711
|
+
meta: {
|
|
35712
|
+
name: "scrape",
|
|
35713
|
+
description: "Capture a landing page to a directory, now. Example: baker landing inspiration scrape https://linear.app --out .baker/inspiration/linear.app"
|
|
35714
|
+
},
|
|
35715
|
+
args: {
|
|
35716
|
+
url: { type: "positional", description: "Page to capture", required: true },
|
|
35717
|
+
out: { type: "string", description: "Output directory", required: true },
|
|
35718
|
+
mobile: {
|
|
35719
|
+
type: "boolean",
|
|
35720
|
+
description: "Phone-viewport pass (--no-mobile to skip)",
|
|
35721
|
+
required: false,
|
|
35722
|
+
default: true
|
|
35723
|
+
},
|
|
35724
|
+
code: { type: "boolean", description: "Bundles + fidelity (--no-code to skip)", required: false, default: true },
|
|
35725
|
+
motion: {
|
|
35726
|
+
type: "boolean",
|
|
35727
|
+
description: "Film moving sections (--no-motion to skip)",
|
|
35728
|
+
required: false,
|
|
35729
|
+
default: true
|
|
35730
|
+
},
|
|
35731
|
+
report: { type: "boolean", description: "Write report.html (--no-report to skip)", required: false, default: true }
|
|
35732
|
+
},
|
|
35733
|
+
run: async ({ args }) => {
|
|
35734
|
+
try {
|
|
35735
|
+
const manifest = await scrapeLanding({
|
|
35736
|
+
url: args.url,
|
|
35737
|
+
outDir: args.out,
|
|
35738
|
+
mobile: args.mobile,
|
|
35739
|
+
code: args.code,
|
|
35740
|
+
motion: args.motion,
|
|
35741
|
+
report: args.report,
|
|
35742
|
+
// Progress goes to stderr so stdout stays a clean JSON envelope.
|
|
35743
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
35744
|
+
`)
|
|
35745
|
+
});
|
|
35746
|
+
const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
|
|
35747
|
+
const recorded = await recordCapture(manifest, args.out);
|
|
35748
|
+
writeJson({
|
|
35749
|
+
ok: true,
|
|
35750
|
+
data: {
|
|
35751
|
+
title: manifest.title,
|
|
35752
|
+
sections: manifest.sections.length,
|
|
35753
|
+
medianFidelity: scored.length > 0 ? scored[Math.floor(scored.length / 2)] : null,
|
|
35754
|
+
pageFidelity: manifest.page.fidelity,
|
|
35755
|
+
out: args.out,
|
|
35756
|
+
report: args.report ? `${args.out}/report.html` : null
|
|
35757
|
+
},
|
|
35758
|
+
hints: [
|
|
35759
|
+
// The command that produces the most and says the least about it. It
|
|
35760
|
+
// returns a count and a directory; without this the agent has no
|
|
35761
|
+
// instruction to open any of it, and a capture nobody looks at taught
|
|
35762
|
+
// nobody anything. A nudge tied to one command's result belongs here,
|
|
35763
|
+
// not only in the tool doc the agent may not re-read.
|
|
35764
|
+
args.report ? `Read ${args.out}/report.html \u2014 every section side by side with its fidelity score.` : `Read the section screenshots in ${args.out}/sections/*/desktop.png before you design.`,
|
|
35765
|
+
`${manifest.sections.length} sections captured to ${args.out}/sections/ \u2014 look at them, then decide which the client's page actually needs.`,
|
|
35766
|
+
INSPIRATION_HINTS.structureNotCopy,
|
|
35767
|
+
INSPIRATION_HINTS.adapt,
|
|
35768
|
+
recorded ? "These sections are recorded for the originality check \u2014 `baker landing critique` will block a publish that ships their copy." : "Could not record this capture, so the originality check cannot see it. Be especially careful not to reuse its copy."
|
|
35769
|
+
]
|
|
35770
|
+
});
|
|
35771
|
+
} catch (error) {
|
|
35772
|
+
if (error instanceof BlockedPageError) {
|
|
35773
|
+
writeJson({ ok: false, error: { code: error.code, message: error.message } });
|
|
35774
|
+
process.exit(1);
|
|
35775
|
+
}
|
|
35776
|
+
reportError(error);
|
|
35777
|
+
}
|
|
35778
|
+
}
|
|
35779
|
+
});
|
|
35780
|
+
|
|
35781
|
+
// src/commands/landing/inspiration/search.ts
|
|
35782
|
+
import { mkdir as mkdir12, writeFile as writeFile16 } from "fs/promises";
|
|
35783
|
+
import path33 from "path";
|
|
35784
|
+
import { defineCommand as defineCommand148 } from "citty";
|
|
35785
|
+
registerSchema({
|
|
35786
|
+
command: "landing.inspiration.search",
|
|
35787
|
+
description: "Search the shared library of real landing-page sections for reference before you design. Start here: baker landing inspiration search 'pricing with a monthly/annual toggle'. Returns the idea behind each section plus a downloaded screenshot you can Read. Defaults to this company's saved sections; pass --scope all for the whole library.",
|
|
35788
|
+
args: {
|
|
35789
|
+
query: { type: "string", description: "What you want to see, in plain English", required: false },
|
|
35790
|
+
type: {
|
|
35791
|
+
type: "string",
|
|
35792
|
+
description: "Comma list of section types: hero,pricing,faq,testimonials,\u2026",
|
|
35793
|
+
required: false
|
|
35794
|
+
},
|
|
35795
|
+
composition: {
|
|
35796
|
+
type: "string",
|
|
35797
|
+
description: "Comma list of composition patterns (references/composition.md)",
|
|
35798
|
+
required: false
|
|
35799
|
+
},
|
|
35800
|
+
register: {
|
|
35801
|
+
type: "string",
|
|
35802
|
+
description: "Comma list of visual registers: dev-tool-minimal,luxury-high-end,\u2026",
|
|
35803
|
+
required: false
|
|
35804
|
+
},
|
|
35805
|
+
interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
|
|
35806
|
+
motion: {
|
|
35807
|
+
type: "string",
|
|
35808
|
+
description: "Comma list of motion kinds: scroll-reveal,marquee,parallax,\u2026",
|
|
35809
|
+
required: false
|
|
35810
|
+
},
|
|
35811
|
+
media: { type: "string", description: "Comma list of media kinds", required: false },
|
|
35812
|
+
device: { type: "string", description: "Comma list of content devices", required: false },
|
|
35813
|
+
theme: { type: "string", description: "light | dark | mixed", required: false },
|
|
35814
|
+
"max-rank": {
|
|
35815
|
+
type: "number",
|
|
35816
|
+
description: "Show>Tell rank ceiling 1-7; 3 means 'rank 3 or better'",
|
|
35817
|
+
required: false
|
|
35818
|
+
},
|
|
35819
|
+
"min-craft": { type: "number", description: "Craft floor 0-1", required: false },
|
|
35820
|
+
"min-fidelity": {
|
|
35821
|
+
type: "number",
|
|
35822
|
+
description: "Only sections whose markup reproduces this well (0-1)",
|
|
35823
|
+
required: false
|
|
35824
|
+
},
|
|
35825
|
+
domain: { type: "string", description: "Restrict to one site", required: false },
|
|
35826
|
+
"similar-to": {
|
|
35827
|
+
type: "string",
|
|
35828
|
+
description: "Section id \u2014 find sections that look like this one",
|
|
35829
|
+
required: false
|
|
35830
|
+
},
|
|
35831
|
+
scope: { type: "string", description: "favorites (default) | all", required: false },
|
|
35832
|
+
limit: { type: "number", description: "Max results (default 8)", required: false },
|
|
35833
|
+
"no-images": { type: "boolean", description: "Skip downloading screenshots", required: false },
|
|
35834
|
+
full: {
|
|
35835
|
+
type: "boolean",
|
|
35836
|
+
description: "Add every classification facet (theme, density, rank, interactions, media, devices, tags, headline)",
|
|
35837
|
+
required: false
|
|
35838
|
+
}
|
|
35839
|
+
}
|
|
35840
|
+
});
|
|
35841
|
+
function buildSearchBody(args) {
|
|
35842
|
+
const body = {};
|
|
35843
|
+
const setList2 = (key, value) => {
|
|
35844
|
+
const list = splitList(value);
|
|
35845
|
+
if (list) Object.assign(body, { [key]: list });
|
|
35846
|
+
};
|
|
35847
|
+
if (args.query) body.query = String(args.query);
|
|
35848
|
+
if (args["similar-to"]) body.similarToSectionId = String(args["similar-to"]);
|
|
35849
|
+
setList2("sectionType", args.type);
|
|
35850
|
+
setList2("composition", args.composition);
|
|
35851
|
+
setList2("visualRegister", args.register);
|
|
35852
|
+
setList2("interaction", args.interaction);
|
|
35853
|
+
setList2("motion", args.motion);
|
|
35854
|
+
setList2("mediaKind", args.media);
|
|
35855
|
+
setList2("contentDevice", args.device);
|
|
35856
|
+
if (args.theme) body.theme = String(args.theme);
|
|
35857
|
+
if (args.domain) body.domain = String(args.domain);
|
|
35858
|
+
const maxRank = parseNumber(args["max-rank"]);
|
|
35859
|
+
if (maxRank !== void 0) body.maxShowTellRank = maxRank;
|
|
35860
|
+
const minCraft = parseNumber(args["min-craft"]);
|
|
35861
|
+
if (minCraft !== void 0) body.minCraftScore = minCraft;
|
|
35862
|
+
const minFidelity = parseNumber(args["min-fidelity"]);
|
|
35863
|
+
if (minFidelity !== void 0) body.minFidelity = minFidelity;
|
|
35864
|
+
const limit = parseNumber(args.limit);
|
|
35865
|
+
body.limit = limit ?? 8;
|
|
35866
|
+
body.scope = args.scope === "all" ? "all" : "favorites";
|
|
35867
|
+
return body;
|
|
35868
|
+
}
|
|
35869
|
+
async function downloadShots(results) {
|
|
35870
|
+
const dir = path33.join(process.cwd(), ".baker", "inspiration");
|
|
35871
|
+
await mkdir12(dir, { recursive: true });
|
|
35872
|
+
const saved = /* @__PURE__ */ new Map();
|
|
35873
|
+
await Promise.all(
|
|
35874
|
+
results.map(async (result) => {
|
|
35875
|
+
if (!result.desktopShotUrl) return;
|
|
35876
|
+
try {
|
|
35877
|
+
const response = await fetch(result.desktopShotUrl);
|
|
35878
|
+
if (!response.ok) return;
|
|
35879
|
+
const file = path33.join(dir, `${result.id}.png`);
|
|
35880
|
+
await writeFile16(file, Buffer.from(await response.arrayBuffer()));
|
|
35881
|
+
saved.set(result.id, path33.relative(process.cwd(), file));
|
|
35882
|
+
} catch {
|
|
35883
|
+
}
|
|
35884
|
+
})
|
|
35885
|
+
);
|
|
35886
|
+
return saved;
|
|
35887
|
+
}
|
|
35888
|
+
var searchCommand2 = defineCommand148({
|
|
35889
|
+
meta: {
|
|
35890
|
+
name: "search",
|
|
35891
|
+
description: "Search real landing-page sections for reference. Example: baker landing inspiration search 'dark developer hero with a terminal' --register dev-tool-minimal --scope all"
|
|
35892
|
+
},
|
|
35893
|
+
args: {
|
|
35894
|
+
query: { type: "positional", description: "What you want to see, in plain English", required: false },
|
|
35895
|
+
type: { type: "string", description: "Comma list of section types", required: false },
|
|
35896
|
+
composition: { type: "string", description: "Comma list of composition patterns", required: false },
|
|
35897
|
+
register: { type: "string", description: "Comma list of visual registers", required: false },
|
|
35898
|
+
interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
|
|
35899
|
+
motion: { type: "string", description: "Comma list of motion kinds", required: false },
|
|
35900
|
+
media: { type: "string", description: "Comma list of media kinds", required: false },
|
|
35901
|
+
device: { type: "string", description: "Comma list of content devices", required: false },
|
|
35902
|
+
theme: { type: "string", description: "light | dark | mixed", required: false },
|
|
35903
|
+
"max-rank": { type: "string", description: "Show>Tell rank ceiling 1-7", required: false },
|
|
35904
|
+
"min-craft": { type: "string", description: "Craft floor 0-1", required: false },
|
|
35905
|
+
"min-fidelity": { type: "string", description: "Reproduction-fidelity floor 0-1", required: false },
|
|
35906
|
+
domain: { type: "string", description: "Restrict to one site", required: false },
|
|
35907
|
+
"similar-to": { type: "string", description: "Section id to find lookalikes of", required: false },
|
|
35908
|
+
scope: { type: "string", description: "favorites (default) | all", required: false, default: "favorites" },
|
|
35909
|
+
limit: { type: "string", description: "Max results (default 8)", required: false },
|
|
35910
|
+
"no-images": { type: "boolean", description: "Skip downloading screenshots", required: false, default: false },
|
|
35911
|
+
full: { type: "boolean", description: "Include every classification facet", required: false, default: false }
|
|
35912
|
+
},
|
|
35913
|
+
run: async ({ args }) => {
|
|
35914
|
+
try {
|
|
35915
|
+
const body = buildSearchBody(args);
|
|
35916
|
+
const data = await apiPost("/api/landing-inspiration/search", body);
|
|
35917
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
35918
|
+
const shots = args["no-images"] ? /* @__PURE__ */ new Map() : await downloadShots(results);
|
|
35919
|
+
const full = args.full;
|
|
35920
|
+
const rows = results.map((result) => ({
|
|
35921
|
+
id: result.id,
|
|
35922
|
+
// The id `page` takes. Its own schema says "from a search result's source
|
|
35923
|
+
// id", and until this line no projection emitted one — so the ordering
|
|
35924
|
+
// view could only be reached straight after `add`, never from a search.
|
|
35925
|
+
page_id: result.sourceId,
|
|
35926
|
+
section: result.sectionType,
|
|
35927
|
+
composition: result.composition,
|
|
35928
|
+
look: result.visualRegister,
|
|
35929
|
+
motion: result.motionSummary,
|
|
35930
|
+
why_it_works: result.whyItWorks,
|
|
35931
|
+
craft: Number(result.craftScore?.toFixed?.(2) ?? result.craftScore),
|
|
35932
|
+
fidelity: result.fidelity,
|
|
35933
|
+
domain: result.domain,
|
|
35934
|
+
// Only worth saying when it means something: >1 marks a section the site
|
|
35935
|
+
// reuses across its pages, which is a stronger signal than a one-off.
|
|
35936
|
+
...result.pageCount > 1 ? { used_on_pages: result.pageCount } : {},
|
|
35937
|
+
// With --no-images there is no file, so hand back the URL instead. A row
|
|
35938
|
+
// with neither is a section the agent is asked to judge sight unseen,
|
|
35939
|
+
// which is the one thing a visual library must never produce.
|
|
35940
|
+
...args["no-images"] ? { screenshot_url: result.desktopShotUrl ?? null } : { screenshot: shots.get(result.id) ?? null },
|
|
35941
|
+
...full ? {
|
|
35942
|
+
theme: result.theme,
|
|
35943
|
+
density: result.density,
|
|
35944
|
+
show_tell_rank: result.showTellRank,
|
|
35945
|
+
interactions: result.interactions,
|
|
35946
|
+
media: result.mediaKinds,
|
|
35947
|
+
devices: result.contentDevices,
|
|
35948
|
+
tags: result.tags,
|
|
35949
|
+
headline: result.headline,
|
|
35950
|
+
source_url: result.sourceUrl
|
|
35951
|
+
} : {}
|
|
35952
|
+
}));
|
|
35953
|
+
const hints = [INSPIRATION_HINTS.adapt];
|
|
35954
|
+
if (rows.length === 0) {
|
|
35955
|
+
hints.push(
|
|
35956
|
+
body.scope === "favorites" ? "No matches in this company's saved sections. Retry with --scope all to search the whole library, or add a page with `baker landing inspiration add <url>`." : "No matches. Loosen the filters, or add reference pages with `baker landing inspiration add <url>`."
|
|
35957
|
+
);
|
|
35958
|
+
} else if (data.moreInFullLibrary > 0) {
|
|
35959
|
+
hints.push(`${data.moreInFullLibrary} more match in the full library \u2014 re-run with --scope all.`);
|
|
35960
|
+
}
|
|
35961
|
+
if (shots.size > 0) hints.push(`Screenshots saved to .baker/inspiration/ \u2014 Read them before deciding.`);
|
|
35962
|
+
writeJson({
|
|
35963
|
+
ok: true,
|
|
35964
|
+
data: { results: rows, scope: data.scope, more_in_full_library: data.moreInFullLibrary, total: data.total },
|
|
35965
|
+
hints
|
|
35966
|
+
});
|
|
35967
|
+
} catch (error) {
|
|
35968
|
+
reportError(error);
|
|
35969
|
+
}
|
|
35970
|
+
}
|
|
35971
|
+
});
|
|
35972
|
+
|
|
35973
|
+
// src/commands/landing/inspiration/view.ts
|
|
35974
|
+
import { mkdir as mkdir13, writeFile as writeFile17 } from "fs/promises";
|
|
35975
|
+
import path34 from "path";
|
|
35976
|
+
import { defineCommand as defineCommand149 } from "citty";
|
|
35977
|
+
registerSchema({
|
|
35978
|
+
command: "landing.inspiration.view",
|
|
35979
|
+
description: "Everything known about one section: composition, motion, design tokens, the copy it uses, why it works, and what must change to make it yours. Downloads the desktop and mobile screenshots plus the motion filmstrip so you can look at them.",
|
|
35980
|
+
args: {
|
|
35981
|
+
id: { type: "string", description: "Section id from search", required: true },
|
|
35982
|
+
full: {
|
|
35983
|
+
type: "boolean",
|
|
35984
|
+
description: "Add the classification facets (theme, density, rank, interactions, media, devices, tags) and the reproduction notes",
|
|
35985
|
+
required: false
|
|
35986
|
+
}
|
|
35987
|
+
}
|
|
35988
|
+
});
|
|
35989
|
+
async function download(url, file) {
|
|
35990
|
+
if (!url) return null;
|
|
35991
|
+
try {
|
|
35992
|
+
const response = await fetch(url);
|
|
35993
|
+
if (!response.ok) return null;
|
|
35994
|
+
await mkdir13(path34.dirname(file), { recursive: true });
|
|
35995
|
+
await writeFile17(file, Buffer.from(await response.arrayBuffer()));
|
|
35996
|
+
return path34.relative(process.cwd(), file);
|
|
35997
|
+
} catch {
|
|
35998
|
+
return null;
|
|
35999
|
+
}
|
|
36000
|
+
}
|
|
36001
|
+
var viewCommand2 = defineCommand149({
|
|
36002
|
+
meta: {
|
|
36003
|
+
name: "view",
|
|
36004
|
+
description: "Full detail for one reference section. Example: baker landing inspiration view k57abc\u2026 \u2014 read the screenshots it saves before you build."
|
|
36005
|
+
},
|
|
36006
|
+
args: {
|
|
36007
|
+
id: { type: "positional", description: "Section id from search", required: true },
|
|
36008
|
+
full: {
|
|
36009
|
+
type: "boolean",
|
|
36010
|
+
description: "Include classification facets and reproduction notes",
|
|
36011
|
+
required: false,
|
|
36012
|
+
default: false
|
|
36013
|
+
}
|
|
36014
|
+
},
|
|
36015
|
+
run: async ({ args }) => {
|
|
36016
|
+
try {
|
|
36017
|
+
const id = args.id;
|
|
36018
|
+
const data = await apiGet("/api/landing-inspiration/section", { id });
|
|
36019
|
+
const section = data.section;
|
|
36020
|
+
const dir = path34.join(process.cwd(), ".baker", "inspiration", id);
|
|
36021
|
+
const [desktop, mobile, filmstrip] = await Promise.all([
|
|
36022
|
+
download(section.desktopShotUrl, path34.join(dir, "desktop.png")),
|
|
36023
|
+
download(section.mobileShotUrl, path34.join(dir, "mobile.png")),
|
|
36024
|
+
download(section.motionFilmstripUrl, path34.join(dir, "motion-filmstrip.png"))
|
|
36025
|
+
]);
|
|
36026
|
+
const full = args.full;
|
|
36027
|
+
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
36028
|
+
if (desktop) hints.push(`Read ${desktop} before you build \u2014 the description is not the section.`);
|
|
36029
|
+
hints.push("Need the mechanics? `baker landing inspiration code <id>` writes the standalone HTML+CSS to disk.");
|
|
36030
|
+
const fidelity = fidelityHint(section.fidelity);
|
|
36031
|
+
if (fidelity) hints.push(fidelity);
|
|
36032
|
+
if (filmstrip) {
|
|
36033
|
+
hints.push(
|
|
36034
|
+
"motion-filmstrip.png shows this section animating in, six frames left-to-right then top-to-bottom, with the elapsed time on each. Read it if you want the motion, not just the layout."
|
|
36035
|
+
);
|
|
36036
|
+
}
|
|
36037
|
+
writeJson({
|
|
36038
|
+
ok: true,
|
|
36039
|
+
data: {
|
|
36040
|
+
id: section.id,
|
|
36041
|
+
page_id: section.sourceId,
|
|
36042
|
+
section: section.sectionType,
|
|
36043
|
+
composition: section.composition,
|
|
36044
|
+
look: section.visualRegister,
|
|
36045
|
+
motion: section.motionSummary,
|
|
36046
|
+
design_tokens: section.designTokens,
|
|
36047
|
+
size: section.boundingBox,
|
|
36048
|
+
copy: {
|
|
36049
|
+
headline: section.headline,
|
|
36050
|
+
subhead: section.subhead,
|
|
36051
|
+
ctas: section.ctaLabels,
|
|
36052
|
+
proof: section.proofSignals
|
|
36053
|
+
},
|
|
36054
|
+
why_it_works: section.whyItWorks,
|
|
36055
|
+
adaptation_notes: section.adaptationNotes,
|
|
36056
|
+
craft: section.craftScore,
|
|
36057
|
+
fidelity: section.fidelity,
|
|
36058
|
+
source_url: section.sourceUrl,
|
|
36059
|
+
screenshots: { desktop, mobile, motion_filmstrip: filmstrip },
|
|
36060
|
+
// Behind --full: exactly the block `search` already gates, plus the
|
|
36061
|
+
// reproduction notes, which `code` returns again for the one caller
|
|
36062
|
+
// that acts on them. Everything above is what you design *from*;
|
|
36063
|
+
// these are how the section was filed and how it rebuilds.
|
|
36064
|
+
...full ? {
|
|
36065
|
+
theme: section.theme,
|
|
36066
|
+
density: section.density,
|
|
36067
|
+
show_tell_rank: section.showTellRank,
|
|
36068
|
+
interactions: section.interactions,
|
|
36069
|
+
media: section.mediaKinds,
|
|
36070
|
+
devices: section.contentDevices,
|
|
36071
|
+
tags: section.tags,
|
|
36072
|
+
reproduction_notes: section.reproductionNotes
|
|
36073
|
+
} : {}
|
|
36074
|
+
},
|
|
36075
|
+
hints
|
|
36076
|
+
});
|
|
36077
|
+
} catch (error) {
|
|
36078
|
+
reportError(error);
|
|
36079
|
+
}
|
|
36080
|
+
}
|
|
36081
|
+
});
|
|
36082
|
+
|
|
36083
|
+
// src/commands/landing/inspiration/index.ts
|
|
36084
|
+
var inspirationCommand = defineCommand150({
|
|
36085
|
+
meta: {
|
|
36086
|
+
name: "inspiration",
|
|
36087
|
+
description: `Reference library of real landing-page sections \u2014 look at how good pages actually solve a problem before you design one.
|
|
36088
|
+
|
|
36089
|
+
Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
|
|
36090
|
+
|
|
36091
|
+
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 and \`baker landing critique\` will block the publish.
|
|
36092
|
+
|
|
36093
|
+
Subcommands:
|
|
36094
|
+
baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
|
|
36095
|
+
baker landing inspiration view <id> \u2014 one section in full: tokens, motion filmstrip, why it works, what to change
|
|
36096
|
+
baker landing inspiration code <id> \u2014 its standalone HTML+CSS, for structure only
|
|
36097
|
+
baker landing inspiration page <id> \u2014 a whole page as a sequence: how it orders its sections
|
|
36098
|
+
baker landing inspiration add <url> \u2014 add a page to the library and save it to this company (returns immediately)
|
|
36099
|
+
baker landing inspiration favorites \u2014 what this company has saved; the default search scope
|
|
36100
|
+
baker landing inspiration favorite <id> \u2014 save a section
|
|
36101
|
+
baker landing inspiration unfavorite <id> \u2014 unsave a section
|
|
36102
|
+
baker landing inspiration scrape <url> \u2014 internal/ops: run the capture locally
|
|
36103
|
+
|
|
36104
|
+
Examples:
|
|
36105
|
+
baker landing inspiration search "pricing with a monthly/annual toggle" --max-rank 3
|
|
36106
|
+
baker landing inspiration search "dark developer hero with a terminal" --register dev-tool-minimal --scope all
|
|
36107
|
+
baker landing inspiration search "testimonial wall with faces and company logos" --motion scroll-reveal
|
|
36108
|
+
baker landing inspiration add https://linear.app
|
|
36109
|
+
|
|
36110
|
+
Full guide: __tooling__/docs/tools/baker/landing.md`
|
|
36111
|
+
},
|
|
36112
|
+
subCommands: {
|
|
36113
|
+
search: searchCommand2,
|
|
36114
|
+
view: viewCommand2,
|
|
36115
|
+
code: codeCommand,
|
|
36116
|
+
page: pageCommand,
|
|
36117
|
+
add: addCommand,
|
|
36118
|
+
favorites: favoritesCommand,
|
|
36119
|
+
favorite: favoriteCommand,
|
|
36120
|
+
unfavorite: unfavoriteCommand,
|
|
36121
|
+
scrape: scrapeCommand
|
|
36122
|
+
}
|
|
36123
|
+
});
|
|
36124
|
+
|
|
33333
36125
|
// src/commands/landing/index.ts
|
|
33334
|
-
var landingCommand =
|
|
36126
|
+
var landingCommand = defineCommand151({
|
|
33335
36127
|
meta: {
|
|
33336
36128
|
name: "landing",
|
|
33337
36129
|
description: `Design-quality tools for landing pages (src/pages/<slug>/).
|
|
@@ -33339,15 +36131,17 @@ var landingCommand = defineCommand143({
|
|
|
33339
36131
|
Start here: \`baker landing critique <slug>\` after building or editing a landing.
|
|
33340
36132
|
|
|
33341
36133
|
Subcommands:
|
|
36134
|
+
baker landing inspiration \u2014 reference library of real landing-page sections; search it during research, BEFORE writing the Direction Contract. Inspiration only: take the mechanism, never the words.
|
|
33342
36135
|
baker landing critique <slug> \u2014 deterministic design-quality critic (advisory): flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) tiered block/warn/advisory, respecting the client's BRAND.md. Records the critique the publish quality gate requires \u2014 run it before finishing a landing.`
|
|
33343
36136
|
},
|
|
33344
36137
|
subCommands: {
|
|
36138
|
+
inspiration: inspirationCommand,
|
|
33345
36139
|
critique: critiqueCommand2
|
|
33346
36140
|
}
|
|
33347
36141
|
});
|
|
33348
36142
|
|
|
33349
36143
|
// src/commands/mcp/index.ts
|
|
33350
|
-
import { defineCommand as
|
|
36144
|
+
import { defineCommand as defineCommand152 } from "citty";
|
|
33351
36145
|
|
|
33352
36146
|
// src/commands/mcp/platforms.ts
|
|
33353
36147
|
function readsKey(label) {
|
|
@@ -33416,7 +36210,7 @@ registerSchema({
|
|
|
33416
36210
|
description: "List everything this chat can reach: managed integrations (Attio, Slack, Gmail, Google Sheets, \u2026), custom MCP servers, and the platforms the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager) which you read through their own `baker` commands. Start here when the user mentions an external tool or platform.",
|
|
33417
36211
|
args: {}
|
|
33418
36212
|
});
|
|
33419
|
-
var connectedCommand =
|
|
36213
|
+
var connectedCommand = defineCommand152({
|
|
33420
36214
|
meta: {
|
|
33421
36215
|
name: "connected",
|
|
33422
36216
|
description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
|
|
@@ -33475,7 +36269,7 @@ registerSchema({
|
|
|
33475
36269
|
description: "List the custom MCP servers this company's chats see (org + company + your own user scope).",
|
|
33476
36270
|
args: {}
|
|
33477
36271
|
});
|
|
33478
|
-
var listCommand13 =
|
|
36272
|
+
var listCommand13 = defineCommand152({
|
|
33479
36273
|
meta: { name: "list", description: "List custom MCP servers visible to this company's chats." },
|
|
33480
36274
|
run: async () => {
|
|
33481
36275
|
try {
|
|
@@ -33512,7 +36306,7 @@ registerSchema({
|
|
|
33512
36306
|
header: { type: "string", description: 'Auth header "Key: Value" (repeatable)', required: false }
|
|
33513
36307
|
}
|
|
33514
36308
|
});
|
|
33515
|
-
var
|
|
36309
|
+
var addCommand2 = defineCommand152({
|
|
33516
36310
|
meta: {
|
|
33517
36311
|
name: "add",
|
|
33518
36312
|
description: `Register a custom MCP server. Tools appear as mcp__<name>__* on the NEXT message.
|
|
@@ -33564,7 +36358,7 @@ registerSchema({
|
|
|
33564
36358
|
description: "Remove a company custom MCP server by name.",
|
|
33565
36359
|
args: { name: { type: "string", description: "Server name to remove", required: true } }
|
|
33566
36360
|
});
|
|
33567
|
-
var removeCommand4 =
|
|
36361
|
+
var removeCommand4 = defineCommand152({
|
|
33568
36362
|
meta: {
|
|
33569
36363
|
name: "remove",
|
|
33570
36364
|
description: `Remove a company custom MCP server by name.
|
|
@@ -33586,7 +36380,7 @@ Example:
|
|
|
33586
36380
|
}
|
|
33587
36381
|
}
|
|
33588
36382
|
});
|
|
33589
|
-
var mcpCommand =
|
|
36383
|
+
var mcpCommand = defineCommand152({
|
|
33590
36384
|
meta: {
|
|
33591
36385
|
name: "mcp",
|
|
33592
36386
|
description: `Third-party tools for this company \u2014 see what's connected, register custom HTTPS MCP endpoints.
|
|
@@ -33606,16 +36400,16 @@ Full guide: __tooling__/docs/tools/baker/mcp.md`
|
|
|
33606
36400
|
subCommands: {
|
|
33607
36401
|
connected: connectedCommand,
|
|
33608
36402
|
list: listCommand13,
|
|
33609
|
-
add:
|
|
36403
|
+
add: addCommand2,
|
|
33610
36404
|
remove: removeCommand4
|
|
33611
36405
|
}
|
|
33612
36406
|
});
|
|
33613
36407
|
|
|
33614
36408
|
// src/commands/research/index.ts
|
|
33615
|
-
import { defineCommand as
|
|
36409
|
+
import { defineCommand as defineCommand163 } from "citty";
|
|
33616
36410
|
|
|
33617
36411
|
// src/commands/research/advertisers.ts
|
|
33618
|
-
import { defineCommand as
|
|
36412
|
+
import { defineCommand as defineCommand153 } from "citty";
|
|
33619
36413
|
|
|
33620
36414
|
// src/commands/research/output.ts
|
|
33621
36415
|
var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
|
|
@@ -33728,7 +36522,7 @@ var FIELDS3 = {
|
|
|
33728
36522
|
etv: "Estimated traffic value (USD)",
|
|
33729
36523
|
visibility: "SERP visibility score (0-1)"
|
|
33730
36524
|
};
|
|
33731
|
-
var advertisersCommand =
|
|
36525
|
+
var advertisersCommand = defineCommand153({
|
|
33732
36526
|
meta: {
|
|
33733
36527
|
name: "advertisers",
|
|
33734
36528
|
description: `Find domains competing for a keyword in Google SERPs.
|
|
@@ -33748,15 +36542,15 @@ Examples:
|
|
|
33748
36542
|
},
|
|
33749
36543
|
run: async ({ args }) => {
|
|
33750
36544
|
const keyword = args.keyword;
|
|
33751
|
-
const
|
|
36545
|
+
const location2 = args.location || void 0;
|
|
33752
36546
|
const language = args.language || void 0;
|
|
33753
36547
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33754
36548
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33755
|
-
const queryContext = buildResearchQueryContext(
|
|
36549
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33756
36550
|
try {
|
|
33757
36551
|
const data = await apiPost("/api/research/advertisers", {
|
|
33758
36552
|
keyword,
|
|
33759
|
-
location,
|
|
36553
|
+
location: location2,
|
|
33760
36554
|
language,
|
|
33761
36555
|
limit,
|
|
33762
36556
|
skipCache
|
|
@@ -33775,7 +36569,7 @@ Examples:
|
|
|
33775
36569
|
});
|
|
33776
36570
|
|
|
33777
36571
|
// src/commands/research/autocomplete.ts
|
|
33778
|
-
import { defineCommand as
|
|
36572
|
+
import { defineCommand as defineCommand154 } from "citty";
|
|
33779
36573
|
registerSchema({
|
|
33780
36574
|
command: "research.autocomplete",
|
|
33781
36575
|
description: "Get Google Autocomplete suggestions for a seed keyword. Useful for keyword expansion and discovering what people actually search for. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
|
|
@@ -33798,7 +36592,7 @@ registerSchema({
|
|
|
33798
36592
|
var FIELDS4 = {
|
|
33799
36593
|
suggestion: "Autocomplete suggestion from Google"
|
|
33800
36594
|
};
|
|
33801
|
-
var autocompleteCommand =
|
|
36595
|
+
var autocompleteCommand = defineCommand154({
|
|
33802
36596
|
meta: {
|
|
33803
36597
|
name: "autocomplete",
|
|
33804
36598
|
description: `Get Google Autocomplete suggestions for keyword expansion.
|
|
@@ -33817,15 +36611,15 @@ Examples:
|
|
|
33817
36611
|
},
|
|
33818
36612
|
run: async ({ args }) => {
|
|
33819
36613
|
const keyword = args.keyword;
|
|
33820
|
-
const
|
|
36614
|
+
const location2 = args.location || void 0;
|
|
33821
36615
|
const language = args.language || void 0;
|
|
33822
36616
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33823
36617
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33824
|
-
const queryContext = buildResearchQueryContext(
|
|
36618
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33825
36619
|
try {
|
|
33826
36620
|
const data = await apiPost("/api/research/autocomplete", {
|
|
33827
36621
|
keyword,
|
|
33828
|
-
location,
|
|
36622
|
+
location: location2,
|
|
33829
36623
|
language,
|
|
33830
36624
|
limit,
|
|
33831
36625
|
skipCache
|
|
@@ -33844,7 +36638,7 @@ Examples:
|
|
|
33844
36638
|
});
|
|
33845
36639
|
|
|
33846
36640
|
// src/commands/research/countries.ts
|
|
33847
|
-
import { defineCommand as
|
|
36641
|
+
import { defineCommand as defineCommand155 } from "citty";
|
|
33848
36642
|
registerSchema({
|
|
33849
36643
|
command: "research.countries",
|
|
33850
36644
|
description: "List all supported country codes for --location flag in research commands.",
|
|
@@ -33901,7 +36695,7 @@ var FIELDS5 = {
|
|
|
33901
36695
|
code: "Country code to pass as --location",
|
|
33902
36696
|
name: "Country name"
|
|
33903
36697
|
};
|
|
33904
|
-
var countriesCommand =
|
|
36698
|
+
var countriesCommand = defineCommand155({
|
|
33905
36699
|
meta: {
|
|
33906
36700
|
name: "countries",
|
|
33907
36701
|
description: "List all supported country codes for --location flag."
|
|
@@ -33912,7 +36706,7 @@ var countriesCommand = defineCommand147({
|
|
|
33912
36706
|
});
|
|
33913
36707
|
|
|
33914
36708
|
// src/commands/research/intent.ts
|
|
33915
|
-
import { defineCommand as
|
|
36709
|
+
import { defineCommand as defineCommand156 } from "citty";
|
|
33916
36710
|
registerSchema({
|
|
33917
36711
|
command: "research.intent",
|
|
33918
36712
|
description: "Classify Google Search intent for keywords. Determines if someone searching is looking to buy, research, or navigate. IMPORTANT: If --language is omitted, defaults to English (en). The response includes a query_context object showing which language was used.",
|
|
@@ -33935,7 +36729,7 @@ var FIELDS6 = {
|
|
|
33935
36729
|
intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
|
|
33936
36730
|
probability: "Confidence score 0.0-1.0"
|
|
33937
36731
|
};
|
|
33938
|
-
var intentCommand =
|
|
36732
|
+
var intentCommand = defineCommand156({
|
|
33939
36733
|
meta: {
|
|
33940
36734
|
name: "intent",
|
|
33941
36735
|
description: `Classify Google Search intent for keywords. Returns intent type and confidence.
|
|
@@ -33983,7 +36777,7 @@ Examples:
|
|
|
33983
36777
|
});
|
|
33984
36778
|
|
|
33985
36779
|
// src/commands/research/keyword-gap.ts
|
|
33986
|
-
import { defineCommand as
|
|
36780
|
+
import { defineCommand as defineCommand157 } from "citty";
|
|
33987
36781
|
registerSchema({
|
|
33988
36782
|
command: "research.keyword-gap",
|
|
33989
36783
|
description: "Find keywords a competitor ranks for (organic or paid) that you don't. Discovers expansion opportunities. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
|
|
@@ -34012,7 +36806,7 @@ var FIELDS7 = {
|
|
|
34012
36806
|
cpc: "Cost per click USD",
|
|
34013
36807
|
their_position: "Competitor's ranking position"
|
|
34014
36808
|
};
|
|
34015
|
-
var keywordGapCommand =
|
|
36809
|
+
var keywordGapCommand = defineCommand157({
|
|
34016
36810
|
meta: {
|
|
34017
36811
|
name: "keyword-gap",
|
|
34018
36812
|
description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
|
|
@@ -34036,18 +36830,18 @@ Examples:
|
|
|
34036
36830
|
run: async ({ args }) => {
|
|
34037
36831
|
const competitor = args.competitor;
|
|
34038
36832
|
const ours = args.ours;
|
|
34039
|
-
const
|
|
36833
|
+
const location2 = args.location || void 0;
|
|
34040
36834
|
const language = args.language || void 0;
|
|
34041
36835
|
const type = args.type || void 0;
|
|
34042
36836
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
34043
36837
|
const offset = args.offset ? Number(args.offset) : void 0;
|
|
34044
36838
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
34045
|
-
const queryContext = buildResearchQueryContext(
|
|
36839
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
34046
36840
|
try {
|
|
34047
36841
|
const result = await apiPost("/api/research/keyword-gap", {
|
|
34048
36842
|
competitor,
|
|
34049
36843
|
ours,
|
|
34050
|
-
location,
|
|
36844
|
+
location: location2,
|
|
34051
36845
|
language,
|
|
34052
36846
|
type,
|
|
34053
36847
|
limit,
|
|
@@ -34086,7 +36880,7 @@ Examples:
|
|
|
34086
36880
|
});
|
|
34087
36881
|
|
|
34088
36882
|
// src/commands/research/keywords-for-site.ts
|
|
34089
|
-
import { defineCommand as
|
|
36883
|
+
import { defineCommand as defineCommand158 } from "citty";
|
|
34090
36884
|
registerSchema({
|
|
34091
36885
|
command: "research.keywords-for-site",
|
|
34092
36886
|
description: "Get keywords a competitor targets in Google. Use --type paid to see only paid keywords, --type organic for organic only. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
|
|
@@ -34119,7 +36913,7 @@ var FIELDS8 = {
|
|
|
34119
36913
|
competition: "LOW, MEDIUM, or HIGH",
|
|
34120
36914
|
competition_index: "Competition score 0-100"
|
|
34121
36915
|
};
|
|
34122
|
-
var keywordsForSiteCommand =
|
|
36916
|
+
var keywordsForSiteCommand = defineCommand158({
|
|
34123
36917
|
meta: {
|
|
34124
36918
|
name: "keywords-for-site",
|
|
34125
36919
|
description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
|
|
@@ -34141,17 +36935,17 @@ Examples:
|
|
|
34141
36935
|
},
|
|
34142
36936
|
run: async ({ args }) => {
|
|
34143
36937
|
const target = args.target;
|
|
34144
|
-
const
|
|
36938
|
+
const location2 = args.location || void 0;
|
|
34145
36939
|
const language = args.language || void 0;
|
|
34146
36940
|
const sort = args.sort || void 0;
|
|
34147
36941
|
const type = args.type || void 0;
|
|
34148
36942
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
34149
36943
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
34150
|
-
const queryContext = buildResearchQueryContext(
|
|
36944
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
34151
36945
|
try {
|
|
34152
36946
|
const data = await apiPost("/api/research/keywords-for-site", {
|
|
34153
36947
|
target,
|
|
34154
|
-
location,
|
|
36948
|
+
location: location2,
|
|
34155
36949
|
language,
|
|
34156
36950
|
sort,
|
|
34157
36951
|
type,
|
|
@@ -34172,7 +36966,7 @@ Examples:
|
|
|
34172
36966
|
});
|
|
34173
36967
|
|
|
34174
36968
|
// src/commands/research/languages.ts
|
|
34175
|
-
import { defineCommand as
|
|
36969
|
+
import { defineCommand as defineCommand159 } from "citty";
|
|
34176
36970
|
registerSchema({
|
|
34177
36971
|
command: "research.languages",
|
|
34178
36972
|
description: "List all supported language codes for --language flag in research commands.",
|
|
@@ -34202,7 +36996,7 @@ var FIELDS9 = {
|
|
|
34202
36996
|
code: "Language code to pass as --language",
|
|
34203
36997
|
name: "Language name (also accepted by --language)"
|
|
34204
36998
|
};
|
|
34205
|
-
var languagesCommand2 =
|
|
36999
|
+
var languagesCommand2 = defineCommand159({
|
|
34206
37000
|
meta: {
|
|
34207
37001
|
name: "languages",
|
|
34208
37002
|
description: "List all supported language codes for --language flag."
|
|
@@ -34213,7 +37007,7 @@ var languagesCommand2 = defineCommand151({
|
|
|
34213
37007
|
});
|
|
34214
37008
|
|
|
34215
37009
|
// src/commands/research/lighthouse.ts
|
|
34216
|
-
import { defineCommand as
|
|
37010
|
+
import { defineCommand as defineCommand160 } from "citty";
|
|
34217
37011
|
registerSchema({
|
|
34218
37012
|
command: "research.lighthouse",
|
|
34219
37013
|
description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
|
|
@@ -34232,7 +37026,7 @@ var FIELDS10 = {
|
|
|
34232
37026
|
speed_index_ms: "Speed Index in ms (good: < 3400)",
|
|
34233
37027
|
interactive_ms: "Time to Interactive in ms (good: < 3800)"
|
|
34234
37028
|
};
|
|
34235
|
-
var lighthouseCommand =
|
|
37029
|
+
var lighthouseCommand = defineCommand160({
|
|
34236
37030
|
meta: {
|
|
34237
37031
|
name: "lighthouse",
|
|
34238
37032
|
description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
|
|
@@ -34270,7 +37064,7 @@ Examples:
|
|
|
34270
37064
|
});
|
|
34271
37065
|
|
|
34272
37066
|
// src/commands/research/relevant-pages.ts
|
|
34273
|
-
import { defineCommand as
|
|
37067
|
+
import { defineCommand as defineCommand161 } from "citty";
|
|
34274
37068
|
registerSchema({
|
|
34275
37069
|
command: "research.relevant-pages",
|
|
34276
37070
|
description: "Get the top pages of a competitor domain with organic traffic and ranking data. Shows which pages drive the most traffic. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
|
|
@@ -34296,7 +37090,7 @@ var FIELDS11 = {
|
|
|
34296
37090
|
keywords: "Total organic keywords the page ranks for",
|
|
34297
37091
|
top_10: "Keywords in positions 1-10"
|
|
34298
37092
|
};
|
|
34299
|
-
var relevantPagesCommand =
|
|
37093
|
+
var relevantPagesCommand = defineCommand161({
|
|
34300
37094
|
meta: {
|
|
34301
37095
|
name: "relevant-pages",
|
|
34302
37096
|
description: `Get the top pages of a competitor domain with traffic data.
|
|
@@ -34315,15 +37109,15 @@ Examples:
|
|
|
34315
37109
|
},
|
|
34316
37110
|
run: async ({ args }) => {
|
|
34317
37111
|
const target = args.target;
|
|
34318
|
-
const
|
|
37112
|
+
const location2 = args.location || void 0;
|
|
34319
37113
|
const language = args.language || void 0;
|
|
34320
37114
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
34321
37115
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
34322
|
-
const queryContext = buildResearchQueryContext(
|
|
37116
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
34323
37117
|
try {
|
|
34324
37118
|
const data = await apiPost("/api/research/relevant-pages", {
|
|
34325
37119
|
target,
|
|
34326
|
-
location,
|
|
37120
|
+
location: location2,
|
|
34327
37121
|
language,
|
|
34328
37122
|
limit,
|
|
34329
37123
|
skipCache
|
|
@@ -34342,7 +37136,7 @@ Examples:
|
|
|
34342
37136
|
});
|
|
34343
37137
|
|
|
34344
37138
|
// src/commands/research/web.ts
|
|
34345
|
-
import { defineCommand as
|
|
37139
|
+
import { defineCommand as defineCommand162 } from "citty";
|
|
34346
37140
|
registerSchema({
|
|
34347
37141
|
command: "research.web",
|
|
34348
37142
|
description: "Search the web with AI to answer marketing questions \u2014 competitors, ICP, pricing, pain points, market trends. Three depth levels: medium (quick, default), high (thorough), xhigh (exhaustive deep research).",
|
|
@@ -34393,7 +37187,7 @@ async function runDeepResearch(question) {
|
|
|
34393
37187
|
}
|
|
34394
37188
|
throw new Error("Deep research timed out");
|
|
34395
37189
|
}
|
|
34396
|
-
var webCommand =
|
|
37190
|
+
var webCommand = defineCommand162({
|
|
34397
37191
|
meta: {
|
|
34398
37192
|
name: "web",
|
|
34399
37193
|
description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
|
|
@@ -34453,7 +37247,7 @@ Examples:
|
|
|
34453
37247
|
});
|
|
34454
37248
|
|
|
34455
37249
|
// src/commands/research/index.ts
|
|
34456
|
-
var researchCommand =
|
|
37250
|
+
var researchCommand = defineCommand163({
|
|
34457
37251
|
meta: {
|
|
34458
37252
|
name: "research",
|
|
34459
37253
|
description: `Competitive intelligence and AI-powered research commands.
|
|
@@ -34494,10 +37288,10 @@ Full guide: __tooling__/docs/tools/baker/research.md`
|
|
|
34494
37288
|
});
|
|
34495
37289
|
|
|
34496
37290
|
// src/commands/scheduled-actions/index.ts
|
|
34497
|
-
import { defineCommand as
|
|
37291
|
+
import { defineCommand as defineCommand170 } from "citty";
|
|
34498
37292
|
|
|
34499
37293
|
// src/commands/scheduled-actions/create.ts
|
|
34500
|
-
import { defineCommand as
|
|
37294
|
+
import { defineCommand as defineCommand164 } from "citty";
|
|
34501
37295
|
|
|
34502
37296
|
// src/commands/scheduled-actions/shared.ts
|
|
34503
37297
|
var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
|
|
@@ -34612,7 +37406,7 @@ registerSchema({
|
|
|
34612
37406
|
prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
|
|
34613
37407
|
}
|
|
34614
37408
|
});
|
|
34615
|
-
var createCommand2 =
|
|
37409
|
+
var createCommand2 = defineCommand164({
|
|
34616
37410
|
meta: {
|
|
34617
37411
|
name: "create",
|
|
34618
37412
|
description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
|
|
@@ -34661,7 +37455,7 @@ var createCommand2 = defineCommand156({
|
|
|
34661
37455
|
});
|
|
34662
37456
|
|
|
34663
37457
|
// src/commands/scheduled-actions/delete.ts
|
|
34664
|
-
import { defineCommand as
|
|
37458
|
+
import { defineCommand as defineCommand165 } from "citty";
|
|
34665
37459
|
registerSchema({
|
|
34666
37460
|
command: "scheduled-actions.delete",
|
|
34667
37461
|
description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
|
|
@@ -34669,7 +37463,7 @@ registerSchema({
|
|
|
34669
37463
|
id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
|
|
34670
37464
|
}
|
|
34671
37465
|
});
|
|
34672
|
-
var deleteCommand2 =
|
|
37466
|
+
var deleteCommand2 = defineCommand165({
|
|
34673
37467
|
meta: {
|
|
34674
37468
|
name: "delete",
|
|
34675
37469
|
description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
|
|
@@ -34698,7 +37492,7 @@ var deleteCommand2 = defineCommand157({
|
|
|
34698
37492
|
});
|
|
34699
37493
|
|
|
34700
37494
|
// src/commands/scheduled-actions/get.ts
|
|
34701
|
-
import { defineCommand as
|
|
37495
|
+
import { defineCommand as defineCommand166 } from "citty";
|
|
34702
37496
|
registerSchema({
|
|
34703
37497
|
command: "scheduled-actions.get",
|
|
34704
37498
|
description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
|
|
@@ -34707,7 +37501,7 @@ registerSchema({
|
|
|
34707
37501
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
34708
37502
|
}
|
|
34709
37503
|
});
|
|
34710
|
-
var getCommand3 =
|
|
37504
|
+
var getCommand3 = defineCommand166({
|
|
34711
37505
|
meta: {
|
|
34712
37506
|
name: "get",
|
|
34713
37507
|
description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
|
|
@@ -34746,7 +37540,7 @@ var getCommand3 = defineCommand158({
|
|
|
34746
37540
|
});
|
|
34747
37541
|
|
|
34748
37542
|
// src/commands/scheduled-actions/list.ts
|
|
34749
|
-
import { defineCommand as
|
|
37543
|
+
import { defineCommand as defineCommand167 } from "citty";
|
|
34750
37544
|
registerSchema({
|
|
34751
37545
|
command: "scheduled-actions.list",
|
|
34752
37546
|
description: "List published scheduled actions. Includes draft state when BAKER_CHAT_ID is set, or --chat <id> to read an earlier chat's staged schedules instead.",
|
|
@@ -34754,7 +37548,7 @@ registerSchema({
|
|
|
34754
37548
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
34755
37549
|
}
|
|
34756
37550
|
});
|
|
34757
|
-
var listCommand14 =
|
|
37551
|
+
var listCommand14 = defineCommand167({
|
|
34758
37552
|
meta: {
|
|
34759
37553
|
name: "list",
|
|
34760
37554
|
description: "List scheduled actions. Includes staged draft ops when BAKER_CHAT_ID is set, or --chat <id> to read an earlier chat's staged schedules instead."
|
|
@@ -34777,7 +37571,7 @@ var listCommand14 = defineCommand159({
|
|
|
34777
37571
|
});
|
|
34778
37572
|
|
|
34779
37573
|
// src/commands/scheduled-actions/trigger.ts
|
|
34780
|
-
import { defineCommand as
|
|
37574
|
+
import { defineCommand as defineCommand168 } from "citty";
|
|
34781
37575
|
registerSchema({
|
|
34782
37576
|
command: "scheduled-actions.trigger",
|
|
34783
37577
|
description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
|
|
@@ -34785,7 +37579,7 @@ registerSchema({
|
|
|
34785
37579
|
id: { type: "string", description: "Published scheduled action ID", required: true }
|
|
34786
37580
|
}
|
|
34787
37581
|
});
|
|
34788
|
-
var triggerCommand =
|
|
37582
|
+
var triggerCommand = defineCommand168({
|
|
34789
37583
|
meta: {
|
|
34790
37584
|
name: "trigger",
|
|
34791
37585
|
description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
|
|
@@ -34822,7 +37616,7 @@ var triggerCommand = defineCommand160({
|
|
|
34822
37616
|
});
|
|
34823
37617
|
|
|
34824
37618
|
// src/commands/scheduled-actions/update.ts
|
|
34825
|
-
import { defineCommand as
|
|
37619
|
+
import { defineCommand as defineCommand169 } from "citty";
|
|
34826
37620
|
registerSchema({
|
|
34827
37621
|
command: "scheduled-actions.update",
|
|
34828
37622
|
description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
|
|
@@ -34847,7 +37641,7 @@ registerSchema({
|
|
|
34847
37641
|
prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
|
|
34848
37642
|
}
|
|
34849
37643
|
});
|
|
34850
|
-
var updateCommand2 =
|
|
37644
|
+
var updateCommand2 = defineCommand169({
|
|
34851
37645
|
meta: {
|
|
34852
37646
|
name: "update",
|
|
34853
37647
|
description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
|
|
@@ -34918,7 +37712,7 @@ var updateCommand2 = defineCommand161({
|
|
|
34918
37712
|
});
|
|
34919
37713
|
|
|
34920
37714
|
// src/commands/scheduled-actions/index.ts
|
|
34921
|
-
var scheduledActionsCommand =
|
|
37715
|
+
var scheduledActionsCommand = defineCommand170({
|
|
34922
37716
|
meta: {
|
|
34923
37717
|
name: "scheduled-actions",
|
|
34924
37718
|
description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
|
|
@@ -34945,14 +37739,14 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
34945
37739
|
});
|
|
34946
37740
|
|
|
34947
37741
|
// src/commands/schema.ts
|
|
34948
|
-
import { defineCommand as
|
|
37742
|
+
import { defineCommand as defineCommand171 } from "citty";
|
|
34949
37743
|
function narrowToFamily(commandName, available) {
|
|
34950
37744
|
const segments = commandName.split(".");
|
|
34951
37745
|
const prefix = segments[0] === "ads" && segments[1] ? `ads.${segments[1]}.` : `${segments[0]}.`;
|
|
34952
37746
|
const siblings = available.filter((name) => name.startsWith(prefix));
|
|
34953
37747
|
return siblings.length > 0 ? siblings : available;
|
|
34954
37748
|
}
|
|
34955
|
-
var schemaCommand =
|
|
37749
|
+
var schemaCommand = defineCommand171({
|
|
34956
37750
|
meta: {
|
|
34957
37751
|
name: "schema",
|
|
34958
37752
|
description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
|
|
@@ -34996,10 +37790,10 @@ var schemaCommand = defineCommand163({
|
|
|
34996
37790
|
});
|
|
34997
37791
|
|
|
34998
37792
|
// src/commands/studio/index.ts
|
|
34999
|
-
import { defineCommand as
|
|
37793
|
+
import { defineCommand as defineCommand180 } from "citty";
|
|
35000
37794
|
|
|
35001
37795
|
// src/commands/studio/animate.ts
|
|
35002
|
-
import { defineCommand as
|
|
37796
|
+
import { defineCommand as defineCommand172 } from "citty";
|
|
35003
37797
|
|
|
35004
37798
|
// src/commands/studio/batch.ts
|
|
35005
37799
|
function projectBatch(generation, full) {
|
|
@@ -35188,7 +37982,7 @@ function parseImageRefs(spec) {
|
|
|
35188
37982
|
}
|
|
35189
37983
|
var defaultDeps = {
|
|
35190
37984
|
ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
|
|
35191
|
-
upload: (
|
|
37985
|
+
upload: (path35) => uploadLocalImage({ file: path35, contentType: detectImageContentType(path35), source: "uploaded" })
|
|
35192
37986
|
};
|
|
35193
37987
|
async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
35194
37988
|
const refs = parseImageRefs(spec);
|
|
@@ -35208,10 +38002,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
|
35208
38002
|
}
|
|
35209
38003
|
return { imageIds, added };
|
|
35210
38004
|
}
|
|
35211
|
-
function uploadFailure(
|
|
38005
|
+
function uploadFailure(path35) {
|
|
35212
38006
|
return (error) => {
|
|
35213
38007
|
if (error instanceof ApiError) throw error;
|
|
35214
|
-
throw new ApiError("VALIDATION_ERROR", `Could not read "${
|
|
38008
|
+
throw new ApiError("VALIDATION_ERROR", `Could not read "${path35}" as an image.`);
|
|
35215
38009
|
};
|
|
35216
38010
|
}
|
|
35217
38011
|
|
|
@@ -35444,7 +38238,7 @@ function costHintsFor(body) {
|
|
|
35444
38238
|
}
|
|
35445
38239
|
return hints;
|
|
35446
38240
|
}
|
|
35447
|
-
var animateCommand =
|
|
38241
|
+
var animateCommand = defineCommand172({
|
|
35448
38242
|
meta: {
|
|
35449
38243
|
name: "animate",
|
|
35450
38244
|
description: "Render a clip. With an image the look is already fixed, so the prompt describes MOVEMENT \u2014 what the camera does, what the subject does, in what order. With --from text there is no image and the prompt is the whole shot.\n\nA rendered clip is NOT usable anywhere until you keep it: `baker studio keep <id> --slot N` is what puts it in the video library. Takes nobody keeps are never ingested, which is what makes a rejected batch cheap.\n\nFor anything longer than 15 seconds, or to build on footage that already exists, use --model bytedance/seedance-2.5: it renders 4-30s and is the only model that reads an existing clip or an existing soundtrack.\n\nExamples:\n baker studio animate 'slow push in, model turns to camera and smiles' --image j57abc123def456ghi789\n baker studio animate 'handheld drift right, steam rising from the cup' --image './out/hero.png' --duration 6 --quality 1080p\n baker studio animate 'product rotates once on a turntable' --image j57abc\u2026,j57def\u2026 --from references\n baker studio animate 'she keeps walking, camera stays with her, then she stops and looks up' --image j57abc\u2026 --from references --from-clip j57batch\u2026:0 --model bytedance/seedance-2.5 --duration 20\n baker studio animate 'hold on the product, then a slow push-in' --from references --from-video j57vid\u2026 --model bytedance/seedance-2.5\n baker studio animate 'slow drone pull-back over a solar farm at golden hour, no people' --from text --model bytedance/seedance-2.5 --duration 12"
|
|
@@ -35563,7 +38357,7 @@ var animateCommand = defineCommand164({
|
|
|
35563
38357
|
});
|
|
35564
38358
|
|
|
35565
38359
|
// src/commands/studio/generate.ts
|
|
35566
|
-
import { defineCommand as
|
|
38360
|
+
import { defineCommand as defineCommand173 } from "citty";
|
|
35567
38361
|
var MODEL_LIST2 = IMAGE_MODEL_IDS;
|
|
35568
38362
|
var DEFAULT_MAX_WAIT_MS2 = 24e4;
|
|
35569
38363
|
registerSchema({
|
|
@@ -35693,7 +38487,7 @@ function buildGenerateBody(args, prompt) {
|
|
|
35693
38487
|
}
|
|
35694
38488
|
return body;
|
|
35695
38489
|
}
|
|
35696
|
-
var generateCommand =
|
|
38490
|
+
var generateCommand = defineCommand173({
|
|
35697
38491
|
meta: {
|
|
35698
38492
|
name: "generate",
|
|
35699
38493
|
description: "Start here to make an image. Renders 1-8 takes of one brief, ingests each into the media library as it lands, and shows the batch in the dashboard Studio next to the ones the client ran.\n\nModel choice: google/gemini-3.1-flash-image-preview (default \u2014 fast, best at editing a reference and at extreme ratios), google/gemini-3-pro-image-preview (highest fidelity, slower), openai/gpt-image-2 (photoreal and the cleanest in-image text \u2014 no --image-size, no 4:5 / 5:4), recraft/recraft-v4.1-pro-vector (vector/flat marks with palette control).\n\n--reference is the biggest quality lever there is: a real logo, product shot, Pinterest pin or sandbox screenshot beats any amount of adjectives.\n\nExamples:\n baker studio generate 'matte black bottle on wet marble, hard studio light, 35mm' --aspect-ratio 3:2 --count 3\n baker studio generate 'this bottle on a sunlit kitchen counter' --reference './src/brand/product.png,https://\u2026/kitchen.jpg'\n baker studio generate 'founder-style selfie, kitchen background, natural light' --skill ugc-selfie-hook\n baker studio generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
|
|
@@ -35771,7 +38565,7 @@ var generateCommand = defineCommand165({
|
|
|
35771
38565
|
});
|
|
35772
38566
|
|
|
35773
38567
|
// src/commands/studio/get.ts
|
|
35774
|
-
import { defineCommand as
|
|
38568
|
+
import { defineCommand as defineCommand174 } from "citty";
|
|
35775
38569
|
registerSchema({
|
|
35776
38570
|
command: "studio.get",
|
|
35777
38571
|
description: "Read one Studio batch: every take, where it lives, and why a take is missing. This is how you pick up a batch that was still rendering when the start command returned.",
|
|
@@ -35780,7 +38574,7 @@ registerSchema({
|
|
|
35780
38574
|
full: { type: "boolean", description: "Include settings, references and attribution", required: false }
|
|
35781
38575
|
}
|
|
35782
38576
|
});
|
|
35783
|
-
var getCommand4 =
|
|
38577
|
+
var getCommand4 = defineCommand174({
|
|
35784
38578
|
meta: {
|
|
35785
38579
|
name: "get",
|
|
35786
38580
|
description: "Read one Studio batch \u2014 the takes, their urls, whether each is in the library, and the reason for any that failed.\n\nExample: baker studio get j57abc123def456ghi789\nExample: baker studio get j57abc123def456ghi789 --full"
|
|
@@ -35809,7 +38603,7 @@ var getCommand4 = defineCommand166({
|
|
|
35809
38603
|
});
|
|
35810
38604
|
|
|
35811
38605
|
// src/commands/studio/improve.ts
|
|
35812
|
-
import { defineCommand as
|
|
38606
|
+
import { defineCommand as defineCommand175 } from "citty";
|
|
35813
38607
|
var DESCRIPTION = "Sharpen a rough brief into directed art direction \u2014 the same rewrite the client gets from the wand in the Studio prompt bar. Reach for it when you are relaying the CLIENT's own words and want them shaped without substituting your voice; when you are writing the art direction yourself, just write it, because you will do a better job than this does.";
|
|
35814
38608
|
registerSchema({
|
|
35815
38609
|
command: "studio.improve",
|
|
@@ -35834,7 +38628,7 @@ registerSchema({
|
|
|
35834
38628
|
}
|
|
35835
38629
|
}
|
|
35836
38630
|
});
|
|
35837
|
-
var improveCommand =
|
|
38631
|
+
var improveCommand = defineCommand175({
|
|
35838
38632
|
meta: {
|
|
35839
38633
|
name: "improve",
|
|
35840
38634
|
description: `${DESCRIPTION}
|
|
@@ -35878,7 +38672,7 @@ Examples:
|
|
|
35878
38672
|
});
|
|
35879
38673
|
|
|
35880
38674
|
// src/commands/studio/keep.ts
|
|
35881
|
-
import { defineCommand as
|
|
38675
|
+
import { defineCommand as defineCommand176 } from "citty";
|
|
35882
38676
|
registerSchema({
|
|
35883
38677
|
command: "studio.keep",
|
|
35884
38678
|
description: "Mark one take as the keeper. For an image this stars it, so the client reviewing the batch sees which one you used. For a CLIP it is the step that puts it in the video library \u2014 until then the clip cannot be used in a canvas, a landing, or an ad.",
|
|
@@ -35888,7 +38682,7 @@ registerSchema({
|
|
|
35888
38682
|
undo: { type: "boolean", description: "Un-star an image, or take a kept clip back out", required: false }
|
|
35889
38683
|
}
|
|
35890
38684
|
});
|
|
35891
|
-
var keepCommand =
|
|
38685
|
+
var keepCommand = defineCommand176({
|
|
35892
38686
|
meta: {
|
|
35893
38687
|
name: "keep",
|
|
35894
38688
|
description: "Mark one take as the keeper. An image gets starred (it was already in the library); a clip gets INGESTED into the video library, which is what makes it usable anywhere else.\n\nExample: baker studio keep j57abc123def456ghi789 --slot 2\nExample: baker studio keep j57abc123def456ghi789 --slot 2 --undo"
|
|
@@ -35939,7 +38733,7 @@ var keepCommand = defineCommand168({
|
|
|
35939
38733
|
});
|
|
35940
38734
|
|
|
35941
38735
|
// src/commands/studio/list.ts
|
|
35942
|
-
import { defineCommand as
|
|
38736
|
+
import { defineCommand as defineCommand177 } from "citty";
|
|
35943
38737
|
registerSchema({
|
|
35944
38738
|
command: "studio.list",
|
|
35945
38739
|
description: "Recent Studio batches for THIS conversation, newest first \u2014 what you have already generated, so you re-use a take instead of paying for it twice. `--all` widens it to everything the company generated, including what people ran themselves in the dashboard.",
|
|
@@ -35950,7 +38744,7 @@ registerSchema({
|
|
|
35950
38744
|
full: { type: "boolean", description: "Include settings, references and attribution", required: false }
|
|
35951
38745
|
}
|
|
35952
38746
|
});
|
|
35953
|
-
var listCommand15 =
|
|
38747
|
+
var listCommand15 = defineCommand177({
|
|
35954
38748
|
meta: {
|
|
35955
38749
|
name: "list",
|
|
35956
38750
|
description: "Recent Studio batches, newest first. Scoped to this conversation unless you pass --all.\n\nExample: baker studio list\nExample: baker studio list --kind video --limit 5\nExample: baker studio list --all # includes batches the client ran in the dashboard"
|
|
@@ -35984,7 +38778,7 @@ var listCommand15 = defineCommand169({
|
|
|
35984
38778
|
});
|
|
35985
38779
|
|
|
35986
38780
|
// src/commands/studio/models.ts
|
|
35987
|
-
import { defineCommand as
|
|
38781
|
+
import { defineCommand as defineCommand178 } from "citty";
|
|
35988
38782
|
var DESCRIPTION2 = "What each Studio model actually accepts: its shapes, resolutions, clip lengths, prompt character cap, how many reference images it takes, and which knobs it has. Read this before a batch you care about \u2014 the models disagree far more than they look like they do, and a setting the chosen model does not have is REFUSED, not ignored.";
|
|
35989
38783
|
registerSchema({
|
|
35990
38784
|
command: "studio.models",
|
|
@@ -36071,7 +38865,7 @@ function buildModelCards(kind, model) {
|
|
|
36071
38865
|
const selected = model ? ids.filter((id) => id === model) : ids;
|
|
36072
38866
|
return selected.map((id) => build(id));
|
|
36073
38867
|
}
|
|
36074
|
-
var modelsCommand =
|
|
38868
|
+
var modelsCommand = defineCommand178({
|
|
36075
38869
|
meta: {
|
|
36076
38870
|
name: "models",
|
|
36077
38871
|
description: `${DESCRIPTION2}
|
|
@@ -36118,13 +38912,13 @@ Examples:
|
|
|
36118
38912
|
});
|
|
36119
38913
|
|
|
36120
38914
|
// src/commands/studio/skills.ts
|
|
36121
|
-
import { defineCommand as
|
|
38915
|
+
import { defineCommand as defineCommand179 } from "citty";
|
|
36122
38916
|
registerSchema({
|
|
36123
38917
|
command: "studio.skills",
|
|
36124
38918
|
description: "The craft directions `studio generate --skill <id>` accepts. Each one carries directed art direction plus the model, shape and take count it wants, so you pick a look by name instead of writing the boilerplate yourself.",
|
|
36125
38919
|
args: {}
|
|
36126
38920
|
});
|
|
36127
|
-
var skillsCommand =
|
|
38921
|
+
var skillsCommand = defineCommand179({
|
|
36128
38922
|
meta: {
|
|
36129
38923
|
name: "skills",
|
|
36130
38924
|
description: "List the craft directions available to `baker studio generate --skill <id>` \u2014 what each one is for, whether it wants a reference image, and the model/shape/count it defaults to.\n\nExample: baker studio skills"
|
|
@@ -36148,7 +38942,7 @@ var skillsCommand = defineCommand171({
|
|
|
36148
38942
|
});
|
|
36149
38943
|
|
|
36150
38944
|
// src/commands/studio/index.ts
|
|
36151
|
-
var studioCommand =
|
|
38945
|
+
var studioCommand = defineCommand180({
|
|
36152
38946
|
meta: {
|
|
36153
38947
|
name: "studio",
|
|
36154
38948
|
description: `Make new imagery and clips. Every batch is recorded and shows up in the dashboard Studio for the client to review, labelled with this conversation.
|
|
@@ -36191,10 +38985,10 @@ Full guide: __tooling__/docs/tools/baker/studio.md`
|
|
|
36191
38985
|
});
|
|
36192
38986
|
|
|
36193
38987
|
// src/commands/tag-manager/index.ts
|
|
36194
|
-
import { defineCommand as
|
|
38988
|
+
import { defineCommand as defineCommand184 } from "citty";
|
|
36195
38989
|
|
|
36196
38990
|
// src/commands/tag-manager/draft.ts
|
|
36197
|
-
import { defineCommand as
|
|
38991
|
+
import { defineCommand as defineCommand181 } from "citty";
|
|
36198
38992
|
|
|
36199
38993
|
// src/commands/tag-manager/shared.ts
|
|
36200
38994
|
import { readFileSync as readFileSync13 } from "fs";
|
|
@@ -36261,10 +39055,10 @@ async function stageOp4(op) {
|
|
|
36261
39055
|
handleError4(err);
|
|
36262
39056
|
}
|
|
36263
39057
|
}
|
|
36264
|
-
async function draftAction3(
|
|
39058
|
+
async function draftAction3(path35, body, chat) {
|
|
36265
39059
|
const chatId = resolveChatId(chat);
|
|
36266
39060
|
try {
|
|
36267
|
-
const data = await apiPost(
|
|
39061
|
+
const data = await apiPost(path35, { chatId, ...body });
|
|
36268
39062
|
writeJsonEnvelope({ ok: true, data });
|
|
36269
39063
|
return data;
|
|
36270
39064
|
} catch (err) {
|
|
@@ -36317,13 +39111,13 @@ registerSchema({
|
|
|
36317
39111
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
36318
39112
|
}
|
|
36319
39113
|
});
|
|
36320
|
-
var draftCommand4 =
|
|
39114
|
+
var draftCommand4 = defineCommand181({
|
|
36321
39115
|
meta: {
|
|
36322
39116
|
name: "draft",
|
|
36323
39117
|
description: "List, show, amend, remove, or clear staged Tag Manager changes for this chat. `list` and `show` take --chat <id> to read an earlier chat's changes instead."
|
|
36324
39118
|
},
|
|
36325
39119
|
subCommands: {
|
|
36326
|
-
list:
|
|
39120
|
+
list: defineCommand181({
|
|
36327
39121
|
meta: {
|
|
36328
39122
|
name: "list",
|
|
36329
39123
|
description: "Review everything staged on this chat (--json for the raw envelope)"
|
|
@@ -36336,7 +39130,7 @@ var draftCommand4 = defineCommand173({
|
|
|
36336
39130
|
await draftList2(args.json === true, args.chat);
|
|
36337
39131
|
}
|
|
36338
39132
|
}),
|
|
36339
|
-
show:
|
|
39133
|
+
show: defineCommand181({
|
|
36340
39134
|
meta: {
|
|
36341
39135
|
name: "show",
|
|
36342
39136
|
description: "Print the full staged payload for one change \u2014 the receipt to verify it looks right before publish (never truncated)."
|
|
@@ -36353,7 +39147,7 @@ var draftCommand4 = defineCommand173({
|
|
|
36353
39147
|
);
|
|
36354
39148
|
}
|
|
36355
39149
|
}),
|
|
36356
|
-
amend:
|
|
39150
|
+
amend: defineCommand181({
|
|
36357
39151
|
meta: {
|
|
36358
39152
|
name: "amend",
|
|
36359
39153
|
description: "Update a staged change in place \u2014 merges a JSON patch into its payload (objects deep-merge, null deletes a key, arrays/scalars replace) and re-validates. Use this instead of remove + re-create."
|
|
@@ -36370,7 +39164,7 @@ var draftCommand4 = defineCommand173({
|
|
|
36370
39164
|
});
|
|
36371
39165
|
}
|
|
36372
39166
|
}),
|
|
36373
|
-
remove:
|
|
39167
|
+
remove: defineCommand181({
|
|
36374
39168
|
meta: { name: "remove", description: "Remove one staged change (cascades to anything depending on it)" },
|
|
36375
39169
|
args: { ref: { type: "positional", description: "Staged ref (gtm_temp_*) or target", required: false } },
|
|
36376
39170
|
run: async ({ args }) => {
|
|
@@ -36379,7 +39173,7 @@ var draftCommand4 = defineCommand173({
|
|
|
36379
39173
|
});
|
|
36380
39174
|
}
|
|
36381
39175
|
}),
|
|
36382
|
-
clear:
|
|
39176
|
+
clear: defineCommand181({
|
|
36383
39177
|
meta: { name: "clear", description: "Discard all Tag Manager changes staged on this chat" },
|
|
36384
39178
|
run: async () => {
|
|
36385
39179
|
await draftAction3("/api/tag-manager/draft/clear", {});
|
|
@@ -36389,7 +39183,7 @@ var draftCommand4 = defineCommand173({
|
|
|
36389
39183
|
});
|
|
36390
39184
|
|
|
36391
39185
|
// src/commands/tag-manager/read.ts
|
|
36392
|
-
import { defineCommand as
|
|
39186
|
+
import { defineCommand as defineCommand182 } from "citty";
|
|
36393
39187
|
registerSchema({
|
|
36394
39188
|
command: "tagManager.containers",
|
|
36395
39189
|
description: "List the Google Tag Manager containers this company's connection can reach. Every container the company connected is flagged `connected: true` \u2014 there can be several, and Baker may read and change all of them. Start here to confirm which containers you are managing.",
|
|
@@ -36430,7 +39224,7 @@ function containersHints(containers) {
|
|
|
36430
39224
|
}))
|
|
36431
39225
|
});
|
|
36432
39226
|
}
|
|
36433
|
-
var containersCommand =
|
|
39227
|
+
var containersCommand = defineCommand182({
|
|
36434
39228
|
meta: {
|
|
36435
39229
|
name: "containers",
|
|
36436
39230
|
description: `List Tag Manager containers reachable by this company's connection.
|
|
@@ -36447,7 +39241,7 @@ Start here:
|
|
|
36447
39241
|
}
|
|
36448
39242
|
}
|
|
36449
39243
|
});
|
|
36450
|
-
var readCommand =
|
|
39244
|
+
var readCommand = defineCommand182({
|
|
36451
39245
|
meta: {
|
|
36452
39246
|
name: "read",
|
|
36453
39247
|
description: `Read the current contents of the Tag Manager container \u2014 always do this before staging changes.
|
|
@@ -36489,7 +39283,7 @@ Examples:
|
|
|
36489
39283
|
});
|
|
36490
39284
|
|
|
36491
39285
|
// src/commands/tag-manager/write-commands.ts
|
|
36492
|
-
import { defineCommand as
|
|
39286
|
+
import { defineCommand as defineCommand183 } from "citty";
|
|
36493
39287
|
var CONTAINER_ARG_DESCRIPTION = "Numeric container id (optional only when one container is connected \u2014 run `baker tag-manager containers`)";
|
|
36494
39288
|
var ENTITIES = [
|
|
36495
39289
|
{
|
|
@@ -36545,10 +39339,10 @@ for (const { entity, noun, createHint } of ENTITIES) {
|
|
|
36545
39339
|
});
|
|
36546
39340
|
}
|
|
36547
39341
|
function entityCommand(entity, noun, example) {
|
|
36548
|
-
return
|
|
39342
|
+
return defineCommand183({
|
|
36549
39343
|
meta: { name: entity, description: `Stage ${noun} changes on this chat's Tag Manager draft` },
|
|
36550
39344
|
subCommands: {
|
|
36551
|
-
create:
|
|
39345
|
+
create: defineCommand183({
|
|
36552
39346
|
meta: {
|
|
36553
39347
|
name: "create",
|
|
36554
39348
|
description: `Stage a new ${noun}
|
|
@@ -36570,7 +39364,7 @@ Examples:
|
|
|
36570
39364
|
});
|
|
36571
39365
|
}
|
|
36572
39366
|
}),
|
|
36573
|
-
update:
|
|
39367
|
+
update: defineCommand183({
|
|
36574
39368
|
meta: {
|
|
36575
39369
|
name: "update",
|
|
36576
39370
|
description: `Stage an update to an existing ${noun} (pass its id or path)`
|
|
@@ -36590,7 +39384,7 @@ Examples:
|
|
|
36590
39384
|
});
|
|
36591
39385
|
}
|
|
36592
39386
|
}),
|
|
36593
|
-
delete:
|
|
39387
|
+
delete: defineCommand183({
|
|
36594
39388
|
meta: { name: "delete", description: `Stage the deletion of a ${noun} (pass its id or path)` },
|
|
36595
39389
|
args: {
|
|
36596
39390
|
id: { type: "positional", description: `${noun} id or path`, required: false },
|
|
@@ -36631,7 +39425,7 @@ function builtinTypes(args) {
|
|
|
36631
39425
|
}
|
|
36632
39426
|
return raw.split(",").map((entry) => entry.trim());
|
|
36633
39427
|
}
|
|
36634
|
-
var builtinCommand =
|
|
39428
|
+
var builtinCommand = defineCommand183({
|
|
36635
39429
|
meta: {
|
|
36636
39430
|
name: "builtin",
|
|
36637
39431
|
description: `Enable or disable built-in variables
|
|
@@ -36641,7 +39435,7 @@ Examples:
|
|
|
36641
39435
|
baker tag-manager builtin disable --types formId`
|
|
36642
39436
|
},
|
|
36643
39437
|
subCommands: {
|
|
36644
|
-
enable:
|
|
39438
|
+
enable: defineCommand183({
|
|
36645
39439
|
meta: { name: "enable", description: "Stage enabling built-in variables" },
|
|
36646
39440
|
args: {
|
|
36647
39441
|
types: { type: "string", description: "Comma-separated types", required: false },
|
|
@@ -36655,7 +39449,7 @@ Examples:
|
|
|
36655
39449
|
});
|
|
36656
39450
|
}
|
|
36657
39451
|
}),
|
|
36658
|
-
disable:
|
|
39452
|
+
disable: defineCommand183({
|
|
36659
39453
|
meta: { name: "disable", description: "Stage disabling built-in variables" },
|
|
36660
39454
|
args: {
|
|
36661
39455
|
types: { type: "string", description: "Comma-separated types", required: false },
|
|
@@ -36673,7 +39467,7 @@ Examples:
|
|
|
36673
39467
|
});
|
|
36674
39468
|
|
|
36675
39469
|
// src/commands/tag-manager/index.ts
|
|
36676
|
-
var tagManagerCommand =
|
|
39470
|
+
var tagManagerCommand = defineCommand184({
|
|
36677
39471
|
meta: {
|
|
36678
39472
|
name: "tag-manager",
|
|
36679
39473
|
description: `Read and change what lives inside the client's Google Tag Manager container \u2014 tags, triggers, variables, folders and built-in variables.
|
|
@@ -36710,7 +39504,7 @@ Full guide: __tooling__/docs/tools/baker/tag-manager.md`
|
|
|
36710
39504
|
});
|
|
36711
39505
|
|
|
36712
39506
|
// src/commands/tags/index.ts
|
|
36713
|
-
import { defineCommand as
|
|
39507
|
+
import { defineCommand as defineCommand185 } from "citty";
|
|
36714
39508
|
|
|
36715
39509
|
// src/commands/tags/shared.ts
|
|
36716
39510
|
function failApi3(err) {
|
|
@@ -36779,7 +39573,7 @@ async function listTags(json) {
|
|
|
36779
39573
|
var listArgs9 = {
|
|
36780
39574
|
json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable list" }
|
|
36781
39575
|
};
|
|
36782
|
-
var listCommand16 =
|
|
39576
|
+
var listCommand16 = defineCommand185({
|
|
36783
39577
|
meta: {
|
|
36784
39578
|
name: "list",
|
|
36785
39579
|
description: "Effective tags for this chat (production + staged), with each tag's full readable config (secrets excluded) \u2014 reuse a stored value to pre-fill a change rather than asking the user. Refs printed here are what flow side-effect tagIds should use. Example: baker tags list"
|
|
@@ -36798,7 +39592,7 @@ async function listDraft3(chat) {
|
|
|
36798
39592
|
failApi3(err);
|
|
36799
39593
|
}
|
|
36800
39594
|
}
|
|
36801
|
-
var draftCommand5 =
|
|
39595
|
+
var draftCommand5 = defineCommand185({
|
|
36802
39596
|
meta: {
|
|
36803
39597
|
name: "draft",
|
|
36804
39598
|
description: "Review the tag changes staged in this chat (read-only). Staged changes were approved via request_tag_input and apply when the chat is published; to amend or drop one, propose a follow-up change through the same tool (a delete on a tag_temp_* ref drops the staged create). Takes --chat <id> to read an earlier chat's staged changes instead."
|
|
@@ -36808,7 +39602,7 @@ var draftCommand5 = defineCommand177({
|
|
|
36808
39602
|
await listDraft3(args.chat);
|
|
36809
39603
|
}
|
|
36810
39604
|
});
|
|
36811
|
-
var tagsCommand3 =
|
|
39605
|
+
var tagsCommand3 = defineCommand185({
|
|
36812
39606
|
meta: {
|
|
36813
39607
|
name: "tags",
|
|
36814
39608
|
description: `Read the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, \u2026) \u2014 production tags plus the changes staged in this chat.
|
|
@@ -36837,10 +39631,10 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
|
|
|
36837
39631
|
});
|
|
36838
39632
|
|
|
36839
39633
|
// src/commands/testimonials/index.ts
|
|
36840
|
-
import { defineCommand as
|
|
39634
|
+
import { defineCommand as defineCommand189 } from "citty";
|
|
36841
39635
|
|
|
36842
39636
|
// src/commands/testimonials/get.ts
|
|
36843
|
-
import { defineCommand as
|
|
39637
|
+
import { defineCommand as defineCommand186 } from "citty";
|
|
36844
39638
|
registerSchema({
|
|
36845
39639
|
command: "testimonials.get",
|
|
36846
39640
|
description: "Get a single testimonial by ID",
|
|
@@ -36848,7 +39642,7 @@ registerSchema({
|
|
|
36848
39642
|
id: { type: "string", description: "Testimonial ID", required: true }
|
|
36849
39643
|
}
|
|
36850
39644
|
});
|
|
36851
|
-
var getCommand5 =
|
|
39645
|
+
var getCommand5 = defineCommand186({
|
|
36852
39646
|
meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
|
|
36853
39647
|
args: {
|
|
36854
39648
|
id: { type: "positional", description: "Testimonial ID", required: false },
|
|
@@ -36885,7 +39679,7 @@ var getCommand5 = defineCommand178({
|
|
|
36885
39679
|
});
|
|
36886
39680
|
|
|
36887
39681
|
// src/commands/testimonials/list.ts
|
|
36888
|
-
import { defineCommand as
|
|
39682
|
+
import { defineCommand as defineCommand187 } from "citty";
|
|
36889
39683
|
registerSchema({
|
|
36890
39684
|
command: "testimonials.list",
|
|
36891
39685
|
description: "List testimonials with optional filters.",
|
|
@@ -36915,7 +39709,7 @@ registerSchema({
|
|
|
36915
39709
|
limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
|
|
36916
39710
|
}
|
|
36917
39711
|
});
|
|
36918
|
-
var listCommand17 =
|
|
39712
|
+
var listCommand17 = defineCommand187({
|
|
36919
39713
|
meta: {
|
|
36920
39714
|
name: "list",
|
|
36921
39715
|
description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
|
|
@@ -36964,7 +39758,7 @@ var listCommand17 = defineCommand179({
|
|
|
36964
39758
|
});
|
|
36965
39759
|
|
|
36966
39760
|
// src/commands/testimonials/search.ts
|
|
36967
|
-
import { defineCommand as
|
|
39761
|
+
import { defineCommand as defineCommand188 } from "citty";
|
|
36968
39762
|
function languageBiasHint(results, requestedLanguage) {
|
|
36969
39763
|
if (requestedLanguage) {
|
|
36970
39764
|
return null;
|
|
@@ -37042,7 +39836,7 @@ function buildSearchRequest(query, args) {
|
|
|
37042
39836
|
}
|
|
37043
39837
|
return body;
|
|
37044
39838
|
}
|
|
37045
|
-
var
|
|
39839
|
+
var searchCommand3 = defineCommand188({
|
|
37046
39840
|
meta: {
|
|
37047
39841
|
name: "search",
|
|
37048
39842
|
description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
|
|
@@ -37098,7 +39892,7 @@ var searchCommand2 = defineCommand180({
|
|
|
37098
39892
|
var tagsCommand4 = makeTagsCommand("testimonials", "testimonial", "/api/testimonials/tags");
|
|
37099
39893
|
|
|
37100
39894
|
// src/commands/testimonials/index.ts
|
|
37101
|
-
var testimonialsCommand =
|
|
39895
|
+
var testimonialsCommand = defineCommand189({
|
|
37102
39896
|
meta: {
|
|
37103
39897
|
name: "testimonials",
|
|
37104
39898
|
description: `Find and browse testimonials in Baker. Subcommands: search, get, list, tags.
|
|
@@ -37113,17 +39907,17 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
|
|
|
37113
39907
|
},
|
|
37114
39908
|
subCommands: {
|
|
37115
39909
|
get: getCommand5,
|
|
37116
|
-
search:
|
|
39910
|
+
search: searchCommand3,
|
|
37117
39911
|
list: listCommand17,
|
|
37118
39912
|
tags: tagsCommand4
|
|
37119
39913
|
}
|
|
37120
39914
|
});
|
|
37121
39915
|
|
|
37122
39916
|
// src/commands/videos/index.ts
|
|
37123
|
-
import { defineCommand as
|
|
39917
|
+
import { defineCommand as defineCommand195 } from "citty";
|
|
37124
39918
|
|
|
37125
39919
|
// src/commands/videos/delete.ts
|
|
37126
|
-
import { defineCommand as
|
|
39920
|
+
import { defineCommand as defineCommand190 } from "citty";
|
|
37127
39921
|
registerSchema({
|
|
37128
39922
|
command: "videos.delete",
|
|
37129
39923
|
description: "Delete a video by ID",
|
|
@@ -37137,7 +39931,7 @@ registerSchema({
|
|
|
37137
39931
|
}
|
|
37138
39932
|
}
|
|
37139
39933
|
});
|
|
37140
|
-
var deleteCommand3 =
|
|
39934
|
+
var deleteCommand3 = defineCommand190({
|
|
37141
39935
|
meta: {
|
|
37142
39936
|
name: "delete",
|
|
37143
39937
|
description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
|
|
@@ -37178,7 +39972,7 @@ var deleteCommand3 = defineCommand182({
|
|
|
37178
39972
|
});
|
|
37179
39973
|
|
|
37180
39974
|
// src/commands/videos/get.ts
|
|
37181
|
-
import { defineCommand as
|
|
39975
|
+
import { defineCommand as defineCommand191 } from "citty";
|
|
37182
39976
|
registerSchema({
|
|
37183
39977
|
command: "videos.get",
|
|
37184
39978
|
description: "Get a single video by ID",
|
|
@@ -37186,7 +39980,7 @@ registerSchema({
|
|
|
37186
39980
|
id: { type: "string", description: "Video ID", required: true }
|
|
37187
39981
|
}
|
|
37188
39982
|
});
|
|
37189
|
-
var getCommand6 =
|
|
39983
|
+
var getCommand6 = defineCommand191({
|
|
37190
39984
|
meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
|
|
37191
39985
|
args: {
|
|
37192
39986
|
id: { type: "positional", description: "Video ID", required: false },
|
|
@@ -37223,7 +40017,7 @@ var getCommand6 = defineCommand183({
|
|
|
37223
40017
|
});
|
|
37224
40018
|
|
|
37225
40019
|
// src/commands/videos/group.ts
|
|
37226
|
-
import { defineCommand as
|
|
40020
|
+
import { defineCommand as defineCommand192 } from "citty";
|
|
37227
40021
|
registerSchema({
|
|
37228
40022
|
command: "videos.group",
|
|
37229
40023
|
description: "List every clip and image that arrived in the same set as this video (carousel slides, one page)",
|
|
@@ -37232,7 +40026,7 @@ registerSchema({
|
|
|
37232
40026
|
"group-key": { type: "string", description: "The set key directly, when you already have it", required: false }
|
|
37233
40027
|
}
|
|
37234
40028
|
});
|
|
37235
|
-
var groupCommand2 =
|
|
40029
|
+
var groupCommand2 = defineCommand192({
|
|
37236
40030
|
meta: {
|
|
37237
40031
|
name: "group",
|
|
37238
40032
|
description: "List every asset that arrived in the same set as this clip \u2014 the other slides of the Instagram post it came from, stills included. A carousel is authored to be read in order, so a clip pulled out of one is usually missing half its meaning. Example: baker videos group <videoId>"
|
|
@@ -37252,7 +40046,7 @@ var groupCommand2 = defineCommand184({
|
|
|
37252
40046
|
});
|
|
37253
40047
|
|
|
37254
40048
|
// src/commands/videos/search.ts
|
|
37255
|
-
import { defineCommand as
|
|
40049
|
+
import { defineCommand as defineCommand193 } from "citty";
|
|
37256
40050
|
registerSchema({
|
|
37257
40051
|
command: "videos.search",
|
|
37258
40052
|
description: "Search videos by text query. Only returns ready videos.",
|
|
@@ -37262,7 +40056,7 @@ registerSchema({
|
|
|
37262
40056
|
tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
|
|
37263
40057
|
}
|
|
37264
40058
|
});
|
|
37265
|
-
var
|
|
40059
|
+
var searchCommand4 = defineCommand193({
|
|
37266
40060
|
meta: {
|
|
37267
40061
|
name: "search",
|
|
37268
40062
|
description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
|
|
@@ -37312,9 +40106,9 @@ var searchCommand3 = defineCommand185({
|
|
|
37312
40106
|
var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
37313
40107
|
|
|
37314
40108
|
// src/commands/videos/upload.ts
|
|
37315
|
-
import { readFile as
|
|
40109
|
+
import { readFile as readFile24, stat as stat7 } from "fs/promises";
|
|
37316
40110
|
import { basename as basename2, extname as extname3 } from "path";
|
|
37317
|
-
import { defineCommand as
|
|
40111
|
+
import { defineCommand as defineCommand194 } from "citty";
|
|
37318
40112
|
var MIME_MAP = {
|
|
37319
40113
|
".mp4": "video/mp4",
|
|
37320
40114
|
".mov": "video/quicktime",
|
|
@@ -37353,7 +40147,7 @@ function detectContentType(filePath) {
|
|
|
37353
40147
|
}
|
|
37354
40148
|
return mime;
|
|
37355
40149
|
}
|
|
37356
|
-
var uploadCommand2 =
|
|
40150
|
+
var uploadCommand2 = defineCommand194({
|
|
37357
40151
|
meta: {
|
|
37358
40152
|
name: "upload",
|
|
37359
40153
|
description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
|
|
@@ -37392,7 +40186,7 @@ var uploadCommand2 = defineCommand186({
|
|
|
37392
40186
|
originalFilename,
|
|
37393
40187
|
descriptionContext
|
|
37394
40188
|
});
|
|
37395
|
-
const fileBuffer = await
|
|
40189
|
+
const fileBuffer = await readFile24(filePath);
|
|
37396
40190
|
const uploadResponse = await fetch(uploadUrl, {
|
|
37397
40191
|
method: "PUT",
|
|
37398
40192
|
headers: { "Content-Type": contentType },
|
|
@@ -37423,7 +40217,7 @@ var uploadCommand2 = defineCommand186({
|
|
|
37423
40217
|
});
|
|
37424
40218
|
|
|
37425
40219
|
// src/commands/videos/index.ts
|
|
37426
|
-
var videosCommand =
|
|
40220
|
+
var videosCommand = defineCommand195({
|
|
37427
40221
|
meta: {
|
|
37428
40222
|
name: "videos",
|
|
37429
40223
|
description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
|
|
@@ -37440,7 +40234,7 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
|
|
|
37440
40234
|
subCommands: {
|
|
37441
40235
|
get: getCommand6,
|
|
37442
40236
|
group: groupCommand2,
|
|
37443
|
-
search:
|
|
40237
|
+
search: searchCommand4,
|
|
37444
40238
|
upload: uploadCommand2,
|
|
37445
40239
|
delete: deleteCommand3,
|
|
37446
40240
|
tags: tagsCommand5
|
|
@@ -37448,19 +40242,19 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
|
|
|
37448
40242
|
});
|
|
37449
40243
|
|
|
37450
40244
|
// src/commands/winning-ads/index.ts
|
|
37451
|
-
import { defineCommand as
|
|
40245
|
+
import { defineCommand as defineCommand208 } from "citty";
|
|
37452
40246
|
|
|
37453
40247
|
// src/commands/winning-ads/advertisers.ts
|
|
37454
|
-
import { defineCommand as
|
|
40248
|
+
import { defineCommand as defineCommand196 } from "citty";
|
|
37455
40249
|
|
|
37456
40250
|
// src/commands/winning-ads/shared.ts
|
|
37457
|
-
function
|
|
40251
|
+
function splitList2(value) {
|
|
37458
40252
|
if (!value) {
|
|
37459
40253
|
return [];
|
|
37460
40254
|
}
|
|
37461
40255
|
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
37462
40256
|
}
|
|
37463
|
-
function
|
|
40257
|
+
function reportError2(err) {
|
|
37464
40258
|
if (err instanceof ApiError) {
|
|
37465
40259
|
writeJson({ ok: false, error: { code: err.code, message: err.message } });
|
|
37466
40260
|
process.exit(1);
|
|
@@ -37504,7 +40298,7 @@ function advertiserNormalizer(record, full) {
|
|
|
37504
40298
|
last_synced_at: record.last_synced_at ?? null
|
|
37505
40299
|
};
|
|
37506
40300
|
}
|
|
37507
|
-
var advertisersCommand2 =
|
|
40301
|
+
var advertisersCommand2 = defineCommand196({
|
|
37508
40302
|
meta: {
|
|
37509
40303
|
name: "advertisers",
|
|
37510
40304
|
description: 'List corpus advertisers by name or domain. Find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id / winners. Example: baker winning-ads advertisers "Deel" --output md'
|
|
@@ -37556,13 +40350,13 @@ var advertisersCommand2 = defineCommand188({
|
|
|
37556
40350
|
advertiserNormalizer
|
|
37557
40351
|
);
|
|
37558
40352
|
} catch (err) {
|
|
37559
|
-
|
|
40353
|
+
reportError2(err);
|
|
37560
40354
|
}
|
|
37561
40355
|
}
|
|
37562
40356
|
});
|
|
37563
40357
|
|
|
37564
40358
|
// src/commands/winning-ads/brief.ts
|
|
37565
|
-
import { defineCommand as
|
|
40359
|
+
import { defineCommand as defineCommand197 } from "citty";
|
|
37566
40360
|
registerSchema({
|
|
37567
40361
|
command: "winning-ads.brief",
|
|
37568
40362
|
description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
|
|
@@ -37608,7 +40402,7 @@ function parseDna(raw) {
|
|
|
37608
40402
|
}
|
|
37609
40403
|
return parsed;
|
|
37610
40404
|
}
|
|
37611
|
-
var briefCommand =
|
|
40405
|
+
var briefCommand = defineCommand197({
|
|
37612
40406
|
meta: {
|
|
37613
40407
|
name: "brief",
|
|
37614
40408
|
description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
|
|
@@ -37638,13 +40432,13 @@ var briefCommand = defineCommand189({
|
|
|
37638
40432
|
const data = await apiPost("/api/ad-library/brief", body);
|
|
37639
40433
|
writeJson({ ok: true, data });
|
|
37640
40434
|
} catch (err) {
|
|
37641
|
-
|
|
40435
|
+
reportError2(err);
|
|
37642
40436
|
}
|
|
37643
40437
|
}
|
|
37644
40438
|
});
|
|
37645
40439
|
|
|
37646
40440
|
// src/commands/winning-ads/content.ts
|
|
37647
|
-
import { defineCommand as
|
|
40441
|
+
import { defineCommand as defineCommand198 } from "citty";
|
|
37648
40442
|
registerSchema({
|
|
37649
40443
|
command: "winning-ads.content",
|
|
37650
40444
|
description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
|
|
@@ -37657,7 +40451,7 @@ registerSchema({
|
|
|
37657
40451
|
}
|
|
37658
40452
|
}
|
|
37659
40453
|
});
|
|
37660
|
-
var contentCommand =
|
|
40454
|
+
var contentCommand = defineCommand198({
|
|
37661
40455
|
meta: {
|
|
37662
40456
|
name: "content",
|
|
37663
40457
|
description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
|
|
@@ -37700,16 +40494,16 @@ var contentCommand = defineCommand190({
|
|
|
37700
40494
|
adContentNormalizer
|
|
37701
40495
|
);
|
|
37702
40496
|
} catch (err) {
|
|
37703
|
-
|
|
40497
|
+
reportError2(err);
|
|
37704
40498
|
}
|
|
37705
40499
|
}
|
|
37706
40500
|
});
|
|
37707
40501
|
|
|
37708
40502
|
// src/commands/winning-ads/feed.ts
|
|
37709
|
-
import { defineCommand as
|
|
40503
|
+
import { defineCommand as defineCommand199 } from "citty";
|
|
37710
40504
|
function buildFeedParams(input) {
|
|
37711
40505
|
const params = {};
|
|
37712
|
-
const advertiser =
|
|
40506
|
+
const advertiser = splitList2(input.advertiser);
|
|
37713
40507
|
if (advertiser.length > 0) {
|
|
37714
40508
|
params.advertiser = advertiser.join(",");
|
|
37715
40509
|
}
|
|
@@ -37722,11 +40516,11 @@ function buildFeedParams(input) {
|
|
|
37722
40516
|
if (input.limit !== void 0 && input.limit !== "") {
|
|
37723
40517
|
params.limit = input.limit;
|
|
37724
40518
|
}
|
|
37725
|
-
const winnerCategory =
|
|
40519
|
+
const winnerCategory = splitList2(input.winnerCategory);
|
|
37726
40520
|
if (winnerCategory.length > 0) {
|
|
37727
40521
|
params.winner_category = winnerCategory.join(",");
|
|
37728
40522
|
}
|
|
37729
|
-
const format =
|
|
40523
|
+
const format = splitList2(input.format);
|
|
37730
40524
|
if (format.length > 0) {
|
|
37731
40525
|
params.format = format.join(",");
|
|
37732
40526
|
}
|
|
@@ -37758,7 +40552,7 @@ registerSchema({
|
|
|
37758
40552
|
format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
|
|
37759
40553
|
}
|
|
37760
40554
|
});
|
|
37761
|
-
var feedCommand =
|
|
40555
|
+
var feedCommand = defineCommand199({
|
|
37762
40556
|
meta: {
|
|
37763
40557
|
name: "feed",
|
|
37764
40558
|
description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
|
|
@@ -37837,13 +40631,13 @@ var feedCommand = defineCommand191({
|
|
|
37837
40631
|
`);
|
|
37838
40632
|
}
|
|
37839
40633
|
} catch (err) {
|
|
37840
|
-
|
|
40634
|
+
reportError2(err);
|
|
37841
40635
|
}
|
|
37842
40636
|
}
|
|
37843
40637
|
});
|
|
37844
40638
|
|
|
37845
40639
|
// src/commands/winning-ads/follow.ts
|
|
37846
|
-
import { defineCommand as
|
|
40640
|
+
import { defineCommand as defineCommand200 } from "citty";
|
|
37847
40641
|
var PLATFORMS = ["meta", "linkedin"];
|
|
37848
40642
|
registerSchema({
|
|
37849
40643
|
command: "winning-ads.follow",
|
|
@@ -37858,7 +40652,7 @@ registerSchema({
|
|
|
37858
40652
|
label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
|
|
37859
40653
|
}
|
|
37860
40654
|
});
|
|
37861
|
-
var followCommand =
|
|
40655
|
+
var followCommand = defineCommand200({
|
|
37862
40656
|
meta: {
|
|
37863
40657
|
name: "follow",
|
|
37864
40658
|
description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
|
|
@@ -37899,19 +40693,19 @@ var followCommand = defineCommand192({
|
|
|
37899
40693
|
}
|
|
37900
40694
|
writeJson({ ok: true, data, hints });
|
|
37901
40695
|
} catch (err) {
|
|
37902
|
-
|
|
40696
|
+
reportError2(err);
|
|
37903
40697
|
}
|
|
37904
40698
|
}
|
|
37905
40699
|
});
|
|
37906
40700
|
|
|
37907
40701
|
// src/commands/winning-ads/follow-competitors.ts
|
|
37908
|
-
import { defineCommand as
|
|
40702
|
+
import { defineCommand as defineCommand201 } from "citty";
|
|
37909
40703
|
var PLATFORMS2 = ["meta", "linkedin"];
|
|
37910
40704
|
var BATCH_TIMEOUT_MS = 3e5;
|
|
37911
40705
|
function buildFollowBatchBody(input) {
|
|
37912
40706
|
const seen = /* @__PURE__ */ new Set();
|
|
37913
40707
|
const inputs = [];
|
|
37914
|
-
for (const domain of
|
|
40708
|
+
for (const domain of splitList2(input.domains)) {
|
|
37915
40709
|
const key = domain.toLowerCase();
|
|
37916
40710
|
if (seen.has(key)) {
|
|
37917
40711
|
continue;
|
|
@@ -37938,7 +40732,7 @@ registerSchema({
|
|
|
37938
40732
|
}
|
|
37939
40733
|
}
|
|
37940
40734
|
});
|
|
37941
|
-
var followCompetitorsCommand =
|
|
40735
|
+
var followCompetitorsCommand = defineCommand201({
|
|
37942
40736
|
meta: {
|
|
37943
40737
|
name: "follow-competitors",
|
|
37944
40738
|
description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
|
|
@@ -38007,13 +40801,13 @@ var followCompetitorsCommand = defineCommand193({
|
|
|
38007
40801
|
}
|
|
38008
40802
|
writeJson({ ok: true, data, hints: hints.length > 0 ? hints : void 0 });
|
|
38009
40803
|
} catch (err) {
|
|
38010
|
-
|
|
40804
|
+
reportError2(err);
|
|
38011
40805
|
}
|
|
38012
40806
|
}
|
|
38013
40807
|
});
|
|
38014
40808
|
|
|
38015
40809
|
// src/commands/winning-ads/following.ts
|
|
38016
|
-
import { defineCommand as
|
|
40810
|
+
import { defineCommand as defineCommand202 } from "citty";
|
|
38017
40811
|
registerSchema({
|
|
38018
40812
|
command: "winning-ads.following",
|
|
38019
40813
|
description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
|
|
@@ -38046,7 +40840,7 @@ function followingNormalizer(record, full) {
|
|
|
38046
40840
|
platforms: Array.isArray(record.platforms) ? record.platforms : []
|
|
38047
40841
|
};
|
|
38048
40842
|
}
|
|
38049
|
-
var followingCommand =
|
|
40843
|
+
var followingCommand = defineCommand202({
|
|
38050
40844
|
meta: {
|
|
38051
40845
|
name: "following",
|
|
38052
40846
|
description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
|
|
@@ -38075,13 +40869,13 @@ var followingCommand = defineCommand194({
|
|
|
38075
40869
|
followingNormalizer
|
|
38076
40870
|
);
|
|
38077
40871
|
} catch (err) {
|
|
38078
|
-
|
|
40872
|
+
reportError2(err);
|
|
38079
40873
|
}
|
|
38080
40874
|
}
|
|
38081
40875
|
});
|
|
38082
40876
|
|
|
38083
40877
|
// src/commands/winning-ads/patterns.ts
|
|
38084
|
-
import { defineCommand as
|
|
40878
|
+
import { defineCommand as defineCommand203 } from "citty";
|
|
38085
40879
|
registerSchema({
|
|
38086
40880
|
command: "winning-ads.patterns",
|
|
38087
40881
|
description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
|
|
@@ -38096,8 +40890,8 @@ registerSchema({
|
|
|
38096
40890
|
}
|
|
38097
40891
|
});
|
|
38098
40892
|
function buildPatternsBody(args) {
|
|
38099
|
-
const winners =
|
|
38100
|
-
const duds =
|
|
40893
|
+
const winners = splitList2(args.winners);
|
|
40894
|
+
const duds = splitList2(args.duds);
|
|
38101
40895
|
if (!winners.length || !duds.length) {
|
|
38102
40896
|
throw new Error("Provide at least one ad id for both --winners and --duds");
|
|
38103
40897
|
}
|
|
@@ -38120,7 +40914,7 @@ function discriminatorRow(record) {
|
|
|
38120
40914
|
top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
|
|
38121
40915
|
};
|
|
38122
40916
|
}
|
|
38123
|
-
var patternsCommand =
|
|
40917
|
+
var patternsCommand = defineCommand203({
|
|
38124
40918
|
meta: {
|
|
38125
40919
|
name: "patterns",
|
|
38126
40920
|
description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
|
|
@@ -38170,13 +40964,13 @@ var patternsCommand = defineCommand195({
|
|
|
38170
40964
|
(record) => discriminatorRow(record)
|
|
38171
40965
|
);
|
|
38172
40966
|
} catch (err) {
|
|
38173
|
-
|
|
40967
|
+
reportError2(err);
|
|
38174
40968
|
}
|
|
38175
40969
|
}
|
|
38176
40970
|
});
|
|
38177
40971
|
|
|
38178
40972
|
// src/commands/winning-ads/search.ts
|
|
38179
|
-
import { defineCommand as
|
|
40973
|
+
import { defineCommand as defineCommand204 } from "citty";
|
|
38180
40974
|
registerSchema({
|
|
38181
40975
|
command: "winning-ads.search",
|
|
38182
40976
|
description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
|
|
@@ -38246,7 +41040,7 @@ function setNumber(body, key, value) {
|
|
|
38246
41040
|
}
|
|
38247
41041
|
}
|
|
38248
41042
|
function setList(target, key, value) {
|
|
38249
|
-
const list =
|
|
41043
|
+
const list = splitList2(value);
|
|
38250
41044
|
if (list.length) {
|
|
38251
41045
|
target[key] = list;
|
|
38252
41046
|
}
|
|
@@ -38256,7 +41050,7 @@ function setString(target, key, value) {
|
|
|
38256
41050
|
target[key] = value;
|
|
38257
41051
|
}
|
|
38258
41052
|
}
|
|
38259
|
-
function
|
|
41053
|
+
function buildSearchBody2(args) {
|
|
38260
41054
|
const body = {};
|
|
38261
41055
|
if (args.query) {
|
|
38262
41056
|
body.free_text_query = args.query;
|
|
@@ -38284,7 +41078,7 @@ function buildSearchBody(args) {
|
|
|
38284
41078
|
}
|
|
38285
41079
|
return body;
|
|
38286
41080
|
}
|
|
38287
|
-
var
|
|
41081
|
+
var searchCommand5 = defineCommand204({
|
|
38288
41082
|
meta: {
|
|
38289
41083
|
name: "search",
|
|
38290
41084
|
description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
|
|
@@ -38360,7 +41154,7 @@ var searchCommand4 = defineCommand196({
|
|
|
38360
41154
|
firstSeenBefore: args["first-seen-before"],
|
|
38361
41155
|
hookArchetype: args["hook-archetype"]
|
|
38362
41156
|
};
|
|
38363
|
-
const body =
|
|
41157
|
+
const body = buildSearchBody2(searchArgs);
|
|
38364
41158
|
if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
|
|
38365
41159
|
writeJson({
|
|
38366
41160
|
ok: false,
|
|
@@ -38393,13 +41187,13 @@ var searchCommand4 = defineCommand196({
|
|
|
38393
41187
|
winningAdNormalizer
|
|
38394
41188
|
);
|
|
38395
41189
|
} catch (err) {
|
|
38396
|
-
|
|
41190
|
+
reportError2(err);
|
|
38397
41191
|
}
|
|
38398
41192
|
}
|
|
38399
41193
|
});
|
|
38400
41194
|
|
|
38401
41195
|
// src/commands/winning-ads/seeds.ts
|
|
38402
|
-
import { defineCommand as
|
|
41196
|
+
import { defineCommand as defineCommand205 } from "citty";
|
|
38403
41197
|
function leanRow(r) {
|
|
38404
41198
|
return {
|
|
38405
41199
|
key: r.key,
|
|
@@ -38427,7 +41221,7 @@ function makeSeedCommand(opts) {
|
|
|
38427
41221
|
limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
|
|
38428
41222
|
}
|
|
38429
41223
|
});
|
|
38430
|
-
return
|
|
41224
|
+
return defineCommand205({
|
|
38431
41225
|
meta: { name: opts.name, description: opts.description },
|
|
38432
41226
|
args: {
|
|
38433
41227
|
platform: { type: "string", description: "Single platform to segment on", required: false },
|
|
@@ -38454,7 +41248,7 @@ function makeSeedCommand(opts) {
|
|
|
38454
41248
|
}
|
|
38455
41249
|
writeOutput({ ok: true, data: projected }, output);
|
|
38456
41250
|
} catch (err) {
|
|
38457
|
-
|
|
41251
|
+
reportError2(err);
|
|
38458
41252
|
}
|
|
38459
41253
|
}
|
|
38460
41254
|
});
|
|
@@ -38476,7 +41270,7 @@ var formatsCommand = makeSeedCommand({
|
|
|
38476
41270
|
});
|
|
38477
41271
|
|
|
38478
41272
|
// src/commands/winning-ads/unfollow.ts
|
|
38479
|
-
import { defineCommand as
|
|
41273
|
+
import { defineCommand as defineCommand206 } from "citty";
|
|
38480
41274
|
registerSchema({
|
|
38481
41275
|
command: "winning-ads.unfollow",
|
|
38482
41276
|
description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
|
|
@@ -38484,7 +41278,7 @@ registerSchema({
|
|
|
38484
41278
|
advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
|
|
38485
41279
|
}
|
|
38486
41280
|
});
|
|
38487
|
-
var unfollowCommand =
|
|
41281
|
+
var unfollowCommand = defineCommand206({
|
|
38488
41282
|
meta: {
|
|
38489
41283
|
name: "unfollow",
|
|
38490
41284
|
description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
|
|
@@ -38499,13 +41293,13 @@ var unfollowCommand = defineCommand198({
|
|
|
38499
41293
|
});
|
|
38500
41294
|
writeJson({ ok: true, data });
|
|
38501
41295
|
} catch (err) {
|
|
38502
|
-
|
|
41296
|
+
reportError2(err);
|
|
38503
41297
|
}
|
|
38504
41298
|
}
|
|
38505
41299
|
});
|
|
38506
41300
|
|
|
38507
41301
|
// src/commands/winning-ads/winners.ts
|
|
38508
|
-
import { defineCommand as
|
|
41302
|
+
import { defineCommand as defineCommand207 } from "citty";
|
|
38509
41303
|
registerSchema({
|
|
38510
41304
|
command: "winning-ads.winners",
|
|
38511
41305
|
description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
|
|
@@ -38515,7 +41309,7 @@ registerSchema({
|
|
|
38515
41309
|
platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
|
|
38516
41310
|
}
|
|
38517
41311
|
});
|
|
38518
|
-
var winnersCommand =
|
|
41312
|
+
var winnersCommand = defineCommand207({
|
|
38519
41313
|
meta: {
|
|
38520
41314
|
name: "winners",
|
|
38521
41315
|
description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
|
|
@@ -38559,13 +41353,13 @@ var winnersCommand = defineCommand199({
|
|
|
38559
41353
|
winningAdNormalizer
|
|
38560
41354
|
);
|
|
38561
41355
|
} catch (err) {
|
|
38562
|
-
|
|
41356
|
+
reportError2(err);
|
|
38563
41357
|
}
|
|
38564
41358
|
}
|
|
38565
41359
|
});
|
|
38566
41360
|
|
|
38567
41361
|
// src/commands/winning-ads/index.ts
|
|
38568
|
-
var winningAdsCommand =
|
|
41362
|
+
var winningAdsCommand = defineCommand208({
|
|
38569
41363
|
meta: {
|
|
38570
41364
|
name: "winning-ads",
|
|
38571
41365
|
description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
|
|
@@ -38603,7 +41397,7 @@ Examples:
|
|
|
38603
41397
|
Full guide: __tooling__/docs/tools/baker/winning-ads.md`
|
|
38604
41398
|
},
|
|
38605
41399
|
subCommands: {
|
|
38606
|
-
search:
|
|
41400
|
+
search: searchCommand5,
|
|
38607
41401
|
advertisers: advertisersCommand2,
|
|
38608
41402
|
follow: followCommand,
|
|
38609
41403
|
"follow-competitors": followCompetitorsCommand,
|
|
@@ -38637,7 +41431,7 @@ function getCliVersion() {
|
|
|
38637
41431
|
}
|
|
38638
41432
|
|
|
38639
41433
|
// src/cli.ts
|
|
38640
|
-
var main =
|
|
41434
|
+
var main = defineCommand209({
|
|
38641
41435
|
meta: {
|
|
38642
41436
|
name: "baker",
|
|
38643
41437
|
version: getCliVersion(),
|