@koda-sl/baker-cli 0.257.0 → 0.258.0-dev.d0de9190f
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 +4 -2
- package/dist/{chunk-SJPRTSGY.js → chunk-HBDGUVUH.js} +4 -4
- package/dist/chunk-HBDGUVUH.js.map +1 -0
- package/dist/cli.js +202 -25
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-SJPRTSGY.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
ulid,
|
|
59
59
|
validateCanvasDeep,
|
|
60
60
|
ytDlpBlockSignal
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-HBDGUVUH.js";
|
|
62
62
|
import {
|
|
63
63
|
csvOrJson,
|
|
64
64
|
daysAgoIso,
|
|
@@ -2309,15 +2309,24 @@ var ASSET_FIELD_TYPES = [
|
|
|
2309
2309
|
* `assetLink.attach` — see that message for what Google actually does with it.
|
|
2310
2310
|
*/
|
|
2311
2311
|
"MARKETING_IMAGE",
|
|
2312
|
-
"BUSINESS_NAME"
|
|
2312
|
+
"BUSINESS_NAME",
|
|
2313
|
+
/**
|
|
2314
|
+
* The one image link a Search campaign takes. It carries the advertiser's logo beside the ad and
|
|
2315
|
+
* is the only image field type that survives Google's field-type check at all — swept live on v23
|
|
2316
|
+
* and v25, campaign and customer level both answer `CUSTOMER_NOT_VERIFIED` (an account fact,
|
|
2317
|
+
* fixed by advertiser verification) rather than a field-type refusal. Ad group level is not a
|
|
2318
|
+
* level Google links it at — see {@link BUSINESS_LOGO_LEVEL_MESSAGE}.
|
|
2319
|
+
*/
|
|
2320
|
+
"BUSINESS_LOGO"
|
|
2313
2321
|
];
|
|
2322
|
+
var BUSINESS_LOGO_LEVEL_MESSAGE = "Google links a BUSINESS_LOGO at campaign or customer level only \u2014 an ad group link comes back UNSUPPORTED_FIELD_TYPE. Attach it to the campaign instead.";
|
|
2314
2323
|
var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
2315
2324
|
"MARKETING_IMAGE",
|
|
2316
2325
|
"SQUARE_MARKETING_IMAGE",
|
|
2317
2326
|
"PORTRAIT_MARKETING_IMAGE",
|
|
2318
2327
|
"AD_IMAGE"
|
|
2319
2328
|
]);
|
|
2320
|
-
var IMAGE_ASSET_LINK_MESSAGE = "Google
|
|
2329
|
+
var IMAGE_ASSET_LINK_MESSAGE = "Google refuses the LINK, not the upload \u2014 so do not stop at this refusal, finish the handover. `assets create` with an image DOES publish: the image lands in the advertiser's Google Ads asset library and is theirs to use. Only this link is refused (MARKETING_IMAGE is a Performance Max asset-group slot \u2014 use `asset-groups attach`; AD_IMAGE comes back UNSUPPORTED_FIELD_TYPE at every level, on every API version). So: stage the image assets anyway, drop this link, and tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets \u2014 name them. Baker stages the Task for that step itself, alongside the image, so it is already in this Session's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload the images; they are uploaded. The one image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO at campaign or customer level, on a verified account.";
|
|
2321
2330
|
function isImageAssetLinkFieldType(fieldType) {
|
|
2322
2331
|
return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
|
|
2323
2332
|
}
|
|
@@ -3214,7 +3223,9 @@ var assetLinkAttachSchema = z4.object({
|
|
|
3214
3223
|
message: `parent is required for a ${value.level}-level asset link (--parent-ref)`
|
|
3215
3224
|
});
|
|
3216
3225
|
}
|
|
3217
|
-
if (
|
|
3226
|
+
if (value.fieldType === "BUSINESS_LOGO" && value.level === "adGroup") {
|
|
3227
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: BUSINESS_LOGO_LEVEL_MESSAGE });
|
|
3228
|
+
} else if (isImageAssetLinkFieldType(value.fieldType)) {
|
|
3218
3229
|
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
|
|
3219
3230
|
} else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
|
|
3220
3231
|
ctx.addIssue({
|
|
@@ -9909,7 +9920,7 @@ function renderSection(title, nodes, lines, showAccount) {
|
|
|
9909
9920
|
}
|
|
9910
9921
|
function renderAdvisories(data, lines) {
|
|
9911
9922
|
const advisories = data.advisories ?? [];
|
|
9912
|
-
if (advisories.length === 0) {
|
|
9923
|
+
if (advisories.length === 0 || data.status === "applied") {
|
|
9913
9924
|
return;
|
|
9914
9925
|
}
|
|
9915
9926
|
lines.push("");
|
|
@@ -9918,15 +9929,37 @@ function renderAdvisories(data, lines) {
|
|
|
9918
9929
|
lines.push(` \u2022 ${advisory.message}`);
|
|
9919
9930
|
}
|
|
9920
9931
|
}
|
|
9932
|
+
function renderOutcome(data, lines) {
|
|
9933
|
+
const counts = /* @__PURE__ */ new Map();
|
|
9934
|
+
for (const op of data.ops) {
|
|
9935
|
+
const status = op.result?.status ?? "pending";
|
|
9936
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
9937
|
+
}
|
|
9938
|
+
const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
|
|
9939
|
+
lines.push(`Google Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
|
|
9940
|
+
for (const op of data.ops) {
|
|
9941
|
+
const status = op.result?.status;
|
|
9942
|
+
if (status !== "failed" && status !== "skipped") {
|
|
9943
|
+
continue;
|
|
9944
|
+
}
|
|
9945
|
+
const reason = op.result?.error ?? op.result?.skippedBecause ?? "no reason recorded";
|
|
9946
|
+
lines.push(` \u2717 ${op.summary} \u2014 ${status}: ${reason}`);
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9921
9949
|
function renderDraftStatus(data) {
|
|
9922
9950
|
if (data.status === "none" || data.count === 0) {
|
|
9923
9951
|
return "No staged Google Ads changes on this chat.";
|
|
9924
9952
|
}
|
|
9925
9953
|
const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to Google Ads." : "Simulated \u2014 publishing completes the whole draft without calling Google Ads.";
|
|
9926
|
-
const lines = [
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9954
|
+
const lines = [];
|
|
9955
|
+
if (data.status === "applied") {
|
|
9956
|
+
renderOutcome(data, lines);
|
|
9957
|
+
} else if (data.status === "publishing") {
|
|
9958
|
+
lines.push(`Google Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
9959
|
+
} else {
|
|
9960
|
+
lines.push(`Google Ads \u2014 ${data.count} staged change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
9961
|
+
}
|
|
9962
|
+
lines.push(modeNote);
|
|
9930
9963
|
const tree = data.tree ?? [];
|
|
9931
9964
|
const showAccount = accountsInTree(tree).size > 1;
|
|
9932
9965
|
renderSection(
|
|
@@ -10043,15 +10076,17 @@ function loadJsonFileArg(path38) {
|
|
|
10043
10076
|
if (typeof path38 !== "string" || path38.length === 0) {
|
|
10044
10077
|
return {};
|
|
10045
10078
|
}
|
|
10079
|
+
const inline = path38.trimStart().startsWith("{");
|
|
10080
|
+
const source = inline ? "inline JSON" : path38;
|
|
10046
10081
|
try {
|
|
10047
|
-
const parsed = JSON.parse(readFileSync2(path38, "utf8"));
|
|
10082
|
+
const parsed = JSON.parse(inline ? path38 : readFileSync2(path38, "utf8"));
|
|
10048
10083
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10049
|
-
failWriteValidation(`${
|
|
10084
|
+
failWriteValidation(`${source} must contain a JSON object`);
|
|
10050
10085
|
}
|
|
10051
10086
|
return parsed;
|
|
10052
10087
|
} catch (err) {
|
|
10053
10088
|
if (err instanceof SyntaxError) {
|
|
10054
|
-
failWriteValidation(`${
|
|
10089
|
+
failWriteValidation(`${source} is not valid JSON: ${err.message}`);
|
|
10055
10090
|
}
|
|
10056
10091
|
throw err;
|
|
10057
10092
|
}
|
|
@@ -13877,7 +13912,10 @@ Examples:
|
|
|
13877
13912
|
import { defineCommand as defineCommand30 } from "citty";
|
|
13878
13913
|
var customerIdArg = { "customer-id": { type: "string", description: "10-digit Google Ads customer id" } };
|
|
13879
13914
|
var fileArg = {
|
|
13880
|
-
file: {
|
|
13915
|
+
file: {
|
|
13916
|
+
type: "string",
|
|
13917
|
+
description: "The op payload: a JSON file path, or the JSON object inline (flags override it)"
|
|
13918
|
+
}
|
|
13881
13919
|
};
|
|
13882
13920
|
var BUDGET_DEMAND_HINTS = [
|
|
13883
13921
|
"Check this daily budget against the search volume you measured for these keywords, not against the plan you had before you measured it. A budget many times larger than the traffic the target pool can absorb never spends, and reads to the client as a number nobody costed.",
|
|
@@ -14877,10 +14915,19 @@ function fileCreateCommand(name, kind, description, hints2) {
|
|
|
14877
14915
|
if (Object.keys(payload).length === 0) {
|
|
14878
14916
|
failWriteValidation(`${kind} needs --file with the op payload`);
|
|
14879
14917
|
}
|
|
14880
|
-
await stageGoogleOp({ kind, customerId, payload }, hints2);
|
|
14918
|
+
await stageGoogleOp({ kind, customerId, payload }, typeof hints2 === "function" ? hints2(payload) : hints2);
|
|
14881
14919
|
}
|
|
14882
14920
|
});
|
|
14883
14921
|
}
|
|
14922
|
+
function imageAssetHints(payload) {
|
|
14923
|
+
if (payload.type !== "image") {
|
|
14924
|
+
return [];
|
|
14925
|
+
}
|
|
14926
|
+
return [
|
|
14927
|
+
"This image publishes into the advertiser's Google Ads asset library \u2014 that half works. What Google refuses is the link that puts it on a Search campaign, so do not stage an `assets attach` for it.",
|
|
14928
|
+
"Finish the handover rather than reporting a limitation: tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets, and name the ones you staged. Baker stages the Task for that step itself the moment you stage the image, so it is already in this chat's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload images they already have. (A Performance Max asset group is different \u2014 images do link there, with `asset-groups attach`.)"
|
|
14929
|
+
];
|
|
14930
|
+
}
|
|
14884
14931
|
var assetsUpdateCommand = defineCommand30({
|
|
14885
14932
|
meta: {
|
|
14886
14933
|
name: "update",
|
|
@@ -14921,17 +14968,18 @@ var assetsUpdateCommand = defineCommand30({
|
|
|
14921
14968
|
var assetsCommand = defineCommand30({
|
|
14922
14969
|
meta: {
|
|
14923
14970
|
name: "assets",
|
|
14924
|
-
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign.
|
|
14971
|
+
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Image extensions are the one thing you cannot finish here \u2014 `create` uploads the image into the advertiser's account, but Google refuses the link that puts it on a Search campaign, so hand that one step over rather than telling them to upload anything. The image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO, at campaign or customer level, on a verified account."
|
|
14925
14972
|
},
|
|
14926
14973
|
subCommands: {
|
|
14927
14974
|
create: fileCreateCommand(
|
|
14928
14975
|
"create",
|
|
14929
14976
|
"google.asset.create",
|
|
14930
|
-
"Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)"
|
|
14977
|
+
"Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)",
|
|
14978
|
+
imageAssetHints
|
|
14931
14979
|
),
|
|
14932
14980
|
update: assetsUpdateCommand,
|
|
14933
14981
|
attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
|
|
14934
|
-
"
|
|
14982
|
+
"Image extensions are not attachable here \u2014 Google refuses AD_IMAGE and every marketing-image field type at every level, on every API version. The upload is not refused, though: stage the images with `assets create`, then tell the user they are already in their Google Ads account and only need adding to the campaign under Assets. Baker stages that Task by itself when the image is staged. Two images that DO work: the advertiser's logo through this command (`BUSINESS_LOGO`, campaign or customer level, on a verified account), and Performance Max asset-group images, which are a different mechanism (`asset-groups attach`)."
|
|
14935
14983
|
]),
|
|
14936
14984
|
detach: statusCommand2("google.assetLink.detach", "asset link")
|
|
14937
14985
|
}
|
|
@@ -18714,7 +18762,7 @@ function renderSection2(title, nodes, lines, showAccount) {
|
|
|
18714
18762
|
}
|
|
18715
18763
|
}
|
|
18716
18764
|
var CAMPAIGN_TREE_ENTITIES = /* @__PURE__ */ new Set(["campaignGroup", "campaign", "creative"]);
|
|
18717
|
-
function
|
|
18765
|
+
function renderOutcome2(data, lines) {
|
|
18718
18766
|
const counts = /* @__PURE__ */ new Map();
|
|
18719
18767
|
for (const op of data.ops ?? []) {
|
|
18720
18768
|
const status = op.result?.status ?? "pending";
|
|
@@ -18738,7 +18786,7 @@ function renderDraftStatus2(data) {
|
|
|
18738
18786
|
const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to LinkedIn." : "Simulated \u2014 publishing completes the whole draft without calling LinkedIn.";
|
|
18739
18787
|
const lines = [];
|
|
18740
18788
|
if (data.status === "applied") {
|
|
18741
|
-
|
|
18789
|
+
renderOutcome2(data, lines);
|
|
18742
18790
|
} else if (data.status === "publishing") {
|
|
18743
18791
|
lines.push(`LinkedIn Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
|
|
18744
18792
|
} else {
|
|
@@ -19573,7 +19621,16 @@ var META_LIMITS = {
|
|
|
19573
19621
|
dynamicTextsMin: 1,
|
|
19574
19622
|
dynamicTextsMax: 5,
|
|
19575
19623
|
/** Dynamic ads mix and match best with at least this many media assets. */
|
|
19576
|
-
dynamicAssetsGoodMin: 3
|
|
19624
|
+
dynamicAssetsGoodMin: 3,
|
|
19625
|
+
/**
|
|
19626
|
+
* Placement asset customization: "every `asset_feed_spec` needs to have more
|
|
19627
|
+
* than one customization rule attached to it" — one rule is not a
|
|
19628
|
+
* customization, it is an ad that serves in one place.
|
|
19629
|
+
* https://developers.facebook.com/docs/marketing-api/dynamic-creative/placement-asset-customization/
|
|
19630
|
+
*/
|
|
19631
|
+
placementRulesMin: 2,
|
|
19632
|
+
/** Asset customization rules per asset feed. */
|
|
19633
|
+
placementRulesMax: 50
|
|
19577
19634
|
},
|
|
19578
19635
|
audience: {
|
|
19579
19636
|
nameMax: 100,
|
|
@@ -19810,6 +19867,56 @@ function positionsMissingRequiredFeed(targeting) {
|
|
|
19810
19867
|
}
|
|
19811
19868
|
return FACEBOOK_POSITIONS_REQUIRING_FEED.filter((required) => positions.includes(required));
|
|
19812
19869
|
}
|
|
19870
|
+
var PLACEMENT_ASSET_CUSTOMIZATION_DOC = "https://developers.facebook.com/docs/marketing-api/dynamic-creative/placement-asset-customization/";
|
|
19871
|
+
var CUSTOMIZATION_POSITION_KEYS = {
|
|
19872
|
+
facebook: "facebook_positions",
|
|
19873
|
+
instagram: "instagram_positions",
|
|
19874
|
+
messenger: "messenger_positions",
|
|
19875
|
+
audience_network: "audience_network_positions",
|
|
19876
|
+
threads: "threads_positions"
|
|
19877
|
+
};
|
|
19878
|
+
var CUSTOMIZATION_PLATFORMS = Object.keys(CUSTOMIZATION_POSITION_KEYS);
|
|
19879
|
+
var CUSTOMIZATION_POSITIONS = {
|
|
19880
|
+
facebook: ["feed", "right_hand_column", "marketplace", "video_feeds", "search", "story", "notification"],
|
|
19881
|
+
instagram: ["stream", "story", "explore", "explore_home", "profile_feed", "ig_search"],
|
|
19882
|
+
messenger: ["sponsored_messages", "story"],
|
|
19883
|
+
audience_network: ["classic", "instream_video", "rewarded_video"],
|
|
19884
|
+
threads: ["threads_stream"]
|
|
19885
|
+
};
|
|
19886
|
+
function customizationPlacementKeys(spec) {
|
|
19887
|
+
const keys = [];
|
|
19888
|
+
for (const platform of spec.publisher_platforms) {
|
|
19889
|
+
for (const position of spec[CUSTOMIZATION_POSITION_KEYS[platform]] ?? []) {
|
|
19890
|
+
keys.push(`${platform}:${position}`);
|
|
19891
|
+
}
|
|
19892
|
+
}
|
|
19893
|
+
return keys;
|
|
19894
|
+
}
|
|
19895
|
+
function customizationSpecProblems(spec) {
|
|
19896
|
+
const problems = [];
|
|
19897
|
+
for (const platform of spec.publisher_platforms) {
|
|
19898
|
+
const key = CUSTOMIZATION_POSITION_KEYS[platform];
|
|
19899
|
+
const positions = spec[key];
|
|
19900
|
+
if (!positions?.length) {
|
|
19901
|
+
problems.push(
|
|
19902
|
+
`publisher_platforms names ${platform}, so ${key} is required \u2014 Meta rejects a customization spec that selects a platform without its positions (${PLACEMENT_ASSET_CUSTOMIZATION_DOC})`
|
|
19903
|
+
);
|
|
19904
|
+
continue;
|
|
19905
|
+
}
|
|
19906
|
+
const known = CUSTOMIZATION_POSITIONS[platform];
|
|
19907
|
+
for (const position of positions) {
|
|
19908
|
+
if (!known.includes(position)) {
|
|
19909
|
+
problems.push(`${key} does not publish "${position}" \u2014 Meta lists ${known.join(", ")}`);
|
|
19910
|
+
}
|
|
19911
|
+
}
|
|
19912
|
+
}
|
|
19913
|
+
if (spec.publisher_platforms.includes("threads") && !spec.instagram_positions?.includes("stream")) {
|
|
19914
|
+
problems.push(
|
|
19915
|
+
'threads placements require instagram alongside them \u2014 add "instagram" to publisher_platforms and "stream" to instagram_positions'
|
|
19916
|
+
);
|
|
19917
|
+
}
|
|
19918
|
+
return problems;
|
|
19919
|
+
}
|
|
19813
19920
|
|
|
19814
19921
|
// ../api/src/ads-meta/ops.ts
|
|
19815
19922
|
var tempRefSchema3 = z25.string().regex(TEMP_REF_REGEX3, "expected a meta_temp_* reference");
|
|
@@ -20290,9 +20397,7 @@ var dynamicCreativeSchema = z25.object({
|
|
|
20290
20397
|
videos: z25.array(dynamicVideoSchema).optional(),
|
|
20291
20398
|
ad_formats: z25.array(z25.enum(AD_FORMATS2)).min(1),
|
|
20292
20399
|
call_to_action_types: z25.array(z25.enum(CTA_TYPES2)).optional(),
|
|
20293
|
-
link_urls: z25.array(z25.object({ website_url: httpsUrlSchema3, display_url: z25.string().optional() })).min(1)
|
|
20294
|
-
/** Multi-language / placement customization — structural passthrough for v1. */
|
|
20295
|
-
asset_customization_rules: z25.array(z25.record(z25.string(), z25.unknown())).optional()
|
|
20400
|
+
link_urls: z25.array(z25.object({ website_url: httpsUrlSchema3, display_url: z25.string().optional() })).min(1)
|
|
20296
20401
|
}).superRefine((p, ctx) => {
|
|
20297
20402
|
if (!(p.images?.length || p.videos?.length)) {
|
|
20298
20403
|
ctx.addIssue({
|
|
@@ -20302,6 +20407,73 @@ var dynamicCreativeSchema = z25.object({
|
|
|
20302
20407
|
});
|
|
20303
20408
|
}
|
|
20304
20409
|
});
|
|
20410
|
+
var placementRuleSchema = z25.object({
|
|
20411
|
+
...imageMediaFields,
|
|
20412
|
+
videoId: videoMediaFields.videoId,
|
|
20413
|
+
videoRef: videoMediaFields.videoRef,
|
|
20414
|
+
thumbnailHash: videoMediaFields.thumbnailHash,
|
|
20415
|
+
publisher_platforms: z25.array(z25.enum(CUSTOMIZATION_PLATFORMS)).min(1),
|
|
20416
|
+
facebook_positions: z25.array(z25.string()).optional(),
|
|
20417
|
+
instagram_positions: z25.array(z25.string()).optional(),
|
|
20418
|
+
messenger_positions: z25.array(z25.string()).optional(),
|
|
20419
|
+
audience_network_positions: z25.array(z25.string()).optional(),
|
|
20420
|
+
threads_positions: z25.array(z25.string()).optional()
|
|
20421
|
+
}).superRefine((p, ctx) => {
|
|
20422
|
+
const media = countImageRefs(p) + countVideoRefs(p);
|
|
20423
|
+
if (media !== 1) {
|
|
20424
|
+
ctx.addIssue({
|
|
20425
|
+
code: "custom",
|
|
20426
|
+
path: ["imageHash"],
|
|
20427
|
+
message: "each placement rule names exactly one image or one video"
|
|
20428
|
+
});
|
|
20429
|
+
}
|
|
20430
|
+
for (const problem of customizationSpecProblems(p)) {
|
|
20431
|
+
ctx.addIssue({ code: "custom", path: ["publisher_platforms"], message: problem });
|
|
20432
|
+
}
|
|
20433
|
+
});
|
|
20434
|
+
var placementCreativeSchema = z25.object({
|
|
20435
|
+
creativeType: z25.literal("placement"),
|
|
20436
|
+
...creativeSharedFields,
|
|
20437
|
+
message: messageSchema,
|
|
20438
|
+
link: httpsUrlSchema3,
|
|
20439
|
+
headline: headlineSchema2.optional(),
|
|
20440
|
+
description: descriptionSchema.optional(),
|
|
20441
|
+
call_to_action: callToActionSchema.optional(),
|
|
20442
|
+
placements: z25.array(placementRuleSchema).min(META_LIMITS.creative.placementRulesMin).max(META_LIMITS.creative.placementRulesMax)
|
|
20443
|
+
}).superRefine((p, ctx) => {
|
|
20444
|
+
const images = p.placements.filter((rule) => countImageRefs(rule) === 1).length;
|
|
20445
|
+
if (images > 0 && images < p.placements.length) {
|
|
20446
|
+
ctx.addIssue({
|
|
20447
|
+
code: "custom",
|
|
20448
|
+
path: ["placements"],
|
|
20449
|
+
message: "one asset feed carries one format \u2014 every placement rule must name an image, or every one a video, not a mix; stage the other format as its own creative"
|
|
20450
|
+
});
|
|
20451
|
+
}
|
|
20452
|
+
const seen = /* @__PURE__ */ new Map();
|
|
20453
|
+
p.placements.forEach((rule, index) => {
|
|
20454
|
+
for (const key of customizationPlacementKeys(rule)) {
|
|
20455
|
+
const first = seen.get(key);
|
|
20456
|
+
if (first !== void 0) {
|
|
20457
|
+
ctx.addIssue({
|
|
20458
|
+
code: "custom",
|
|
20459
|
+
path: ["placements", index],
|
|
20460
|
+
message: `${key.replace(":", " ")} is already claimed by rule ${first + 1} \u2014 a placement takes one asset, so Meta cannot resolve two rules naming it`
|
|
20461
|
+
});
|
|
20462
|
+
} else {
|
|
20463
|
+
seen.set(key, index);
|
|
20464
|
+
}
|
|
20465
|
+
}
|
|
20466
|
+
});
|
|
20467
|
+
for (const [index, rule] of p.placements.entries()) {
|
|
20468
|
+
if (rule.instagram_positions?.includes("explore_home") && countVideoRefs(rule) === 1) {
|
|
20469
|
+
ctx.addIssue({
|
|
20470
|
+
code: "custom",
|
|
20471
|
+
path: ["placements", index],
|
|
20472
|
+
message: `Instagram explore_home only supports SINGLE_IMAGE, so it cannot take a video (${PLACEMENT_ASSET_CUSTOMIZATION_DOC})`
|
|
20473
|
+
});
|
|
20474
|
+
}
|
|
20475
|
+
}
|
|
20476
|
+
});
|
|
20305
20477
|
var existingPostCreativeSchema = z25.object({
|
|
20306
20478
|
creativeType: z25.literal("existing_post"),
|
|
20307
20479
|
name: creativeSharedFields.name,
|
|
@@ -20315,6 +20487,7 @@ var creativeContentSchema2 = z25.discriminatedUnion("creativeType", [
|
|
|
20315
20487
|
singleCreativeSchema,
|
|
20316
20488
|
carouselCreativeSchema2,
|
|
20317
20489
|
dynamicCreativeSchema,
|
|
20490
|
+
placementCreativeSchema,
|
|
20318
20491
|
existingPostCreativeSchema
|
|
20319
20492
|
]);
|
|
20320
20493
|
var adCreativeCreateSchema = creativeContentSchema2;
|
|
@@ -20888,13 +21061,17 @@ var adCreativesCreateCommand = defineCommand56({
|
|
|
20888
21061
|
meta: {
|
|
20889
21062
|
name: "create",
|
|
20890
21063
|
description: `Stage a new ad creative. ${STAGED_NOTE2}
|
|
20891
|
-
Single-image/video via flags; carousel/dynamic via --file (creativeType carousel|dynamic).
|
|
21064
|
+
Single-image/video via flags; carousel/dynamic/placement via --file (creativeType carousel|dynamic|placement).
|
|
20892
21065
|
Reference media by --image-hash (already on Meta) or --image-ref meta_temp_* (a staged media upload).
|
|
21066
|
+
Start here for "the feed asset is 4:5, Stories needs 9:16": creativeType "placement" puts BOTH in one ad, one asset per placement \u2014 then repoint the running ad at it (ads update <adId> --creative <ref>) instead of staging a second ad that competes with it.
|
|
20893
21067
|
Example: baker ads meta creatives create --page 555 --message "Save now" --link https://x.com --headline "Best cover" --image-hash abc123\u2026 --cta SHOP_NOW`
|
|
20894
21068
|
},
|
|
20895
21069
|
args: {
|
|
20896
21070
|
...accountArgs2,
|
|
20897
|
-
type: {
|
|
21071
|
+
type: {
|
|
21072
|
+
type: "string",
|
|
21073
|
+
description: "single|carousel|dynamic|placement|existing_post (default single). placement = one ad, a different asset per placement"
|
|
21074
|
+
},
|
|
20898
21075
|
name: { type: "string", description: "Creative name" },
|
|
20899
21076
|
page: { type: "string", description: "Facebook Page id (identity)" },
|
|
20900
21077
|
"instagram-user": { type: "string", description: "Instagram account id for IG placements" },
|