cli-meta-ads 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +188 -0
- package/AI_CONTEXT.md +144 -0
- package/CLAUDE.md +183 -0
- package/README.md +590 -0
- package/REQUIREMENTS.md +148 -0
- package/dist/auth/constants.d.ts +1 -0
- package/dist/auth/constants.js +1 -0
- package/dist/auth/guards.d.ts +5 -0
- package/dist/auth/guards.js +16 -0
- package/dist/auth/login.d.ts +28 -0
- package/dist/auth/login.js +222 -0
- package/dist/cli/action.d.ts +11 -0
- package/dist/cli/action.js +77 -0
- package/dist/cli/build-cli.d.ts +2 -0
- package/dist/cli/build-cli.js +110 -0
- package/dist/cli/context.d.ts +24 -0
- package/dist/cli/context.js +19 -0
- package/dist/client/meta-api-client.d.ts +50 -0
- package/dist/client/meta-api-client.js +258 -0
- package/dist/client/meta-discovery.d.ts +13 -0
- package/dist/client/meta-discovery.js +88 -0
- package/dist/commands/accounts.d.ts +4 -0
- package/dist/commands/accounts.js +42 -0
- package/dist/commands/ads.d.ts +4 -0
- package/dist/commands/ads.js +148 -0
- package/dist/commands/adsets.d.ts +4 -0
- package/dist/commands/adsets.js +49 -0
- package/dist/commands/anomalies.d.ts +4 -0
- package/dist/commands/anomalies.js +44 -0
- package/dist/commands/assets.d.ts +4 -0
- package/dist/commands/assets.js +116 -0
- package/dist/commands/audiences.d.ts +4 -0
- package/dist/commands/audiences.js +40 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +139 -0
- package/dist/commands/campaigns.d.ts +4 -0
- package/dist/commands/campaigns.js +273 -0
- package/dist/commands/capi.d.ts +4 -0
- package/dist/commands/capi.js +64 -0
- package/dist/commands/creatives.d.ts +4 -0
- package/dist/commands/creatives.js +49 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.js +88 -0
- package/dist/commands/helpers.d.ts +13 -0
- package/dist/commands/helpers.js +50 -0
- package/dist/commands/launch.d.ts +4 -0
- package/dist/commands/launch.js +109 -0
- package/dist/commands/performance.d.ts +4 -0
- package/dist/commands/performance.js +55 -0
- package/dist/commands/pixel.d.ts +4 -0
- package/dist/commands/pixel.js +68 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +30 -0
- package/dist/config/file-config.d.ts +6 -0
- package/dist/config/file-config.js +174 -0
- package/dist/config/types.d.ts +32 -0
- package/dist/config/types.js +1 -0
- package/dist/domain/account-scope.d.ts +7 -0
- package/dist/domain/account-scope.js +28 -0
- package/dist/domain/analytics.d.ts +52 -0
- package/dist/domain/analytics.js +125 -0
- package/dist/domain/approval-service.d.ts +10 -0
- package/dist/domain/approval-service.js +48 -0
- package/dist/domain/asset-feed-compiler.d.ts +43 -0
- package/dist/domain/asset-feed-compiler.js +104 -0
- package/dist/domain/launch-service.d.ts +200 -0
- package/dist/domain/launch-service.js +558 -0
- package/dist/domain/meta-ads-service.d.ts +620 -0
- package/dist/domain/meta-ads-service.js +841 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/output/render.d.ts +3 -0
- package/dist/output/render.js +103 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/dist/utils/currency.d.ts +4 -0
- package/dist/utils/currency.js +40 -0
- package/dist/utils/date-range.d.ts +20 -0
- package/dist/utils/date-range.js +115 -0
- package/dist/utils/errors.d.ts +35 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +23 -0
- package/dist/utils/meta-placement-assets.d.ts +44 -0
- package/dist/utils/meta-placement-assets.js +315 -0
- package/dist/utils/security.d.ts +5 -0
- package/dist/utils/security.js +104 -0
- package/dist/validators/common.d.ts +10 -0
- package/dist/validators/common.js +56 -0
- package/dist/validators/create-spec.d.ts +373 -0
- package/dist/validators/create-spec.js +394 -0
- package/dist/validators/launch-spec.d.ts +229 -0
- package/dist/validators/launch-spec.js +371 -0
- package/docs/TECHNICAL.md +480 -0
- package/examples/README.md +29 -0
- package/examples/launch/assets/feed4x5.png +0 -0
- package/examples/launch/assets/story9x16.png +0 -0
- package/examples/launch/multi-format-launch.json +90 -0
- package/examples/single-object/ad.json +6 -0
- package/examples/single-object/adset.json +30 -0
- package/examples/single-object/campaign.json +6 -0
- package/examples/single-object/creative.json +19 -0
- package/package.json +62 -0
- package/skills/meta-cli-operator/SKILL.md +105 -0
- package/skills/meta-cli-operator/agents/openai.yaml +4 -0
- package/skills/meta-cli-operator/references/update-matrix.md +117 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AppError, ExitCode } from "../utils/errors.js";
|
|
3
|
+
import { normalizeObjectId } from "../utils/ids.js";
|
|
4
|
+
import { expandPlacementTargeting, hasPlacementFormats, launchCreativeFormatKeys, pickFeedPlacementIds, pickUnsupportedPlacementIds, pickVerticalPlacementIds } from "../utils/meta-placement-assets.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { bidStrategySchema, callToActionSchema, campaignObjectiveSchema, optimizationGoalSchema, promotedObjectSchema, resolveExistingFilePath, readSpecFile, targetingSpecSchema } from "./create-spec.js";
|
|
7
|
+
const nonEmptyStringSchema = z.string().trim().min(1);
|
|
8
|
+
const refSchema = nonEmptyStringSchema;
|
|
9
|
+
const pausedStatusSchema = z.literal("PAUSED");
|
|
10
|
+
const metaIdSchema = (label) => z.string().trim().min(1).transform((value) => normalizeObjectId(value, label));
|
|
11
|
+
const launchCampaignSpecSchema = z.object({
|
|
12
|
+
bidStrategy: bidStrategySchema.optional(),
|
|
13
|
+
dailyBudget: z.number().int().positive().optional(),
|
|
14
|
+
lifetimeBudget: z.number().int().positive().optional(),
|
|
15
|
+
ref: refSchema,
|
|
16
|
+
name: nonEmptyStringSchema,
|
|
17
|
+
objective: campaignObjectiveSchema,
|
|
18
|
+
specialAdCategories: z.array(nonEmptyStringSchema).default([]),
|
|
19
|
+
status: pausedStatusSchema.optional()
|
|
20
|
+
}).strict().superRefine((value, ctx) => {
|
|
21
|
+
if (value.dailyBudget !== undefined && value.lifetimeBudget !== undefined) {
|
|
22
|
+
ctx.addIssue({
|
|
23
|
+
code: z.ZodIssueCode.custom,
|
|
24
|
+
message: "Provide either dailyBudget or lifetimeBudget for campaign budget optimization, not both."
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (value.bidStrategy !== undefined
|
|
28
|
+
&& value.dailyBudget === undefined
|
|
29
|
+
&& value.lifetimeBudget === undefined) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: z.ZodIssueCode.custom,
|
|
32
|
+
message: "Campaign bidStrategy requires dailyBudget or lifetimeBudget."
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const launchAdSetSpecSchema = z.object({
|
|
37
|
+
ref: refSchema,
|
|
38
|
+
campaignRef: refSchema,
|
|
39
|
+
bidAmount: z.number().int().positive().optional(),
|
|
40
|
+
bidStrategy: bidStrategySchema.optional(),
|
|
41
|
+
billingEvent: z.enum(["IMPRESSIONS", "CLICKS"]),
|
|
42
|
+
dailyBudget: z.number().int().positive().optional(),
|
|
43
|
+
endTime: nonEmptyStringSchema.optional(),
|
|
44
|
+
lifetimeBudget: z.number().int().positive().optional(),
|
|
45
|
+
name: nonEmptyStringSchema,
|
|
46
|
+
optimizationGoal: optimizationGoalSchema,
|
|
47
|
+
promotedObject: promotedObjectSchema,
|
|
48
|
+
startTime: nonEmptyStringSchema.optional(),
|
|
49
|
+
status: pausedStatusSchema.optional(),
|
|
50
|
+
targeting: targetingSpecSchema
|
|
51
|
+
}).strict().superRefine((value, ctx) => {
|
|
52
|
+
if (value.dailyBudget === undefined && value.lifetimeBudget === undefined) {
|
|
53
|
+
ctx.addIssue({
|
|
54
|
+
code: z.ZodIssueCode.custom,
|
|
55
|
+
message: "Either dailyBudget or lifetimeBudget is required."
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (value.dailyBudget !== undefined && value.lifetimeBudget !== undefined) {
|
|
59
|
+
ctx.addIssue({
|
|
60
|
+
code: z.ZodIssueCode.custom,
|
|
61
|
+
message: "Provide either dailyBudget or lifetimeBudget, not both."
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (value.bidStrategy === "LOWEST_COST_WITHOUT_CAP" && value.bidAmount !== undefined) {
|
|
65
|
+
ctx.addIssue({
|
|
66
|
+
code: z.ZodIssueCode.custom,
|
|
67
|
+
message: "bidAmount cannot be combined with LOWEST_COST_WITHOUT_CAP."
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if ((value.bidStrategy === "LOWEST_COST_WITH_BID_CAP" || value.bidStrategy === "COST_CAP")
|
|
71
|
+
&& value.bidAmount === undefined) {
|
|
72
|
+
ctx.addIssue({
|
|
73
|
+
code: z.ZodIssueCode.custom,
|
|
74
|
+
message: `${value.bidStrategy} requires bidAmount.`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if ((value.optimizationGoal === "OFFSITE_CONVERSIONS" || value.optimizationGoal === "VALUE")
|
|
78
|
+
&& (!value.promotedObject.pixelId || !value.promotedObject.customEventType)) {
|
|
79
|
+
ctx.addIssue({
|
|
80
|
+
code: z.ZodIssueCode.custom,
|
|
81
|
+
message: `${value.optimizationGoal} requires promotedObject.pixelId and promotedObject.customEventType.`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if ((value.optimizationGoal === "CONVERSATIONS"
|
|
85
|
+
|| value.optimizationGoal === "LEAD_GENERATION"
|
|
86
|
+
|| value.optimizationGoal === "QUALITY_CALL"
|
|
87
|
+
|| value.optimizationGoal === "QUALITY_LEAD")
|
|
88
|
+
&& !value.promotedObject.pageId) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: z.ZodIssueCode.custom,
|
|
91
|
+
message: `${value.optimizationGoal} requires promotedObject.pageId.`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const imageAssetSpecSchema = z.object({
|
|
96
|
+
ref: refSchema,
|
|
97
|
+
kind: z.literal("image"),
|
|
98
|
+
file: nonEmptyStringSchema,
|
|
99
|
+
name: nonEmptyStringSchema.optional()
|
|
100
|
+
}).strict();
|
|
101
|
+
const videoAssetSpecSchema = z.object({
|
|
102
|
+
ref: refSchema,
|
|
103
|
+
kind: z.literal("video"),
|
|
104
|
+
file: nonEmptyStringSchema,
|
|
105
|
+
name: nonEmptyStringSchema.optional()
|
|
106
|
+
}).strict();
|
|
107
|
+
const imageCropValuesSchema = z.array(z.number());
|
|
108
|
+
const imageCropsSchema = z.record(z.string(), imageCropValuesSchema);
|
|
109
|
+
const launchPlatformCustomizationOverrideSchema = z.object({
|
|
110
|
+
assetRef: refSchema,
|
|
111
|
+
captionIds: z.array(nonEmptyStringSchema).optional(),
|
|
112
|
+
imageCrops: imageCropsSchema.optional()
|
|
113
|
+
}).strict();
|
|
114
|
+
const launchCreativeFormatOverrideSchema = z.object({
|
|
115
|
+
assetRef: refSchema,
|
|
116
|
+
captionIds: z.array(nonEmptyStringSchema).optional(),
|
|
117
|
+
imageCrops: imageCropsSchema.optional()
|
|
118
|
+
}).strict();
|
|
119
|
+
const launchCreativeFormatsSchema = z.object({
|
|
120
|
+
feed4x5: launchCreativeFormatOverrideSchema.optional(),
|
|
121
|
+
square1x1: launchCreativeFormatOverrideSchema.optional(),
|
|
122
|
+
story9x16: launchCreativeFormatOverrideSchema.optional()
|
|
123
|
+
}).strict().superRefine((value, ctx) => {
|
|
124
|
+
if (!launchCreativeFormatKeys.some((key) => value[key])) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: z.ZodIssueCode.custom,
|
|
127
|
+
message: "formats must include at least one supported format override."
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const launchPlatformCustomizationsSchema = z.object({
|
|
132
|
+
instagram: launchPlatformCustomizationOverrideSchema.optional()
|
|
133
|
+
}).strict().superRefine((value, ctx) => {
|
|
134
|
+
if (!value.instagram) {
|
|
135
|
+
ctx.addIssue({
|
|
136
|
+
code: z.ZodIssueCode.custom,
|
|
137
|
+
message: "platformCustomizations must include at least one supported platform override."
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const imageCreativeSpecSchema = z.object({
|
|
142
|
+
ref: refSchema,
|
|
143
|
+
kind: z.literal("link-image"),
|
|
144
|
+
assetRef: refSchema.optional(),
|
|
145
|
+
formats: launchCreativeFormatsSchema.optional(),
|
|
146
|
+
linkData: z.object({
|
|
147
|
+
callToAction: callToActionSchema.optional(),
|
|
148
|
+
description: nonEmptyStringSchema.optional(),
|
|
149
|
+
headline: nonEmptyStringSchema.optional(),
|
|
150
|
+
link: z.string().trim().url(),
|
|
151
|
+
message: nonEmptyStringSchema
|
|
152
|
+
}).strict(),
|
|
153
|
+
name: nonEmptyStringSchema.optional(),
|
|
154
|
+
pageId: metaIdSchema("page id"),
|
|
155
|
+
platformCustomizations: launchPlatformCustomizationsSchema.optional(),
|
|
156
|
+
status: pausedStatusSchema.optional()
|
|
157
|
+
}).strict().superRefine((value, ctx) => {
|
|
158
|
+
if (!value.assetRef && !hasPlacementFormats(value.formats)) {
|
|
159
|
+
ctx.addIssue({
|
|
160
|
+
code: z.ZodIssueCode.custom,
|
|
161
|
+
message: "Creative must include assetRef or formats."
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (value.platformCustomizations && value.formats) {
|
|
165
|
+
ctx.addIssue({
|
|
166
|
+
code: z.ZodIssueCode.custom,
|
|
167
|
+
message: "formats cannot be combined with legacy platformCustomizations."
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const videoCreativeSpecSchema = z.object({
|
|
172
|
+
ref: refSchema,
|
|
173
|
+
kind: z.literal("video-link"),
|
|
174
|
+
assetRef: refSchema.optional(),
|
|
175
|
+
formats: launchCreativeFormatsSchema.optional(),
|
|
176
|
+
name: nonEmptyStringSchema.optional(),
|
|
177
|
+
pageId: metaIdSchema("page id"),
|
|
178
|
+
platformCustomizations: launchPlatformCustomizationsSchema.optional(),
|
|
179
|
+
status: pausedStatusSchema.optional(),
|
|
180
|
+
videoData: z.object({
|
|
181
|
+
callToAction: callToActionSchema.optional(),
|
|
182
|
+
description: nonEmptyStringSchema.optional(),
|
|
183
|
+
link: z.string().trim().url(),
|
|
184
|
+
message: nonEmptyStringSchema,
|
|
185
|
+
title: nonEmptyStringSchema.optional()
|
|
186
|
+
}).strict()
|
|
187
|
+
}).strict().superRefine((value, ctx) => {
|
|
188
|
+
if (!value.assetRef && !hasPlacementFormats(value.formats)) {
|
|
189
|
+
ctx.addIssue({
|
|
190
|
+
code: z.ZodIssueCode.custom,
|
|
191
|
+
message: "Creative must include assetRef or formats."
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (value.platformCustomizations && value.formats) {
|
|
195
|
+
ctx.addIssue({
|
|
196
|
+
code: z.ZodIssueCode.custom,
|
|
197
|
+
message: "formats cannot be combined with legacy platformCustomizations."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const launchAdSpecSchema = z.object({
|
|
202
|
+
ref: refSchema,
|
|
203
|
+
name: nonEmptyStringSchema,
|
|
204
|
+
adSetRef: refSchema,
|
|
205
|
+
creativeRef: refSchema,
|
|
206
|
+
status: pausedStatusSchema.optional()
|
|
207
|
+
}).strict();
|
|
208
|
+
export const launchSpecSchema = z.object({
|
|
209
|
+
version: z.literal(1),
|
|
210
|
+
campaign: launchCampaignSpecSchema,
|
|
211
|
+
adSets: z.array(launchAdSetSpecSchema).default([]),
|
|
212
|
+
assets: z.array(z.discriminatedUnion("kind", [imageAssetSpecSchema, videoAssetSpecSchema])).default([]),
|
|
213
|
+
creatives: z.array(z.discriminatedUnion("kind", [imageCreativeSpecSchema, videoCreativeSpecSchema])).default([]),
|
|
214
|
+
ads: z.array(launchAdSpecSchema).default([])
|
|
215
|
+
}).strict().superRefine((value, ctx) => {
|
|
216
|
+
const allRefs = new Map();
|
|
217
|
+
const registerRef = (ref, owner) => {
|
|
218
|
+
const previous = allRefs.get(ref);
|
|
219
|
+
if (previous) {
|
|
220
|
+
ctx.addIssue({
|
|
221
|
+
code: z.ZodIssueCode.custom,
|
|
222
|
+
message: `Duplicate ref '${ref}' used by ${previous} and ${owner}.`
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
allRefs.set(ref, owner);
|
|
227
|
+
};
|
|
228
|
+
registerRef(value.campaign.ref, "campaign");
|
|
229
|
+
value.adSets.forEach((entry) => registerRef(entry.ref, `adSet:${entry.name}`));
|
|
230
|
+
value.assets.forEach((entry) => registerRef(entry.ref, `asset:${entry.file}`));
|
|
231
|
+
value.creatives.forEach((entry) => registerRef(entry.ref, `creative:${entry.kind}`));
|
|
232
|
+
value.ads.forEach((entry) => registerRef(entry.ref, `ad:${entry.name}`));
|
|
233
|
+
const assetKinds = new Map(value.assets.map((entry) => [entry.ref, entry.kind]));
|
|
234
|
+
const creativeRefs = new Set(value.creatives.map((entry) => entry.ref));
|
|
235
|
+
const adSetRefs = new Set(value.adSets.map((entry) => entry.ref));
|
|
236
|
+
const adSetPlacements = new Map(value.adSets.map((entry) => [entry.ref, expandPlacementTargeting(entry.targeting)]));
|
|
237
|
+
value.adSets.forEach((entry) => {
|
|
238
|
+
if (entry.campaignRef !== value.campaign.ref) {
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
message: `Ad set '${entry.ref}' references unknown campaignRef '${entry.campaignRef}'.`
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
value.creatives.forEach((entry) => {
|
|
246
|
+
const requireAssetRef = (assetRef, source) => {
|
|
247
|
+
const assetKind = assetKinds.get(assetRef);
|
|
248
|
+
if (!assetKind) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
message: `Creative '${entry.ref}' references unknown ${source} '${assetRef}'.`
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (entry.kind === "link-image" && assetKind !== "image") {
|
|
256
|
+
ctx.addIssue({
|
|
257
|
+
code: z.ZodIssueCode.custom,
|
|
258
|
+
message: `Creative '${entry.ref}' requires an image ${source}.`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (entry.kind === "video-link" && assetKind !== "video") {
|
|
262
|
+
ctx.addIssue({
|
|
263
|
+
code: z.ZodIssueCode.custom,
|
|
264
|
+
message: `Creative '${entry.ref}' requires a video ${source}.`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
if (entry.assetRef) {
|
|
269
|
+
requireAssetRef(entry.assetRef, "assetRef");
|
|
270
|
+
}
|
|
271
|
+
launchCreativeFormatKeys.forEach((formatKey) => {
|
|
272
|
+
const formatOverride = entry.formats?.[formatKey];
|
|
273
|
+
if (formatOverride) {
|
|
274
|
+
requireAssetRef(formatOverride.assetRef, `formats.${formatKey}.assetRef`);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
const instagramOverride = entry.platformCustomizations?.instagram;
|
|
278
|
+
if (!instagramOverride) {
|
|
279
|
+
// Continue into placement validation below.
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
requireAssetRef(instagramOverride.assetRef, "platform customization assetRef");
|
|
283
|
+
}
|
|
284
|
+
const adsForCreative = value.ads.filter((item) => item.creativeRef === entry.ref);
|
|
285
|
+
const expandedPlacementIds = [...new Set(adsForCreative.flatMap((item) => adSetPlacements.get(item.adSetRef)?.placementIds ?? []))];
|
|
286
|
+
const explicitPlacementIds = [...new Set(adsForCreative.flatMap((item) => adSetPlacements.get(item.adSetRef)?.explicitPlacementIds ?? []))];
|
|
287
|
+
const automaticPlacements = adsForCreative.some((item) => adSetPlacements.get(item.adSetRef)?.automatic);
|
|
288
|
+
const requiredVerticalPlacements = [
|
|
289
|
+
...new Set([
|
|
290
|
+
...pickVerticalPlacementIds(explicitPlacementIds),
|
|
291
|
+
...(automaticPlacements && hasPlacementFormats(entry.formats)
|
|
292
|
+
? pickVerticalPlacementIds(expandedPlacementIds)
|
|
293
|
+
: [])
|
|
294
|
+
])
|
|
295
|
+
];
|
|
296
|
+
const requiredFeedPlacements = [
|
|
297
|
+
...new Set([
|
|
298
|
+
...pickFeedPlacementIds(explicitPlacementIds),
|
|
299
|
+
...(automaticPlacements && hasPlacementFormats(entry.formats)
|
|
300
|
+
? pickFeedPlacementIds(expandedPlacementIds)
|
|
301
|
+
: [])
|
|
302
|
+
])
|
|
303
|
+
];
|
|
304
|
+
const requiredUnsupportedPlacements = [
|
|
305
|
+
...new Set([
|
|
306
|
+
...pickUnsupportedPlacementIds(explicitPlacementIds),
|
|
307
|
+
...(automaticPlacements && hasPlacementFormats(entry.formats)
|
|
308
|
+
? pickUnsupportedPlacementIds(expandedPlacementIds)
|
|
309
|
+
: [])
|
|
310
|
+
])
|
|
311
|
+
];
|
|
312
|
+
const hasFeedAsset = Boolean(entry.assetRef || entry.formats?.feed4x5 || entry.formats?.square1x1);
|
|
313
|
+
const hasStoryAsset = Boolean(entry.formats?.story9x16);
|
|
314
|
+
const hasLegacyInstagramVerticalCoverage = Boolean(entry.platformCustomizations?.instagram)
|
|
315
|
+
&& requiredVerticalPlacements.length > 0
|
|
316
|
+
&& requiredVerticalPlacements.every((placementId) => placementId.startsWith("instagram."));
|
|
317
|
+
if (requiredVerticalPlacements.length > 0 && !hasStoryAsset && !hasLegacyInstagramVerticalCoverage) {
|
|
318
|
+
ctx.addIssue({
|
|
319
|
+
code: z.ZodIssueCode.custom,
|
|
320
|
+
message: `Creative '${entry.ref}' targets story/reels placements (${requiredVerticalPlacements.join(", ")}) but does not provide formats.story9x16.`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (requiredFeedPlacements.length > 0 && !hasFeedAsset) {
|
|
324
|
+
ctx.addIssue({
|
|
325
|
+
code: z.ZodIssueCode.custom,
|
|
326
|
+
message: `Creative '${entry.ref}' targets feed-like placements (${requiredFeedPlacements.join(", ")}) but does not provide assetRef, formats.feed4x5, or formats.square1x1.`
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (requiredUnsupportedPlacements.length > 0 && hasPlacementFormats(entry.formats) && !entry.assetRef) {
|
|
330
|
+
ctx.addIssue({
|
|
331
|
+
code: z.ZodIssueCode.custom,
|
|
332
|
+
message: `Creative '${entry.ref}' targets placements outside the current formats abstraction (${requiredUnsupportedPlacements.join(", ")}) and must provide assetRef as the explicit fallback asset.`
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
value.ads.forEach((entry) => {
|
|
337
|
+
if (!adSetRefs.has(entry.adSetRef)) {
|
|
338
|
+
ctx.addIssue({
|
|
339
|
+
code: z.ZodIssueCode.custom,
|
|
340
|
+
message: `Ad '${entry.ref}' references unknown adSetRef '${entry.adSetRef}'.`
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (!creativeRefs.has(entry.creativeRef)) {
|
|
344
|
+
ctx.addIssue({
|
|
345
|
+
code: z.ZodIssueCode.custom,
|
|
346
|
+
message: `Ad '${entry.ref}' references unknown creativeRef '${entry.creativeRef}'.`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
export async function readLaunchSpecFile(specPath) {
|
|
352
|
+
const { spec, specPath: absoluteSpecPath } = await readSpecFile(specPath, launchSpecSchema, "Launch");
|
|
353
|
+
const specDir = path.dirname(absoluteSpecPath);
|
|
354
|
+
const assets = await Promise.all(spec.assets.map(async (asset) => ({
|
|
355
|
+
...asset,
|
|
356
|
+
file: await resolveExistingFilePath(asset.file, `Asset file for ref '${asset.ref}'`, specDir)
|
|
357
|
+
})));
|
|
358
|
+
return {
|
|
359
|
+
spec: {
|
|
360
|
+
...spec,
|
|
361
|
+
assets
|
|
362
|
+
},
|
|
363
|
+
specPath: absoluteSpecPath
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
export function requireReceiptPath(value) {
|
|
367
|
+
if (!value?.trim()) {
|
|
368
|
+
throw new AppError("A receipt path is required.", ExitCode.Usage);
|
|
369
|
+
}
|
|
370
|
+
return value;
|
|
371
|
+
}
|