@koda-sl/baker-cli 0.181.1 → 0.182.0-dev.3c0641b3f
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 +28 -1
- package/dist/{chunk-RWHEFQXI.js → chunk-UZ37VVP4.js} +4 -4
- package/dist/chunk-UZ37VVP4.js.map +1 -0
- package/dist/cli.js +2617 -258
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-RWHEFQXI.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
toModelSafeImage,
|
|
39
39
|
ulid,
|
|
40
40
|
validateCanvasDeep
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-UZ37VVP4.js";
|
|
42
42
|
import {
|
|
43
43
|
csvOrJson,
|
|
44
44
|
daysAgoIso,
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
} from "./chunk-YL3HDEIJ.js";
|
|
82
82
|
|
|
83
83
|
// src/cli.ts
|
|
84
|
-
import { defineCommand as
|
|
84
|
+
import { defineCommand as defineCommand199, runMain } from "citty";
|
|
85
85
|
|
|
86
86
|
// src/commands/actions/index.ts
|
|
87
87
|
import { defineCommand as defineCommand18 } from "citty";
|
|
@@ -7388,19 +7388,19 @@ Examples:
|
|
|
7388
7388
|
},
|
|
7389
7389
|
run: async ({ args }) => {
|
|
7390
7390
|
const customerId = await resolveCustomerId(args);
|
|
7391
|
-
const
|
|
7391
|
+
const window2 = resolveChangesWindow({
|
|
7392
7392
|
days: args.days ? Number(args.days) : void 0,
|
|
7393
7393
|
scope: args.scope
|
|
7394
7394
|
});
|
|
7395
|
-
if (!
|
|
7396
|
-
writeJsonEnvelope({ ok: false, error: { ...
|
|
7395
|
+
if (!window2.ok) {
|
|
7396
|
+
writeJsonEnvelope({ ok: false, error: { ...window2.error, retryable: false } });
|
|
7397
7397
|
process.exit(1);
|
|
7398
7398
|
return;
|
|
7399
7399
|
}
|
|
7400
7400
|
const body = {
|
|
7401
7401
|
customerId,
|
|
7402
|
-
days:
|
|
7403
|
-
scope:
|
|
7402
|
+
days: window2.days,
|
|
7403
|
+
scope: window2.scope,
|
|
7404
7404
|
limit: args.limit ? Number(args.limit) : 50
|
|
7405
7405
|
};
|
|
7406
7406
|
const managerId = getManagerIdForCustomer(customerId);
|
|
@@ -7417,7 +7417,7 @@ Examples:
|
|
|
7417
7417
|
const first = data[0];
|
|
7418
7418
|
const fields = first ? Object.keys(first) : [];
|
|
7419
7419
|
const fieldDescs = getFieldDescriptions(fields);
|
|
7420
|
-
writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints:
|
|
7420
|
+
writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window2.hints });
|
|
7421
7421
|
} catch (err) {
|
|
7422
7422
|
if (err instanceof ApiError) {
|
|
7423
7423
|
writeAdsJson(parseApiError(err.message, "", customerId, err.code));
|
|
@@ -7774,8 +7774,8 @@ function countStaccato(text) {
|
|
|
7774
7774
|
let first = "";
|
|
7775
7775
|
let runStart = 0;
|
|
7776
7776
|
for (const [i, s] of sentences.entries()) {
|
|
7777
|
-
const
|
|
7778
|
-
if (
|
|
7777
|
+
const words3 = s.split(/\s+/).filter(Boolean).length;
|
|
7778
|
+
if (words3 > 0 && words3 <= STACCATO_MAX_WORDS) {
|
|
7779
7779
|
if (run === 0) runStart = i;
|
|
7780
7780
|
run++;
|
|
7781
7781
|
if (run === STACCATO_RUN) {
|
|
@@ -7793,10 +7793,10 @@ function countTitleCaseHeadings(text) {
|
|
|
7793
7793
|
let first = "";
|
|
7794
7794
|
for (const m of text.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
|
|
7795
7795
|
const heading = (m[1] ?? "").trim();
|
|
7796
|
-
const
|
|
7797
|
-
if (
|
|
7798
|
-
const capitalized =
|
|
7799
|
-
if (capitalized /
|
|
7796
|
+
const words3 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
|
|
7797
|
+
if (words3.length < 4) continue;
|
|
7798
|
+
const capitalized = words3.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
|
|
7799
|
+
if (capitalized / words3.length < 0.8) continue;
|
|
7800
7800
|
count++;
|
|
7801
7801
|
if (!first) first = heading;
|
|
7802
7802
|
}
|
|
@@ -8125,11 +8125,11 @@ function rawTextEntries(value) {
|
|
|
8125
8125
|
const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
8126
8126
|
return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
8127
8127
|
}
|
|
8128
|
-
function rawFileEntries(
|
|
8129
|
-
if (typeof
|
|
8128
|
+
function rawFileEntries(path34) {
|
|
8129
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
8130
8130
|
return [];
|
|
8131
8131
|
}
|
|
8132
|
-
return readFileSync2(
|
|
8132
|
+
return readFileSync2(path34, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
8133
8133
|
}
|
|
8134
8134
|
function keywordEntries(args) {
|
|
8135
8135
|
const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
|
|
@@ -8152,19 +8152,19 @@ function keywordEntries(args) {
|
|
|
8152
8152
|
}
|
|
8153
8153
|
return entries;
|
|
8154
8154
|
}
|
|
8155
|
-
function loadJsonFileArg(
|
|
8156
|
-
if (typeof
|
|
8155
|
+
function loadJsonFileArg(path34) {
|
|
8156
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
8157
8157
|
return {};
|
|
8158
8158
|
}
|
|
8159
8159
|
try {
|
|
8160
|
-
const parsed = JSON.parse(readFileSync2(
|
|
8160
|
+
const parsed = JSON.parse(readFileSync2(path34, "utf8"));
|
|
8161
8161
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8162
|
-
failWriteValidation(`${
|
|
8162
|
+
failWriteValidation(`${path34} must contain a JSON object`);
|
|
8163
8163
|
}
|
|
8164
8164
|
return parsed;
|
|
8165
8165
|
} catch (err) {
|
|
8166
8166
|
if (err instanceof SyntaxError) {
|
|
8167
|
-
failWriteValidation(`${
|
|
8167
|
+
failWriteValidation(`${path34} is not valid JSON: ${err.message}`);
|
|
8168
8168
|
}
|
|
8169
8169
|
throw err;
|
|
8170
8170
|
}
|
|
@@ -8294,10 +8294,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
|
|
|
8294
8294
|
async function stageTarget(kind, customerId, target, hints) {
|
|
8295
8295
|
await stageGoogleOp({ kind, customerId, target }, hints);
|
|
8296
8296
|
}
|
|
8297
|
-
async function draftAction(
|
|
8297
|
+
async function draftAction(path34, body, chat) {
|
|
8298
8298
|
try {
|
|
8299
8299
|
const chatId = resolveChatId(chat);
|
|
8300
|
-
const response = await apiPost(
|
|
8300
|
+
const response = await apiPost(path34, { chatId, ...body });
|
|
8301
8301
|
writeJsonEnvelope(response);
|
|
8302
8302
|
} catch (err) {
|
|
8303
8303
|
handleGoogleError(err);
|
|
@@ -10965,19 +10965,19 @@ function failWriteValidation2(message) {
|
|
|
10965
10965
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
10966
10966
|
process.exit(1);
|
|
10967
10967
|
}
|
|
10968
|
-
function loadJsonFileArg2(
|
|
10969
|
-
if (typeof
|
|
10968
|
+
function loadJsonFileArg2(path34) {
|
|
10969
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
10970
10970
|
return {};
|
|
10971
10971
|
}
|
|
10972
10972
|
try {
|
|
10973
|
-
const parsed = JSON.parse(readFileSync4(
|
|
10973
|
+
const parsed = JSON.parse(readFileSync4(path34, "utf8"));
|
|
10974
10974
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10975
|
-
failWriteValidation2(`${
|
|
10975
|
+
failWriteValidation2(`${path34} must contain a JSON object`);
|
|
10976
10976
|
}
|
|
10977
10977
|
return parsed;
|
|
10978
10978
|
} catch (err) {
|
|
10979
10979
|
if (err instanceof SyntaxError) {
|
|
10980
|
-
failWriteValidation2(`${
|
|
10980
|
+
failWriteValidation2(`${path34} is not valid JSON: ${err.message}`);
|
|
10981
10981
|
}
|
|
10982
10982
|
throw err;
|
|
10983
10983
|
}
|
|
@@ -11062,15 +11062,15 @@ function parseLocaleFlag(value) {
|
|
|
11062
11062
|
}
|
|
11063
11063
|
return { language: match[1], country: match[2].toUpperCase() };
|
|
11064
11064
|
}
|
|
11065
|
-
function loadTargetingFileArg(
|
|
11066
|
-
if (typeof
|
|
11065
|
+
function loadTargetingFileArg(path34) {
|
|
11066
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
11067
11067
|
return void 0;
|
|
11068
11068
|
}
|
|
11069
|
-
const parsed = loadJsonFileArg2(
|
|
11069
|
+
const parsed = loadJsonFileArg2(path34);
|
|
11070
11070
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
11071
11071
|
if (!criteria.include) {
|
|
11072
11072
|
failWriteValidation2(
|
|
11073
|
-
`${
|
|
11073
|
+
`${path34} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
11074
11074
|
);
|
|
11075
11075
|
}
|
|
11076
11076
|
return criteria;
|
|
@@ -11105,14 +11105,14 @@ function parseCsvLine(line) {
|
|
|
11105
11105
|
cells.push(current);
|
|
11106
11106
|
return cells.map((cell) => cell.trim());
|
|
11107
11107
|
}
|
|
11108
|
-
function parseListFileArg(
|
|
11109
|
-
if (typeof
|
|
11108
|
+
function parseListFileArg(path34, maxRows) {
|
|
11109
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
11110
11110
|
return void 0;
|
|
11111
11111
|
}
|
|
11112
|
-
const raw = readFileSync4(
|
|
11112
|
+
const raw = readFileSync4(path34, "utf8");
|
|
11113
11113
|
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
11114
11114
|
if (lines.length < 2) {
|
|
11115
|
-
failWriteValidation2(`${
|
|
11115
|
+
failWriteValidation2(`${path34} needs a header row and at least one data row`);
|
|
11116
11116
|
}
|
|
11117
11117
|
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
11118
11118
|
const rows = [];
|
|
@@ -11131,7 +11131,7 @@ function parseListFileArg(path28, maxRows) {
|
|
|
11131
11131
|
}
|
|
11132
11132
|
}
|
|
11133
11133
|
if (rows.length > maxRows) {
|
|
11134
|
-
failWriteValidation2(`${
|
|
11134
|
+
failWriteValidation2(`${path34} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
11135
11135
|
}
|
|
11136
11136
|
return { columns, rows };
|
|
11137
11137
|
}
|
|
@@ -11227,11 +11227,11 @@ function readPositionals(args) {
|
|
|
11227
11227
|
function splitIdList(raw) {
|
|
11228
11228
|
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
11229
11229
|
}
|
|
11230
|
-
function idsFileEntries(
|
|
11231
|
-
if (typeof
|
|
11230
|
+
function idsFileEntries(path34) {
|
|
11231
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
11232
11232
|
return [];
|
|
11233
11233
|
}
|
|
11234
|
-
return readFileSync4(
|
|
11234
|
+
return readFileSync4(path34, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
|
|
11235
11235
|
}
|
|
11236
11236
|
function requireTargets(args, entity) {
|
|
11237
11237
|
const positionals = readPositionals(args);
|
|
@@ -13854,9 +13854,9 @@ function compactRow(row) {
|
|
|
13854
13854
|
...destination.postUrn ? { postUrn: destination.postUrn } : {}
|
|
13855
13855
|
};
|
|
13856
13856
|
}
|
|
13857
|
-
function readPath(row,
|
|
13857
|
+
function readPath(row, path34) {
|
|
13858
13858
|
let current = row;
|
|
13859
|
-
for (const segment of
|
|
13859
|
+
for (const segment of path34.split(".")) {
|
|
13860
13860
|
const record = asRecord2(current);
|
|
13861
13861
|
if (!record) return void 0;
|
|
13862
13862
|
current = record[segment];
|
|
@@ -13866,10 +13866,10 @@ function readPath(row, path28) {
|
|
|
13866
13866
|
function projectFields(rows, paths) {
|
|
13867
13867
|
return rows.map((row) => {
|
|
13868
13868
|
const projected = {};
|
|
13869
|
-
for (const
|
|
13870
|
-
const value = readPath(row,
|
|
13869
|
+
for (const path34 of paths) {
|
|
13870
|
+
const value = readPath(row, path34);
|
|
13871
13871
|
if (value !== void 0) {
|
|
13872
|
-
projected[
|
|
13872
|
+
projected[path34] = value;
|
|
13873
13873
|
}
|
|
13874
13874
|
}
|
|
13875
13875
|
return projected;
|
|
@@ -15076,11 +15076,11 @@ var updateStatusSchema = z18.enum(UPDATE_STATUSES);
|
|
|
15076
15076
|
function currencyMinimums2(currencyCode) {
|
|
15077
15077
|
return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
|
|
15078
15078
|
}
|
|
15079
|
-
function validateDailyBudgetFloor(money, ctx,
|
|
15079
|
+
function validateDailyBudgetFloor(money, ctx, path34) {
|
|
15080
15080
|
if (money?.currencyCode) {
|
|
15081
15081
|
const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
|
|
15082
15082
|
if (Number(money.amount) < min) {
|
|
15083
|
-
ctx.addIssue({ code: "custom", path:
|
|
15083
|
+
ctx.addIssue({ code: "custom", path: path34, message: `below the ${min} ${money.currencyCode} daily minimum` });
|
|
15084
15084
|
}
|
|
15085
15085
|
}
|
|
15086
15086
|
}
|
|
@@ -15723,19 +15723,19 @@ function failWriteValidation3(message) {
|
|
|
15723
15723
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
15724
15724
|
process.exit(1);
|
|
15725
15725
|
}
|
|
15726
|
-
function loadJsonFileArg3(
|
|
15727
|
-
if (typeof
|
|
15726
|
+
function loadJsonFileArg3(path34) {
|
|
15727
|
+
if (typeof path34 !== "string" || path34.length === 0) {
|
|
15728
15728
|
return {};
|
|
15729
15729
|
}
|
|
15730
15730
|
try {
|
|
15731
|
-
const parsed = JSON.parse(readFileSync8(
|
|
15731
|
+
const parsed = JSON.parse(readFileSync8(path34, "utf8"));
|
|
15732
15732
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15733
|
-
failWriteValidation3(`${
|
|
15733
|
+
failWriteValidation3(`${path34} must contain a JSON object`);
|
|
15734
15734
|
}
|
|
15735
15735
|
return parsed;
|
|
15736
15736
|
} catch (err) {
|
|
15737
15737
|
if (err instanceof SyntaxError) {
|
|
15738
|
-
failWriteValidation3(`${
|
|
15738
|
+
failWriteValidation3(`${path34} is not valid JSON: ${err.message}`);
|
|
15739
15739
|
}
|
|
15740
15740
|
throw err;
|
|
15741
15741
|
}
|
|
@@ -19797,11 +19797,11 @@ function estSpeechS(text) {
|
|
|
19797
19797
|
var OBSERVED_WPS_MIN = 1;
|
|
19798
19798
|
var OBSERVED_WPS_MAX = 6;
|
|
19799
19799
|
function estSpeechWindowS(text, startS, endS) {
|
|
19800
|
-
const
|
|
19801
|
-
const
|
|
19802
|
-
if (
|
|
19803
|
-
const wps =
|
|
19804
|
-
if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return
|
|
19800
|
+
const words3 = wordCount(text);
|
|
19801
|
+
const window2 = (endS ?? 0) - (startS ?? 0);
|
|
19802
|
+
if (words3 > 0 && window2 > 0.3) {
|
|
19803
|
+
const wps = words3 / window2;
|
|
19804
|
+
if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
|
|
19805
19805
|
}
|
|
19806
19806
|
return estSpeechS(text);
|
|
19807
19807
|
}
|
|
@@ -20656,10 +20656,10 @@ function scrubFloatSentences(text, floatDescs) {
|
|
|
20656
20656
|
if (floatDescs.length === 0 || !text) return text;
|
|
20657
20657
|
const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
|
|
20658
20658
|
const kept = text.split(/(?<=[.!?])\s+/).filter((sentence) => {
|
|
20659
|
-
const
|
|
20659
|
+
const words3 = new Set(floatTokens(sentence));
|
|
20660
20660
|
return !tokenSets.some((ts) => {
|
|
20661
20661
|
let hits = 0;
|
|
20662
|
-
for (const w of
|
|
20662
|
+
for (const w of words3) if (ts.has(w)) hits++;
|
|
20663
20663
|
return hits >= 2;
|
|
20664
20664
|
});
|
|
20665
20665
|
}).join(" ").trim();
|
|
@@ -22850,15 +22850,15 @@ function collectClipAdvisories(scene, i, out) {
|
|
|
22850
22850
|
const round22 = (n) => Math.round(n * 100) / 100;
|
|
22851
22851
|
const original = scene.duration_s ?? 5;
|
|
22852
22852
|
if (original > 15) out.clamped.push({ scene: i, original_s: original, clip_s: snapToSeedance(original) });
|
|
22853
|
-
const
|
|
22854
|
-
if (
|
|
22855
|
-
out.oversize.push({ scene: i, scene_s: round22(
|
|
22853
|
+
const window2 = sceneDurationS(scene);
|
|
22854
|
+
if (window2 > SEEDANCE_SAFE_MAX_S)
|
|
22855
|
+
out.oversize.push({ scene: i, scene_s: round22(window2), clip_s: ceilToSeedance(window2) });
|
|
22856
22856
|
const speech = (scene.dialogue ?? []).reduce(
|
|
22857
22857
|
(s, line) => s + (line.line ? estSpeechWindowS(line.line, line.start_s, line.end_s) : 0),
|
|
22858
22858
|
0
|
|
22859
22859
|
);
|
|
22860
|
-
if (speech >
|
|
22861
|
-
out.overstuffed.push({ scene: i, scene_s: round22(
|
|
22860
|
+
if (speech > window2 * OVERSTUFF_RATIO)
|
|
22861
|
+
out.overstuffed.push({ scene: i, scene_s: round22(window2), est_speech_s: round22(speech) });
|
|
22862
22862
|
}
|
|
22863
22863
|
function videoReport(input, elementsInput) {
|
|
22864
22864
|
const blueprint = VideoBlueprint.parse(input);
|
|
@@ -26531,12 +26531,12 @@ function listFlowSlugs() {
|
|
|
26531
26531
|
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
|
|
26532
26532
|
}
|
|
26533
26533
|
function readFlowTree(slug) {
|
|
26534
|
-
const
|
|
26535
|
-
if (!existsSync4(
|
|
26534
|
+
const path34 = join3(flowsDir(), slug, "_data.json");
|
|
26535
|
+
if (!existsSync4(path34)) {
|
|
26536
26536
|
failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
|
|
26537
26537
|
}
|
|
26538
26538
|
try {
|
|
26539
|
-
return JSON.parse(readFileSync9(
|
|
26539
|
+
return JSON.parse(readFileSync9(path34, "utf-8"));
|
|
26540
26540
|
} catch (error) {
|
|
26541
26541
|
failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
26542
26542
|
}
|
|
@@ -26949,10 +26949,10 @@ async function stageOps(ops) {
|
|
|
26949
26949
|
handleError(err);
|
|
26950
26950
|
}
|
|
26951
26951
|
}
|
|
26952
|
-
async function draftAction2(
|
|
26952
|
+
async function draftAction2(path34, body, chat) {
|
|
26953
26953
|
const chatId = resolveChatId(chat);
|
|
26954
26954
|
try {
|
|
26955
|
-
const data = await apiPost(
|
|
26955
|
+
const data = await apiPost(path34, { chatId, ...body });
|
|
26956
26956
|
writeJsonEnvelope({ ok: true, data });
|
|
26957
26957
|
return data;
|
|
26958
26958
|
} catch (err) {
|
|
@@ -28976,9 +28976,9 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
28976
28976
|
}
|
|
28977
28977
|
return readFile19(pathOrUrl);
|
|
28978
28978
|
}
|
|
28979
|
-
async function isDirectory(
|
|
28979
|
+
async function isDirectory(path34) {
|
|
28980
28980
|
try {
|
|
28981
|
-
const s = await stat4(
|
|
28981
|
+
const s = await stat4(path34);
|
|
28982
28982
|
return s.isDirectory();
|
|
28983
28983
|
} catch {
|
|
28984
28984
|
return false;
|
|
@@ -31598,11 +31598,11 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
31598
31598
|
});
|
|
31599
31599
|
|
|
31600
31600
|
// src/commands/landing/index.ts
|
|
31601
|
-
import { defineCommand as
|
|
31601
|
+
import { defineCommand as defineCommand151 } from "citty";
|
|
31602
31602
|
|
|
31603
31603
|
// src/commands/landing/critique.ts
|
|
31604
31604
|
import { readdir as readdir8, stat as stat6 } from "fs/promises";
|
|
31605
|
-
import
|
|
31605
|
+
import path28 from "path";
|
|
31606
31606
|
import { defineCommand as defineCommand142 } from "citty";
|
|
31607
31607
|
|
|
31608
31608
|
// src/engine/landing/lib/brand-tokens.ts
|
|
@@ -32011,6 +32011,11 @@ var RULE_META = {
|
|
|
32011
32011
|
severity: "block",
|
|
32012
32012
|
note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
|
|
32013
32013
|
},
|
|
32014
|
+
"copied-reference-copy": {
|
|
32015
|
+
family: "originality",
|
|
32016
|
+
severity: "block",
|
|
32017
|
+
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."
|
|
32018
|
+
},
|
|
32014
32019
|
"broken-image": {
|
|
32015
32020
|
family: "integrity",
|
|
32016
32021
|
severity: "block",
|
|
@@ -32191,6 +32196,64 @@ var SEVERITY_WEIGHT = {
|
|
|
32191
32196
|
advisory: 0.05
|
|
32192
32197
|
};
|
|
32193
32198
|
|
|
32199
|
+
// src/engine/landing/lib/originality.ts
|
|
32200
|
+
var MIN_COMPARABLE_LENGTH = 12;
|
|
32201
|
+
var NEAR_MATCH_RATIO = 0.8;
|
|
32202
|
+
function normalize(value) {
|
|
32203
|
+
return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
|
|
32204
|
+
}
|
|
32205
|
+
function words2(value) {
|
|
32206
|
+
return normalize(value).split(" ").filter(Boolean);
|
|
32207
|
+
}
|
|
32208
|
+
function copyOverlapRatio(candidate, reference) {
|
|
32209
|
+
const referenceWords = words2(reference);
|
|
32210
|
+
if (referenceWords.length === 0) return 0;
|
|
32211
|
+
const candidateWords = new Set(words2(candidate));
|
|
32212
|
+
const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
|
|
32213
|
+
return shared / referenceWords.length;
|
|
32214
|
+
}
|
|
32215
|
+
function isVerbatimReuse(candidate, reference) {
|
|
32216
|
+
const normalizedCandidate = normalize(candidate);
|
|
32217
|
+
const normalizedReference = normalize(reference);
|
|
32218
|
+
if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
|
|
32219
|
+
if (normalizedCandidate.includes(normalizedReference)) return true;
|
|
32220
|
+
return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
|
|
32221
|
+
}
|
|
32222
|
+
function extractVisibleStrings(text) {
|
|
32223
|
+
const found = [];
|
|
32224
|
+
const lines = text.split("\n");
|
|
32225
|
+
for (const [index, line] of lines.entries()) {
|
|
32226
|
+
for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
|
|
32227
|
+
const value = match[1]?.trim();
|
|
32228
|
+
if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
|
|
32229
|
+
}
|
|
32230
|
+
}
|
|
32231
|
+
return found;
|
|
32232
|
+
}
|
|
32233
|
+
function detectOriginality(sources, references) {
|
|
32234
|
+
if (references.length === 0) return [];
|
|
32235
|
+
const findings = [];
|
|
32236
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32237
|
+
for (const source of sources) {
|
|
32238
|
+
for (const { value, line } of extractVisibleStrings(source.text)) {
|
|
32239
|
+
for (const reference of references) {
|
|
32240
|
+
const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
|
|
32241
|
+
if (!hit) continue;
|
|
32242
|
+
const key = `${source.path}:${line}:${normalize(hit)}`;
|
|
32243
|
+
if (seen.has(key)) continue;
|
|
32244
|
+
seen.add(key);
|
|
32245
|
+
findings.push({
|
|
32246
|
+
id: "copied-reference-copy",
|
|
32247
|
+
snippet: value.slice(0, 120),
|
|
32248
|
+
file: source.path,
|
|
32249
|
+
line
|
|
32250
|
+
});
|
|
32251
|
+
}
|
|
32252
|
+
}
|
|
32253
|
+
}
|
|
32254
|
+
return findings;
|
|
32255
|
+
}
|
|
32256
|
+
|
|
32194
32257
|
// src/engine/landing/lib/rules.ts
|
|
32195
32258
|
var cap2 = (m, i) => m[i] ?? "";
|
|
32196
32259
|
var num = (m, i) => Number(m[i] ?? 0);
|
|
@@ -32595,8 +32658,8 @@ var ANALYZERS = [
|
|
|
32595
32658
|
const lines = text.split("\n");
|
|
32596
32659
|
for (const m of text.matchAll(/(?:-webkit-)?background-clip\s*:\s*text/gi)) {
|
|
32597
32660
|
const line = lineOf(text, m.index ?? 0);
|
|
32598
|
-
const
|
|
32599
|
-
if (/gradient\(/i.test(
|
|
32661
|
+
const window2 = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
|
|
32662
|
+
if (/gradient\(/i.test(window2)) {
|
|
32600
32663
|
out.push({ id: "gradient-text", snippet: "background-clip: text + gradient", file, line });
|
|
32601
32664
|
}
|
|
32602
32665
|
}
|
|
@@ -32718,7 +32781,16 @@ function dedupe(findings) {
|
|
|
32718
32781
|
}
|
|
32719
32782
|
|
|
32720
32783
|
// src/engine/landing/lib/critique.ts
|
|
32721
|
-
var FAMILIES = [
|
|
32784
|
+
var FAMILIES = [
|
|
32785
|
+
"typography",
|
|
32786
|
+
"color",
|
|
32787
|
+
"borders_depth",
|
|
32788
|
+
"motion",
|
|
32789
|
+
"spacing",
|
|
32790
|
+
"copy",
|
|
32791
|
+
"integrity",
|
|
32792
|
+
"originality"
|
|
32793
|
+
];
|
|
32722
32794
|
function round4(n) {
|
|
32723
32795
|
return Math.round(n * 100) / 100;
|
|
32724
32796
|
}
|
|
@@ -32737,6 +32809,7 @@ function critiqueLanding(input) {
|
|
|
32737
32809
|
const raws = [];
|
|
32738
32810
|
for (const source of sources) raws.push(...detectSource(source));
|
|
32739
32811
|
raws.push(...detectPage(sources));
|
|
32812
|
+
raws.push(...detectOriginality(sources, input.references ?? []));
|
|
32740
32813
|
for (const raw of raws) {
|
|
32741
32814
|
const meta = RULE_META[raw.id];
|
|
32742
32815
|
if (!meta) continue;
|
|
@@ -32774,41 +32847,77 @@ function describeCounts(findings) {
|
|
|
32774
32847
|
return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
|
|
32775
32848
|
}
|
|
32776
32849
|
|
|
32777
|
-
// src/
|
|
32778
|
-
import { mkdir as mkdir8,
|
|
32850
|
+
// src/engine/landing/lib/referenceStore.ts
|
|
32851
|
+
import { mkdir as mkdir8, readFile as readFile22, writeFile as writeFile11 } from "fs/promises";
|
|
32779
32852
|
import path25 from "path";
|
|
32853
|
+
var REFERENCES_FILE = ".cache/inspiration-refs.json";
|
|
32854
|
+
var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
32855
|
+
async function readReferences(projectRoot) {
|
|
32856
|
+
try {
|
|
32857
|
+
const raw = await readFile22(path25.join(projectRoot, REFERENCES_FILE), "utf8");
|
|
32858
|
+
const parsed = JSON.parse(raw);
|
|
32859
|
+
if (!Array.isArray(parsed)) return [];
|
|
32860
|
+
const cutoff = Date.now() - REFERENCE_TTL_MS;
|
|
32861
|
+
return parsed.filter((entry) => {
|
|
32862
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
32863
|
+
const candidate = entry;
|
|
32864
|
+
if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
|
|
32865
|
+
const at = Date.parse(candidate.consultedAt ?? "");
|
|
32866
|
+
return Number.isNaN(at) ? true : at >= cutoff;
|
|
32867
|
+
});
|
|
32868
|
+
} catch {
|
|
32869
|
+
return [];
|
|
32870
|
+
}
|
|
32871
|
+
}
|
|
32872
|
+
async function recordReference(projectRoot, reference) {
|
|
32873
|
+
try {
|
|
32874
|
+
const existing = await readReferences(projectRoot);
|
|
32875
|
+
const merged = [...existing.filter((entry) => entry.sectionId !== reference.sectionId), reference];
|
|
32876
|
+
const file = path25.join(projectRoot, REFERENCES_FILE);
|
|
32877
|
+
await mkdir8(path25.dirname(file), { recursive: true });
|
|
32878
|
+
await writeFile11(file, `${JSON.stringify(merged, null, 2)}
|
|
32879
|
+
`);
|
|
32880
|
+
return true;
|
|
32881
|
+
} catch {
|
|
32882
|
+
return false;
|
|
32883
|
+
}
|
|
32884
|
+
}
|
|
32885
|
+
|
|
32886
|
+
// src/commands/landing/snapshot.ts
|
|
32887
|
+
import { mkdir as mkdir9, rename as rename2, writeFile as writeFile12 } from "fs/promises";
|
|
32888
|
+
import path26 from "path";
|
|
32780
32889
|
var CRITIC_VERSION = "2";
|
|
32781
32890
|
function critiqueCacheDir(projectRoot) {
|
|
32782
|
-
return
|
|
32891
|
+
return path26.join(projectRoot, ".cache", "landing-critique");
|
|
32783
32892
|
}
|
|
32784
32893
|
function snapshotPath(projectRoot, slug) {
|
|
32785
|
-
return
|
|
32894
|
+
return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
|
|
32786
32895
|
}
|
|
32787
32896
|
async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
32788
|
-
await
|
|
32897
|
+
await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
|
|
32789
32898
|
const dest = snapshotPath(projectRoot, snapshot.slug);
|
|
32790
32899
|
const tmp = `${dest}.tmp`;
|
|
32791
|
-
await
|
|
32900
|
+
await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
|
|
32792
32901
|
`, "utf8");
|
|
32793
32902
|
await rename2(tmp, dest);
|
|
32794
32903
|
}
|
|
32795
32904
|
|
|
32796
32905
|
// src/commands/landing/source-version.ts
|
|
32797
|
-
import { readdir as readdir7, readFile as
|
|
32798
|
-
import
|
|
32906
|
+
import { readdir as readdir7, readFile as readFile23, stat as stat5 } from "fs/promises";
|
|
32907
|
+
import path27 from "path";
|
|
32799
32908
|
async function landingSourceRelPaths(landingDir) {
|
|
32800
32909
|
const rel = [];
|
|
32801
|
-
if (await isFile(
|
|
32802
|
-
const componentsDir =
|
|
32910
|
+
if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
|
|
32911
|
+
const componentsDir = path27.join(landingDir, "_components");
|
|
32803
32912
|
for (const abs of await walkAstro(componentsDir)) {
|
|
32804
|
-
rel.push(
|
|
32913
|
+
rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
|
|
32805
32914
|
}
|
|
32806
32915
|
return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
32807
32916
|
}
|
|
32808
32917
|
async function readLandingSources(landingDir) {
|
|
32809
32918
|
const rel = await landingSourceRelPaths(landingDir);
|
|
32810
32919
|
const out = [];
|
|
32811
|
-
for (const r of rel) out.push({ path: r, text: await
|
|
32920
|
+
for (const r of rel) out.push({ path: r, text: await readFile23(path27.join(landingDir, r), "utf8") });
|
|
32812
32921
|
return out;
|
|
32813
32922
|
}
|
|
32814
32923
|
async function computeLandingSourceSha(landingDir) {
|
|
@@ -32817,7 +32926,7 @@ async function computeLandingSourceSha(landingDir) {
|
|
|
32817
32926
|
for (const r of rel) {
|
|
32818
32927
|
let bytes;
|
|
32819
32928
|
try {
|
|
32820
|
-
bytes = await
|
|
32929
|
+
bytes = await readFile23(path27.join(landingDir, r));
|
|
32821
32930
|
} catch {
|
|
32822
32931
|
bytes = Buffer.alloc(0);
|
|
32823
32932
|
}
|
|
@@ -32841,7 +32950,7 @@ async function walkAstro(dir) {
|
|
|
32841
32950
|
}
|
|
32842
32951
|
const out = [];
|
|
32843
32952
|
for (const entry of entries) {
|
|
32844
|
-
const abs =
|
|
32953
|
+
const abs = path27.join(dir, entry.name);
|
|
32845
32954
|
if (entry.isDirectory()) out.push(...await walkAstro(abs));
|
|
32846
32955
|
else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
|
|
32847
32956
|
}
|
|
@@ -32902,14 +33011,14 @@ var critiqueCommand2 = defineCommand142({
|
|
|
32902
33011
|
{ availableSlugs: await listLandingSlugs(projectRoot) }
|
|
32903
33012
|
);
|
|
32904
33013
|
}
|
|
32905
|
-
if (!await isDir(
|
|
33014
|
+
if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
|
|
32906
33015
|
fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
|
|
32907
33016
|
availableSlugs: await listLandingSlugs(projectRoot)
|
|
32908
33017
|
});
|
|
32909
33018
|
}
|
|
32910
33019
|
}
|
|
32911
|
-
const brand = await loadBrandTokens(projectRoot);
|
|
32912
|
-
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
|
|
33020
|
+
const [brand, references] = await Promise.all([loadBrandTokens(projectRoot), readReferences(projectRoot)]);
|
|
33021
|
+
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, references)));
|
|
32913
33022
|
const landings = results.map(({ slug, report }) => ({
|
|
32914
33023
|
slug,
|
|
32915
33024
|
overall: report.overall,
|
|
@@ -32940,10 +33049,10 @@ var critiqueCommand2 = defineCommand142({
|
|
|
32940
33049
|
);
|
|
32941
33050
|
}
|
|
32942
33051
|
});
|
|
32943
|
-
async function critiqueOne(projectRoot, slug, brand) {
|
|
32944
|
-
const landingDir =
|
|
33052
|
+
async function critiqueOne(projectRoot, slug, brand, references) {
|
|
33053
|
+
const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
|
|
32945
33054
|
const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
|
|
32946
|
-
const report = critiqueLanding({ slug, sources, brand });
|
|
33055
|
+
const report = critiqueLanding({ slug, sources, brand, references });
|
|
32947
33056
|
let snapshotFailed = false;
|
|
32948
33057
|
try {
|
|
32949
33058
|
await writeCritiqueSnapshot(projectRoot, {
|
|
@@ -32961,7 +33070,7 @@ async function critiqueOne(projectRoot, slug, brand) {
|
|
|
32961
33070
|
}
|
|
32962
33071
|
async function listLandingSlugs(projectRoot) {
|
|
32963
33072
|
try {
|
|
32964
|
-
const entries = await readdir8(
|
|
33073
|
+
const entries = await readdir8(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
32965
33074
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
32966
33075
|
} catch {
|
|
32967
33076
|
return [];
|
|
@@ -32986,8 +33095,2256 @@ async function isDir(p) {
|
|
|
32986
33095
|
}
|
|
32987
33096
|
}
|
|
32988
33097
|
|
|
33098
|
+
// src/commands/landing/inspiration/index.ts
|
|
33099
|
+
import { defineCommand as defineCommand150 } from "citty";
|
|
33100
|
+
|
|
33101
|
+
// src/commands/landing/inspiration/add.ts
|
|
33102
|
+
import { defineCommand as defineCommand143 } from "citty";
|
|
33103
|
+
|
|
33104
|
+
// src/commands/landing/inspiration/shared.ts
|
|
33105
|
+
var INSPIRATION_HINTS = {
|
|
33106
|
+
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.",
|
|
33107
|
+
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."
|
|
33108
|
+
};
|
|
33109
|
+
function fidelityHint(fidelity) {
|
|
33110
|
+
if (fidelity == null) return null;
|
|
33111
|
+
if (fidelity >= 0.9) return null;
|
|
33112
|
+
if (fidelity >= 0.75) {
|
|
33113
|
+
return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 the markup is close but not exact. Trust the screenshot over the code.`;
|
|
33114
|
+
}
|
|
33115
|
+
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.`;
|
|
33116
|
+
}
|
|
33117
|
+
function reportError(error) {
|
|
33118
|
+
if (error instanceof ApiError) {
|
|
33119
|
+
writeJson({ ok: false, error: { code: error.code, message: error.message } });
|
|
33120
|
+
} else {
|
|
33121
|
+
writeJson({
|
|
33122
|
+
ok: false,
|
|
33123
|
+
error: { code: "UNKNOWN", message: error instanceof Error ? error.message : String(error) }
|
|
33124
|
+
});
|
|
33125
|
+
}
|
|
33126
|
+
process.exit(1);
|
|
33127
|
+
}
|
|
33128
|
+
function splitList(value) {
|
|
33129
|
+
if (!value) return void 0;
|
|
33130
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
33131
|
+
return parts.length > 0 ? parts : void 0;
|
|
33132
|
+
}
|
|
33133
|
+
function parseNumber(value) {
|
|
33134
|
+
if (value === void 0 || value === "") return void 0;
|
|
33135
|
+
const parsed = Number(value);
|
|
33136
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
33137
|
+
}
|
|
33138
|
+
|
|
33139
|
+
// src/commands/landing/inspiration/add.ts
|
|
33140
|
+
registerSchema({
|
|
33141
|
+
command: "landing.inspiration.add",
|
|
33142
|
+
description: "Add a 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.",
|
|
33143
|
+
args: {
|
|
33144
|
+
url: { type: "string", description: "Landing page address", required: true },
|
|
33145
|
+
note: { type: "string", description: "Why this page is worth keeping", required: false }
|
|
33146
|
+
}
|
|
33147
|
+
});
|
|
33148
|
+
var addCommand = defineCommand143({
|
|
33149
|
+
meta: {
|
|
33150
|
+
name: "add",
|
|
33151
|
+
description: "Add a landing page to the reference library. Example: baker landing inspiration add https://linear.app --note 'the client likes this density'"
|
|
33152
|
+
},
|
|
33153
|
+
args: {
|
|
33154
|
+
url: { type: "positional", description: "Landing page address", required: true },
|
|
33155
|
+
note: { type: "string", description: "Why this page is worth keeping", required: false }
|
|
33156
|
+
},
|
|
33157
|
+
run: async ({ args }) => {
|
|
33158
|
+
try {
|
|
33159
|
+
const data = await apiPost("/api/landing-inspiration/add", {
|
|
33160
|
+
url: args.url,
|
|
33161
|
+
note: args.note,
|
|
33162
|
+
favorite: true
|
|
33163
|
+
});
|
|
33164
|
+
const hints = data.alreadyKnown ? [
|
|
33165
|
+
"This page was already in the library, so nothing was re-studied \u2014 its sections are searchable now.",
|
|
33166
|
+
`Search it with: baker landing inspiration search --domain ${new URL(data.canonicalUrl).hostname}`
|
|
33167
|
+
] : [
|
|
33168
|
+
"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.",
|
|
33169
|
+
"Meanwhile, search what is already in the library: baker landing inspiration search '<what you want to see>' --scope all"
|
|
33170
|
+
];
|
|
33171
|
+
writeJson({
|
|
33172
|
+
ok: true,
|
|
33173
|
+
data: { id: data.sourceId, url: data.canonicalUrl, status: data.status, already_known: data.alreadyKnown },
|
|
33174
|
+
hints
|
|
33175
|
+
});
|
|
33176
|
+
} catch (error) {
|
|
33177
|
+
reportError(error);
|
|
33178
|
+
}
|
|
33179
|
+
}
|
|
33180
|
+
});
|
|
33181
|
+
|
|
33182
|
+
// src/commands/landing/inspiration/code.ts
|
|
33183
|
+
import { mkdir as mkdir10, writeFile as writeFile13 } from "fs/promises";
|
|
33184
|
+
import path29 from "path";
|
|
33185
|
+
import { defineCommand as defineCommand144 } from "citty";
|
|
33186
|
+
registerSchema({
|
|
33187
|
+
command: "landing.inspiration.code",
|
|
33188
|
+
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.",
|
|
33189
|
+
args: {
|
|
33190
|
+
id: { type: "string", description: "Section id from search", required: true },
|
|
33191
|
+
full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
|
|
33192
|
+
}
|
|
33193
|
+
});
|
|
33194
|
+
var codeCommand = defineCommand144({
|
|
33195
|
+
meta: {
|
|
33196
|
+
name: "code",
|
|
33197
|
+
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."
|
|
33198
|
+
},
|
|
33199
|
+
args: {
|
|
33200
|
+
id: { type: "positional", description: "Section id from search", required: true },
|
|
33201
|
+
full: { type: "boolean", description: "Also print the markup inline", required: false, default: false }
|
|
33202
|
+
},
|
|
33203
|
+
run: async ({ args }) => {
|
|
33204
|
+
try {
|
|
33205
|
+
const id = args.id;
|
|
33206
|
+
const data = await apiGet("/api/landing-inspiration/section-code", { id });
|
|
33207
|
+
const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
|
|
33208
|
+
await mkdir10(dir, { recursive: true });
|
|
33209
|
+
const file = path29.join(dir, "section.html");
|
|
33210
|
+
await writeFile13(file, data.html);
|
|
33211
|
+
const recorded = await recordReference(process.cwd(), {
|
|
33212
|
+
sectionId: id,
|
|
33213
|
+
sourceUrl: data.sourceUrl,
|
|
33214
|
+
copyStrings: data.copyStrings,
|
|
33215
|
+
consultedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33216
|
+
});
|
|
33217
|
+
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
33218
|
+
const fidelity = fidelityHint(data.fidelity);
|
|
33219
|
+
if (fidelity) hints.push(fidelity);
|
|
33220
|
+
if (data.fidelityNote) hints.push(data.fidelityNote);
|
|
33221
|
+
if (!recorded) {
|
|
33222
|
+
hints.push(
|
|
33223
|
+
"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."
|
|
33224
|
+
);
|
|
33225
|
+
}
|
|
33226
|
+
writeJson({
|
|
33227
|
+
ok: true,
|
|
33228
|
+
data: {
|
|
33229
|
+
id,
|
|
33230
|
+
file: path29.relative(process.cwd(), file),
|
|
33231
|
+
bytes: data.html.length,
|
|
33232
|
+
fidelity: data.fidelity,
|
|
33233
|
+
css_custom_properties: data.cssCustomProperties,
|
|
33234
|
+
reproduction_notes: data.reproductionNotes,
|
|
33235
|
+
adaptation_notes: data.adaptationNotes,
|
|
33236
|
+
source_url: data.sourceUrl,
|
|
33237
|
+
...args.full ? { html: data.html } : {}
|
|
33238
|
+
},
|
|
33239
|
+
hints
|
|
33240
|
+
});
|
|
33241
|
+
} catch (error) {
|
|
33242
|
+
reportError(error);
|
|
33243
|
+
}
|
|
33244
|
+
}
|
|
33245
|
+
});
|
|
33246
|
+
|
|
33247
|
+
// src/commands/landing/inspiration/favorites.ts
|
|
33248
|
+
import { defineCommand as defineCommand145 } from "citty";
|
|
33249
|
+
registerSchema({
|
|
33250
|
+
command: "landing.inspiration.favorites",
|
|
33251
|
+
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.",
|
|
33252
|
+
args: {
|
|
33253
|
+
type: { type: "string", description: "Comma list of section types to filter by", required: false },
|
|
33254
|
+
limit: { type: "number", description: "Max results (default 30)", required: false }
|
|
33255
|
+
}
|
|
33256
|
+
});
|
|
33257
|
+
var favoritesCommand = defineCommand145({
|
|
33258
|
+
meta: {
|
|
33259
|
+
name: "favorites",
|
|
33260
|
+
description: "List this company's saved reference sections. Example: baker landing inspiration favorites --type hero,pricing"
|
|
33261
|
+
},
|
|
33262
|
+
args: {
|
|
33263
|
+
type: { type: "string", description: "Comma list of section types", required: false },
|
|
33264
|
+
limit: { type: "string", description: "Max results (default 30)", required: false }
|
|
33265
|
+
},
|
|
33266
|
+
run: async ({ args }) => {
|
|
33267
|
+
try {
|
|
33268
|
+
const params = {};
|
|
33269
|
+
const types = splitList(args.type);
|
|
33270
|
+
if (types) params.type = types.join(",");
|
|
33271
|
+
const limit = parseNumber(args.limit);
|
|
33272
|
+
params.limit = String(limit ?? 30);
|
|
33273
|
+
const data = await apiGet("/api/landing-inspiration/favorites", params);
|
|
33274
|
+
const sections = Array.isArray(data?.sections) ? data.sections : [];
|
|
33275
|
+
writeJson({
|
|
33276
|
+
ok: true,
|
|
33277
|
+
data: {
|
|
33278
|
+
sections: sections.map((section) => ({
|
|
33279
|
+
id: section.id,
|
|
33280
|
+
section: section.sectionType,
|
|
33281
|
+
composition: section.composition,
|
|
33282
|
+
look: section.visualRegister,
|
|
33283
|
+
why_it_works: section.whyItWorks,
|
|
33284
|
+
domain: section.domain
|
|
33285
|
+
}))
|
|
33286
|
+
},
|
|
33287
|
+
hints: sections.length === 0 ? [
|
|
33288
|
+
"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`."
|
|
33289
|
+
] : [
|
|
33290
|
+
"These are the client's taste signals \u2014 read them as direction, not as a component library.",
|
|
33291
|
+
INSPIRATION_HINTS.adapt
|
|
33292
|
+
]
|
|
33293
|
+
});
|
|
33294
|
+
} catch (error) {
|
|
33295
|
+
reportError(error);
|
|
33296
|
+
}
|
|
33297
|
+
}
|
|
33298
|
+
});
|
|
33299
|
+
registerSchema({
|
|
33300
|
+
command: "landing.inspiration.favorite",
|
|
33301
|
+
description: "Save a reference section (or a whole page) to this company, so it shows up in the default search scope.",
|
|
33302
|
+
args: {
|
|
33303
|
+
id: { type: "string", description: "Section id, or page id with --page", required: true },
|
|
33304
|
+
page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false },
|
|
33305
|
+
note: { type: "string", description: "Why this is worth keeping", required: false }
|
|
33306
|
+
}
|
|
33307
|
+
});
|
|
33308
|
+
var favoriteCommand = defineCommand145({
|
|
33309
|
+
meta: {
|
|
33310
|
+
name: "favorite",
|
|
33311
|
+
description: "Save a reference section to this company. Example: baker landing inspiration favorite k57abc\u2026"
|
|
33312
|
+
},
|
|
33313
|
+
args: {
|
|
33314
|
+
id: { type: "positional", description: "Section id (or page id with --page)", required: true },
|
|
33315
|
+
page: { type: "boolean", description: "Treat the id as a page", required: false, default: false },
|
|
33316
|
+
note: { type: "string", description: "Why this is worth keeping", required: false }
|
|
33317
|
+
},
|
|
33318
|
+
run: async ({ args }) => {
|
|
33319
|
+
try {
|
|
33320
|
+
const id = args.id;
|
|
33321
|
+
const body = args.page ? { sourceId: id } : { sectionId: id };
|
|
33322
|
+
const data = await apiPost("/api/landing-inspiration/favorite", {
|
|
33323
|
+
...body,
|
|
33324
|
+
note: args.note
|
|
33325
|
+
});
|
|
33326
|
+
writeJson({ ok: true, data: { id, favorited: data.favorited } });
|
|
33327
|
+
} catch (error) {
|
|
33328
|
+
reportError(error);
|
|
33329
|
+
}
|
|
33330
|
+
}
|
|
33331
|
+
});
|
|
33332
|
+
registerSchema({
|
|
33333
|
+
command: "landing.inspiration.unfavorite",
|
|
33334
|
+
description: "Remove a reference section (or page) from this company's saved set.",
|
|
33335
|
+
args: {
|
|
33336
|
+
id: { type: "string", description: "Section id, or page id with --page", required: true },
|
|
33337
|
+
page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false }
|
|
33338
|
+
}
|
|
33339
|
+
});
|
|
33340
|
+
var unfavoriteCommand = defineCommand145({
|
|
33341
|
+
meta: {
|
|
33342
|
+
name: "unfavorite",
|
|
33343
|
+
description: "Remove a reference section from this company's saved set. Example: baker landing inspiration unfavorite k57abc\u2026"
|
|
33344
|
+
},
|
|
33345
|
+
args: {
|
|
33346
|
+
id: { type: "positional", description: "Section id (or page id with --page)", required: true },
|
|
33347
|
+
page: { type: "boolean", description: "Treat the id as a page", required: false, default: false }
|
|
33348
|
+
},
|
|
33349
|
+
run: async ({ args }) => {
|
|
33350
|
+
try {
|
|
33351
|
+
const id = args.id;
|
|
33352
|
+
const body = args.page ? { sourceId: id } : { sectionId: id };
|
|
33353
|
+
const data = await apiPost("/api/landing-inspiration/unfavorite", body);
|
|
33354
|
+
writeJson({ ok: true, data: { id, favorited: data.favorited } });
|
|
33355
|
+
} catch (error) {
|
|
33356
|
+
reportError(error);
|
|
33357
|
+
}
|
|
33358
|
+
}
|
|
33359
|
+
});
|
|
33360
|
+
|
|
33361
|
+
// src/commands/landing/inspiration/page.ts
|
|
33362
|
+
import { defineCommand as defineCommand146 } from "citty";
|
|
33363
|
+
registerSchema({
|
|
33364
|
+
command: "landing.inspiration.page",
|
|
33365
|
+
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.",
|
|
33366
|
+
args: { id: { type: "string", description: "Page id (from a search result's source id)", required: true } }
|
|
33367
|
+
});
|
|
33368
|
+
var pageCommand = defineCommand146({
|
|
33369
|
+
meta: {
|
|
33370
|
+
name: "page",
|
|
33371
|
+
description: "Show how a reference page sequences its sections. Example: baker landing inspiration page j91xyz\u2026 \u2014 the blueprint, not the pixels."
|
|
33372
|
+
},
|
|
33373
|
+
args: { id: { type: "positional", description: "Page id", required: true } },
|
|
33374
|
+
run: async ({ args }) => {
|
|
33375
|
+
try {
|
|
33376
|
+
const data = await apiGet("/api/landing-inspiration/page", { id: args.id });
|
|
33377
|
+
if (data.status !== "indexed") {
|
|
33378
|
+
writeJson({
|
|
33379
|
+
ok: true,
|
|
33380
|
+
data: { id: data.id, url: data.url, status: data.status, sections: [] },
|
|
33381
|
+
hints: [
|
|
33382
|
+
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."
|
|
33383
|
+
]
|
|
33384
|
+
});
|
|
33385
|
+
return;
|
|
33386
|
+
}
|
|
33387
|
+
writeJson({
|
|
33388
|
+
ok: true,
|
|
33389
|
+
data: {
|
|
33390
|
+
id: data.id,
|
|
33391
|
+
url: data.url,
|
|
33392
|
+
domain: data.domain,
|
|
33393
|
+
title: data.title,
|
|
33394
|
+
archetype: data.archetype,
|
|
33395
|
+
stack: data.detectedStack,
|
|
33396
|
+
page_fidelity: data.pageFidelity,
|
|
33397
|
+
blueprint: data.sectionOrder,
|
|
33398
|
+
sections: data.sections.map((section) => ({
|
|
33399
|
+
id: section.id,
|
|
33400
|
+
position: section.index,
|
|
33401
|
+
section: section.sectionType,
|
|
33402
|
+
composition: section.composition,
|
|
33403
|
+
look: section.visualRegister,
|
|
33404
|
+
motion: section.motionSummary,
|
|
33405
|
+
why_it_works: section.whyItWorks,
|
|
33406
|
+
craft: section.craftScore
|
|
33407
|
+
}))
|
|
33408
|
+
},
|
|
33409
|
+
hints: [
|
|
33410
|
+
"The blueprint is the section order top to bottom \u2014 the answer to 'how do good pages in this category sequence themselves'.",
|
|
33411
|
+
"Use `baker landing inspiration view <section id>` for any section worth a closer look.",
|
|
33412
|
+
INSPIRATION_HINTS.adapt
|
|
33413
|
+
]
|
|
33414
|
+
});
|
|
33415
|
+
} catch (error) {
|
|
33416
|
+
reportError(error);
|
|
33417
|
+
}
|
|
33418
|
+
}
|
|
33419
|
+
});
|
|
33420
|
+
|
|
33421
|
+
// src/commands/landing/inspiration/scrape.ts
|
|
33422
|
+
import { defineCommand as defineCommand147 } from "citty";
|
|
33423
|
+
|
|
33424
|
+
// src/engine/landing-library/blocked.ts
|
|
33425
|
+
var CHALLENGE_PHRASES = [
|
|
33426
|
+
"just a moment",
|
|
33427
|
+
"attention required",
|
|
33428
|
+
"verify you are human",
|
|
33429
|
+
"checking your browser",
|
|
33430
|
+
"enable javascript and cookies to continue",
|
|
33431
|
+
"unusual traffic",
|
|
33432
|
+
"access denied",
|
|
33433
|
+
"you have been blocked",
|
|
33434
|
+
"request unsuccessful",
|
|
33435
|
+
"are you a robot",
|
|
33436
|
+
"security check",
|
|
33437
|
+
"ddos protection",
|
|
33438
|
+
"captcha"
|
|
33439
|
+
];
|
|
33440
|
+
var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
|
|
33441
|
+
function detectBlockedPage(page) {
|
|
33442
|
+
if (page.status !== null && page.status >= 400) {
|
|
33443
|
+
return {
|
|
33444
|
+
code: "HTTP_ERROR",
|
|
33445
|
+
message: `The site returned ${page.status} instead of the page \u2014 it may be blocking automated visits.`
|
|
33446
|
+
};
|
|
33447
|
+
}
|
|
33448
|
+
const haystack = `${page.title}
|
|
33449
|
+
${page.bodyText.slice(0, 2e3)}`.toLowerCase();
|
|
33450
|
+
const phrase = CHALLENGE_PHRASES.find((candidate) => haystack.includes(candidate));
|
|
33451
|
+
if (phrase) {
|
|
33452
|
+
return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
|
|
33453
|
+
}
|
|
33454
|
+
const html = page.html?.toLowerCase() ?? "";
|
|
33455
|
+
if (CHALLENGE_MARKERS.some((marker) => html.includes(marker))) {
|
|
33456
|
+
return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
|
|
33457
|
+
}
|
|
33458
|
+
return null;
|
|
33459
|
+
}
|
|
33460
|
+
var BlockedPageError = class extends Error {
|
|
33461
|
+
code;
|
|
33462
|
+
constructor(blocked) {
|
|
33463
|
+
super(blocked.message);
|
|
33464
|
+
this.name = "BlockedPageError";
|
|
33465
|
+
this.code = blocked.code;
|
|
33466
|
+
}
|
|
33467
|
+
};
|
|
33468
|
+
|
|
33469
|
+
// src/engine/landing-library/run.ts
|
|
33470
|
+
import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
|
|
33471
|
+
import path31 from "path";
|
|
33472
|
+
|
|
33473
|
+
// src/engine/landing-library/browser.ts
|
|
33474
|
+
import { createRequire } from "module";
|
|
33475
|
+
var require_ = createRequire(import.meta.url);
|
|
33476
|
+
var pwSpecifier = ["play", "wright"].join("");
|
|
33477
|
+
var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
|
|
33478
|
+
var MOBILE_VIEWPORT = { width: 390, height: 844 };
|
|
33479
|
+
var DEVICE_SCALE_FACTOR = 2;
|
|
33480
|
+
async function launchBrowser() {
|
|
33481
|
+
const playwright = require_(pwSpecifier);
|
|
33482
|
+
return await playwright.chromium.launch({
|
|
33483
|
+
headless: true,
|
|
33484
|
+
args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
|
|
33485
|
+
});
|
|
33486
|
+
}
|
|
33487
|
+
async function newPage(browser, viewport, opts = { motion: false }) {
|
|
33488
|
+
const context = await browser.newContext({
|
|
33489
|
+
viewport,
|
|
33490
|
+
deviceScaleFactor: DEVICE_SCALE_FACTOR,
|
|
33491
|
+
// Still captures freeze motion so screenshots are reproducible; the motion
|
|
33492
|
+
// pass re-opens a context with animation enabled.
|
|
33493
|
+
reducedMotion: opts.motion ? "no-preference" : "reduce",
|
|
33494
|
+
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"
|
|
33495
|
+
});
|
|
33496
|
+
const page = await context.newPage();
|
|
33497
|
+
return { context, page };
|
|
33498
|
+
}
|
|
33499
|
+
|
|
33500
|
+
// src/engine/landing-library/usedCss.ts
|
|
33501
|
+
async function collectUsedCss(page, selector) {
|
|
33502
|
+
const collected = await page.evaluate(inPageCollectUsedCss, selector);
|
|
33503
|
+
const extra = [];
|
|
33504
|
+
for (const href of collected.unreadableHrefs) {
|
|
33505
|
+
const text = await fetchStylesheet(page, href);
|
|
33506
|
+
if (text) extra.push(`/* ${href} */
|
|
33507
|
+
${text}`);
|
|
33508
|
+
}
|
|
33509
|
+
return { ...collected, css: [collected.css, ...extra].filter(Boolean).join("\n\n") };
|
|
33510
|
+
}
|
|
33511
|
+
async function fetchStylesheet(page, href) {
|
|
33512
|
+
try {
|
|
33513
|
+
const response = await page.request.get(href, { timeout: 1e4 });
|
|
33514
|
+
if (!response.ok()) return null;
|
|
33515
|
+
return await response.text();
|
|
33516
|
+
} catch {
|
|
33517
|
+
return null;
|
|
33518
|
+
}
|
|
33519
|
+
}
|
|
33520
|
+
var inPageCollectUsedCss = (selector) => {
|
|
33521
|
+
const root = document.querySelector(selector);
|
|
33522
|
+
const empty = {
|
|
33523
|
+
css: "",
|
|
33524
|
+
variables: "",
|
|
33525
|
+
inheritedSeed: "",
|
|
33526
|
+
unreadableHrefs: [],
|
|
33527
|
+
stats: { totalRules: 0, keptRules: 0, fontFaces: 0, keyframes: 0 }
|
|
33528
|
+
};
|
|
33529
|
+
if (!root) return empty;
|
|
33530
|
+
const INHERITED = [
|
|
33531
|
+
"color",
|
|
33532
|
+
"font-family",
|
|
33533
|
+
"font-size",
|
|
33534
|
+
"font-weight",
|
|
33535
|
+
"line-height",
|
|
33536
|
+
"letter-spacing",
|
|
33537
|
+
"text-align",
|
|
33538
|
+
"background-color",
|
|
33539
|
+
"-webkit-font-smoothing"
|
|
33540
|
+
];
|
|
33541
|
+
const unreadableHrefs = [];
|
|
33542
|
+
const kept = [];
|
|
33543
|
+
const fontFaces = [];
|
|
33544
|
+
const keyframesByName = /* @__PURE__ */ new Map();
|
|
33545
|
+
let totalRules = 0;
|
|
33546
|
+
let keptStyleRules = 0;
|
|
33547
|
+
const matchesInSection = (selectorText) => {
|
|
33548
|
+
for (const part of selectorText.split(",")) {
|
|
33549
|
+
const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
|
|
33550
|
+
if (!base) continue;
|
|
33551
|
+
try {
|
|
33552
|
+
if (root.matches(base) || root.querySelector(base)) return true;
|
|
33553
|
+
} catch {
|
|
33554
|
+
return true;
|
|
33555
|
+
}
|
|
33556
|
+
}
|
|
33557
|
+
return false;
|
|
33558
|
+
};
|
|
33559
|
+
const walk = (rules, sink) => {
|
|
33560
|
+
for (const rule of Array.from(rules)) {
|
|
33561
|
+
totalRules++;
|
|
33562
|
+
if (rule instanceof CSSStyleRule) {
|
|
33563
|
+
if (matchesInSection(rule.selectorText)) {
|
|
33564
|
+
sink.push(rule.cssText);
|
|
33565
|
+
keptStyleRules++;
|
|
33566
|
+
}
|
|
33567
|
+
continue;
|
|
33568
|
+
}
|
|
33569
|
+
if (rule instanceof CSSFontFaceRule) {
|
|
33570
|
+
fontFaces.push(rule.cssText);
|
|
33571
|
+
continue;
|
|
33572
|
+
}
|
|
33573
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
33574
|
+
keyframesByName.set(rule.name, rule.cssText);
|
|
33575
|
+
continue;
|
|
33576
|
+
}
|
|
33577
|
+
const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
|
|
33578
|
+
if (grouping) {
|
|
33579
|
+
const inner = [];
|
|
33580
|
+
walk(rule.cssRules, inner);
|
|
33581
|
+
if (inner.length === 0) continue;
|
|
33582
|
+
const condition = rule.conditionText ?? "";
|
|
33583
|
+
const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
|
|
33584
|
+
sink.push(`${prelude} {
|
|
33585
|
+
${inner.join("\n")}
|
|
33586
|
+
}`);
|
|
33587
|
+
}
|
|
33588
|
+
}
|
|
33589
|
+
};
|
|
33590
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
33591
|
+
const owner = sheet.ownerNode;
|
|
33592
|
+
if (owner?.hasAttribute?.("data-baker-freeze")) continue;
|
|
33593
|
+
try {
|
|
33594
|
+
walk(sheet.cssRules, kept);
|
|
33595
|
+
} catch {
|
|
33596
|
+
if (sheet.href) unreadableHrefs.push(sheet.href);
|
|
33597
|
+
}
|
|
33598
|
+
}
|
|
33599
|
+
const keptText = kept.join("\n");
|
|
33600
|
+
const usedKeyframes = [];
|
|
33601
|
+
for (const [name, text] of keyframesByName) {
|
|
33602
|
+
if (new RegExp(`(^|[\\s:,])${name}([\\s;,}]|$)`).test(keptText)) usedKeyframes.push(text);
|
|
33603
|
+
}
|
|
33604
|
+
const rootStyle = getComputedStyle(root);
|
|
33605
|
+
const variableDeclarations = [];
|
|
33606
|
+
for (const property of Array.from(rootStyle)) {
|
|
33607
|
+
if (!property.startsWith("--")) continue;
|
|
33608
|
+
const value = rootStyle.getPropertyValue(property).trim();
|
|
33609
|
+
if (value) variableDeclarations.push(` ${property}: ${value};`);
|
|
33610
|
+
}
|
|
33611
|
+
const seed = INHERITED.map((property) => ` ${property}: ${rootStyle.getPropertyValue(property)};`).join("\n");
|
|
33612
|
+
return {
|
|
33613
|
+
css: [...fontFaces, ...usedKeyframes, ...kept].join("\n"),
|
|
33614
|
+
variables: variableDeclarations.length ? `:root {
|
|
33615
|
+
${variableDeclarations.join("\n")}
|
|
33616
|
+
}` : "",
|
|
33617
|
+
inheritedSeed: seed,
|
|
33618
|
+
unreadableHrefs,
|
|
33619
|
+
stats: {
|
|
33620
|
+
totalRules,
|
|
33621
|
+
keptRules: keptStyleRules,
|
|
33622
|
+
fontFaces: fontFaces.length,
|
|
33623
|
+
keyframes: usedKeyframes.length
|
|
33624
|
+
}
|
|
33625
|
+
};
|
|
33626
|
+
};
|
|
33627
|
+
|
|
33628
|
+
// src/engine/landing-library/bundle.ts
|
|
33629
|
+
async function buildSectionBundle(page, selector, pageUrl) {
|
|
33630
|
+
const used = await collectUsedCss(page, selector);
|
|
33631
|
+
const extracted = await page.evaluate(inPageExtractMarkup, {
|
|
33632
|
+
selector,
|
|
33633
|
+
pageUrl,
|
|
33634
|
+
rootAttribute: SECTION_ROOT_ATTRIBUTE
|
|
33635
|
+
});
|
|
33636
|
+
const css = [
|
|
33637
|
+
"*, *::before, *::after { box-sizing: border-box; }",
|
|
33638
|
+
"html, body { margin: 0; padding: 0; }",
|
|
33639
|
+
used.variables,
|
|
33640
|
+
`body {
|
|
33641
|
+
${used.inheritedSeed}
|
|
33642
|
+
}`,
|
|
33643
|
+
used.css
|
|
33644
|
+
].filter(Boolean).join("\n\n");
|
|
33645
|
+
const html = `<!doctype html>
|
|
33646
|
+
<html ${extracted.rootAttributes}>
|
|
33647
|
+
<head>
|
|
33648
|
+
<meta charset="utf-8">
|
|
33649
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
33650
|
+
<title>Section from ${escapeHtml2(extracted.host)}</title>
|
|
33651
|
+
<style>
|
|
33652
|
+
${css}
|
|
33653
|
+
</style>
|
|
33654
|
+
</head>
|
|
33655
|
+
<body ${extracted.bodyAttributes}>
|
|
33656
|
+
${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
|
|
33657
|
+
</body>
|
|
33658
|
+
</html>
|
|
33659
|
+
`;
|
|
33660
|
+
return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
|
|
33661
|
+
}
|
|
33662
|
+
function escapeHtml2(value) {
|
|
33663
|
+
return value.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c] ?? c);
|
|
33664
|
+
}
|
|
33665
|
+
var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
|
|
33666
|
+
function wrapInLayoutContext(html, context) {
|
|
33667
|
+
let wrapped = context.width > 0 && context.width < context.viewportWidth ? `<div style="width:${context.width}px;margin:0 auto;">
|
|
33668
|
+
${html}
|
|
33669
|
+
</div>` : html;
|
|
33670
|
+
for (const ancestor of [...context.ancestors].reverse()) {
|
|
33671
|
+
const attributes = ancestor.attributes ? ` ${ancestor.attributes}` : "";
|
|
33672
|
+
const container = ancestor.containerType && ancestor.containerType !== "normal" ? `container-type:${ancestor.containerType};${ancestor.containerName && ancestor.containerName !== "none" ? `container-name:${ancestor.containerName};` : ""}` : "";
|
|
33673
|
+
wrapped = `<${ancestor.tag}${attributes} style="${NEUTRALIZED_ANCESTOR_STYLE}${container}">
|
|
33674
|
+
${wrapped}
|
|
33675
|
+
</${ancestor.tag}>`;
|
|
33676
|
+
}
|
|
33677
|
+
return wrapped;
|
|
33678
|
+
}
|
|
33679
|
+
var NEUTRALIZED_ANCESTOR_STYLE = [
|
|
33680
|
+
"display:block !important",
|
|
33681
|
+
"position:static !important",
|
|
33682
|
+
"margin:0 !important",
|
|
33683
|
+
"padding:0 !important",
|
|
33684
|
+
"border:0 !important",
|
|
33685
|
+
"width:auto !important",
|
|
33686
|
+
"min-width:0 !important",
|
|
33687
|
+
"max-width:none !important",
|
|
33688
|
+
"height:auto !important",
|
|
33689
|
+
"min-height:0 !important",
|
|
33690
|
+
"max-height:none !important",
|
|
33691
|
+
"transform:none !important",
|
|
33692
|
+
"overflow:visible !important",
|
|
33693
|
+
""
|
|
33694
|
+
].join(";");
|
|
33695
|
+
var inPageExtractMarkup = ({
|
|
33696
|
+
selector,
|
|
33697
|
+
pageUrl,
|
|
33698
|
+
rootAttribute
|
|
33699
|
+
}) => {
|
|
33700
|
+
const root = document.querySelector(selector);
|
|
33701
|
+
const emptyContext = { ancestors: [], width: 0, viewportWidth: 0 };
|
|
33702
|
+
const serializeAttributes = (element) => {
|
|
33703
|
+
if (!element) return "";
|
|
33704
|
+
return Array.from(element.attributes).filter((attribute) => attribute.name !== "style").map((attribute) => `${attribute.name}="${attribute.value.replace(/"/g, """)}"`).join(" ");
|
|
33705
|
+
};
|
|
33706
|
+
const rootAttributes = serializeAttributes(document.documentElement) || 'lang="en"';
|
|
33707
|
+
const bodyAttributes = serializeAttributes(document.body);
|
|
33708
|
+
if (!root) {
|
|
33709
|
+
return {
|
|
33710
|
+
html: "",
|
|
33711
|
+
assetUrls: [],
|
|
33712
|
+
host: "",
|
|
33713
|
+
layoutContext: emptyContext,
|
|
33714
|
+
rootAttributes,
|
|
33715
|
+
bodyAttributes
|
|
33716
|
+
};
|
|
33717
|
+
}
|
|
33718
|
+
const ancestors = [];
|
|
33719
|
+
for (let ancestor = root.parentElement; ancestor && ancestor !== document.body; ) {
|
|
33720
|
+
const style = getComputedStyle(ancestor);
|
|
33721
|
+
ancestors.unshift({
|
|
33722
|
+
tag: /^[a-zA-Z][a-zA-Z0-9-]*$/.test(ancestor.tagName) ? ancestor.tagName.toLowerCase() : "div",
|
|
33723
|
+
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(" "),
|
|
33724
|
+
containerType: style.containerType,
|
|
33725
|
+
containerName: style.containerName
|
|
33726
|
+
});
|
|
33727
|
+
ancestor = ancestor.parentElement;
|
|
33728
|
+
}
|
|
33729
|
+
const layoutContext = {
|
|
33730
|
+
ancestors,
|
|
33731
|
+
width: Math.round(root.getBoundingClientRect().width),
|
|
33732
|
+
viewportWidth: window.innerWidth
|
|
33733
|
+
};
|
|
33734
|
+
const absolute = (value) => {
|
|
33735
|
+
try {
|
|
33736
|
+
return new URL(value, pageUrl).href;
|
|
33737
|
+
} catch {
|
|
33738
|
+
return value;
|
|
33739
|
+
}
|
|
33740
|
+
};
|
|
33741
|
+
const clone = root.cloneNode(true);
|
|
33742
|
+
clone.setAttribute(rootAttribute, "");
|
|
33743
|
+
const assetUrls = /* @__PURE__ */ new Set();
|
|
33744
|
+
for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
|
|
33745
|
+
for (const attribute of ["src", "href", "poster"]) {
|
|
33746
|
+
const value = element.getAttribute(attribute);
|
|
33747
|
+
if (!value || value.startsWith("data:") || value.startsWith("#")) continue;
|
|
33748
|
+
const url = absolute(value);
|
|
33749
|
+
element.setAttribute(attribute, url);
|
|
33750
|
+
if (attribute !== "href" || element.tagName === "LINK") assetUrls.add(url);
|
|
33751
|
+
}
|
|
33752
|
+
const srcset = element.getAttribute("srcset");
|
|
33753
|
+
if (srcset) {
|
|
33754
|
+
element.setAttribute(
|
|
33755
|
+
"srcset",
|
|
33756
|
+
srcset.split(",").map((candidate) => {
|
|
33757
|
+
const [url, descriptor] = candidate.trim().split(/\s+/, 2);
|
|
33758
|
+
if (!url) return candidate;
|
|
33759
|
+
const resolved = absolute(url);
|
|
33760
|
+
assetUrls.add(resolved);
|
|
33761
|
+
return descriptor ? `${resolved} ${descriptor}` : resolved;
|
|
33762
|
+
}).join(", ")
|
|
33763
|
+
);
|
|
33764
|
+
}
|
|
33765
|
+
const style = element.getAttribute("style");
|
|
33766
|
+
if (style?.includes("url(")) {
|
|
33767
|
+
element.setAttribute(
|
|
33768
|
+
"style",
|
|
33769
|
+
style.replace(/url\((['"]?)([^'")]+)\1\)/g, (_match, quote, url) => {
|
|
33770
|
+
if (url.startsWith("data:")) return `url(${quote}${url}${quote})`;
|
|
33771
|
+
const resolved = absolute(url);
|
|
33772
|
+
assetUrls.add(resolved);
|
|
33773
|
+
return `url(${quote}${resolved}${quote})`;
|
|
33774
|
+
})
|
|
33775
|
+
);
|
|
33776
|
+
}
|
|
33777
|
+
}
|
|
33778
|
+
return {
|
|
33779
|
+
html: clone.outerHTML,
|
|
33780
|
+
assetUrls: Array.from(assetUrls),
|
|
33781
|
+
host: location.host,
|
|
33782
|
+
layoutContext,
|
|
33783
|
+
rootAttributes,
|
|
33784
|
+
bodyAttributes
|
|
33785
|
+
};
|
|
33786
|
+
};
|
|
33787
|
+
|
|
33788
|
+
// src/engine/landing-library/capture.ts
|
|
33789
|
+
async function captureSection(page, section) {
|
|
33790
|
+
const locator = page.locator(section.selector).first();
|
|
33791
|
+
if (await locator.count().catch(() => 0)) {
|
|
33792
|
+
const shot = await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
|
|
33793
|
+
if (shot) return shot;
|
|
33794
|
+
}
|
|
33795
|
+
const doc = await page.evaluate(() => ({
|
|
33796
|
+
width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
33797
|
+
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
|
|
33798
|
+
}));
|
|
33799
|
+
const width = Math.min(section.rect.width, doc.width - section.rect.left);
|
|
33800
|
+
const height = Math.min(section.rect.height, doc.height - section.rect.top);
|
|
33801
|
+
if (width <= 0 || height <= 0) return null;
|
|
33802
|
+
return await page.screenshot({
|
|
33803
|
+
type: "png",
|
|
33804
|
+
clip: { x: section.rect.left, y: section.rect.top, width, height },
|
|
33805
|
+
timeout: 15e3
|
|
33806
|
+
}).catch(() => null);
|
|
33807
|
+
}
|
|
33808
|
+
async function captureSectionOnMobile(page, section) {
|
|
33809
|
+
const locator = page.locator(section.selector).first();
|
|
33810
|
+
if (!await locator.count().catch(() => 0)) return null;
|
|
33811
|
+
if (!await locator.isVisible().catch(() => false)) return null;
|
|
33812
|
+
return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
|
|
33813
|
+
}
|
|
33814
|
+
|
|
33815
|
+
// src/engine/landing-library/fidelity.ts
|
|
33816
|
+
import sharp4 from "sharp";
|
|
33817
|
+
var COMPARISON_SIZE = 32;
|
|
33818
|
+
function pixelSimilarity(a, b) {
|
|
33819
|
+
const length = Math.min(a.length, b.length);
|
|
33820
|
+
if (length === 0) return 0;
|
|
33821
|
+
let total = 0;
|
|
33822
|
+
for (let i = 0; i < length; i++) {
|
|
33823
|
+
total += Math.abs((a[i] ?? 0) - (b[i] ?? 0));
|
|
33824
|
+
}
|
|
33825
|
+
return 1 - total / (length * 255);
|
|
33826
|
+
}
|
|
33827
|
+
async function scoreFidelity(live, rendered) {
|
|
33828
|
+
const [liveMeta, renderedMeta] = await Promise.all([sharp4(live).metadata(), sharp4(rendered).metadata()]);
|
|
33829
|
+
const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
|
|
33830
|
+
const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
|
|
33831
|
+
const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
|
|
33832
|
+
const score = pixelSimilarity(livePixels, renderedPixels);
|
|
33833
|
+
const heightRatio = liveSize.height > 0 && renderedSize.height > 0 ? Math.min(liveSize.height, renderedSize.height) / Math.max(liveSize.height, renderedSize.height) : 0;
|
|
33834
|
+
return {
|
|
33835
|
+
score,
|
|
33836
|
+
liveSize,
|
|
33837
|
+
renderedSize,
|
|
33838
|
+
...heightRatio < 0.8 ? { note: `height differs by ${Math.round((1 - heightRatio) * 100)}% \u2014 the bundle reflowed` } : {}
|
|
33839
|
+
};
|
|
33840
|
+
}
|
|
33841
|
+
async function toGreyGrid(image) {
|
|
33842
|
+
const raw = await sharp4(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
|
|
33843
|
+
return new Uint8Array(raw);
|
|
33844
|
+
}
|
|
33845
|
+
|
|
33846
|
+
// src/engine/landing-library/motion.ts
|
|
33847
|
+
var EMPTY_MOTION = {
|
|
33848
|
+
hasMotion: false,
|
|
33849
|
+
summary: "no motion",
|
|
33850
|
+
entrance: [],
|
|
33851
|
+
hover: [],
|
|
33852
|
+
scroll: [],
|
|
33853
|
+
loop: [],
|
|
33854
|
+
libraries: [],
|
|
33855
|
+
respectsReducedMotion: false
|
|
33856
|
+
};
|
|
33857
|
+
function isWorthFilming(motion) {
|
|
33858
|
+
return motion.entrance.length + motion.scroll.length + motion.loop.length > 0;
|
|
33859
|
+
}
|
|
33860
|
+
async function collectMotion(page, selector) {
|
|
33861
|
+
const raw = await page.evaluate(inPageCollectMotion, selector).catch(() => null);
|
|
33862
|
+
if (!raw) return EMPTY_MOTION;
|
|
33863
|
+
return { ...raw, summary: summarizeMotion(raw) };
|
|
33864
|
+
}
|
|
33865
|
+
function summarizeMotion(motion) {
|
|
33866
|
+
const parts = [];
|
|
33867
|
+
const describe = (label, effects) => {
|
|
33868
|
+
if (effects.length === 0) return;
|
|
33869
|
+
const kinds = [...new Set(effects.map((effect) => effect.kind))].slice(0, 3);
|
|
33870
|
+
parts.push(`${label} ${kinds.join("/")}`);
|
|
33871
|
+
};
|
|
33872
|
+
describe("entrance", motion.entrance);
|
|
33873
|
+
describe("hover", motion.hover);
|
|
33874
|
+
describe("scroll", motion.scroll);
|
|
33875
|
+
describe("loop", motion.loop);
|
|
33876
|
+
if (motion.libraries.length > 0) parts.push(`via ${motion.libraries.join("/")}`);
|
|
33877
|
+
if (parts.length === 0) return "no motion";
|
|
33878
|
+
return parts.join(", ") + (motion.respectsReducedMotion ? "" : " (ignores reduced-motion)");
|
|
33879
|
+
}
|
|
33880
|
+
var inPageCollectMotion = (selector) => {
|
|
33881
|
+
const root = document.querySelector(selector);
|
|
33882
|
+
const empty = {
|
|
33883
|
+
hasMotion: false,
|
|
33884
|
+
entrance: [],
|
|
33885
|
+
hover: [],
|
|
33886
|
+
scroll: [],
|
|
33887
|
+
loop: [],
|
|
33888
|
+
libraries: [],
|
|
33889
|
+
respectsReducedMotion: false
|
|
33890
|
+
};
|
|
33891
|
+
if (!root) return empty;
|
|
33892
|
+
const entrance = [];
|
|
33893
|
+
const hover = [];
|
|
33894
|
+
const scroll = [];
|
|
33895
|
+
const loop = [];
|
|
33896
|
+
const keyframesByName = /* @__PURE__ */ new Map();
|
|
33897
|
+
let respectsReducedMotion = false;
|
|
33898
|
+
const inSection = (selectorText) => {
|
|
33899
|
+
for (const part of selectorText.split(",")) {
|
|
33900
|
+
const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
|
|
33901
|
+
if (!base) continue;
|
|
33902
|
+
try {
|
|
33903
|
+
if (root.matches(base) || root.querySelector(base)) return true;
|
|
33904
|
+
} catch {
|
|
33905
|
+
return true;
|
|
33906
|
+
}
|
|
33907
|
+
}
|
|
33908
|
+
return false;
|
|
33909
|
+
};
|
|
33910
|
+
const toMs = (value) => {
|
|
33911
|
+
const first = value.split(",")[0]?.trim() ?? "";
|
|
33912
|
+
if (first.endsWith("ms")) return Number.parseFloat(first);
|
|
33913
|
+
if (first.endsWith("s")) return Number.parseFloat(first) * 1e3;
|
|
33914
|
+
return void 0;
|
|
33915
|
+
};
|
|
33916
|
+
const classifyKeyframes = (body) => {
|
|
33917
|
+
const text = body.toLowerCase();
|
|
33918
|
+
const fades = /opacity\s*:\s*0(\.0+)?\s*[;}]/.test(text);
|
|
33919
|
+
if (/translatey\(\s*-?\d/.test(text)) {
|
|
33920
|
+
const upward = /translatey\(\s*(\d|\.)/.test(text);
|
|
33921
|
+
return fades ? upward ? "fade-up" : "fade-down" : upward ? "slide-up" : "slide-down";
|
|
33922
|
+
}
|
|
33923
|
+
if (/translatex\(\s*-?\d/.test(text)) return fades ? "fade-in-x" : "slide-in-x";
|
|
33924
|
+
if (/scale\(/.test(text)) return fades ? "fade-zoom" : "zoom";
|
|
33925
|
+
if (/rotate\(/.test(text)) return "spin";
|
|
33926
|
+
if (fades) return "fade";
|
|
33927
|
+
return "animate";
|
|
33928
|
+
};
|
|
33929
|
+
const classifyHover = (style) => {
|
|
33930
|
+
const transform = style.transform ?? "";
|
|
33931
|
+
if (/translatey\(\s*-/i.test(transform)) return "lift";
|
|
33932
|
+
if (/scale\(\s*(1\.\d|[2-9])/i.test(transform)) return "grow";
|
|
33933
|
+
if (/rotate\(/i.test(transform)) return "tilt";
|
|
33934
|
+
if (transform && transform !== "none") return "shift";
|
|
33935
|
+
if (style.boxShadow) return "shadow";
|
|
33936
|
+
if (style.opacity) return "dim";
|
|
33937
|
+
if (style.backgroundColor || style.color) return "recolor";
|
|
33938
|
+
return null;
|
|
33939
|
+
};
|
|
33940
|
+
const readStyleRule = (rule, insideScrollTimeline) => {
|
|
33941
|
+
const style = rule.style;
|
|
33942
|
+
const isHover = /:hover\b/.test(rule.selectorText);
|
|
33943
|
+
if (isHover) {
|
|
33944
|
+
const kind = classifyHover(style);
|
|
33945
|
+
if (!kind || !inSection(rule.selectorText)) return;
|
|
33946
|
+
hover.push({
|
|
33947
|
+
kind,
|
|
33948
|
+
selector: rule.selectorText.slice(0, 120),
|
|
33949
|
+
durationMs: toMs(style.transitionDuration ?? ""),
|
|
33950
|
+
easing: style.transitionTimingFunction || void 0
|
|
33951
|
+
});
|
|
33952
|
+
return;
|
|
33953
|
+
}
|
|
33954
|
+
const animationName = style.animationName;
|
|
33955
|
+
if (!animationName || animationName === "none") return;
|
|
33956
|
+
if (!inSection(rule.selectorText)) return;
|
|
33957
|
+
const effect = {
|
|
33958
|
+
kind: animationName,
|
|
33959
|
+
selector: rule.selectorText.slice(0, 120),
|
|
33960
|
+
durationMs: toMs(style.animationDuration ?? ""),
|
|
33961
|
+
delayMs: toMs(style.animationDelay ?? ""),
|
|
33962
|
+
easing: style.animationTimingFunction || void 0
|
|
33963
|
+
};
|
|
33964
|
+
const infinite = (style.animationIterationCount ?? "").includes("infinite");
|
|
33965
|
+
const scrollDriven = insideScrollTimeline || Boolean(style.getPropertyValue("animation-timeline"));
|
|
33966
|
+
if (scrollDriven) scroll.push(effect);
|
|
33967
|
+
else if (infinite) loop.push(effect);
|
|
33968
|
+
else entrance.push(effect);
|
|
33969
|
+
};
|
|
33970
|
+
const walk = (rules, insideScrollTimeline) => {
|
|
33971
|
+
for (const rule of Array.from(rules)) {
|
|
33972
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
33973
|
+
keyframesByName.set(rule.name, rule.cssText);
|
|
33974
|
+
continue;
|
|
33975
|
+
}
|
|
33976
|
+
if (rule instanceof CSSStyleRule) {
|
|
33977
|
+
try {
|
|
33978
|
+
readStyleRule(rule, insideScrollTimeline);
|
|
33979
|
+
} catch {
|
|
33980
|
+
}
|
|
33981
|
+
continue;
|
|
33982
|
+
}
|
|
33983
|
+
if (rule instanceof CSSMediaRule) {
|
|
33984
|
+
if (rule.conditionText.includes("prefers-reduced-motion")) {
|
|
33985
|
+
respectsReducedMotion = true;
|
|
33986
|
+
continue;
|
|
33987
|
+
}
|
|
33988
|
+
walk(rule.cssRules, insideScrollTimeline);
|
|
33989
|
+
continue;
|
|
33990
|
+
}
|
|
33991
|
+
const grouping = rule;
|
|
33992
|
+
if (grouping.cssRules) walk(grouping.cssRules, insideScrollTimeline);
|
|
33993
|
+
}
|
|
33994
|
+
};
|
|
33995
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
33996
|
+
if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
|
|
33997
|
+
try {
|
|
33998
|
+
walk(sheet.cssRules, false);
|
|
33999
|
+
} catch {
|
|
34000
|
+
}
|
|
34001
|
+
}
|
|
34002
|
+
for (const effect of [...entrance, ...loop, ...scroll]) {
|
|
34003
|
+
const body = keyframesByName.get(effect.kind);
|
|
34004
|
+
if (!body) continue;
|
|
34005
|
+
const classified = classifyKeyframes(body);
|
|
34006
|
+
effect.kind = loop.includes(effect) && classified.startsWith("slide") ? "marquee" : classified;
|
|
34007
|
+
}
|
|
34008
|
+
const libraries = [];
|
|
34009
|
+
const scoped = window ?? {};
|
|
34010
|
+
if (scoped.gsap || document.querySelector("[data-gsap]")) libraries.push("gsap");
|
|
34011
|
+
if (document.querySelector("[data-framer-name], [data-projection-id]")) libraries.push("framer-motion");
|
|
34012
|
+
if (document.querySelector("[data-aos]")) libraries.push("aos");
|
|
34013
|
+
if (scoped.Lenis || document.querySelector("[data-lenis]")) libraries.push("lenis");
|
|
34014
|
+
if (document.querySelector("[data-scroll], [data-scroll-container]")) libraries.push("locomotive");
|
|
34015
|
+
return {
|
|
34016
|
+
hasMotion: entrance.length + hover.length + scroll.length + loop.length + libraries.length > 0,
|
|
34017
|
+
entrance: entrance.slice(0, 12),
|
|
34018
|
+
hover: hover.slice(0, 12),
|
|
34019
|
+
scroll: scroll.slice(0, 12),
|
|
34020
|
+
loop: loop.slice(0, 12),
|
|
34021
|
+
libraries,
|
|
34022
|
+
respectsReducedMotion
|
|
34023
|
+
};
|
|
34024
|
+
};
|
|
34025
|
+
|
|
34026
|
+
// src/engine/landing-library/motionTake.ts
|
|
34027
|
+
import sharp5 from "sharp";
|
|
34028
|
+
|
|
34029
|
+
// src/engine/landing-library/prepare.ts
|
|
34030
|
+
var CONSENT_SELECTORS = [
|
|
34031
|
+
"#onetrust-accept-btn-handler",
|
|
34032
|
+
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
|
|
34033
|
+
"button#didomi-notice-agree-button",
|
|
34034
|
+
"[aria-label='Accept all']",
|
|
34035
|
+
"[data-testid='uc-accept-all-button']",
|
|
34036
|
+
".cc-allow",
|
|
34037
|
+
".cookie-accept"
|
|
34038
|
+
];
|
|
34039
|
+
var CONSENT_TEXTS = ["Accept all", "Accept All", "Allow all", "I agree", "Got it", "Aceptar todo"];
|
|
34040
|
+
var CONSENT_HOSTS = [
|
|
34041
|
+
"transcend-cdn.com",
|
|
34042
|
+
"cookielaw.org",
|
|
34043
|
+
"onetrust.com",
|
|
34044
|
+
"cookiebot.com",
|
|
34045
|
+
"osano.com",
|
|
34046
|
+
"trustarc.com",
|
|
34047
|
+
"truste.com",
|
|
34048
|
+
"usercentrics.eu",
|
|
34049
|
+
"didomi.io",
|
|
34050
|
+
"privacy-center.org",
|
|
34051
|
+
"iubenda.com",
|
|
34052
|
+
"termly.io",
|
|
34053
|
+
"cookieyes.com",
|
|
34054
|
+
"sp-prod.net",
|
|
34055
|
+
"quantcast.com",
|
|
34056
|
+
"consensu.org",
|
|
34057
|
+
"ketch.com",
|
|
34058
|
+
"secureprivacy.ai",
|
|
34059
|
+
"civicuk.com"
|
|
34060
|
+
];
|
|
34061
|
+
async function blockConsentManagers(page) {
|
|
34062
|
+
await page.route("**/*", (route) => {
|
|
34063
|
+
let host = "";
|
|
34064
|
+
try {
|
|
34065
|
+
host = new URL(route.request().url()).host;
|
|
34066
|
+
} catch {
|
|
34067
|
+
return route.continue();
|
|
34068
|
+
}
|
|
34069
|
+
const isConsentVendor = CONSENT_HOSTS.some((vendor) => host === vendor || host.endsWith(`.${vendor}`));
|
|
34070
|
+
return isConsentVendor ? route.abort() : route.continue();
|
|
34071
|
+
});
|
|
34072
|
+
}
|
|
34073
|
+
var ignore = () => void 0;
|
|
34074
|
+
async function preparePage(page, url, timeoutMs) {
|
|
34075
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
|
|
34076
|
+
const status = response?.status() ?? null;
|
|
34077
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(ignore);
|
|
34078
|
+
await dismissConsent(page, 4e3);
|
|
34079
|
+
await scrollThroughPage(page);
|
|
34080
|
+
await dismissConsent(page, 1e3);
|
|
34081
|
+
await page.evaluate(async () => {
|
|
34082
|
+
await document.fonts.ready;
|
|
34083
|
+
});
|
|
34084
|
+
await freezeMotion(page);
|
|
34085
|
+
await unpinOverlays(page);
|
|
34086
|
+
const measured = await page.evaluate(() => ({
|
|
34087
|
+
finalUrl: location.href,
|
|
34088
|
+
title: document.title,
|
|
34089
|
+
documentHeight: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
34090
|
+
bodyText: (document.body?.innerText ?? "").slice(0, 2e3)
|
|
34091
|
+
}));
|
|
34092
|
+
return { ...measured, status };
|
|
34093
|
+
}
|
|
34094
|
+
async function settleConsent(page) {
|
|
34095
|
+
await dismissConsent(page, 4e3);
|
|
34096
|
+
await page.mouse.wheel(0, 400).catch(ignore);
|
|
34097
|
+
await page.waitForTimeout(1500);
|
|
34098
|
+
await dismissConsent(page, 2e3);
|
|
34099
|
+
await page.evaluate(() => window.scrollTo(0, 0)).catch(ignore);
|
|
34100
|
+
}
|
|
34101
|
+
async function dismissConsent(page, waitForBannerMs) {
|
|
34102
|
+
await page.locator(CONSENT_SELECTORS.join(", ")).first().waitFor({ state: "attached", timeout: waitForBannerMs }).catch(ignore);
|
|
34103
|
+
for (const selector of CONSENT_SELECTORS) {
|
|
34104
|
+
const found = page.locator(selector).first();
|
|
34105
|
+
if (!await found.count().catch(() => 0)) continue;
|
|
34106
|
+
await found.click({ timeout: 2e3, force: true }).catch(ignore);
|
|
34107
|
+
await page.waitForTimeout(400);
|
|
34108
|
+
return;
|
|
34109
|
+
}
|
|
34110
|
+
for (const text of CONSENT_TEXTS) {
|
|
34111
|
+
const button = page.getByRole("button", { name: text, exact: false }).first();
|
|
34112
|
+
if (!await button.count().catch(() => 0)) continue;
|
|
34113
|
+
if (!await button.isVisible().catch(() => false)) continue;
|
|
34114
|
+
await button.click({ timeout: 2e3, force: true }).catch(ignore);
|
|
34115
|
+
await page.waitForTimeout(400);
|
|
34116
|
+
return;
|
|
34117
|
+
}
|
|
34118
|
+
}
|
|
34119
|
+
async function scrollThroughPage(page) {
|
|
34120
|
+
await page.evaluate(async () => {
|
|
34121
|
+
const step = 600;
|
|
34122
|
+
const pause = () => new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
34123
|
+
for (let i = 0; i < 40; i++) {
|
|
34124
|
+
window.scrollBy(0, step);
|
|
34125
|
+
await pause();
|
|
34126
|
+
const reachedBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
34127
|
+
if (reachedBottom) break;
|
|
34128
|
+
}
|
|
34129
|
+
window.scrollTo(0, 0);
|
|
34130
|
+
await pause();
|
|
34131
|
+
});
|
|
34132
|
+
await page.waitForTimeout(500);
|
|
34133
|
+
}
|
|
34134
|
+
var MAX_HEADER_HEIGHT = 220;
|
|
34135
|
+
async function unpinOverlays(page) {
|
|
34136
|
+
await page.evaluate((maxHeaderHeight) => {
|
|
34137
|
+
window.scrollTo(0, 0);
|
|
34138
|
+
for (const element of Array.from(document.querySelectorAll("*"))) {
|
|
34139
|
+
const position = getComputedStyle(element).position;
|
|
34140
|
+
if (position === "sticky") {
|
|
34141
|
+
element.style.setProperty("position", "static", "important");
|
|
34142
|
+
continue;
|
|
34143
|
+
}
|
|
34144
|
+
if (position !== "fixed") continue;
|
|
34145
|
+
const rect = element.getBoundingClientRect();
|
|
34146
|
+
const isTopAnchoredHeader = rect.top <= 8 && rect.height > 0 && rect.height <= maxHeaderHeight;
|
|
34147
|
+
if (isTopAnchoredHeader) {
|
|
34148
|
+
element.style.setProperty("position", "absolute", "important");
|
|
34149
|
+
element.style.setProperty("bottom", "auto", "important");
|
|
34150
|
+
} else {
|
|
34151
|
+
element.style.setProperty("display", "none", "important");
|
|
34152
|
+
}
|
|
34153
|
+
}
|
|
34154
|
+
}, MAX_HEADER_HEIGHT);
|
|
34155
|
+
await page.waitForTimeout(250);
|
|
34156
|
+
}
|
|
34157
|
+
async function freezeMotion(page) {
|
|
34158
|
+
await page.evaluate(() => {
|
|
34159
|
+
const highestTimer = window.setTimeout(() => void 0, 0);
|
|
34160
|
+
for (let id = 1; id <= highestTimer; id++) window.clearInterval(id);
|
|
34161
|
+
const style = document.createElement("style");
|
|
34162
|
+
style.setAttribute("data-baker-freeze", "true");
|
|
34163
|
+
style.textContent = `*, *::before, *::after {
|
|
34164
|
+
animation-play-state: paused !important;
|
|
34165
|
+
animation-delay: 0s !important;
|
|
34166
|
+
transition: none !important;
|
|
34167
|
+
}`;
|
|
34168
|
+
document.head.appendChild(style);
|
|
34169
|
+
for (const video of Array.from(document.querySelectorAll("video"))) {
|
|
34170
|
+
video.pause();
|
|
34171
|
+
}
|
|
34172
|
+
});
|
|
34173
|
+
await page.waitForTimeout(250);
|
|
34174
|
+
}
|
|
34175
|
+
|
|
34176
|
+
// src/engine/landing-library/geometry.ts
|
|
34177
|
+
function adaptiveGapThreshold(bands) {
|
|
34178
|
+
const sorted = [...bands].sort((a, b) => a.top - b.top);
|
|
34179
|
+
const gaps = [];
|
|
34180
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
34181
|
+
const current = sorted[i];
|
|
34182
|
+
const next = sorted[i + 1];
|
|
34183
|
+
if (!current || !next) continue;
|
|
34184
|
+
if (next.top > current.bottom) {
|
|
34185
|
+
const gap = next.top - current.bottom;
|
|
34186
|
+
if (gap < 200) gaps.push(gap);
|
|
34187
|
+
}
|
|
34188
|
+
}
|
|
34189
|
+
gaps.sort((a, b) => a - b);
|
|
34190
|
+
const p75 = gaps[Math.floor(gaps.length * 0.75)];
|
|
34191
|
+
if (p75 === void 0) return 15;
|
|
34192
|
+
return Math.max(10, Math.min(50, p75));
|
|
34193
|
+
}
|
|
34194
|
+
function bandsCollide(a, b, margin, maxGap) {
|
|
34195
|
+
const shrunkA = { top: a.top + margin, bottom: a.bottom - margin };
|
|
34196
|
+
const shrunkB = { top: b.top + margin, bottom: b.bottom - margin };
|
|
34197
|
+
const overlaps = shrunkA.top <= shrunkB.bottom && shrunkA.bottom >= shrunkB.top || shrunkB.top <= shrunkA.bottom && shrunkB.bottom >= shrunkA.top;
|
|
34198
|
+
if (overlaps) return true;
|
|
34199
|
+
const gap = Math.min(Math.abs(shrunkA.bottom - shrunkB.top), Math.abs(shrunkB.bottom - shrunkA.top));
|
|
34200
|
+
return gap <= maxGap;
|
|
34201
|
+
}
|
|
34202
|
+
function shouldPromoteToParent(group) {
|
|
34203
|
+
const isRunt = group.childCount < 3 || group.height < 80;
|
|
34204
|
+
return isRunt && group.parentHeight < 1600;
|
|
34205
|
+
}
|
|
34206
|
+
|
|
34207
|
+
// src/engine/landing-library/segment.ts
|
|
34208
|
+
async function installPageRuntime(page) {
|
|
34209
|
+
await page.addInitScript({
|
|
34210
|
+
content: `
|
|
34211
|
+
window.__name = window.__name || function (target) { return target; };
|
|
34212
|
+
window.__bakerGeom = {
|
|
34213
|
+
adaptiveGapThreshold: ${adaptiveGapThreshold.toString()},
|
|
34214
|
+
bandsCollide: ${bandsCollide.toString()},
|
|
34215
|
+
shouldPromoteToParent: ${shouldPromoteToParent.toString()},
|
|
34216
|
+
};`
|
|
34217
|
+
});
|
|
34218
|
+
}
|
|
34219
|
+
async function segmentPage(page, options) {
|
|
34220
|
+
return await page.evaluate(inPageSegment, options);
|
|
34221
|
+
}
|
|
34222
|
+
var inPageSegment = (options) => {
|
|
34223
|
+
const geom = window.__bakerGeom;
|
|
34224
|
+
const scrollX = window.scrollX;
|
|
34225
|
+
const scrollY = window.scrollY;
|
|
34226
|
+
const docWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0);
|
|
34227
|
+
const docHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0);
|
|
34228
|
+
const rectOf = (element) => {
|
|
34229
|
+
const r = element.getBoundingClientRect();
|
|
34230
|
+
const top = Math.max(0, Math.round(r.top + scrollY));
|
|
34231
|
+
const left = Math.max(0, Math.round(r.left + scrollX));
|
|
34232
|
+
return {
|
|
34233
|
+
top,
|
|
34234
|
+
left,
|
|
34235
|
+
bottom: Math.min(docHeight, Math.round(r.bottom + scrollY)),
|
|
34236
|
+
right: Math.min(docWidth, Math.round(r.right + scrollX))
|
|
34237
|
+
};
|
|
34238
|
+
};
|
|
34239
|
+
const syntheticRect = (element) => {
|
|
34240
|
+
const children = Array.from(element.children);
|
|
34241
|
+
if (children.length === 0) return null;
|
|
34242
|
+
let top = Number.POSITIVE_INFINITY;
|
|
34243
|
+
let left = Number.POSITIVE_INFINITY;
|
|
34244
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
34245
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
34246
|
+
for (const child of children) {
|
|
34247
|
+
const r = rectOf(child);
|
|
34248
|
+
if (r.bottom - r.top <= 0 && r.right - r.left <= 0) continue;
|
|
34249
|
+
top = Math.min(top, r.top);
|
|
34250
|
+
left = Math.min(left, r.left);
|
|
34251
|
+
bottom = Math.max(bottom, r.bottom);
|
|
34252
|
+
right = Math.max(right, r.right);
|
|
34253
|
+
}
|
|
34254
|
+
if (!Number.isFinite(top) || !Number.isFinite(bottom)) return null;
|
|
34255
|
+
return { top, left, bottom, right };
|
|
34256
|
+
};
|
|
34257
|
+
const boxOf = (element) => {
|
|
34258
|
+
const style = getComputedStyle(element);
|
|
34259
|
+
const r = style.display === "contents" ? syntheticRect(element) : rectOf(element);
|
|
34260
|
+
if (!r) return null;
|
|
34261
|
+
return { ...r, height: r.bottom - r.top, width: r.right - r.left };
|
|
34262
|
+
};
|
|
34263
|
+
const hasHiddenAncestor = (element) => {
|
|
34264
|
+
let current = element;
|
|
34265
|
+
while (current && current !== document.documentElement) {
|
|
34266
|
+
const style = getComputedStyle(current);
|
|
34267
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return true;
|
|
34268
|
+
current = current.parentElement;
|
|
34269
|
+
}
|
|
34270
|
+
return false;
|
|
34271
|
+
};
|
|
34272
|
+
const isClippedByAncestor = (element, box) => {
|
|
34273
|
+
let parent = element.parentElement;
|
|
34274
|
+
while (parent && parent !== document.documentElement) {
|
|
34275
|
+
const style = getComputedStyle(parent);
|
|
34276
|
+
const clips = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden" || style.overflow === "clip";
|
|
34277
|
+
if (clips) {
|
|
34278
|
+
const p = rectOf(parent);
|
|
34279
|
+
const intersects = box.left < p.right && box.right > p.left && box.top < p.bottom && box.bottom > p.top;
|
|
34280
|
+
if (!intersects) return true;
|
|
34281
|
+
}
|
|
34282
|
+
parent = parent.parentElement;
|
|
34283
|
+
}
|
|
34284
|
+
return false;
|
|
34285
|
+
};
|
|
34286
|
+
const directText = (element) => {
|
|
34287
|
+
let text = "";
|
|
34288
|
+
for (const node of Array.from(element.childNodes)) {
|
|
34289
|
+
if (node.nodeType === 3) text += node.textContent ?? "";
|
|
34290
|
+
}
|
|
34291
|
+
return text.trim();
|
|
34292
|
+
};
|
|
34293
|
+
const NON_COPY_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
|
|
34294
|
+
const visibleText = (root) => {
|
|
34295
|
+
let text = "";
|
|
34296
|
+
const hiddenCache = /* @__PURE__ */ new Map();
|
|
34297
|
+
const isHidden = (element) => {
|
|
34298
|
+
const cached = hiddenCache.get(element);
|
|
34299
|
+
if (cached !== void 0) return cached;
|
|
34300
|
+
const style = getComputedStyle(element);
|
|
34301
|
+
const hidden = style.display === "none" || style.visibility === "hidden";
|
|
34302
|
+
hiddenCache.set(element, hidden);
|
|
34303
|
+
return hidden;
|
|
34304
|
+
};
|
|
34305
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
34306
|
+
acceptNode: (node) => {
|
|
34307
|
+
let parent = node.parentElement;
|
|
34308
|
+
while (parent) {
|
|
34309
|
+
if (NON_COPY_TAGS.has(parent.tagName) || isHidden(parent)) return NodeFilter.FILTER_REJECT;
|
|
34310
|
+
if (parent === root) break;
|
|
34311
|
+
parent = parent.parentElement;
|
|
34312
|
+
}
|
|
34313
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
34314
|
+
}
|
|
34315
|
+
});
|
|
34316
|
+
while (walker.nextNode() && text.length < 400) {
|
|
34317
|
+
text += ` ${walker.currentNode.textContent ?? ""}`;
|
|
34318
|
+
}
|
|
34319
|
+
return text.replace(/\s+/g, " ").trim();
|
|
34320
|
+
};
|
|
34321
|
+
const GRAPHIC_TAGS = /* @__PURE__ */ new Set(["img", "svg", "video", "picture", "canvas", "iframe"]);
|
|
34322
|
+
const hasBackgroundImage = (element) => {
|
|
34323
|
+
const bg = getComputedStyle(element).backgroundImage;
|
|
34324
|
+
return Boolean(bg) && bg !== "none" && bg.includes("url(");
|
|
34325
|
+
};
|
|
34326
|
+
const isContentLeaf = (element) => {
|
|
34327
|
+
const tag = element.tagName.toLowerCase();
|
|
34328
|
+
if (GRAPHIC_TAGS.has(tag)) return true;
|
|
34329
|
+
if (directText(element).length > 0) return true;
|
|
34330
|
+
return hasBackgroundImage(element);
|
|
34331
|
+
};
|
|
34332
|
+
const selectorFor = (element) => {
|
|
34333
|
+
const parts = [];
|
|
34334
|
+
let current = element;
|
|
34335
|
+
while (current && current !== document.documentElement) {
|
|
34336
|
+
const tag = current.tagName.toLowerCase();
|
|
34337
|
+
if (tag === "body") {
|
|
34338
|
+
parts.unshift("body");
|
|
34339
|
+
break;
|
|
34340
|
+
}
|
|
34341
|
+
const parent = current.parentElement;
|
|
34342
|
+
if (!parent) {
|
|
34343
|
+
parts.unshift(tag);
|
|
34344
|
+
break;
|
|
34345
|
+
}
|
|
34346
|
+
const sameTag = Array.from(parent.children).filter((c) => c.tagName === current?.tagName);
|
|
34347
|
+
const position = sameTag.indexOf(current) + 1;
|
|
34348
|
+
parts.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${position})` : tag);
|
|
34349
|
+
current = parent;
|
|
34350
|
+
}
|
|
34351
|
+
return parts.join(" > ");
|
|
34352
|
+
};
|
|
34353
|
+
const leaves = [];
|
|
34354
|
+
const all = document.querySelectorAll("*");
|
|
34355
|
+
const limit = Math.min(all.length, options.maxElements);
|
|
34356
|
+
for (let i = 0; i < limit; i++) {
|
|
34357
|
+
const element = all[i];
|
|
34358
|
+
if (!element) continue;
|
|
34359
|
+
const tag = element.tagName.toLowerCase();
|
|
34360
|
+
if (tag === "script" || tag === "style" || tag === "noscript" || tag === "link" || tag === "head") continue;
|
|
34361
|
+
if (!isContentLeaf(element)) continue;
|
|
34362
|
+
const box = boxOf(element);
|
|
34363
|
+
if (!box || box.height <= 0 || box.width <= 0) continue;
|
|
34364
|
+
if (box.height > options.maxSectionHeight) continue;
|
|
34365
|
+
if (hasHiddenAncestor(element)) continue;
|
|
34366
|
+
if (isClippedByAncestor(element, box)) continue;
|
|
34367
|
+
leaves.push({
|
|
34368
|
+
element,
|
|
34369
|
+
top: box.top,
|
|
34370
|
+
bottom: box.bottom,
|
|
34371
|
+
left: box.left,
|
|
34372
|
+
right: box.right,
|
|
34373
|
+
height: box.height
|
|
34374
|
+
});
|
|
34375
|
+
}
|
|
34376
|
+
if (leaves.length === 0) return [];
|
|
34377
|
+
const gapThreshold = geom.adaptiveGapThreshold(leaves.map((l) => ({ top: l.top, bottom: l.bottom })));
|
|
34378
|
+
const commonAncestor = (elements) => {
|
|
34379
|
+
let ancestor = elements[0] ?? null;
|
|
34380
|
+
while (ancestor && !elements.every((e) => ancestor?.contains(e))) {
|
|
34381
|
+
ancestor = ancestor.parentElement;
|
|
34382
|
+
}
|
|
34383
|
+
return ancestor;
|
|
34384
|
+
};
|
|
34385
|
+
const abandon = (members) => members.map((m) => ({ ...m, sealed: true }));
|
|
34386
|
+
const findCoveringAncestor = (start, bounds) => {
|
|
34387
|
+
let element = start;
|
|
34388
|
+
while (element) {
|
|
34389
|
+
const box = boxOf(element);
|
|
34390
|
+
if (!box) return null;
|
|
34391
|
+
if (box.height > options.maxSectionHeight) return null;
|
|
34392
|
+
const covers = box.top <= bounds.top && box.bottom >= bounds.bottom && box.left <= bounds.left && box.right >= bounds.right;
|
|
34393
|
+
if (covers) return { element, box };
|
|
34394
|
+
if (!element.parentElement || element.parentElement === document.documentElement) return null;
|
|
34395
|
+
element = element.parentElement;
|
|
34396
|
+
}
|
|
34397
|
+
return null;
|
|
34398
|
+
};
|
|
34399
|
+
const mergeBucket = (members) => {
|
|
34400
|
+
const bounds = {
|
|
34401
|
+
top: Math.min(...members.map((m) => m.top)),
|
|
34402
|
+
bottom: Math.max(...members.map((m) => m.bottom)),
|
|
34403
|
+
left: Math.min(...members.map((m) => m.left)),
|
|
34404
|
+
right: Math.max(...members.map((m) => m.right))
|
|
34405
|
+
};
|
|
34406
|
+
const covering = findCoveringAncestor(commonAncestor(members.map((m) => m.root)), bounds);
|
|
34407
|
+
if (!covering) return abandon(members);
|
|
34408
|
+
return [
|
|
34409
|
+
{
|
|
34410
|
+
root: covering.element,
|
|
34411
|
+
top: covering.box.top,
|
|
34412
|
+
bottom: covering.box.bottom,
|
|
34413
|
+
left: covering.box.left,
|
|
34414
|
+
right: covering.box.right,
|
|
34415
|
+
height: covering.box.height,
|
|
34416
|
+
leaves: members.flatMap((m) => m.leaves),
|
|
34417
|
+
sealed: false
|
|
34418
|
+
}
|
|
34419
|
+
];
|
|
34420
|
+
};
|
|
34421
|
+
const clusterOnce = (groups2) => {
|
|
34422
|
+
const buckets = [];
|
|
34423
|
+
for (const group of groups2) {
|
|
34424
|
+
if (group.sealed) {
|
|
34425
|
+
buckets.push([group]);
|
|
34426
|
+
continue;
|
|
34427
|
+
}
|
|
34428
|
+
const target = buckets.find(
|
|
34429
|
+
(bucket) => bucket.some(
|
|
34430
|
+
(member) => !member.sealed && geom.bandsCollide(
|
|
34431
|
+
{ top: member.top, bottom: member.bottom },
|
|
34432
|
+
{ top: group.top, bottom: group.bottom },
|
|
34433
|
+
options.collisionMargin,
|
|
34434
|
+
gapThreshold
|
|
34435
|
+
)
|
|
34436
|
+
)
|
|
34437
|
+
);
|
|
34438
|
+
if (target) target.push(group);
|
|
34439
|
+
else buckets.push([group]);
|
|
34440
|
+
}
|
|
34441
|
+
if (buckets.length === groups2.length) return groups2;
|
|
34442
|
+
return buckets.flatMap((bucket) => bucket.length === 1 ? [bucket[0]] : mergeBucket(bucket));
|
|
34443
|
+
};
|
|
34444
|
+
let groups = leaves.map((leaf) => ({
|
|
34445
|
+
root: leaf.element,
|
|
34446
|
+
top: leaf.top,
|
|
34447
|
+
bottom: leaf.bottom,
|
|
34448
|
+
left: leaf.left,
|
|
34449
|
+
right: leaf.right,
|
|
34450
|
+
height: leaf.height,
|
|
34451
|
+
leaves: [leaf],
|
|
34452
|
+
sealed: false
|
|
34453
|
+
}));
|
|
34454
|
+
for (let pass = 0; pass < 40; pass++) {
|
|
34455
|
+
const next = clusterOnce(groups);
|
|
34456
|
+
if (next.length === groups.length) break;
|
|
34457
|
+
groups = next;
|
|
34458
|
+
}
|
|
34459
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
34460
|
+
let promoted = false;
|
|
34461
|
+
groups = groups.map((group) => {
|
|
34462
|
+
const parent = group.root.parentElement;
|
|
34463
|
+
if (!parent || parent === document.documentElement || parent === document.body) return group;
|
|
34464
|
+
const parentBox = boxOf(parent);
|
|
34465
|
+
if (!parentBox) return group;
|
|
34466
|
+
if (!geom.shouldPromoteToParent({
|
|
34467
|
+
childCount: group.leaves.length,
|
|
34468
|
+
height: group.height,
|
|
34469
|
+
parentHeight: parentBox.height
|
|
34470
|
+
})) {
|
|
34471
|
+
return group;
|
|
34472
|
+
}
|
|
34473
|
+
promoted = true;
|
|
34474
|
+
return {
|
|
34475
|
+
...group,
|
|
34476
|
+
root: parent,
|
|
34477
|
+
top: parentBox.top,
|
|
34478
|
+
bottom: parentBox.bottom,
|
|
34479
|
+
left: parentBox.left,
|
|
34480
|
+
right: parentBox.right,
|
|
34481
|
+
height: parentBox.height
|
|
34482
|
+
};
|
|
34483
|
+
});
|
|
34484
|
+
if (!promoted) break;
|
|
34485
|
+
groups = clusterOnce(groups);
|
|
34486
|
+
}
|
|
34487
|
+
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;
|
|
34488
|
+
const containsBox = (outer, inner) => outer.top - 4 <= inner.top && outer.bottom + 4 >= inner.bottom && outer.left - 4 <= inner.left && outer.right + 4 >= inner.right;
|
|
34489
|
+
const deduped = [];
|
|
34490
|
+
const area = (g) => (g.right - g.left) * g.height;
|
|
34491
|
+
for (const group of groups.slice().sort((a, b) => area(b) - area(a) || b.leaves.length - a.leaves.length)) {
|
|
34492
|
+
const duplicate = deduped.some(
|
|
34493
|
+
(kept) => kept.root === group.root || kept.root.contains(group.root) || sameBox(kept, group) || containsBox(kept, group)
|
|
34494
|
+
);
|
|
34495
|
+
if (!duplicate) deduped.push(group);
|
|
34496
|
+
}
|
|
34497
|
+
return deduped.filter((group) => group.height > 0 && group.right - group.left > 0).sort((a, b) => a.top - b.top).map((group, index) => ({
|
|
34498
|
+
index,
|
|
34499
|
+
selector: selectorFor(group.root),
|
|
34500
|
+
rect: {
|
|
34501
|
+
top: group.top,
|
|
34502
|
+
left: group.left,
|
|
34503
|
+
width: group.right - group.left,
|
|
34504
|
+
height: group.height
|
|
34505
|
+
},
|
|
34506
|
+
leafCount: group.leaves.length,
|
|
34507
|
+
textPreview: visibleText(group.root).slice(0, 200)
|
|
34508
|
+
}));
|
|
34509
|
+
};
|
|
34510
|
+
|
|
34511
|
+
// src/engine/landing-library/motionTake.ts
|
|
34512
|
+
var FRAME_TIMES_MS = [0, 120, 260, 450, 800, 1400];
|
|
34513
|
+
var FRAME_WIDTH = 460;
|
|
34514
|
+
var GRID_COLUMNS = 3;
|
|
34515
|
+
var LABEL_HEIGHT = 22;
|
|
34516
|
+
async function captureMotionTake(browser, url, selector, timeoutMs = 45e3) {
|
|
34517
|
+
const { context, page } = await newPage(browser, DESKTOP_VIEWPORT, { motion: true });
|
|
34518
|
+
try {
|
|
34519
|
+
await blockConsentManagers(page);
|
|
34520
|
+
await installPageRuntime(page);
|
|
34521
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
|
|
34522
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => void 0);
|
|
34523
|
+
await settleConsent(page);
|
|
34524
|
+
const target = page.locator(selector).first();
|
|
34525
|
+
if (!await target.count().catch(() => 0)) return null;
|
|
34526
|
+
await page.evaluate((sectionSelector) => {
|
|
34527
|
+
const element = document.querySelector(sectionSelector);
|
|
34528
|
+
if (!element) return;
|
|
34529
|
+
const top = element.getBoundingClientRect().top + window.scrollY;
|
|
34530
|
+
window.scrollTo(0, Math.max(0, top - window.innerHeight - 200));
|
|
34531
|
+
}, selector);
|
|
34532
|
+
await page.waitForTimeout(600);
|
|
34533
|
+
await page.evaluate((sectionSelector) => {
|
|
34534
|
+
document.querySelector(sectionSelector)?.scrollIntoView({ block: "center" });
|
|
34535
|
+
}, selector);
|
|
34536
|
+
const frames = [];
|
|
34537
|
+
let previous = 0;
|
|
34538
|
+
for (const time of FRAME_TIMES_MS) {
|
|
34539
|
+
await page.waitForTimeout(Math.max(0, time - previous));
|
|
34540
|
+
previous = time;
|
|
34541
|
+
const shot = await page.screenshot({ type: "png", timeout: 1e4 }).catch(() => null);
|
|
34542
|
+
if (shot) frames.push(shot);
|
|
34543
|
+
}
|
|
34544
|
+
if (frames.length === 0) return null;
|
|
34545
|
+
return { filmstrip: await composeFilmstrip(frames), frameCount: frames.length };
|
|
34546
|
+
} catch {
|
|
34547
|
+
return null;
|
|
34548
|
+
} finally {
|
|
34549
|
+
await context.close();
|
|
34550
|
+
}
|
|
34551
|
+
}
|
|
34552
|
+
async function composeFilmstrip(frames) {
|
|
34553
|
+
const scaled = await Promise.all(frames.map((frame) => sharp5(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
|
|
34554
|
+
const first = await sharp5(scaled[0]).metadata();
|
|
34555
|
+
const frameHeight = first.height ?? 300;
|
|
34556
|
+
const cellHeight = frameHeight + LABEL_HEIGHT;
|
|
34557
|
+
const rows = Math.ceil(scaled.length / GRID_COLUMNS);
|
|
34558
|
+
const width = FRAME_WIDTH * Math.min(GRID_COLUMNS, scaled.length);
|
|
34559
|
+
const composites = scaled.flatMap((frame, index) => {
|
|
34560
|
+
const column = index % GRID_COLUMNS;
|
|
34561
|
+
const row = Math.floor(index / GRID_COLUMNS);
|
|
34562
|
+
const label = Buffer.from(
|
|
34563
|
+
`<svg width="${FRAME_WIDTH}" height="${LABEL_HEIGHT}">
|
|
34564
|
+
<rect width="100%" height="100%" fill="#111827"/>
|
|
34565
|
+
<text x="8" y="15" font-family="monospace" font-size="12" fill="#f9fafb">+${FRAME_TIMES_MS[index] ?? 0}ms</text>
|
|
34566
|
+
</svg>`
|
|
34567
|
+
);
|
|
34568
|
+
return [
|
|
34569
|
+
{ input: label, left: column * FRAME_WIDTH, top: row * cellHeight },
|
|
34570
|
+
{ input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
|
|
34571
|
+
];
|
|
34572
|
+
});
|
|
34573
|
+
return await sharp5({
|
|
34574
|
+
create: {
|
|
34575
|
+
width,
|
|
34576
|
+
height: cellHeight * rows,
|
|
34577
|
+
channels: 3,
|
|
34578
|
+
background: { r: 17, g: 24, b: 39 }
|
|
34579
|
+
}
|
|
34580
|
+
}).composite(composites).png().toBuffer();
|
|
34581
|
+
}
|
|
34582
|
+
|
|
34583
|
+
// src/engine/landing-library/renderBundle.ts
|
|
34584
|
+
var SETTLE_ANIMATIONS_CSS = `*, *::before, *::after {
|
|
34585
|
+
animation-play-state: running !important;
|
|
34586
|
+
animation-delay: 0s !important;
|
|
34587
|
+
animation-duration: 1ms !important;
|
|
34588
|
+
animation-iteration-count: 1 !important;
|
|
34589
|
+
animation-fill-mode: forwards !important;
|
|
34590
|
+
transition: none !important;
|
|
34591
|
+
}`;
|
|
34592
|
+
async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
|
|
34593
|
+
const { wholePage = false, timeoutMs = 2e4 } = options;
|
|
34594
|
+
const { context, page } = await newPage(browser, { width: viewportWidth, height: 900 });
|
|
34595
|
+
try {
|
|
34596
|
+
await page.setContent(html, { waitUntil: "load", timeout: timeoutMs });
|
|
34597
|
+
await page.addStyleTag({ content: SETTLE_ANIMATIONS_CSS });
|
|
34598
|
+
await page.evaluate(async () => {
|
|
34599
|
+
await document.fonts.ready;
|
|
34600
|
+
});
|
|
34601
|
+
await page.waitForTimeout(300);
|
|
34602
|
+
if (!wholePage) {
|
|
34603
|
+
for (const selector of [`[${SECTION_ROOT_ATTRIBUTE}]`, "body > *"]) {
|
|
34604
|
+
const target = page.locator(selector).first();
|
|
34605
|
+
if (!await target.count()) continue;
|
|
34606
|
+
const shot = await target.screenshot({ type: "png", timeout: timeoutMs }).catch(() => null);
|
|
34607
|
+
if (shot) return shot;
|
|
34608
|
+
}
|
|
34609
|
+
}
|
|
34610
|
+
return await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
34611
|
+
} catch {
|
|
34612
|
+
return null;
|
|
34613
|
+
} finally {
|
|
34614
|
+
await context.close();
|
|
34615
|
+
}
|
|
34616
|
+
}
|
|
34617
|
+
|
|
34618
|
+
// src/engine/landing-library/report.ts
|
|
34619
|
+
import { writeFile as writeFile14 } from "fs/promises";
|
|
34620
|
+
import path30 from "path";
|
|
34621
|
+
async function writeCaptureReport(manifest, outDir) {
|
|
34622
|
+
const file = path30.join(outDir, "report.html");
|
|
34623
|
+
await writeFile14(file, renderReport(manifest));
|
|
34624
|
+
return file;
|
|
34625
|
+
}
|
|
34626
|
+
function escapeHtml3(value) {
|
|
34627
|
+
return value.replace(
|
|
34628
|
+
/[&<>"']/g,
|
|
34629
|
+
(character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character] ?? character
|
|
34630
|
+
);
|
|
34631
|
+
}
|
|
34632
|
+
function fidelityTone(fidelity) {
|
|
34633
|
+
if (fidelity === null) return "unknown";
|
|
34634
|
+
if (fidelity >= 0.95) return "good";
|
|
34635
|
+
if (fidelity >= 0.85) return "fair";
|
|
34636
|
+
return "poor";
|
|
34637
|
+
}
|
|
34638
|
+
function medianFidelity(sections) {
|
|
34639
|
+
const scored = sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
|
|
34640
|
+
return scored.length === 0 ? null : scored[Math.floor(scored.length / 2)] ?? null;
|
|
34641
|
+
}
|
|
34642
|
+
function formatFidelity(fidelity) {
|
|
34643
|
+
return fidelity === null ? "\u2014" : fidelity.toFixed(2);
|
|
34644
|
+
}
|
|
34645
|
+
function renderSection3(section) {
|
|
34646
|
+
const tone = fidelityTone(section.fidelity);
|
|
34647
|
+
const rendered = section.bundle ? section.bundle.replace(/section\.html$/, "section-rendered.png") : null;
|
|
34648
|
+
const shot = (label, src, note) => {
|
|
34649
|
+
if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
|
|
34650
|
+
return `<figure class="shot">
|
|
34651
|
+
<figcaption>${label}${note ? ` <span class="note">${escapeHtml3(note)}</span>` : ""}</figcaption>
|
|
34652
|
+
<a href="${escapeHtml3(src)}" target="_blank" rel="noopener"><img src="${escapeHtml3(src)}" alt="" loading="lazy"></a>
|
|
34653
|
+
</figure>`;
|
|
34654
|
+
};
|
|
34655
|
+
return `<section class="card">
|
|
34656
|
+
<header>
|
|
34657
|
+
<h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
|
|
34658
|
+
<span class="badge ${tone}">fidelity ${formatFidelity(section.fidelity)}</span>
|
|
34659
|
+
${section.fidelityNote ? `<span class="badge warn">${escapeHtml3(section.fidelityNote)}</span>` : ""}
|
|
34660
|
+
${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml3(section.motion.summary)}</span>` : ""}
|
|
34661
|
+
</header>
|
|
34662
|
+
<p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
|
|
34663
|
+
<div class="shots">
|
|
34664
|
+
${shot("Live", section.desktopShot)}
|
|
34665
|
+
${shot("Reproduction", rendered, "rendered from the extracted bundle")}
|
|
34666
|
+
${shot("Mobile", section.mobileShot)}
|
|
34667
|
+
</div>
|
|
34668
|
+
${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
|
|
34669
|
+
<footer>
|
|
34670
|
+
<code>${escapeHtml3(section.selector)}</code>
|
|
34671
|
+
${section.bundle ? `<a href="${escapeHtml3(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
|
|
34672
|
+
</footer>
|
|
34673
|
+
</section>`;
|
|
34674
|
+
}
|
|
34675
|
+
function renderReport(manifest) {
|
|
34676
|
+
const median = medianFidelity(manifest.sections);
|
|
34677
|
+
const moving = manifest.sections.filter((section) => section.motionFilmstrip !== null).length;
|
|
34678
|
+
return `<!doctype html>
|
|
34679
|
+
<html lang="en">
|
|
34680
|
+
<head>
|
|
34681
|
+
<meta charset="utf-8">
|
|
34682
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
34683
|
+
<title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
|
|
34684
|
+
<style>
|
|
34685
|
+
:root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
|
|
34686
|
+
@media (prefers-color-scheme: dark) {
|
|
34687
|
+
:root { --line: #27272a; --muted: #a1a1aa; --bg: #09090b; --card: #131316; }
|
|
34688
|
+
}
|
|
34689
|
+
* { box-sizing: border-box; }
|
|
34690
|
+
body { margin: 0; padding: 24px; background: var(--bg);
|
|
34691
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
34692
|
+
h1 { margin: 0 0 4px; font-size: 20px; }
|
|
34693
|
+
a { color: inherit; }
|
|
34694
|
+
.sub { color: var(--muted); margin: 0 0 20px; }
|
|
34695
|
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
|
|
34696
|
+
.stat { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; background: var(--card); }
|
|
34697
|
+
.stat b { display: block; font-size: 18px; }
|
|
34698
|
+
.stat span { color: var(--muted); font-size: 12px; }
|
|
34699
|
+
.card { border: 1px solid var(--line); border-radius: 12px; background: var(--card);
|
|
34700
|
+
padding: 16px; margin-bottom: 16px; }
|
|
34701
|
+
.card header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
|
|
34702
|
+
.card h2 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
34703
|
+
.index { display: inline-block; min-width: 26px; color: var(--muted); }
|
|
34704
|
+
.badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
|
|
34705
|
+
.badge.good { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
|
|
34706
|
+
.badge.fair { background: #fef3c7; color: #92400e; border-color: #fde68a; }
|
|
34707
|
+
.badge.poor { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
|
|
34708
|
+
.badge.warn { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
|
|
34709
|
+
.badge.motion { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
|
|
34710
|
+
.preview { color: var(--muted); margin: 0 0 12px; font-size: 13px; }
|
|
34711
|
+
.shots { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; align-items: start; }
|
|
34712
|
+
.filmstrip { margin-top: 12px; }
|
|
34713
|
+
figure { margin: 0; }
|
|
34714
|
+
figcaption { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
34715
|
+
figcaption .note { opacity: .75; }
|
|
34716
|
+
.shot img { width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px;
|
|
34717
|
+
background: #fff; display: block; }
|
|
34718
|
+
.shot.empty { border: 1px dashed var(--line); border-radius: 8px; padding: 20px; text-align: center; }
|
|
34719
|
+
.card footer { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px;
|
|
34720
|
+
font-size: 12px; color: var(--muted); }
|
|
34721
|
+
code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
|
|
34722
|
+
</style>
|
|
34723
|
+
</head>
|
|
34724
|
+
<body>
|
|
34725
|
+
<h1>${escapeHtml3(manifest.title || "Capture report")}</h1>
|
|
34726
|
+
<p class="sub"><a href="${escapeHtml3(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml3(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml3(manifest.capturedAt)}</p>
|
|
34727
|
+
|
|
34728
|
+
<div class="stats">
|
|
34729
|
+
<div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
|
|
34730
|
+
<div class="stat"><b>${formatFidelity(median)}</b><span>median fidelity</span></div>
|
|
34731
|
+
<div class="stat"><b>${formatFidelity(manifest.page.fidelity)}</b><span>whole page</span></div>
|
|
34732
|
+
<div class="stat"><b>${moving}</b><span>filmed moving</span></div>
|
|
34733
|
+
<div class="stat"><b>${manifest.documentHeight}px</b><span>page height</span></div>
|
|
34734
|
+
</div>
|
|
34735
|
+
|
|
34736
|
+
<section class="card">
|
|
34737
|
+
<header>
|
|
34738
|
+
<h2>Whole page</h2>
|
|
34739
|
+
<span class="badge ${fidelityTone(manifest.page.fidelity)}">fidelity ${formatFidelity(manifest.page.fidelity)}</span>
|
|
34740
|
+
</header>
|
|
34741
|
+
<p class="preview">The same extractor rooted at <body> \u2014 one standalone file that should render like the original.</p>
|
|
34742
|
+
<div class="shots">
|
|
34743
|
+
<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>
|
|
34744
|
+
${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>`}
|
|
34745
|
+
</div>
|
|
34746
|
+
</section>
|
|
34747
|
+
|
|
34748
|
+
${manifest.sections.map(renderSection3).join("\n")}
|
|
34749
|
+
</body>
|
|
34750
|
+
</html>
|
|
34751
|
+
`;
|
|
34752
|
+
}
|
|
34753
|
+
|
|
34754
|
+
// src/engine/landing-library/types.ts
|
|
34755
|
+
var DEFAULT_SEGMENT_OPTIONS = {
|
|
34756
|
+
maxSectionHeight: 2160,
|
|
34757
|
+
collisionMargin: 2,
|
|
34758
|
+
maxElements: 15e3
|
|
34759
|
+
};
|
|
34760
|
+
|
|
34761
|
+
// src/engine/landing-library/visualHash.ts
|
|
34762
|
+
import sharp6 from "sharp";
|
|
34763
|
+
var HASH_WIDTH = 9;
|
|
34764
|
+
var HASH_HEIGHT = 8;
|
|
34765
|
+
async function perceptualHash(image) {
|
|
34766
|
+
try {
|
|
34767
|
+
const raw = await sharp6(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
|
|
34768
|
+
return bitsToHex(rowGradientBits(new Uint8Array(raw)));
|
|
34769
|
+
} catch {
|
|
34770
|
+
return null;
|
|
34771
|
+
}
|
|
34772
|
+
}
|
|
34773
|
+
function rowGradientBits(pixels) {
|
|
34774
|
+
const bits = [];
|
|
34775
|
+
for (let y = 0; y < HASH_HEIGHT; y++) {
|
|
34776
|
+
for (let x = 0; x < HASH_WIDTH - 1; x++) {
|
|
34777
|
+
const left = pixels[y * HASH_WIDTH + x] ?? 0;
|
|
34778
|
+
const right = pixels[y * HASH_WIDTH + x + 1] ?? 0;
|
|
34779
|
+
bits.push(left > right);
|
|
34780
|
+
}
|
|
34781
|
+
}
|
|
34782
|
+
return bits;
|
|
34783
|
+
}
|
|
34784
|
+
function bitsToHex(bits) {
|
|
34785
|
+
let hex = "";
|
|
34786
|
+
for (let index = 0; index < bits.length; index += 4) {
|
|
34787
|
+
let nibble = 0;
|
|
34788
|
+
for (let offset = 0; offset < 4; offset++) {
|
|
34789
|
+
if (bits[index + offset]) nibble |= 1 << 3 - offset;
|
|
34790
|
+
}
|
|
34791
|
+
hex += nibble.toString(16);
|
|
34792
|
+
}
|
|
34793
|
+
return hex;
|
|
34794
|
+
}
|
|
34795
|
+
|
|
34796
|
+
// src/engine/landing-library/run.ts
|
|
34797
|
+
async function reproducePage(args) {
|
|
34798
|
+
const { browser, page, outDir, pageUrl, livePageShot } = args;
|
|
34799
|
+
const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
|
|
34800
|
+
if (!built) return { bundle: null, fidelity: null };
|
|
34801
|
+
await writeFile15(path31.join(outDir, "page.html"), built.html);
|
|
34802
|
+
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
|
|
34803
|
+
wholePage: true,
|
|
34804
|
+
timeoutMs: 6e4
|
|
34805
|
+
});
|
|
34806
|
+
if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
|
|
34807
|
+
await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
|
|
34808
|
+
const { score, note } = await scoreFidelity(livePageShot, rendered);
|
|
34809
|
+
return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
|
|
34810
|
+
}
|
|
34811
|
+
async function captureOneSection(args) {
|
|
34812
|
+
const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
|
|
34813
|
+
const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
|
|
34814
|
+
await mkdir11(dir, { recursive: true });
|
|
34815
|
+
const desktop = await captureSection(page, candidate);
|
|
34816
|
+
if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
|
|
34817
|
+
const visualHash = desktop ? await perceptualHash(desktop) : null;
|
|
34818
|
+
const motion = await collectMotion(page, candidate.selector);
|
|
34819
|
+
const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
|
|
34820
|
+
let fidelity = null;
|
|
34821
|
+
let fidelityNote;
|
|
34822
|
+
if (built) {
|
|
34823
|
+
await writeFile15(path31.join(dir, "section.html"), built.html);
|
|
34824
|
+
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
|
|
34825
|
+
if (rendered && desktop) {
|
|
34826
|
+
await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
|
|
34827
|
+
const result = await scoreFidelity(desktop, rendered);
|
|
34828
|
+
fidelity = result.score;
|
|
34829
|
+
fidelityNote = result.note;
|
|
34830
|
+
}
|
|
34831
|
+
}
|
|
34832
|
+
return {
|
|
34833
|
+
...candidate,
|
|
34834
|
+
desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
|
|
34835
|
+
mobileShot: null,
|
|
34836
|
+
bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
|
|
34837
|
+
fidelity,
|
|
34838
|
+
...fidelityNote ? { fidelityNote } : {},
|
|
34839
|
+
...built ? { cssStats: built.stats } : {},
|
|
34840
|
+
motion,
|
|
34841
|
+
motionFilmstrip: null,
|
|
34842
|
+
visualHash
|
|
34843
|
+
};
|
|
34844
|
+
}
|
|
34845
|
+
async function captureMobileShots(args) {
|
|
34846
|
+
const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs } = args;
|
|
34847
|
+
const mobile = await newPage(browser, MOBILE_VIEWPORT);
|
|
34848
|
+
try {
|
|
34849
|
+
await blockConsentManagers(mobile.page);
|
|
34850
|
+
await installPageRuntime(mobile.page);
|
|
34851
|
+
await preparePage(mobile.page, pageUrl, timeoutMs);
|
|
34852
|
+
for (const section of sections) {
|
|
34853
|
+
const shot = await captureSectionOnMobile(mobile.page, section);
|
|
34854
|
+
if (!shot) continue;
|
|
34855
|
+
const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
|
|
34856
|
+
await writeFile15(file, shot);
|
|
34857
|
+
section.mobileShot = path31.relative(outDir, file);
|
|
34858
|
+
}
|
|
34859
|
+
} finally {
|
|
34860
|
+
await mobile.context.close();
|
|
34861
|
+
}
|
|
34862
|
+
}
|
|
34863
|
+
async function captureMotionTakes(args) {
|
|
34864
|
+
const { browser, sections, sectionsDir, outDir, pageUrl, log } = args;
|
|
34865
|
+
const moving = sections.filter((section) => isWorthFilming(section.motion));
|
|
34866
|
+
if (moving.length === 0) return;
|
|
34867
|
+
log(`filming ${moving.length} moving sections`);
|
|
34868
|
+
for (const section of moving) {
|
|
34869
|
+
const take = await captureMotionTake(browser, pageUrl, section.selector);
|
|
34870
|
+
if (!take) continue;
|
|
34871
|
+
const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
|
|
34872
|
+
const file = path31.join(dir, "motion-filmstrip.png");
|
|
34873
|
+
await writeFile15(file, take.filmstrip);
|
|
34874
|
+
section.motionFilmstrip = path31.relative(outDir, file);
|
|
34875
|
+
log(` [${section.index}] ${section.motion.summary}`);
|
|
34876
|
+
}
|
|
34877
|
+
}
|
|
34878
|
+
async function scrapeLanding(options) {
|
|
34879
|
+
const timeoutMs = options.timeoutMs ?? 45e3;
|
|
34880
|
+
const log = options.onProgress ?? (() => void 0);
|
|
34881
|
+
const sectionsDir = path31.join(options.outDir, "sections");
|
|
34882
|
+
const browser = await launchBrowser();
|
|
34883
|
+
try {
|
|
34884
|
+
const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
|
|
34885
|
+
await blockConsentManagers(page);
|
|
34886
|
+
await installPageRuntime(page);
|
|
34887
|
+
log(`loading ${options.url}`);
|
|
34888
|
+
const prepared = await preparePage(page, options.url, timeoutMs);
|
|
34889
|
+
const blocked = detectBlockedPage({
|
|
34890
|
+
status: prepared.status,
|
|
34891
|
+
title: prepared.title,
|
|
34892
|
+
bodyText: prepared.bodyText,
|
|
34893
|
+
html: await page.content().catch(() => "")
|
|
34894
|
+
});
|
|
34895
|
+
if (blocked) throw new BlockedPageError(blocked);
|
|
34896
|
+
await mkdir11(sectionsDir, { recursive: true });
|
|
34897
|
+
log("segmenting");
|
|
34898
|
+
const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
|
|
34899
|
+
log(`found ${candidates.length} sections`);
|
|
34900
|
+
const sections = [];
|
|
34901
|
+
for (const candidate of candidates) {
|
|
34902
|
+
const section = await captureOneSection({
|
|
34903
|
+
browser,
|
|
34904
|
+
page,
|
|
34905
|
+
candidate,
|
|
34906
|
+
sectionsDir,
|
|
34907
|
+
outDir: options.outDir,
|
|
34908
|
+
pageUrl: prepared.finalUrl,
|
|
34909
|
+
withCode: options.code !== false
|
|
34910
|
+
});
|
|
34911
|
+
sections.push(section);
|
|
34912
|
+
log(
|
|
34913
|
+
` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
|
|
34914
|
+
);
|
|
34915
|
+
}
|
|
34916
|
+
const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
34917
|
+
if (fullPage) await writeFile15(path31.join(options.outDir, "full-page.png"), fullPage);
|
|
34918
|
+
const reproduction = options.code === false ? { bundle: null, fidelity: null } : await reproducePage({
|
|
34919
|
+
browser,
|
|
34920
|
+
page,
|
|
34921
|
+
outDir: options.outDir,
|
|
34922
|
+
pageUrl: prepared.finalUrl,
|
|
34923
|
+
livePageShot: fullPage
|
|
34924
|
+
});
|
|
34925
|
+
log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
|
|
34926
|
+
await context.close();
|
|
34927
|
+
if (options.mobile !== false) {
|
|
34928
|
+
log("capturing mobile");
|
|
34929
|
+
await captureMobileShots({
|
|
34930
|
+
browser,
|
|
34931
|
+
sections,
|
|
34932
|
+
sectionsDir,
|
|
34933
|
+
outDir: options.outDir,
|
|
34934
|
+
pageUrl: prepared.finalUrl,
|
|
34935
|
+
timeoutMs
|
|
34936
|
+
});
|
|
34937
|
+
}
|
|
34938
|
+
if (options.motion !== false) {
|
|
34939
|
+
await captureMotionTakes({
|
|
34940
|
+
browser,
|
|
34941
|
+
sections,
|
|
34942
|
+
sectionsDir,
|
|
34943
|
+
outDir: options.outDir,
|
|
34944
|
+
pageUrl: prepared.finalUrl,
|
|
34945
|
+
log
|
|
34946
|
+
});
|
|
34947
|
+
}
|
|
34948
|
+
const manifest = {
|
|
34949
|
+
url: options.url,
|
|
34950
|
+
finalUrl: prepared.finalUrl,
|
|
34951
|
+
title: prepared.title,
|
|
34952
|
+
documentHeight: prepared.documentHeight,
|
|
34953
|
+
viewport: DESKTOP_VIEWPORT,
|
|
34954
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34955
|
+
sections,
|
|
34956
|
+
page: reproduction
|
|
34957
|
+
};
|
|
34958
|
+
await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
34959
|
+
`);
|
|
34960
|
+
if (options.report !== false) {
|
|
34961
|
+
const reportPath = await writeCaptureReport(manifest, options.outDir);
|
|
34962
|
+
log(`report: ${reportPath}`);
|
|
34963
|
+
}
|
|
34964
|
+
return manifest;
|
|
34965
|
+
} finally {
|
|
34966
|
+
await browser.close();
|
|
34967
|
+
}
|
|
34968
|
+
}
|
|
34969
|
+
|
|
34970
|
+
// src/commands/landing/inspiration/scrape.ts
|
|
34971
|
+
registerSchema({
|
|
34972
|
+
command: "landing.inspiration.scrape",
|
|
34973
|
+
description: "Internal/ops: capture one landing page into a directory of section screenshots, standalone HTML bundles and motion filmstrips. The ingest job runs this; use it locally to debug a page that studied badly.",
|
|
34974
|
+
args: {
|
|
34975
|
+
url: { type: "string", description: "Page to capture", required: true },
|
|
34976
|
+
out: { type: "string", description: "Output directory", required: true },
|
|
34977
|
+
"no-mobile": { type: "boolean", description: "Skip the phone-viewport pass", required: false },
|
|
34978
|
+
"no-code": {
|
|
34979
|
+
type: "boolean",
|
|
34980
|
+
description: "Segment and screenshot only \u2014 no bundles, no fidelity",
|
|
34981
|
+
required: false
|
|
34982
|
+
},
|
|
34983
|
+
"no-motion": {
|
|
34984
|
+
type: "boolean",
|
|
34985
|
+
description: "Skip filming sections that move (roughly halves runtime)",
|
|
34986
|
+
required: false
|
|
34987
|
+
},
|
|
34988
|
+
"no-report": { type: "boolean", description: "Skip writing report.html", required: false }
|
|
34989
|
+
}
|
|
34990
|
+
});
|
|
34991
|
+
var scrapeCommand = defineCommand147({
|
|
34992
|
+
meta: {
|
|
34993
|
+
name: "scrape",
|
|
34994
|
+
description: "Internal/ops: capture a landing page to a directory. Example: baker landing inspiration scrape https://linear.app --out /tmp/linear"
|
|
34995
|
+
},
|
|
34996
|
+
args: {
|
|
34997
|
+
url: { type: "positional", description: "Page to capture", required: true },
|
|
34998
|
+
out: { type: "string", description: "Output directory", required: true },
|
|
34999
|
+
"no-mobile": { type: "boolean", description: "Skip the phone-viewport pass", required: false, default: false },
|
|
35000
|
+
"no-code": { type: "boolean", description: "Segment and screenshot only", required: false, default: false },
|
|
35001
|
+
"no-motion": { type: "boolean", description: "Skip filming sections that move", required: false, default: false },
|
|
35002
|
+
"no-report": { type: "boolean", description: "Skip writing report.html", required: false, default: false }
|
|
35003
|
+
},
|
|
35004
|
+
run: async ({ args }) => {
|
|
35005
|
+
try {
|
|
35006
|
+
const manifest = await scrapeLanding({
|
|
35007
|
+
url: args.url,
|
|
35008
|
+
outDir: args.out,
|
|
35009
|
+
mobile: !args["no-mobile"],
|
|
35010
|
+
code: !args["no-code"],
|
|
35011
|
+
motion: !args["no-motion"],
|
|
35012
|
+
report: !args["no-report"],
|
|
35013
|
+
// Progress goes to stderr so stdout stays a clean JSON envelope.
|
|
35014
|
+
onProgress: (message) => process.stderr.write(`${message}
|
|
35015
|
+
`)
|
|
35016
|
+
});
|
|
35017
|
+
const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
|
|
35018
|
+
writeJson({
|
|
35019
|
+
ok: true,
|
|
35020
|
+
data: {
|
|
35021
|
+
title: manifest.title,
|
|
35022
|
+
sections: manifest.sections.length,
|
|
35023
|
+
medianFidelity: scored.length > 0 ? scored[Math.floor(scored.length / 2)] : null,
|
|
35024
|
+
pageFidelity: manifest.page.fidelity,
|
|
35025
|
+
out: args.out,
|
|
35026
|
+
report: args["no-report"] ? null : `${args.out}/report.html`
|
|
35027
|
+
}
|
|
35028
|
+
});
|
|
35029
|
+
} catch (error) {
|
|
35030
|
+
if (error instanceof BlockedPageError) {
|
|
35031
|
+
writeJson({ ok: false, error: { code: error.code, message: error.message } });
|
|
35032
|
+
process.exit(1);
|
|
35033
|
+
}
|
|
35034
|
+
reportError(error);
|
|
35035
|
+
}
|
|
35036
|
+
}
|
|
35037
|
+
});
|
|
35038
|
+
|
|
35039
|
+
// src/commands/landing/inspiration/search.ts
|
|
35040
|
+
import { mkdir as mkdir12, writeFile as writeFile16 } from "fs/promises";
|
|
35041
|
+
import path32 from "path";
|
|
35042
|
+
import { defineCommand as defineCommand148 } from "citty";
|
|
35043
|
+
registerSchema({
|
|
35044
|
+
command: "landing.inspiration.search",
|
|
35045
|
+
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.",
|
|
35046
|
+
args: {
|
|
35047
|
+
query: { type: "string", description: "What you want to see, in plain English", required: false },
|
|
35048
|
+
type: {
|
|
35049
|
+
type: "string",
|
|
35050
|
+
description: "Comma list of section types: hero,pricing,faq,testimonials,\u2026",
|
|
35051
|
+
required: false
|
|
35052
|
+
},
|
|
35053
|
+
composition: {
|
|
35054
|
+
type: "string",
|
|
35055
|
+
description: "Comma list of composition patterns (references/composition.md)",
|
|
35056
|
+
required: false
|
|
35057
|
+
},
|
|
35058
|
+
register: {
|
|
35059
|
+
type: "string",
|
|
35060
|
+
description: "Comma list of visual registers: dev-tool-minimal,luxury-high-end,\u2026",
|
|
35061
|
+
required: false
|
|
35062
|
+
},
|
|
35063
|
+
interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
|
|
35064
|
+
motion: {
|
|
35065
|
+
type: "string",
|
|
35066
|
+
description: "Comma list of motion kinds: scroll-reveal,marquee,parallax,\u2026",
|
|
35067
|
+
required: false
|
|
35068
|
+
},
|
|
35069
|
+
media: { type: "string", description: "Comma list of media kinds", required: false },
|
|
35070
|
+
device: { type: "string", description: "Comma list of content devices", required: false },
|
|
35071
|
+
theme: { type: "string", description: "light | dark | mixed", required: false },
|
|
35072
|
+
"max-rank": {
|
|
35073
|
+
type: "number",
|
|
35074
|
+
description: "Show>Tell rank ceiling 1-7; 3 means 'rank 3 or better'",
|
|
35075
|
+
required: false
|
|
35076
|
+
},
|
|
35077
|
+
"min-craft": { type: "number", description: "Craft floor 0-1", required: false },
|
|
35078
|
+
"min-fidelity": {
|
|
35079
|
+
type: "number",
|
|
35080
|
+
description: "Only sections whose markup reproduces this well (0-1)",
|
|
35081
|
+
required: false
|
|
35082
|
+
},
|
|
35083
|
+
domain: { type: "string", description: "Restrict to one site", required: false },
|
|
35084
|
+
"similar-to": {
|
|
35085
|
+
type: "string",
|
|
35086
|
+
description: "Section id \u2014 find sections that look like this one",
|
|
35087
|
+
required: false
|
|
35088
|
+
},
|
|
35089
|
+
scope: { type: "string", description: "favorites (default) | all", required: false },
|
|
35090
|
+
limit: { type: "number", description: "Max results (default 8)", required: false },
|
|
35091
|
+
"no-images": { type: "boolean", description: "Skip downloading screenshots", required: false }
|
|
35092
|
+
}
|
|
35093
|
+
});
|
|
35094
|
+
function buildSearchBody(args) {
|
|
35095
|
+
const body = {};
|
|
35096
|
+
const setList2 = (key, value) => {
|
|
35097
|
+
const list = splitList(value);
|
|
35098
|
+
if (list) Object.assign(body, { [key]: list });
|
|
35099
|
+
};
|
|
35100
|
+
if (args.query) body.query = String(args.query);
|
|
35101
|
+
if (args["similar-to"]) body.similarToSectionId = String(args["similar-to"]);
|
|
35102
|
+
setList2("sectionType", args.type);
|
|
35103
|
+
setList2("composition", args.composition);
|
|
35104
|
+
setList2("visualRegister", args.register);
|
|
35105
|
+
setList2("interaction", args.interaction);
|
|
35106
|
+
setList2("motion", args.motion);
|
|
35107
|
+
setList2("mediaKind", args.media);
|
|
35108
|
+
setList2("contentDevice", args.device);
|
|
35109
|
+
if (args.theme) body.theme = String(args.theme);
|
|
35110
|
+
if (args.domain) body.domain = String(args.domain);
|
|
35111
|
+
const maxRank = parseNumber(args["max-rank"]);
|
|
35112
|
+
if (maxRank !== void 0) body.maxShowTellRank = maxRank;
|
|
35113
|
+
const minCraft = parseNumber(args["min-craft"]);
|
|
35114
|
+
if (minCraft !== void 0) body.minCraftScore = minCraft;
|
|
35115
|
+
const minFidelity = parseNumber(args["min-fidelity"]);
|
|
35116
|
+
if (minFidelity !== void 0) body.minFidelity = minFidelity;
|
|
35117
|
+
const limit = parseNumber(args.limit);
|
|
35118
|
+
body.limit = limit ?? 8;
|
|
35119
|
+
body.scope = args.scope === "all" ? "all" : "favorites";
|
|
35120
|
+
return body;
|
|
35121
|
+
}
|
|
35122
|
+
async function downloadShots(results) {
|
|
35123
|
+
const dir = path32.join(process.cwd(), ".baker", "inspiration");
|
|
35124
|
+
await mkdir12(dir, { recursive: true });
|
|
35125
|
+
const saved = /* @__PURE__ */ new Map();
|
|
35126
|
+
await Promise.all(
|
|
35127
|
+
results.map(async (result) => {
|
|
35128
|
+
if (!result.desktopShotUrl) return;
|
|
35129
|
+
try {
|
|
35130
|
+
const response = await fetch(result.desktopShotUrl);
|
|
35131
|
+
if (!response.ok) return;
|
|
35132
|
+
const file = path32.join(dir, `${result.id}.png`);
|
|
35133
|
+
await writeFile16(file, Buffer.from(await response.arrayBuffer()));
|
|
35134
|
+
saved.set(result.id, path32.relative(process.cwd(), file));
|
|
35135
|
+
} catch {
|
|
35136
|
+
}
|
|
35137
|
+
})
|
|
35138
|
+
);
|
|
35139
|
+
return saved;
|
|
35140
|
+
}
|
|
35141
|
+
var searchCommand2 = defineCommand148({
|
|
35142
|
+
meta: {
|
|
35143
|
+
name: "search",
|
|
35144
|
+
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"
|
|
35145
|
+
},
|
|
35146
|
+
args: {
|
|
35147
|
+
query: { type: "positional", description: "What you want to see, in plain English", required: false },
|
|
35148
|
+
type: { type: "string", description: "Comma list of section types", required: false },
|
|
35149
|
+
composition: { type: "string", description: "Comma list of composition patterns", required: false },
|
|
35150
|
+
register: { type: "string", description: "Comma list of visual registers", required: false },
|
|
35151
|
+
interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
|
|
35152
|
+
motion: { type: "string", description: "Comma list of motion kinds", required: false },
|
|
35153
|
+
media: { type: "string", description: "Comma list of media kinds", required: false },
|
|
35154
|
+
device: { type: "string", description: "Comma list of content devices", required: false },
|
|
35155
|
+
theme: { type: "string", description: "light | dark | mixed", required: false },
|
|
35156
|
+
"max-rank": { type: "string", description: "Show>Tell rank ceiling 1-7", required: false },
|
|
35157
|
+
"min-craft": { type: "string", description: "Craft floor 0-1", required: false },
|
|
35158
|
+
"min-fidelity": { type: "string", description: "Reproduction-fidelity floor 0-1", required: false },
|
|
35159
|
+
domain: { type: "string", description: "Restrict to one site", required: false },
|
|
35160
|
+
"similar-to": { type: "string", description: "Section id to find lookalikes of", required: false },
|
|
35161
|
+
scope: { type: "string", description: "favorites (default) | all", required: false, default: "favorites" },
|
|
35162
|
+
limit: { type: "string", description: "Max results (default 8)", required: false },
|
|
35163
|
+
"no-images": { type: "boolean", description: "Skip downloading screenshots", required: false, default: false },
|
|
35164
|
+
full: { type: "boolean", description: "Include every classification facet", required: false, default: false }
|
|
35165
|
+
},
|
|
35166
|
+
run: async ({ args }) => {
|
|
35167
|
+
try {
|
|
35168
|
+
const body = buildSearchBody(args);
|
|
35169
|
+
const data = await apiPost("/api/landing-inspiration/search", body);
|
|
35170
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
35171
|
+
const shots = args["no-images"] ? /* @__PURE__ */ new Map() : await downloadShots(results);
|
|
35172
|
+
const full = args.full;
|
|
35173
|
+
const rows = results.map((result) => ({
|
|
35174
|
+
id: result.id,
|
|
35175
|
+
section: result.sectionType,
|
|
35176
|
+
composition: result.composition,
|
|
35177
|
+
look: result.visualRegister,
|
|
35178
|
+
motion: result.motionSummary,
|
|
35179
|
+
why_it_works: result.whyItWorks,
|
|
35180
|
+
craft: Number(result.craftScore?.toFixed?.(2) ?? result.craftScore),
|
|
35181
|
+
fidelity: result.fidelity,
|
|
35182
|
+
domain: result.domain,
|
|
35183
|
+
// Only worth saying when it means something: >1 marks a section the site
|
|
35184
|
+
// reuses across its pages, which is a stronger signal than a one-off.
|
|
35185
|
+
...result.pageCount > 1 ? { used_on_pages: result.pageCount } : {},
|
|
35186
|
+
screenshot: shots.get(result.id) ?? null,
|
|
35187
|
+
...full ? {
|
|
35188
|
+
theme: result.theme,
|
|
35189
|
+
density: result.density,
|
|
35190
|
+
show_tell_rank: result.showTellRank,
|
|
35191
|
+
interactions: result.interactions,
|
|
35192
|
+
media: result.mediaKinds,
|
|
35193
|
+
devices: result.contentDevices,
|
|
35194
|
+
tags: result.tags,
|
|
35195
|
+
headline: result.headline,
|
|
35196
|
+
source_url: result.sourceUrl
|
|
35197
|
+
} : {}
|
|
35198
|
+
}));
|
|
35199
|
+
const hints = [INSPIRATION_HINTS.adapt];
|
|
35200
|
+
if (rows.length === 0) {
|
|
35201
|
+
hints.push(
|
|
35202
|
+
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>`."
|
|
35203
|
+
);
|
|
35204
|
+
} else if (data.moreInFullLibrary > 0) {
|
|
35205
|
+
hints.push(`${data.moreInFullLibrary} more match in the full library \u2014 re-run with --scope all.`);
|
|
35206
|
+
}
|
|
35207
|
+
if (shots.size > 0) hints.push(`Screenshots saved to .baker/inspiration/ \u2014 Read them before deciding.`);
|
|
35208
|
+
writeJson({
|
|
35209
|
+
ok: true,
|
|
35210
|
+
data: { results: rows, scope: data.scope, more_in_full_library: data.moreInFullLibrary, total: data.total },
|
|
35211
|
+
hints
|
|
35212
|
+
});
|
|
35213
|
+
} catch (error) {
|
|
35214
|
+
reportError(error);
|
|
35215
|
+
}
|
|
35216
|
+
}
|
|
35217
|
+
});
|
|
35218
|
+
|
|
35219
|
+
// src/commands/landing/inspiration/view.ts
|
|
35220
|
+
import { mkdir as mkdir13, writeFile as writeFile17 } from "fs/promises";
|
|
35221
|
+
import path33 from "path";
|
|
35222
|
+
import { defineCommand as defineCommand149 } from "citty";
|
|
35223
|
+
registerSchema({
|
|
35224
|
+
command: "landing.inspiration.view",
|
|
35225
|
+
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.",
|
|
35226
|
+
args: { id: { type: "string", description: "Section id from search", required: true } }
|
|
35227
|
+
});
|
|
35228
|
+
async function download(url, file) {
|
|
35229
|
+
if (!url) return null;
|
|
35230
|
+
try {
|
|
35231
|
+
const response = await fetch(url);
|
|
35232
|
+
if (!response.ok) return null;
|
|
35233
|
+
await mkdir13(path33.dirname(file), { recursive: true });
|
|
35234
|
+
await writeFile17(file, Buffer.from(await response.arrayBuffer()));
|
|
35235
|
+
return path33.relative(process.cwd(), file);
|
|
35236
|
+
} catch {
|
|
35237
|
+
return null;
|
|
35238
|
+
}
|
|
35239
|
+
}
|
|
35240
|
+
var viewCommand2 = defineCommand149({
|
|
35241
|
+
meta: {
|
|
35242
|
+
name: "view",
|
|
35243
|
+
description: "Full detail for one reference section. Example: baker landing inspiration view k57abc\u2026 \u2014 read the screenshots it saves before you build."
|
|
35244
|
+
},
|
|
35245
|
+
args: { id: { type: "positional", description: "Section id from search", required: true } },
|
|
35246
|
+
run: async ({ args }) => {
|
|
35247
|
+
try {
|
|
35248
|
+
const id = args.id;
|
|
35249
|
+
const data = await apiGet("/api/landing-inspiration/section", { id });
|
|
35250
|
+
const section = data.section;
|
|
35251
|
+
const dir = path33.join(process.cwd(), ".baker", "inspiration", id);
|
|
35252
|
+
const [desktop, mobile, filmstrip] = await Promise.all([
|
|
35253
|
+
download(section.desktopShotUrl, path33.join(dir, "desktop.png")),
|
|
35254
|
+
download(section.mobileShotUrl, path33.join(dir, "mobile.png")),
|
|
35255
|
+
download(section.motionFilmstripUrl, path33.join(dir, "motion-filmstrip.png"))
|
|
35256
|
+
]);
|
|
35257
|
+
const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
35258
|
+
const fidelity = fidelityHint(section.fidelity);
|
|
35259
|
+
if (fidelity) hints.push(fidelity);
|
|
35260
|
+
if (filmstrip) {
|
|
35261
|
+
hints.push(
|
|
35262
|
+
"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."
|
|
35263
|
+
);
|
|
35264
|
+
}
|
|
35265
|
+
writeJson({
|
|
35266
|
+
ok: true,
|
|
35267
|
+
data: {
|
|
35268
|
+
id: section.id,
|
|
35269
|
+
section: section.sectionType,
|
|
35270
|
+
composition: section.composition,
|
|
35271
|
+
look: section.visualRegister,
|
|
35272
|
+
theme: section.theme,
|
|
35273
|
+
density: section.density,
|
|
35274
|
+
show_tell_rank: section.showTellRank,
|
|
35275
|
+
interactions: section.interactions,
|
|
35276
|
+
media: section.mediaKinds,
|
|
35277
|
+
devices: section.contentDevices,
|
|
35278
|
+
tags: section.tags,
|
|
35279
|
+
motion: section.motionSummary,
|
|
35280
|
+
design_tokens: section.designTokens,
|
|
35281
|
+
size: section.boundingBox,
|
|
35282
|
+
copy: {
|
|
35283
|
+
headline: section.headline,
|
|
35284
|
+
subhead: section.subhead,
|
|
35285
|
+
ctas: section.ctaLabels,
|
|
35286
|
+
proof: section.proofSignals
|
|
35287
|
+
},
|
|
35288
|
+
why_it_works: section.whyItWorks,
|
|
35289
|
+
adaptation_notes: section.adaptationNotes,
|
|
35290
|
+
reproduction_notes: section.reproductionNotes,
|
|
35291
|
+
craft: section.craftScore,
|
|
35292
|
+
fidelity: section.fidelity,
|
|
35293
|
+
source_url: section.sourceUrl,
|
|
35294
|
+
screenshots: { desktop, mobile, motion_filmstrip: filmstrip }
|
|
35295
|
+
},
|
|
35296
|
+
hints
|
|
35297
|
+
});
|
|
35298
|
+
} catch (error) {
|
|
35299
|
+
reportError(error);
|
|
35300
|
+
}
|
|
35301
|
+
}
|
|
35302
|
+
});
|
|
35303
|
+
|
|
35304
|
+
// src/commands/landing/inspiration/index.ts
|
|
35305
|
+
var inspirationCommand = defineCommand150({
|
|
35306
|
+
meta: {
|
|
35307
|
+
name: "inspiration",
|
|
35308
|
+
description: `Reference library of real landing-page sections \u2014 look at how good pages actually solve a problem before you design one.
|
|
35309
|
+
|
|
35310
|
+
Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
|
|
35311
|
+
|
|
35312
|
+
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.
|
|
35313
|
+
|
|
35314
|
+
Subcommands:
|
|
35315
|
+
baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
|
|
35316
|
+
baker landing inspiration view <id> \u2014 one section in full: tokens, motion filmstrip, why it works, what to change
|
|
35317
|
+
baker landing inspiration code <id> \u2014 its standalone HTML+CSS, for structure only
|
|
35318
|
+
baker landing inspiration page <id> \u2014 a whole page as a sequence: how it orders its sections
|
|
35319
|
+
baker landing inspiration add <url> \u2014 add a page to the library and save it to this company (returns immediately)
|
|
35320
|
+
baker landing inspiration favorites \u2014 what this company has saved; the default search scope
|
|
35321
|
+
baker landing inspiration favorite <id> \u2014 save a section
|
|
35322
|
+
baker landing inspiration unfavorite <id> \u2014 unsave a section
|
|
35323
|
+
baker landing inspiration scrape <url> \u2014 internal/ops: run the capture locally
|
|
35324
|
+
|
|
35325
|
+
Examples:
|
|
35326
|
+
baker landing inspiration search "pricing with a monthly/annual toggle" --max-rank 3
|
|
35327
|
+
baker landing inspiration search "dark developer hero with a terminal" --register dev-tool-minimal --scope all
|
|
35328
|
+
baker landing inspiration search "testimonial wall with faces and company logos" --motion scroll-reveal
|
|
35329
|
+
baker landing inspiration add https://linear.app
|
|
35330
|
+
|
|
35331
|
+
Full guide: __tooling__/docs/tools/baker/landing.md`
|
|
35332
|
+
},
|
|
35333
|
+
subCommands: {
|
|
35334
|
+
search: searchCommand2,
|
|
35335
|
+
view: viewCommand2,
|
|
35336
|
+
code: codeCommand,
|
|
35337
|
+
page: pageCommand,
|
|
35338
|
+
add: addCommand,
|
|
35339
|
+
favorites: favoritesCommand,
|
|
35340
|
+
favorite: favoriteCommand,
|
|
35341
|
+
unfavorite: unfavoriteCommand,
|
|
35342
|
+
scrape: scrapeCommand
|
|
35343
|
+
}
|
|
35344
|
+
});
|
|
35345
|
+
|
|
32989
35346
|
// src/commands/landing/index.ts
|
|
32990
|
-
var landingCommand =
|
|
35347
|
+
var landingCommand = defineCommand151({
|
|
32991
35348
|
meta: {
|
|
32992
35349
|
name: "landing",
|
|
32993
35350
|
description: `Design-quality tools for landing pages (src/pages/<slug>/).
|
|
@@ -32995,15 +35352,17 @@ var landingCommand = defineCommand143({
|
|
|
32995
35352
|
Start here: \`baker landing critique <slug>\` after building or editing a landing.
|
|
32996
35353
|
|
|
32997
35354
|
Subcommands:
|
|
35355
|
+
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.
|
|
32998
35356
|
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.`
|
|
32999
35357
|
},
|
|
33000
35358
|
subCommands: {
|
|
35359
|
+
inspiration: inspirationCommand,
|
|
33001
35360
|
critique: critiqueCommand2
|
|
33002
35361
|
}
|
|
33003
35362
|
});
|
|
33004
35363
|
|
|
33005
35364
|
// src/commands/mcp/index.ts
|
|
33006
|
-
import { defineCommand as
|
|
35365
|
+
import { defineCommand as defineCommand152 } from "citty";
|
|
33007
35366
|
|
|
33008
35367
|
// src/commands/mcp/platforms.ts
|
|
33009
35368
|
function readsKey(label) {
|
|
@@ -33072,7 +35431,7 @@ registerSchema({
|
|
|
33072
35431
|
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.",
|
|
33073
35432
|
args: {}
|
|
33074
35433
|
});
|
|
33075
|
-
var connectedCommand =
|
|
35434
|
+
var connectedCommand = defineCommand152({
|
|
33076
35435
|
meta: {
|
|
33077
35436
|
name: "connected",
|
|
33078
35437
|
description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
|
|
@@ -33131,7 +35490,7 @@ registerSchema({
|
|
|
33131
35490
|
description: "List the custom MCP servers this company's chats see (org + company + your own user scope).",
|
|
33132
35491
|
args: {}
|
|
33133
35492
|
});
|
|
33134
|
-
var listCommand13 =
|
|
35493
|
+
var listCommand13 = defineCommand152({
|
|
33135
35494
|
meta: { name: "list", description: "List custom MCP servers visible to this company's chats." },
|
|
33136
35495
|
run: async () => {
|
|
33137
35496
|
try {
|
|
@@ -33168,7 +35527,7 @@ registerSchema({
|
|
|
33168
35527
|
header: { type: "string", description: 'Auth header "Key: Value" (repeatable)', required: false }
|
|
33169
35528
|
}
|
|
33170
35529
|
});
|
|
33171
|
-
var
|
|
35530
|
+
var addCommand2 = defineCommand152({
|
|
33172
35531
|
meta: {
|
|
33173
35532
|
name: "add",
|
|
33174
35533
|
description: `Register a custom MCP server. Tools appear as mcp__<name>__* on the NEXT message.
|
|
@@ -33220,7 +35579,7 @@ registerSchema({
|
|
|
33220
35579
|
description: "Remove a company custom MCP server by name.",
|
|
33221
35580
|
args: { name: { type: "string", description: "Server name to remove", required: true } }
|
|
33222
35581
|
});
|
|
33223
|
-
var removeCommand4 =
|
|
35582
|
+
var removeCommand4 = defineCommand152({
|
|
33224
35583
|
meta: {
|
|
33225
35584
|
name: "remove",
|
|
33226
35585
|
description: `Remove a company custom MCP server by name.
|
|
@@ -33242,7 +35601,7 @@ Example:
|
|
|
33242
35601
|
}
|
|
33243
35602
|
}
|
|
33244
35603
|
});
|
|
33245
|
-
var mcpCommand =
|
|
35604
|
+
var mcpCommand = defineCommand152({
|
|
33246
35605
|
meta: {
|
|
33247
35606
|
name: "mcp",
|
|
33248
35607
|
description: `Third-party tools for this company \u2014 see what's connected, register custom HTTPS MCP endpoints.
|
|
@@ -33262,16 +35621,16 @@ Full guide: __tooling__/docs/tools/baker/mcp.md`
|
|
|
33262
35621
|
subCommands: {
|
|
33263
35622
|
connected: connectedCommand,
|
|
33264
35623
|
list: listCommand13,
|
|
33265
|
-
add:
|
|
35624
|
+
add: addCommand2,
|
|
33266
35625
|
remove: removeCommand4
|
|
33267
35626
|
}
|
|
33268
35627
|
});
|
|
33269
35628
|
|
|
33270
35629
|
// src/commands/research/index.ts
|
|
33271
|
-
import { defineCommand as
|
|
35630
|
+
import { defineCommand as defineCommand163 } from "citty";
|
|
33272
35631
|
|
|
33273
35632
|
// src/commands/research/advertisers.ts
|
|
33274
|
-
import { defineCommand as
|
|
35633
|
+
import { defineCommand as defineCommand153 } from "citty";
|
|
33275
35634
|
|
|
33276
35635
|
// src/commands/research/output.ts
|
|
33277
35636
|
var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
|
|
@@ -33384,7 +35743,7 @@ var FIELDS3 = {
|
|
|
33384
35743
|
etv: "Estimated traffic value (USD)",
|
|
33385
35744
|
visibility: "SERP visibility score (0-1)"
|
|
33386
35745
|
};
|
|
33387
|
-
var advertisersCommand =
|
|
35746
|
+
var advertisersCommand = defineCommand153({
|
|
33388
35747
|
meta: {
|
|
33389
35748
|
name: "advertisers",
|
|
33390
35749
|
description: `Find domains competing for a keyword in Google SERPs.
|
|
@@ -33404,15 +35763,15 @@ Examples:
|
|
|
33404
35763
|
},
|
|
33405
35764
|
run: async ({ args }) => {
|
|
33406
35765
|
const keyword = args.keyword;
|
|
33407
|
-
const
|
|
35766
|
+
const location2 = args.location || void 0;
|
|
33408
35767
|
const language = args.language || void 0;
|
|
33409
35768
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33410
35769
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33411
|
-
const queryContext = buildResearchQueryContext(
|
|
35770
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33412
35771
|
try {
|
|
33413
35772
|
const data = await apiPost("/api/research/advertisers", {
|
|
33414
35773
|
keyword,
|
|
33415
|
-
location,
|
|
35774
|
+
location: location2,
|
|
33416
35775
|
language,
|
|
33417
35776
|
limit,
|
|
33418
35777
|
skipCache
|
|
@@ -33431,7 +35790,7 @@ Examples:
|
|
|
33431
35790
|
});
|
|
33432
35791
|
|
|
33433
35792
|
// src/commands/research/autocomplete.ts
|
|
33434
|
-
import { defineCommand as
|
|
35793
|
+
import { defineCommand as defineCommand154 } from "citty";
|
|
33435
35794
|
registerSchema({
|
|
33436
35795
|
command: "research.autocomplete",
|
|
33437
35796
|
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).",
|
|
@@ -33454,7 +35813,7 @@ registerSchema({
|
|
|
33454
35813
|
var FIELDS4 = {
|
|
33455
35814
|
suggestion: "Autocomplete suggestion from Google"
|
|
33456
35815
|
};
|
|
33457
|
-
var autocompleteCommand =
|
|
35816
|
+
var autocompleteCommand = defineCommand154({
|
|
33458
35817
|
meta: {
|
|
33459
35818
|
name: "autocomplete",
|
|
33460
35819
|
description: `Get Google Autocomplete suggestions for keyword expansion.
|
|
@@ -33473,15 +35832,15 @@ Examples:
|
|
|
33473
35832
|
},
|
|
33474
35833
|
run: async ({ args }) => {
|
|
33475
35834
|
const keyword = args.keyword;
|
|
33476
|
-
const
|
|
35835
|
+
const location2 = args.location || void 0;
|
|
33477
35836
|
const language = args.language || void 0;
|
|
33478
35837
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33479
35838
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33480
|
-
const queryContext = buildResearchQueryContext(
|
|
35839
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33481
35840
|
try {
|
|
33482
35841
|
const data = await apiPost("/api/research/autocomplete", {
|
|
33483
35842
|
keyword,
|
|
33484
|
-
location,
|
|
35843
|
+
location: location2,
|
|
33485
35844
|
language,
|
|
33486
35845
|
limit,
|
|
33487
35846
|
skipCache
|
|
@@ -33500,7 +35859,7 @@ Examples:
|
|
|
33500
35859
|
});
|
|
33501
35860
|
|
|
33502
35861
|
// src/commands/research/countries.ts
|
|
33503
|
-
import { defineCommand as
|
|
35862
|
+
import { defineCommand as defineCommand155 } from "citty";
|
|
33504
35863
|
registerSchema({
|
|
33505
35864
|
command: "research.countries",
|
|
33506
35865
|
description: "List all supported country codes for --location flag in research commands.",
|
|
@@ -33557,7 +35916,7 @@ var FIELDS5 = {
|
|
|
33557
35916
|
code: "Country code to pass as --location",
|
|
33558
35917
|
name: "Country name"
|
|
33559
35918
|
};
|
|
33560
|
-
var countriesCommand =
|
|
35919
|
+
var countriesCommand = defineCommand155({
|
|
33561
35920
|
meta: {
|
|
33562
35921
|
name: "countries",
|
|
33563
35922
|
description: "List all supported country codes for --location flag."
|
|
@@ -33568,7 +35927,7 @@ var countriesCommand = defineCommand147({
|
|
|
33568
35927
|
});
|
|
33569
35928
|
|
|
33570
35929
|
// src/commands/research/intent.ts
|
|
33571
|
-
import { defineCommand as
|
|
35930
|
+
import { defineCommand as defineCommand156 } from "citty";
|
|
33572
35931
|
registerSchema({
|
|
33573
35932
|
command: "research.intent",
|
|
33574
35933
|
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.",
|
|
@@ -33591,7 +35950,7 @@ var FIELDS6 = {
|
|
|
33591
35950
|
intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
|
|
33592
35951
|
probability: "Confidence score 0.0-1.0"
|
|
33593
35952
|
};
|
|
33594
|
-
var intentCommand =
|
|
35953
|
+
var intentCommand = defineCommand156({
|
|
33595
35954
|
meta: {
|
|
33596
35955
|
name: "intent",
|
|
33597
35956
|
description: `Classify Google Search intent for keywords. Returns intent type and confidence.
|
|
@@ -33639,7 +35998,7 @@ Examples:
|
|
|
33639
35998
|
});
|
|
33640
35999
|
|
|
33641
36000
|
// src/commands/research/keyword-gap.ts
|
|
33642
|
-
import { defineCommand as
|
|
36001
|
+
import { defineCommand as defineCommand157 } from "citty";
|
|
33643
36002
|
registerSchema({
|
|
33644
36003
|
command: "research.keyword-gap",
|
|
33645
36004
|
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.",
|
|
@@ -33668,7 +36027,7 @@ var FIELDS7 = {
|
|
|
33668
36027
|
cpc: "Cost per click USD",
|
|
33669
36028
|
their_position: "Competitor's ranking position"
|
|
33670
36029
|
};
|
|
33671
|
-
var keywordGapCommand =
|
|
36030
|
+
var keywordGapCommand = defineCommand157({
|
|
33672
36031
|
meta: {
|
|
33673
36032
|
name: "keyword-gap",
|
|
33674
36033
|
description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
|
|
@@ -33692,18 +36051,18 @@ Examples:
|
|
|
33692
36051
|
run: async ({ args }) => {
|
|
33693
36052
|
const competitor = args.competitor;
|
|
33694
36053
|
const ours = args.ours;
|
|
33695
|
-
const
|
|
36054
|
+
const location2 = args.location || void 0;
|
|
33696
36055
|
const language = args.language || void 0;
|
|
33697
36056
|
const type = args.type || void 0;
|
|
33698
36057
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33699
36058
|
const offset = args.offset ? Number(args.offset) : void 0;
|
|
33700
36059
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33701
|
-
const queryContext = buildResearchQueryContext(
|
|
36060
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33702
36061
|
try {
|
|
33703
36062
|
const result = await apiPost("/api/research/keyword-gap", {
|
|
33704
36063
|
competitor,
|
|
33705
36064
|
ours,
|
|
33706
|
-
location,
|
|
36065
|
+
location: location2,
|
|
33707
36066
|
language,
|
|
33708
36067
|
type,
|
|
33709
36068
|
limit,
|
|
@@ -33742,7 +36101,7 @@ Examples:
|
|
|
33742
36101
|
});
|
|
33743
36102
|
|
|
33744
36103
|
// src/commands/research/keywords-for-site.ts
|
|
33745
|
-
import { defineCommand as
|
|
36104
|
+
import { defineCommand as defineCommand158 } from "citty";
|
|
33746
36105
|
registerSchema({
|
|
33747
36106
|
command: "research.keywords-for-site",
|
|
33748
36107
|
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.",
|
|
@@ -33775,7 +36134,7 @@ var FIELDS8 = {
|
|
|
33775
36134
|
competition: "LOW, MEDIUM, or HIGH",
|
|
33776
36135
|
competition_index: "Competition score 0-100"
|
|
33777
36136
|
};
|
|
33778
|
-
var keywordsForSiteCommand =
|
|
36137
|
+
var keywordsForSiteCommand = defineCommand158({
|
|
33779
36138
|
meta: {
|
|
33780
36139
|
name: "keywords-for-site",
|
|
33781
36140
|
description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
|
|
@@ -33797,17 +36156,17 @@ Examples:
|
|
|
33797
36156
|
},
|
|
33798
36157
|
run: async ({ args }) => {
|
|
33799
36158
|
const target = args.target;
|
|
33800
|
-
const
|
|
36159
|
+
const location2 = args.location || void 0;
|
|
33801
36160
|
const language = args.language || void 0;
|
|
33802
36161
|
const sort = args.sort || void 0;
|
|
33803
36162
|
const type = args.type || void 0;
|
|
33804
36163
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33805
36164
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33806
|
-
const queryContext = buildResearchQueryContext(
|
|
36165
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33807
36166
|
try {
|
|
33808
36167
|
const data = await apiPost("/api/research/keywords-for-site", {
|
|
33809
36168
|
target,
|
|
33810
|
-
location,
|
|
36169
|
+
location: location2,
|
|
33811
36170
|
language,
|
|
33812
36171
|
sort,
|
|
33813
36172
|
type,
|
|
@@ -33828,7 +36187,7 @@ Examples:
|
|
|
33828
36187
|
});
|
|
33829
36188
|
|
|
33830
36189
|
// src/commands/research/languages.ts
|
|
33831
|
-
import { defineCommand as
|
|
36190
|
+
import { defineCommand as defineCommand159 } from "citty";
|
|
33832
36191
|
registerSchema({
|
|
33833
36192
|
command: "research.languages",
|
|
33834
36193
|
description: "List all supported language codes for --language flag in research commands.",
|
|
@@ -33858,7 +36217,7 @@ var FIELDS9 = {
|
|
|
33858
36217
|
code: "Language code to pass as --language",
|
|
33859
36218
|
name: "Language name (also accepted by --language)"
|
|
33860
36219
|
};
|
|
33861
|
-
var languagesCommand2 =
|
|
36220
|
+
var languagesCommand2 = defineCommand159({
|
|
33862
36221
|
meta: {
|
|
33863
36222
|
name: "languages",
|
|
33864
36223
|
description: "List all supported language codes for --language flag."
|
|
@@ -33869,7 +36228,7 @@ var languagesCommand2 = defineCommand151({
|
|
|
33869
36228
|
});
|
|
33870
36229
|
|
|
33871
36230
|
// src/commands/research/lighthouse.ts
|
|
33872
|
-
import { defineCommand as
|
|
36231
|
+
import { defineCommand as defineCommand160 } from "citty";
|
|
33873
36232
|
registerSchema({
|
|
33874
36233
|
command: "research.lighthouse",
|
|
33875
36234
|
description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
|
|
@@ -33888,7 +36247,7 @@ var FIELDS10 = {
|
|
|
33888
36247
|
speed_index_ms: "Speed Index in ms (good: < 3400)",
|
|
33889
36248
|
interactive_ms: "Time to Interactive in ms (good: < 3800)"
|
|
33890
36249
|
};
|
|
33891
|
-
var lighthouseCommand =
|
|
36250
|
+
var lighthouseCommand = defineCommand160({
|
|
33892
36251
|
meta: {
|
|
33893
36252
|
name: "lighthouse",
|
|
33894
36253
|
description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
|
|
@@ -33926,7 +36285,7 @@ Examples:
|
|
|
33926
36285
|
});
|
|
33927
36286
|
|
|
33928
36287
|
// src/commands/research/relevant-pages.ts
|
|
33929
|
-
import { defineCommand as
|
|
36288
|
+
import { defineCommand as defineCommand161 } from "citty";
|
|
33930
36289
|
registerSchema({
|
|
33931
36290
|
command: "research.relevant-pages",
|
|
33932
36291
|
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).",
|
|
@@ -33952,7 +36311,7 @@ var FIELDS11 = {
|
|
|
33952
36311
|
keywords: "Total organic keywords the page ranks for",
|
|
33953
36312
|
top_10: "Keywords in positions 1-10"
|
|
33954
36313
|
};
|
|
33955
|
-
var relevantPagesCommand =
|
|
36314
|
+
var relevantPagesCommand = defineCommand161({
|
|
33956
36315
|
meta: {
|
|
33957
36316
|
name: "relevant-pages",
|
|
33958
36317
|
description: `Get the top pages of a competitor domain with traffic data.
|
|
@@ -33971,15 +36330,15 @@ Examples:
|
|
|
33971
36330
|
},
|
|
33972
36331
|
run: async ({ args }) => {
|
|
33973
36332
|
const target = args.target;
|
|
33974
|
-
const
|
|
36333
|
+
const location2 = args.location || void 0;
|
|
33975
36334
|
const language = args.language || void 0;
|
|
33976
36335
|
const limit = args.limit ? Number(args.limit) : void 0;
|
|
33977
36336
|
const skipCache = args["no-cache"] ? true : void 0;
|
|
33978
|
-
const queryContext = buildResearchQueryContext(
|
|
36337
|
+
const queryContext = buildResearchQueryContext(location2, language);
|
|
33979
36338
|
try {
|
|
33980
36339
|
const data = await apiPost("/api/research/relevant-pages", {
|
|
33981
36340
|
target,
|
|
33982
|
-
location,
|
|
36341
|
+
location: location2,
|
|
33983
36342
|
language,
|
|
33984
36343
|
limit,
|
|
33985
36344
|
skipCache
|
|
@@ -33998,7 +36357,7 @@ Examples:
|
|
|
33998
36357
|
});
|
|
33999
36358
|
|
|
34000
36359
|
// src/commands/research/web.ts
|
|
34001
|
-
import { defineCommand as
|
|
36360
|
+
import { defineCommand as defineCommand162 } from "citty";
|
|
34002
36361
|
registerSchema({
|
|
34003
36362
|
command: "research.web",
|
|
34004
36363
|
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).",
|
|
@@ -34049,7 +36408,7 @@ async function runDeepResearch(question) {
|
|
|
34049
36408
|
}
|
|
34050
36409
|
throw new Error("Deep research timed out");
|
|
34051
36410
|
}
|
|
34052
|
-
var webCommand =
|
|
36411
|
+
var webCommand = defineCommand162({
|
|
34053
36412
|
meta: {
|
|
34054
36413
|
name: "web",
|
|
34055
36414
|
description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
|
|
@@ -34109,7 +36468,7 @@ Examples:
|
|
|
34109
36468
|
});
|
|
34110
36469
|
|
|
34111
36470
|
// src/commands/research/index.ts
|
|
34112
|
-
var researchCommand =
|
|
36471
|
+
var researchCommand = defineCommand163({
|
|
34113
36472
|
meta: {
|
|
34114
36473
|
name: "research",
|
|
34115
36474
|
description: `Competitive intelligence and AI-powered research commands.
|
|
@@ -34150,10 +36509,10 @@ Full guide: __tooling__/docs/tools/baker/research.md`
|
|
|
34150
36509
|
});
|
|
34151
36510
|
|
|
34152
36511
|
// src/commands/scheduled-actions/index.ts
|
|
34153
|
-
import { defineCommand as
|
|
36512
|
+
import { defineCommand as defineCommand170 } from "citty";
|
|
34154
36513
|
|
|
34155
36514
|
// src/commands/scheduled-actions/create.ts
|
|
34156
|
-
import { defineCommand as
|
|
36515
|
+
import { defineCommand as defineCommand164 } from "citty";
|
|
34157
36516
|
|
|
34158
36517
|
// src/commands/scheduled-actions/shared.ts
|
|
34159
36518
|
var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
|
|
@@ -34268,7 +36627,7 @@ registerSchema({
|
|
|
34268
36627
|
prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
|
|
34269
36628
|
}
|
|
34270
36629
|
});
|
|
34271
|
-
var createCommand2 =
|
|
36630
|
+
var createCommand2 = defineCommand164({
|
|
34272
36631
|
meta: {
|
|
34273
36632
|
name: "create",
|
|
34274
36633
|
description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
|
|
@@ -34317,7 +36676,7 @@ var createCommand2 = defineCommand156({
|
|
|
34317
36676
|
});
|
|
34318
36677
|
|
|
34319
36678
|
// src/commands/scheduled-actions/delete.ts
|
|
34320
|
-
import { defineCommand as
|
|
36679
|
+
import { defineCommand as defineCommand165 } from "citty";
|
|
34321
36680
|
registerSchema({
|
|
34322
36681
|
command: "scheduled-actions.delete",
|
|
34323
36682
|
description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
|
|
@@ -34325,7 +36684,7 @@ registerSchema({
|
|
|
34325
36684
|
id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
|
|
34326
36685
|
}
|
|
34327
36686
|
});
|
|
34328
|
-
var deleteCommand2 =
|
|
36687
|
+
var deleteCommand2 = defineCommand165({
|
|
34329
36688
|
meta: {
|
|
34330
36689
|
name: "delete",
|
|
34331
36690
|
description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
|
|
@@ -34354,7 +36713,7 @@ var deleteCommand2 = defineCommand157({
|
|
|
34354
36713
|
});
|
|
34355
36714
|
|
|
34356
36715
|
// src/commands/scheduled-actions/get.ts
|
|
34357
|
-
import { defineCommand as
|
|
36716
|
+
import { defineCommand as defineCommand166 } from "citty";
|
|
34358
36717
|
registerSchema({
|
|
34359
36718
|
command: "scheduled-actions.get",
|
|
34360
36719
|
description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
|
|
@@ -34363,7 +36722,7 @@ registerSchema({
|
|
|
34363
36722
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
34364
36723
|
}
|
|
34365
36724
|
});
|
|
34366
|
-
var getCommand3 =
|
|
36725
|
+
var getCommand3 = defineCommand166({
|
|
34367
36726
|
meta: {
|
|
34368
36727
|
name: "get",
|
|
34369
36728
|
description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
|
|
@@ -34402,7 +36761,7 @@ var getCommand3 = defineCommand158({
|
|
|
34402
36761
|
});
|
|
34403
36762
|
|
|
34404
36763
|
// src/commands/scheduled-actions/list.ts
|
|
34405
|
-
import { defineCommand as
|
|
36764
|
+
import { defineCommand as defineCommand167 } from "citty";
|
|
34406
36765
|
registerSchema({
|
|
34407
36766
|
command: "scheduled-actions.list",
|
|
34408
36767
|
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.",
|
|
@@ -34410,7 +36769,7 @@ registerSchema({
|
|
|
34410
36769
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
34411
36770
|
}
|
|
34412
36771
|
});
|
|
34413
|
-
var listCommand14 =
|
|
36772
|
+
var listCommand14 = defineCommand167({
|
|
34414
36773
|
meta: {
|
|
34415
36774
|
name: "list",
|
|
34416
36775
|
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."
|
|
@@ -34433,7 +36792,7 @@ var listCommand14 = defineCommand159({
|
|
|
34433
36792
|
});
|
|
34434
36793
|
|
|
34435
36794
|
// src/commands/scheduled-actions/trigger.ts
|
|
34436
|
-
import { defineCommand as
|
|
36795
|
+
import { defineCommand as defineCommand168 } from "citty";
|
|
34437
36796
|
registerSchema({
|
|
34438
36797
|
command: "scheduled-actions.trigger",
|
|
34439
36798
|
description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
|
|
@@ -34441,7 +36800,7 @@ registerSchema({
|
|
|
34441
36800
|
id: { type: "string", description: "Published scheduled action ID", required: true }
|
|
34442
36801
|
}
|
|
34443
36802
|
});
|
|
34444
|
-
var triggerCommand =
|
|
36803
|
+
var triggerCommand = defineCommand168({
|
|
34445
36804
|
meta: {
|
|
34446
36805
|
name: "trigger",
|
|
34447
36806
|
description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
|
|
@@ -34478,7 +36837,7 @@ var triggerCommand = defineCommand160({
|
|
|
34478
36837
|
});
|
|
34479
36838
|
|
|
34480
36839
|
// src/commands/scheduled-actions/update.ts
|
|
34481
|
-
import { defineCommand as
|
|
36840
|
+
import { defineCommand as defineCommand169 } from "citty";
|
|
34482
36841
|
registerSchema({
|
|
34483
36842
|
command: "scheduled-actions.update",
|
|
34484
36843
|
description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
|
|
@@ -34503,7 +36862,7 @@ registerSchema({
|
|
|
34503
36862
|
prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
|
|
34504
36863
|
}
|
|
34505
36864
|
});
|
|
34506
|
-
var updateCommand2 =
|
|
36865
|
+
var updateCommand2 = defineCommand169({
|
|
34507
36866
|
meta: {
|
|
34508
36867
|
name: "update",
|
|
34509
36868
|
description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
|
|
@@ -34574,7 +36933,7 @@ var updateCommand2 = defineCommand161({
|
|
|
34574
36933
|
});
|
|
34575
36934
|
|
|
34576
36935
|
// src/commands/scheduled-actions/index.ts
|
|
34577
|
-
var scheduledActionsCommand =
|
|
36936
|
+
var scheduledActionsCommand = defineCommand170({
|
|
34578
36937
|
meta: {
|
|
34579
36938
|
name: "scheduled-actions",
|
|
34580
36939
|
description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
|
|
@@ -34601,14 +36960,14 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
34601
36960
|
});
|
|
34602
36961
|
|
|
34603
36962
|
// src/commands/schema.ts
|
|
34604
|
-
import { defineCommand as
|
|
36963
|
+
import { defineCommand as defineCommand171 } from "citty";
|
|
34605
36964
|
function narrowToFamily(commandName, available) {
|
|
34606
36965
|
const segments = commandName.split(".");
|
|
34607
36966
|
const prefix = segments[0] === "ads" && segments[1] ? `ads.${segments[1]}.` : `${segments[0]}.`;
|
|
34608
36967
|
const siblings = available.filter((name) => name.startsWith(prefix));
|
|
34609
36968
|
return siblings.length > 0 ? siblings : available;
|
|
34610
36969
|
}
|
|
34611
|
-
var schemaCommand =
|
|
36970
|
+
var schemaCommand = defineCommand171({
|
|
34612
36971
|
meta: {
|
|
34613
36972
|
name: "schema",
|
|
34614
36973
|
description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
|
|
@@ -34652,10 +37011,10 @@ var schemaCommand = defineCommand163({
|
|
|
34652
37011
|
});
|
|
34653
37012
|
|
|
34654
37013
|
// src/commands/tag-manager/index.ts
|
|
34655
|
-
import { defineCommand as
|
|
37014
|
+
import { defineCommand as defineCommand175 } from "citty";
|
|
34656
37015
|
|
|
34657
37016
|
// src/commands/tag-manager/draft.ts
|
|
34658
|
-
import { defineCommand as
|
|
37017
|
+
import { defineCommand as defineCommand172 } from "citty";
|
|
34659
37018
|
|
|
34660
37019
|
// src/commands/tag-manager/shared.ts
|
|
34661
37020
|
import { readFileSync as readFileSync13 } from "fs";
|
|
@@ -34722,10 +37081,10 @@ async function stageOp4(op) {
|
|
|
34722
37081
|
handleError4(err);
|
|
34723
37082
|
}
|
|
34724
37083
|
}
|
|
34725
|
-
async function draftAction3(
|
|
37084
|
+
async function draftAction3(path34, body, chat) {
|
|
34726
37085
|
const chatId = resolveChatId(chat);
|
|
34727
37086
|
try {
|
|
34728
|
-
const data = await apiPost(
|
|
37087
|
+
const data = await apiPost(path34, { chatId, ...body });
|
|
34729
37088
|
writeJsonEnvelope({ ok: true, data });
|
|
34730
37089
|
return data;
|
|
34731
37090
|
} catch (err) {
|
|
@@ -34778,13 +37137,13 @@ registerSchema({
|
|
|
34778
37137
|
chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
|
|
34779
37138
|
}
|
|
34780
37139
|
});
|
|
34781
|
-
var draftCommand4 =
|
|
37140
|
+
var draftCommand4 = defineCommand172({
|
|
34782
37141
|
meta: {
|
|
34783
37142
|
name: "draft",
|
|
34784
37143
|
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."
|
|
34785
37144
|
},
|
|
34786
37145
|
subCommands: {
|
|
34787
|
-
list:
|
|
37146
|
+
list: defineCommand172({
|
|
34788
37147
|
meta: {
|
|
34789
37148
|
name: "list",
|
|
34790
37149
|
description: "Review everything staged on this chat (--json for the raw envelope)"
|
|
@@ -34797,7 +37156,7 @@ var draftCommand4 = defineCommand164({
|
|
|
34797
37156
|
await draftList2(args.json === true, args.chat);
|
|
34798
37157
|
}
|
|
34799
37158
|
}),
|
|
34800
|
-
show:
|
|
37159
|
+
show: defineCommand172({
|
|
34801
37160
|
meta: {
|
|
34802
37161
|
name: "show",
|
|
34803
37162
|
description: "Print the full staged payload for one change \u2014 the receipt to verify it looks right before publish (never truncated)."
|
|
@@ -34814,7 +37173,7 @@ var draftCommand4 = defineCommand164({
|
|
|
34814
37173
|
);
|
|
34815
37174
|
}
|
|
34816
37175
|
}),
|
|
34817
|
-
amend:
|
|
37176
|
+
amend: defineCommand172({
|
|
34818
37177
|
meta: {
|
|
34819
37178
|
name: "amend",
|
|
34820
37179
|
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."
|
|
@@ -34831,7 +37190,7 @@ var draftCommand4 = defineCommand164({
|
|
|
34831
37190
|
});
|
|
34832
37191
|
}
|
|
34833
37192
|
}),
|
|
34834
|
-
remove:
|
|
37193
|
+
remove: defineCommand172({
|
|
34835
37194
|
meta: { name: "remove", description: "Remove one staged change (cascades to anything depending on it)" },
|
|
34836
37195
|
args: { ref: { type: "positional", description: "Staged ref (gtm_temp_*) or target", required: false } },
|
|
34837
37196
|
run: async ({ args }) => {
|
|
@@ -34840,7 +37199,7 @@ var draftCommand4 = defineCommand164({
|
|
|
34840
37199
|
});
|
|
34841
37200
|
}
|
|
34842
37201
|
}),
|
|
34843
|
-
clear:
|
|
37202
|
+
clear: defineCommand172({
|
|
34844
37203
|
meta: { name: "clear", description: "Discard all Tag Manager changes staged on this chat" },
|
|
34845
37204
|
run: async () => {
|
|
34846
37205
|
await draftAction3("/api/tag-manager/draft/clear", {});
|
|
@@ -34850,7 +37209,7 @@ var draftCommand4 = defineCommand164({
|
|
|
34850
37209
|
});
|
|
34851
37210
|
|
|
34852
37211
|
// src/commands/tag-manager/read.ts
|
|
34853
|
-
import { defineCommand as
|
|
37212
|
+
import { defineCommand as defineCommand173 } from "citty";
|
|
34854
37213
|
registerSchema({
|
|
34855
37214
|
command: "tagManager.containers",
|
|
34856
37215
|
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.",
|
|
@@ -34891,7 +37250,7 @@ function containersHints(containers) {
|
|
|
34891
37250
|
}))
|
|
34892
37251
|
});
|
|
34893
37252
|
}
|
|
34894
|
-
var containersCommand =
|
|
37253
|
+
var containersCommand = defineCommand173({
|
|
34895
37254
|
meta: {
|
|
34896
37255
|
name: "containers",
|
|
34897
37256
|
description: `List Tag Manager containers reachable by this company's connection.
|
|
@@ -34908,7 +37267,7 @@ Start here:
|
|
|
34908
37267
|
}
|
|
34909
37268
|
}
|
|
34910
37269
|
});
|
|
34911
|
-
var readCommand =
|
|
37270
|
+
var readCommand = defineCommand173({
|
|
34912
37271
|
meta: {
|
|
34913
37272
|
name: "read",
|
|
34914
37273
|
description: `Read the current contents of the Tag Manager container \u2014 always do this before staging changes.
|
|
@@ -34950,7 +37309,7 @@ Examples:
|
|
|
34950
37309
|
});
|
|
34951
37310
|
|
|
34952
37311
|
// src/commands/tag-manager/write-commands.ts
|
|
34953
|
-
import { defineCommand as
|
|
37312
|
+
import { defineCommand as defineCommand174 } from "citty";
|
|
34954
37313
|
var CONTAINER_ARG_DESCRIPTION = "Numeric container id (optional only when one container is connected \u2014 run `baker tag-manager containers`)";
|
|
34955
37314
|
var ENTITIES = [
|
|
34956
37315
|
{
|
|
@@ -35006,10 +37365,10 @@ for (const { entity, noun, createHint } of ENTITIES) {
|
|
|
35006
37365
|
});
|
|
35007
37366
|
}
|
|
35008
37367
|
function entityCommand(entity, noun, example) {
|
|
35009
|
-
return
|
|
37368
|
+
return defineCommand174({
|
|
35010
37369
|
meta: { name: entity, description: `Stage ${noun} changes on this chat's Tag Manager draft` },
|
|
35011
37370
|
subCommands: {
|
|
35012
|
-
create:
|
|
37371
|
+
create: defineCommand174({
|
|
35013
37372
|
meta: {
|
|
35014
37373
|
name: "create",
|
|
35015
37374
|
description: `Stage a new ${noun}
|
|
@@ -35031,7 +37390,7 @@ Examples:
|
|
|
35031
37390
|
});
|
|
35032
37391
|
}
|
|
35033
37392
|
}),
|
|
35034
|
-
update:
|
|
37393
|
+
update: defineCommand174({
|
|
35035
37394
|
meta: {
|
|
35036
37395
|
name: "update",
|
|
35037
37396
|
description: `Stage an update to an existing ${noun} (pass its id or path)`
|
|
@@ -35051,7 +37410,7 @@ Examples:
|
|
|
35051
37410
|
});
|
|
35052
37411
|
}
|
|
35053
37412
|
}),
|
|
35054
|
-
delete:
|
|
37413
|
+
delete: defineCommand174({
|
|
35055
37414
|
meta: { name: "delete", description: `Stage the deletion of a ${noun} (pass its id or path)` },
|
|
35056
37415
|
args: {
|
|
35057
37416
|
id: { type: "positional", description: `${noun} id or path`, required: false },
|
|
@@ -35092,7 +37451,7 @@ function builtinTypes(args) {
|
|
|
35092
37451
|
}
|
|
35093
37452
|
return raw.split(",").map((entry) => entry.trim());
|
|
35094
37453
|
}
|
|
35095
|
-
var builtinCommand =
|
|
37454
|
+
var builtinCommand = defineCommand174({
|
|
35096
37455
|
meta: {
|
|
35097
37456
|
name: "builtin",
|
|
35098
37457
|
description: `Enable or disable built-in variables
|
|
@@ -35102,7 +37461,7 @@ Examples:
|
|
|
35102
37461
|
baker tag-manager builtin disable --types formId`
|
|
35103
37462
|
},
|
|
35104
37463
|
subCommands: {
|
|
35105
|
-
enable:
|
|
37464
|
+
enable: defineCommand174({
|
|
35106
37465
|
meta: { name: "enable", description: "Stage enabling built-in variables" },
|
|
35107
37466
|
args: {
|
|
35108
37467
|
types: { type: "string", description: "Comma-separated types", required: false },
|
|
@@ -35116,7 +37475,7 @@ Examples:
|
|
|
35116
37475
|
});
|
|
35117
37476
|
}
|
|
35118
37477
|
}),
|
|
35119
|
-
disable:
|
|
37478
|
+
disable: defineCommand174({
|
|
35120
37479
|
meta: { name: "disable", description: "Stage disabling built-in variables" },
|
|
35121
37480
|
args: {
|
|
35122
37481
|
types: { type: "string", description: "Comma-separated types", required: false },
|
|
@@ -35134,7 +37493,7 @@ Examples:
|
|
|
35134
37493
|
});
|
|
35135
37494
|
|
|
35136
37495
|
// src/commands/tag-manager/index.ts
|
|
35137
|
-
var tagManagerCommand =
|
|
37496
|
+
var tagManagerCommand = defineCommand175({
|
|
35138
37497
|
meta: {
|
|
35139
37498
|
name: "tag-manager",
|
|
35140
37499
|
description: `Read and change what lives inside the client's Google Tag Manager container \u2014 tags, triggers, variables, folders and built-in variables.
|
|
@@ -35171,7 +37530,7 @@ Full guide: __tooling__/docs/tools/baker/tag-manager.md`
|
|
|
35171
37530
|
});
|
|
35172
37531
|
|
|
35173
37532
|
// src/commands/tags/index.ts
|
|
35174
|
-
import { defineCommand as
|
|
37533
|
+
import { defineCommand as defineCommand176 } from "citty";
|
|
35175
37534
|
|
|
35176
37535
|
// src/commands/tags/shared.ts
|
|
35177
37536
|
function failApi3(err) {
|
|
@@ -35240,7 +37599,7 @@ async function listTags(json) {
|
|
|
35240
37599
|
var listArgs9 = {
|
|
35241
37600
|
json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable list" }
|
|
35242
37601
|
};
|
|
35243
|
-
var listCommand15 =
|
|
37602
|
+
var listCommand15 = defineCommand176({
|
|
35244
37603
|
meta: {
|
|
35245
37604
|
name: "list",
|
|
35246
37605
|
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"
|
|
@@ -35259,7 +37618,7 @@ async function listDraft3(chat) {
|
|
|
35259
37618
|
failApi3(err);
|
|
35260
37619
|
}
|
|
35261
37620
|
}
|
|
35262
|
-
var draftCommand5 =
|
|
37621
|
+
var draftCommand5 = defineCommand176({
|
|
35263
37622
|
meta: {
|
|
35264
37623
|
name: "draft",
|
|
35265
37624
|
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."
|
|
@@ -35269,7 +37628,7 @@ var draftCommand5 = defineCommand168({
|
|
|
35269
37628
|
await listDraft3(args.chat);
|
|
35270
37629
|
}
|
|
35271
37630
|
});
|
|
35272
|
-
var tagsCommand3 =
|
|
37631
|
+
var tagsCommand3 = defineCommand176({
|
|
35273
37632
|
meta: {
|
|
35274
37633
|
name: "tags",
|
|
35275
37634
|
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.
|
|
@@ -35298,10 +37657,10 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
|
|
|
35298
37657
|
});
|
|
35299
37658
|
|
|
35300
37659
|
// src/commands/testimonials/index.ts
|
|
35301
|
-
import { defineCommand as
|
|
37660
|
+
import { defineCommand as defineCommand180 } from "citty";
|
|
35302
37661
|
|
|
35303
37662
|
// src/commands/testimonials/get.ts
|
|
35304
|
-
import { defineCommand as
|
|
37663
|
+
import { defineCommand as defineCommand177 } from "citty";
|
|
35305
37664
|
registerSchema({
|
|
35306
37665
|
command: "testimonials.get",
|
|
35307
37666
|
description: "Get a single testimonial by ID",
|
|
@@ -35309,7 +37668,7 @@ registerSchema({
|
|
|
35309
37668
|
id: { type: "string", description: "Testimonial ID", required: true }
|
|
35310
37669
|
}
|
|
35311
37670
|
});
|
|
35312
|
-
var getCommand4 =
|
|
37671
|
+
var getCommand4 = defineCommand177({
|
|
35313
37672
|
meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
|
|
35314
37673
|
args: {
|
|
35315
37674
|
id: { type: "positional", description: "Testimonial ID", required: false },
|
|
@@ -35346,7 +37705,7 @@ var getCommand4 = defineCommand169({
|
|
|
35346
37705
|
});
|
|
35347
37706
|
|
|
35348
37707
|
// src/commands/testimonials/list.ts
|
|
35349
|
-
import { defineCommand as
|
|
37708
|
+
import { defineCommand as defineCommand178 } from "citty";
|
|
35350
37709
|
registerSchema({
|
|
35351
37710
|
command: "testimonials.list",
|
|
35352
37711
|
description: "List testimonials with optional filters.",
|
|
@@ -35376,7 +37735,7 @@ registerSchema({
|
|
|
35376
37735
|
limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
|
|
35377
37736
|
}
|
|
35378
37737
|
});
|
|
35379
|
-
var listCommand16 =
|
|
37738
|
+
var listCommand16 = defineCommand178({
|
|
35380
37739
|
meta: {
|
|
35381
37740
|
name: "list",
|
|
35382
37741
|
description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
|
|
@@ -35425,7 +37784,7 @@ var listCommand16 = defineCommand170({
|
|
|
35425
37784
|
});
|
|
35426
37785
|
|
|
35427
37786
|
// src/commands/testimonials/search.ts
|
|
35428
|
-
import { defineCommand as
|
|
37787
|
+
import { defineCommand as defineCommand179 } from "citty";
|
|
35429
37788
|
function languageBiasHint(results, requestedLanguage) {
|
|
35430
37789
|
if (requestedLanguage) {
|
|
35431
37790
|
return null;
|
|
@@ -35503,7 +37862,7 @@ function buildSearchRequest(query, args) {
|
|
|
35503
37862
|
}
|
|
35504
37863
|
return body;
|
|
35505
37864
|
}
|
|
35506
|
-
var
|
|
37865
|
+
var searchCommand3 = defineCommand179({
|
|
35507
37866
|
meta: {
|
|
35508
37867
|
name: "search",
|
|
35509
37868
|
description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
|
|
@@ -35559,7 +37918,7 @@ var searchCommand2 = defineCommand171({
|
|
|
35559
37918
|
var tagsCommand4 = makeTagsCommand("testimonials", "testimonial", "/api/testimonials/tags");
|
|
35560
37919
|
|
|
35561
37920
|
// src/commands/testimonials/index.ts
|
|
35562
|
-
var testimonialsCommand =
|
|
37921
|
+
var testimonialsCommand = defineCommand180({
|
|
35563
37922
|
meta: {
|
|
35564
37923
|
name: "testimonials",
|
|
35565
37924
|
description: `Find and browse testimonials in Baker. Subcommands: search, get, list, tags.
|
|
@@ -35574,17 +37933,17 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
|
|
|
35574
37933
|
},
|
|
35575
37934
|
subCommands: {
|
|
35576
37935
|
get: getCommand4,
|
|
35577
|
-
search:
|
|
37936
|
+
search: searchCommand3,
|
|
35578
37937
|
list: listCommand16,
|
|
35579
37938
|
tags: tagsCommand4
|
|
35580
37939
|
}
|
|
35581
37940
|
});
|
|
35582
37941
|
|
|
35583
37942
|
// src/commands/videos/index.ts
|
|
35584
|
-
import { defineCommand as
|
|
37943
|
+
import { defineCommand as defineCommand185 } from "citty";
|
|
35585
37944
|
|
|
35586
37945
|
// src/commands/videos/delete.ts
|
|
35587
|
-
import { defineCommand as
|
|
37946
|
+
import { defineCommand as defineCommand181 } from "citty";
|
|
35588
37947
|
registerSchema({
|
|
35589
37948
|
command: "videos.delete",
|
|
35590
37949
|
description: "Delete a video by ID",
|
|
@@ -35598,7 +37957,7 @@ registerSchema({
|
|
|
35598
37957
|
}
|
|
35599
37958
|
}
|
|
35600
37959
|
});
|
|
35601
|
-
var deleteCommand3 =
|
|
37960
|
+
var deleteCommand3 = defineCommand181({
|
|
35602
37961
|
meta: {
|
|
35603
37962
|
name: "delete",
|
|
35604
37963
|
description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
|
|
@@ -35639,7 +37998,7 @@ var deleteCommand3 = defineCommand173({
|
|
|
35639
37998
|
});
|
|
35640
37999
|
|
|
35641
38000
|
// src/commands/videos/get.ts
|
|
35642
|
-
import { defineCommand as
|
|
38001
|
+
import { defineCommand as defineCommand182 } from "citty";
|
|
35643
38002
|
registerSchema({
|
|
35644
38003
|
command: "videos.get",
|
|
35645
38004
|
description: "Get a single video by ID",
|
|
@@ -35647,7 +38006,7 @@ registerSchema({
|
|
|
35647
38006
|
id: { type: "string", description: "Video ID", required: true }
|
|
35648
38007
|
}
|
|
35649
38008
|
});
|
|
35650
|
-
var getCommand5 =
|
|
38009
|
+
var getCommand5 = defineCommand182({
|
|
35651
38010
|
meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
|
|
35652
38011
|
args: {
|
|
35653
38012
|
id: { type: "positional", description: "Video ID", required: false },
|
|
@@ -35684,7 +38043,7 @@ var getCommand5 = defineCommand174({
|
|
|
35684
38043
|
});
|
|
35685
38044
|
|
|
35686
38045
|
// src/commands/videos/search.ts
|
|
35687
|
-
import { defineCommand as
|
|
38046
|
+
import { defineCommand as defineCommand183 } from "citty";
|
|
35688
38047
|
registerSchema({
|
|
35689
38048
|
command: "videos.search",
|
|
35690
38049
|
description: "Search videos by text query. Only returns ready videos.",
|
|
@@ -35694,7 +38053,7 @@ registerSchema({
|
|
|
35694
38053
|
tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
|
|
35695
38054
|
}
|
|
35696
38055
|
});
|
|
35697
|
-
var
|
|
38056
|
+
var searchCommand4 = defineCommand183({
|
|
35698
38057
|
meta: {
|
|
35699
38058
|
name: "search",
|
|
35700
38059
|
description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
|
|
@@ -35744,9 +38103,9 @@ var searchCommand3 = defineCommand175({
|
|
|
35744
38103
|
var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
35745
38104
|
|
|
35746
38105
|
// src/commands/videos/upload.ts
|
|
35747
|
-
import { readFile as
|
|
38106
|
+
import { readFile as readFile24, stat as stat7 } from "fs/promises";
|
|
35748
38107
|
import { extname as extname3 } from "path";
|
|
35749
|
-
import { defineCommand as
|
|
38108
|
+
import { defineCommand as defineCommand184 } from "citty";
|
|
35750
38109
|
var MIME_MAP = {
|
|
35751
38110
|
".mp4": "video/mp4",
|
|
35752
38111
|
".mov": "video/quicktime",
|
|
@@ -35780,7 +38139,7 @@ function detectContentType(filePath) {
|
|
|
35780
38139
|
}
|
|
35781
38140
|
return mime;
|
|
35782
38141
|
}
|
|
35783
|
-
var uploadCommand2 =
|
|
38142
|
+
var uploadCommand2 = defineCommand184({
|
|
35784
38143
|
meta: {
|
|
35785
38144
|
name: "upload",
|
|
35786
38145
|
description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
|
|
@@ -35809,7 +38168,7 @@ var uploadCommand2 = defineCommand176({
|
|
|
35809
38168
|
return;
|
|
35810
38169
|
}
|
|
35811
38170
|
const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
|
|
35812
|
-
const fileBuffer = await
|
|
38171
|
+
const fileBuffer = await readFile24(filePath);
|
|
35813
38172
|
const uploadResponse = await fetch(uploadUrl, {
|
|
35814
38173
|
method: "PUT",
|
|
35815
38174
|
headers: { "Content-Type": contentType },
|
|
@@ -35834,7 +38193,7 @@ var uploadCommand2 = defineCommand176({
|
|
|
35834
38193
|
});
|
|
35835
38194
|
|
|
35836
38195
|
// src/commands/videos/index.ts
|
|
35837
|
-
var videosCommand =
|
|
38196
|
+
var videosCommand = defineCommand185({
|
|
35838
38197
|
meta: {
|
|
35839
38198
|
name: "videos",
|
|
35840
38199
|
description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
|
|
@@ -35850,7 +38209,7 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
|
|
|
35850
38209
|
},
|
|
35851
38210
|
subCommands: {
|
|
35852
38211
|
get: getCommand5,
|
|
35853
|
-
search:
|
|
38212
|
+
search: searchCommand4,
|
|
35854
38213
|
upload: uploadCommand2,
|
|
35855
38214
|
delete: deleteCommand3,
|
|
35856
38215
|
tags: tagsCommand5
|
|
@@ -35858,19 +38217,19 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
|
|
|
35858
38217
|
});
|
|
35859
38218
|
|
|
35860
38219
|
// src/commands/winning-ads/index.ts
|
|
35861
|
-
import { defineCommand as
|
|
38220
|
+
import { defineCommand as defineCommand198 } from "citty";
|
|
35862
38221
|
|
|
35863
38222
|
// src/commands/winning-ads/advertisers.ts
|
|
35864
|
-
import { defineCommand as
|
|
38223
|
+
import { defineCommand as defineCommand186 } from "citty";
|
|
35865
38224
|
|
|
35866
38225
|
// src/commands/winning-ads/shared.ts
|
|
35867
|
-
function
|
|
38226
|
+
function splitList2(value) {
|
|
35868
38227
|
if (!value) {
|
|
35869
38228
|
return [];
|
|
35870
38229
|
}
|
|
35871
38230
|
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
35872
38231
|
}
|
|
35873
|
-
function
|
|
38232
|
+
function reportError2(err) {
|
|
35874
38233
|
if (err instanceof ApiError) {
|
|
35875
38234
|
writeJson({ ok: false, error: { code: err.code, message: err.message } });
|
|
35876
38235
|
process.exit(1);
|
|
@@ -35914,7 +38273,7 @@ function advertiserNormalizer(record, full) {
|
|
|
35914
38273
|
last_synced_at: record.last_synced_at ?? null
|
|
35915
38274
|
};
|
|
35916
38275
|
}
|
|
35917
|
-
var advertisersCommand2 =
|
|
38276
|
+
var advertisersCommand2 = defineCommand186({
|
|
35918
38277
|
meta: {
|
|
35919
38278
|
name: "advertisers",
|
|
35920
38279
|
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'
|
|
@@ -35966,13 +38325,13 @@ var advertisersCommand2 = defineCommand178({
|
|
|
35966
38325
|
advertiserNormalizer
|
|
35967
38326
|
);
|
|
35968
38327
|
} catch (err) {
|
|
35969
|
-
|
|
38328
|
+
reportError2(err);
|
|
35970
38329
|
}
|
|
35971
38330
|
}
|
|
35972
38331
|
});
|
|
35973
38332
|
|
|
35974
38333
|
// src/commands/winning-ads/brief.ts
|
|
35975
|
-
import { defineCommand as
|
|
38334
|
+
import { defineCommand as defineCommand187 } from "citty";
|
|
35976
38335
|
registerSchema({
|
|
35977
38336
|
command: "winning-ads.brief",
|
|
35978
38337
|
description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
|
|
@@ -36018,7 +38377,7 @@ function parseDna(raw) {
|
|
|
36018
38377
|
}
|
|
36019
38378
|
return parsed;
|
|
36020
38379
|
}
|
|
36021
|
-
var briefCommand =
|
|
38380
|
+
var briefCommand = defineCommand187({
|
|
36022
38381
|
meta: {
|
|
36023
38382
|
name: "brief",
|
|
36024
38383
|
description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
|
|
@@ -36048,13 +38407,13 @@ var briefCommand = defineCommand179({
|
|
|
36048
38407
|
const data = await apiPost("/api/ad-library/brief", body);
|
|
36049
38408
|
writeJson({ ok: true, data });
|
|
36050
38409
|
} catch (err) {
|
|
36051
|
-
|
|
38410
|
+
reportError2(err);
|
|
36052
38411
|
}
|
|
36053
38412
|
}
|
|
36054
38413
|
});
|
|
36055
38414
|
|
|
36056
38415
|
// src/commands/winning-ads/content.ts
|
|
36057
|
-
import { defineCommand as
|
|
38416
|
+
import { defineCommand as defineCommand188 } from "citty";
|
|
36058
38417
|
registerSchema({
|
|
36059
38418
|
command: "winning-ads.content",
|
|
36060
38419
|
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.",
|
|
@@ -36067,7 +38426,7 @@ registerSchema({
|
|
|
36067
38426
|
}
|
|
36068
38427
|
}
|
|
36069
38428
|
});
|
|
36070
|
-
var contentCommand =
|
|
38429
|
+
var contentCommand = defineCommand188({
|
|
36071
38430
|
meta: {
|
|
36072
38431
|
name: "content",
|
|
36073
38432
|
description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
|
|
@@ -36110,16 +38469,16 @@ var contentCommand = defineCommand180({
|
|
|
36110
38469
|
adContentNormalizer
|
|
36111
38470
|
);
|
|
36112
38471
|
} catch (err) {
|
|
36113
|
-
|
|
38472
|
+
reportError2(err);
|
|
36114
38473
|
}
|
|
36115
38474
|
}
|
|
36116
38475
|
});
|
|
36117
38476
|
|
|
36118
38477
|
// src/commands/winning-ads/feed.ts
|
|
36119
|
-
import { defineCommand as
|
|
38478
|
+
import { defineCommand as defineCommand189 } from "citty";
|
|
36120
38479
|
function buildFeedParams(input) {
|
|
36121
38480
|
const params = {};
|
|
36122
|
-
const advertiser =
|
|
38481
|
+
const advertiser = splitList2(input.advertiser);
|
|
36123
38482
|
if (advertiser.length > 0) {
|
|
36124
38483
|
params.advertiser = advertiser.join(",");
|
|
36125
38484
|
}
|
|
@@ -36132,11 +38491,11 @@ function buildFeedParams(input) {
|
|
|
36132
38491
|
if (input.limit !== void 0 && input.limit !== "") {
|
|
36133
38492
|
params.limit = input.limit;
|
|
36134
38493
|
}
|
|
36135
|
-
const winnerCategory =
|
|
38494
|
+
const winnerCategory = splitList2(input.winnerCategory);
|
|
36136
38495
|
if (winnerCategory.length > 0) {
|
|
36137
38496
|
params.winner_category = winnerCategory.join(",");
|
|
36138
38497
|
}
|
|
36139
|
-
const format =
|
|
38498
|
+
const format = splitList2(input.format);
|
|
36140
38499
|
if (format.length > 0) {
|
|
36141
38500
|
params.format = format.join(",");
|
|
36142
38501
|
}
|
|
@@ -36168,7 +38527,7 @@ registerSchema({
|
|
|
36168
38527
|
format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
|
|
36169
38528
|
}
|
|
36170
38529
|
});
|
|
36171
|
-
var feedCommand =
|
|
38530
|
+
var feedCommand = defineCommand189({
|
|
36172
38531
|
meta: {
|
|
36173
38532
|
name: "feed",
|
|
36174
38533
|
description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
|
|
@@ -36247,13 +38606,13 @@ var feedCommand = defineCommand181({
|
|
|
36247
38606
|
`);
|
|
36248
38607
|
}
|
|
36249
38608
|
} catch (err) {
|
|
36250
|
-
|
|
38609
|
+
reportError2(err);
|
|
36251
38610
|
}
|
|
36252
38611
|
}
|
|
36253
38612
|
});
|
|
36254
38613
|
|
|
36255
38614
|
// src/commands/winning-ads/follow.ts
|
|
36256
|
-
import { defineCommand as
|
|
38615
|
+
import { defineCommand as defineCommand190 } from "citty";
|
|
36257
38616
|
var PLATFORMS = ["meta", "linkedin"];
|
|
36258
38617
|
registerSchema({
|
|
36259
38618
|
command: "winning-ads.follow",
|
|
@@ -36268,7 +38627,7 @@ registerSchema({
|
|
|
36268
38627
|
label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
|
|
36269
38628
|
}
|
|
36270
38629
|
});
|
|
36271
|
-
var followCommand =
|
|
38630
|
+
var followCommand = defineCommand190({
|
|
36272
38631
|
meta: {
|
|
36273
38632
|
name: "follow",
|
|
36274
38633
|
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'
|
|
@@ -36309,19 +38668,19 @@ var followCommand = defineCommand182({
|
|
|
36309
38668
|
}
|
|
36310
38669
|
writeJson({ ok: true, data, hints });
|
|
36311
38670
|
} catch (err) {
|
|
36312
|
-
|
|
38671
|
+
reportError2(err);
|
|
36313
38672
|
}
|
|
36314
38673
|
}
|
|
36315
38674
|
});
|
|
36316
38675
|
|
|
36317
38676
|
// src/commands/winning-ads/follow-competitors.ts
|
|
36318
|
-
import { defineCommand as
|
|
38677
|
+
import { defineCommand as defineCommand191 } from "citty";
|
|
36319
38678
|
var PLATFORMS2 = ["meta", "linkedin"];
|
|
36320
38679
|
var BATCH_TIMEOUT_MS = 3e5;
|
|
36321
38680
|
function buildFollowBatchBody(input) {
|
|
36322
38681
|
const seen = /* @__PURE__ */ new Set();
|
|
36323
38682
|
const inputs = [];
|
|
36324
|
-
for (const domain of
|
|
38683
|
+
for (const domain of splitList2(input.domains)) {
|
|
36325
38684
|
const key = domain.toLowerCase();
|
|
36326
38685
|
if (seen.has(key)) {
|
|
36327
38686
|
continue;
|
|
@@ -36348,7 +38707,7 @@ registerSchema({
|
|
|
36348
38707
|
}
|
|
36349
38708
|
}
|
|
36350
38709
|
});
|
|
36351
|
-
var followCompetitorsCommand =
|
|
38710
|
+
var followCompetitorsCommand = defineCommand191({
|
|
36352
38711
|
meta: {
|
|
36353
38712
|
name: "follow-competitors",
|
|
36354
38713
|
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"'
|
|
@@ -36417,13 +38776,13 @@ var followCompetitorsCommand = defineCommand183({
|
|
|
36417
38776
|
}
|
|
36418
38777
|
writeJson({ ok: true, data, hints: hints.length > 0 ? hints : void 0 });
|
|
36419
38778
|
} catch (err) {
|
|
36420
|
-
|
|
38779
|
+
reportError2(err);
|
|
36421
38780
|
}
|
|
36422
38781
|
}
|
|
36423
38782
|
});
|
|
36424
38783
|
|
|
36425
38784
|
// src/commands/winning-ads/following.ts
|
|
36426
|
-
import { defineCommand as
|
|
38785
|
+
import { defineCommand as defineCommand192 } from "citty";
|
|
36427
38786
|
registerSchema({
|
|
36428
38787
|
command: "winning-ads.following",
|
|
36429
38788
|
description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
|
|
@@ -36456,7 +38815,7 @@ function followingNormalizer(record, full) {
|
|
|
36456
38815
|
platforms: Array.isArray(record.platforms) ? record.platforms : []
|
|
36457
38816
|
};
|
|
36458
38817
|
}
|
|
36459
|
-
var followingCommand =
|
|
38818
|
+
var followingCommand = defineCommand192({
|
|
36460
38819
|
meta: {
|
|
36461
38820
|
name: "following",
|
|
36462
38821
|
description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
|
|
@@ -36485,13 +38844,13 @@ var followingCommand = defineCommand184({
|
|
|
36485
38844
|
followingNormalizer
|
|
36486
38845
|
);
|
|
36487
38846
|
} catch (err) {
|
|
36488
|
-
|
|
38847
|
+
reportError2(err);
|
|
36489
38848
|
}
|
|
36490
38849
|
}
|
|
36491
38850
|
});
|
|
36492
38851
|
|
|
36493
38852
|
// src/commands/winning-ads/patterns.ts
|
|
36494
|
-
import { defineCommand as
|
|
38853
|
+
import { defineCommand as defineCommand193 } from "citty";
|
|
36495
38854
|
registerSchema({
|
|
36496
38855
|
command: "winning-ads.patterns",
|
|
36497
38856
|
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.",
|
|
@@ -36506,8 +38865,8 @@ registerSchema({
|
|
|
36506
38865
|
}
|
|
36507
38866
|
});
|
|
36508
38867
|
function buildPatternsBody(args) {
|
|
36509
|
-
const winners =
|
|
36510
|
-
const duds =
|
|
38868
|
+
const winners = splitList2(args.winners);
|
|
38869
|
+
const duds = splitList2(args.duds);
|
|
36511
38870
|
if (!winners.length || !duds.length) {
|
|
36512
38871
|
throw new Error("Provide at least one ad id for both --winners and --duds");
|
|
36513
38872
|
}
|
|
@@ -36530,7 +38889,7 @@ function discriminatorRow(record) {
|
|
|
36530
38889
|
top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
|
|
36531
38890
|
};
|
|
36532
38891
|
}
|
|
36533
|
-
var patternsCommand =
|
|
38892
|
+
var patternsCommand = defineCommand193({
|
|
36534
38893
|
meta: {
|
|
36535
38894
|
name: "patterns",
|
|
36536
38895
|
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"
|
|
@@ -36580,13 +38939,13 @@ var patternsCommand = defineCommand185({
|
|
|
36580
38939
|
(record) => discriminatorRow(record)
|
|
36581
38940
|
);
|
|
36582
38941
|
} catch (err) {
|
|
36583
|
-
|
|
38942
|
+
reportError2(err);
|
|
36584
38943
|
}
|
|
36585
38944
|
}
|
|
36586
38945
|
});
|
|
36587
38946
|
|
|
36588
38947
|
// src/commands/winning-ads/search.ts
|
|
36589
|
-
import { defineCommand as
|
|
38948
|
+
import { defineCommand as defineCommand194 } from "citty";
|
|
36590
38949
|
registerSchema({
|
|
36591
38950
|
command: "winning-ads.search",
|
|
36592
38951
|
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.",
|
|
@@ -36656,7 +39015,7 @@ function setNumber(body, key, value) {
|
|
|
36656
39015
|
}
|
|
36657
39016
|
}
|
|
36658
39017
|
function setList(target, key, value) {
|
|
36659
|
-
const list =
|
|
39018
|
+
const list = splitList2(value);
|
|
36660
39019
|
if (list.length) {
|
|
36661
39020
|
target[key] = list;
|
|
36662
39021
|
}
|
|
@@ -36666,7 +39025,7 @@ function setString(target, key, value) {
|
|
|
36666
39025
|
target[key] = value;
|
|
36667
39026
|
}
|
|
36668
39027
|
}
|
|
36669
|
-
function
|
|
39028
|
+
function buildSearchBody2(args) {
|
|
36670
39029
|
const body = {};
|
|
36671
39030
|
if (args.query) {
|
|
36672
39031
|
body.free_text_query = args.query;
|
|
@@ -36694,7 +39053,7 @@ function buildSearchBody(args) {
|
|
|
36694
39053
|
}
|
|
36695
39054
|
return body;
|
|
36696
39055
|
}
|
|
36697
|
-
var
|
|
39056
|
+
var searchCommand5 = defineCommand194({
|
|
36698
39057
|
meta: {
|
|
36699
39058
|
name: "search",
|
|
36700
39059
|
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"
|
|
@@ -36770,7 +39129,7 @@ var searchCommand4 = defineCommand186({
|
|
|
36770
39129
|
firstSeenBefore: args["first-seen-before"],
|
|
36771
39130
|
hookArchetype: args["hook-archetype"]
|
|
36772
39131
|
};
|
|
36773
|
-
const body =
|
|
39132
|
+
const body = buildSearchBody2(searchArgs);
|
|
36774
39133
|
if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
|
|
36775
39134
|
writeJson({
|
|
36776
39135
|
ok: false,
|
|
@@ -36803,13 +39162,13 @@ var searchCommand4 = defineCommand186({
|
|
|
36803
39162
|
winningAdNormalizer
|
|
36804
39163
|
);
|
|
36805
39164
|
} catch (err) {
|
|
36806
|
-
|
|
39165
|
+
reportError2(err);
|
|
36807
39166
|
}
|
|
36808
39167
|
}
|
|
36809
39168
|
});
|
|
36810
39169
|
|
|
36811
39170
|
// src/commands/winning-ads/seeds.ts
|
|
36812
|
-
import { defineCommand as
|
|
39171
|
+
import { defineCommand as defineCommand195 } from "citty";
|
|
36813
39172
|
function leanRow(r) {
|
|
36814
39173
|
return {
|
|
36815
39174
|
key: r.key,
|
|
@@ -36837,7 +39196,7 @@ function makeSeedCommand(opts) {
|
|
|
36837
39196
|
limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
|
|
36838
39197
|
}
|
|
36839
39198
|
});
|
|
36840
|
-
return
|
|
39199
|
+
return defineCommand195({
|
|
36841
39200
|
meta: { name: opts.name, description: opts.description },
|
|
36842
39201
|
args: {
|
|
36843
39202
|
platform: { type: "string", description: "Single platform to segment on", required: false },
|
|
@@ -36864,7 +39223,7 @@ function makeSeedCommand(opts) {
|
|
|
36864
39223
|
}
|
|
36865
39224
|
writeOutput({ ok: true, data: projected }, output);
|
|
36866
39225
|
} catch (err) {
|
|
36867
|
-
|
|
39226
|
+
reportError2(err);
|
|
36868
39227
|
}
|
|
36869
39228
|
}
|
|
36870
39229
|
});
|
|
@@ -36886,7 +39245,7 @@ var formatsCommand = makeSeedCommand({
|
|
|
36886
39245
|
});
|
|
36887
39246
|
|
|
36888
39247
|
// src/commands/winning-ads/unfollow.ts
|
|
36889
|
-
import { defineCommand as
|
|
39248
|
+
import { defineCommand as defineCommand196 } from "citty";
|
|
36890
39249
|
registerSchema({
|
|
36891
39250
|
command: "winning-ads.unfollow",
|
|
36892
39251
|
description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
|
|
@@ -36894,7 +39253,7 @@ registerSchema({
|
|
|
36894
39253
|
advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
|
|
36895
39254
|
}
|
|
36896
39255
|
});
|
|
36897
|
-
var unfollowCommand =
|
|
39256
|
+
var unfollowCommand = defineCommand196({
|
|
36898
39257
|
meta: {
|
|
36899
39258
|
name: "unfollow",
|
|
36900
39259
|
description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
|
|
@@ -36909,13 +39268,13 @@ var unfollowCommand = defineCommand188({
|
|
|
36909
39268
|
});
|
|
36910
39269
|
writeJson({ ok: true, data });
|
|
36911
39270
|
} catch (err) {
|
|
36912
|
-
|
|
39271
|
+
reportError2(err);
|
|
36913
39272
|
}
|
|
36914
39273
|
}
|
|
36915
39274
|
});
|
|
36916
39275
|
|
|
36917
39276
|
// src/commands/winning-ads/winners.ts
|
|
36918
|
-
import { defineCommand as
|
|
39277
|
+
import { defineCommand as defineCommand197 } from "citty";
|
|
36919
39278
|
registerSchema({
|
|
36920
39279
|
command: "winning-ads.winners",
|
|
36921
39280
|
description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
|
|
@@ -36925,7 +39284,7 @@ registerSchema({
|
|
|
36925
39284
|
platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
|
|
36926
39285
|
}
|
|
36927
39286
|
});
|
|
36928
|
-
var winnersCommand =
|
|
39287
|
+
var winnersCommand = defineCommand197({
|
|
36929
39288
|
meta: {
|
|
36930
39289
|
name: "winners",
|
|
36931
39290
|
description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
|
|
@@ -36969,13 +39328,13 @@ var winnersCommand = defineCommand189({
|
|
|
36969
39328
|
winningAdNormalizer
|
|
36970
39329
|
);
|
|
36971
39330
|
} catch (err) {
|
|
36972
|
-
|
|
39331
|
+
reportError2(err);
|
|
36973
39332
|
}
|
|
36974
39333
|
}
|
|
36975
39334
|
});
|
|
36976
39335
|
|
|
36977
39336
|
// src/commands/winning-ads/index.ts
|
|
36978
|
-
var winningAdsCommand =
|
|
39337
|
+
var winningAdsCommand = defineCommand198({
|
|
36979
39338
|
meta: {
|
|
36980
39339
|
name: "winning-ads",
|
|
36981
39340
|
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.
|
|
@@ -37013,7 +39372,7 @@ Examples:
|
|
|
37013
39372
|
Full guide: __tooling__/docs/tools/baker/winning-ads.md`
|
|
37014
39373
|
},
|
|
37015
39374
|
subCommands: {
|
|
37016
|
-
search:
|
|
39375
|
+
search: searchCommand5,
|
|
37017
39376
|
advertisers: advertisersCommand2,
|
|
37018
39377
|
follow: followCommand,
|
|
37019
39378
|
"follow-competitors": followCompetitorsCommand,
|
|
@@ -37047,7 +39406,7 @@ function getCliVersion() {
|
|
|
37047
39406
|
}
|
|
37048
39407
|
|
|
37049
39408
|
// src/cli.ts
|
|
37050
|
-
var main =
|
|
39409
|
+
var main = defineCommand199({
|
|
37051
39410
|
meta: {
|
|
37052
39411
|
name: "baker",
|
|
37053
39412
|
version: getCliVersion(),
|