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,394 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AppError, ExitCode } from "../utils/errors.js";
|
|
5
|
+
import { normalizeObjectId } from "../utils/ids.js";
|
|
6
|
+
import { hasPlacementFormats, launchCreativeFormatKeys } from "../utils/meta-placement-assets.js";
|
|
7
|
+
export const pausedStatusSchema = z.literal("PAUSED");
|
|
8
|
+
export const nonEmptyStringSchema = z.string().trim().min(1);
|
|
9
|
+
const metaIdSchema = (label) => z.string().trim().min(1).transform((value) => normalizeObjectId(value, label));
|
|
10
|
+
export const campaignObjectiveValues = [
|
|
11
|
+
"APP_INSTALLS",
|
|
12
|
+
"BRAND_AWARENESS",
|
|
13
|
+
"CONVERSIONS",
|
|
14
|
+
"EVENT_RESPONSES",
|
|
15
|
+
"LEAD_GENERATION",
|
|
16
|
+
"LINK_CLICKS",
|
|
17
|
+
"LOCAL_AWARENESS",
|
|
18
|
+
"MESSAGES",
|
|
19
|
+
"OFFER_CLAIMS",
|
|
20
|
+
"OUTCOME_APP_PROMOTION",
|
|
21
|
+
"OUTCOME_AWARENESS",
|
|
22
|
+
"OUTCOME_ENGAGEMENT",
|
|
23
|
+
"OUTCOME_LEADS",
|
|
24
|
+
"OUTCOME_SALES",
|
|
25
|
+
"OUTCOME_TRAFFIC",
|
|
26
|
+
"PAGE_LIKES",
|
|
27
|
+
"POST_ENGAGEMENT",
|
|
28
|
+
"PRODUCT_CATALOG_SALES",
|
|
29
|
+
"REACH",
|
|
30
|
+
"STORE_VISITS",
|
|
31
|
+
"VIDEO_VIEWS"
|
|
32
|
+
];
|
|
33
|
+
export const bidStrategyValues = [
|
|
34
|
+
"LOWEST_COST_WITHOUT_CAP",
|
|
35
|
+
"LOWEST_COST_WITH_BID_CAP",
|
|
36
|
+
"COST_CAP"
|
|
37
|
+
];
|
|
38
|
+
export const optimizationGoalValues = [
|
|
39
|
+
"APP_INSTALLS",
|
|
40
|
+
"APP_INSTALLS_AND_OFFSITE_CONVERSIONS",
|
|
41
|
+
"CONVERSATIONS",
|
|
42
|
+
"ENGAGED_PAGE_VIEWS",
|
|
43
|
+
"ENGAGED_USERS",
|
|
44
|
+
"EVENT_RESPONSES",
|
|
45
|
+
"IMPRESSIONS",
|
|
46
|
+
"LANDING_PAGE_VIEWS",
|
|
47
|
+
"LEAD_GENERATION",
|
|
48
|
+
"LINK_CLICKS",
|
|
49
|
+
"OFFSITE_CONVERSIONS",
|
|
50
|
+
"PAGE_LIKES",
|
|
51
|
+
"POST_ENGAGEMENT",
|
|
52
|
+
"QUALITY_CALL",
|
|
53
|
+
"QUALITY_LEAD",
|
|
54
|
+
"REACH",
|
|
55
|
+
"THRUPLAY",
|
|
56
|
+
"VALUE",
|
|
57
|
+
"VISIT_INSTAGRAM_PROFILE"
|
|
58
|
+
];
|
|
59
|
+
export const campaignObjectiveSchema = z.enum(campaignObjectiveValues);
|
|
60
|
+
export const bidStrategySchema = z.enum(bidStrategyValues);
|
|
61
|
+
export const optimizationGoalSchema = z.enum(optimizationGoalValues);
|
|
62
|
+
export const callToActionSchema = z.enum(["SHOP_NOW", "LEARN_MORE", "SIGN_UP"]);
|
|
63
|
+
const automationToggleSchema = z.union([z.literal(0), z.literal(1)]);
|
|
64
|
+
function hasCampaignBudget(value) {
|
|
65
|
+
return value.dailyBudget !== undefined || value.lifetimeBudget !== undefined;
|
|
66
|
+
}
|
|
67
|
+
function normalizeAdvantageAudienceTargeting(value) {
|
|
68
|
+
if (value.targeting_automation?.advantage_audience !== 1) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...value,
|
|
73
|
+
age_max: value.age_max !== undefined && value.age_max < 65 ? 65 : value.age_max,
|
|
74
|
+
age_min: value.age_min !== undefined && value.age_min > 25 ? 25 : value.age_min
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const targetingAutomationSchema = z.object({
|
|
78
|
+
advantage_audience: automationToggleSchema.optional(),
|
|
79
|
+
individual_setting: z.record(z.string(), automationToggleSchema).optional()
|
|
80
|
+
}).catchall(z.unknown());
|
|
81
|
+
const rawTargetingSpecSchema = z.object({
|
|
82
|
+
age_max: z.number().int().positive().optional(),
|
|
83
|
+
age_min: z.number().int().positive().optional(),
|
|
84
|
+
targeting_automation: targetingAutomationSchema.optional()
|
|
85
|
+
}).catchall(z.unknown()).refine((value) => Object.keys(value).length > 0, "targeting must include at least one key.");
|
|
86
|
+
export const targetingSpecSchema = rawTargetingSpecSchema
|
|
87
|
+
.transform((value) => normalizeAdvantageAudienceTargeting(value))
|
|
88
|
+
.superRefine((value, ctx) => {
|
|
89
|
+
if (value.age_min !== undefined
|
|
90
|
+
&& value.age_max !== undefined
|
|
91
|
+
&& value.age_min > value.age_max) {
|
|
92
|
+
ctx.addIssue({
|
|
93
|
+
code: z.ZodIssueCode.custom,
|
|
94
|
+
message: "targeting.age_min cannot be greater than targeting.age_max."
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
export const promotedObjectSchema = z.object({
|
|
99
|
+
applicationId: metaIdSchema("application id").optional(),
|
|
100
|
+
customConversionId: metaIdSchema("custom conversion id").optional(),
|
|
101
|
+
customEventType: nonEmptyStringSchema.optional(),
|
|
102
|
+
eventId: metaIdSchema("event id").optional(),
|
|
103
|
+
instagramActorId: metaIdSchema("instagram actor id").optional(),
|
|
104
|
+
objectStoreUrl: z.string().trim().url().optional(),
|
|
105
|
+
offlineConversionDataSetId: metaIdSchema("offline conversion data set id").optional(),
|
|
106
|
+
pageId: metaIdSchema("page id").optional(),
|
|
107
|
+
pixelId: metaIdSchema("pixel id").optional(),
|
|
108
|
+
productSetId: metaIdSchema("product set id").optional(),
|
|
109
|
+
whatsappPhoneNumber: nonEmptyStringSchema.optional()
|
|
110
|
+
}).strict().superRefine((value, ctx) => {
|
|
111
|
+
if (Object.keys(value).length === 0) {
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
message: "promotedObject must include at least one supported field."
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const imageCropValuesSchema = z.array(z.number());
|
|
119
|
+
const imageCropsSchema = z.record(z.string(), imageCropValuesSchema);
|
|
120
|
+
export const creativePlatformCustomizationOverrideSchema = z.object({
|
|
121
|
+
captionIds: z.array(nonEmptyStringSchema).optional(),
|
|
122
|
+
imageCrops: imageCropsSchema.optional(),
|
|
123
|
+
imageHash: nonEmptyStringSchema.optional(),
|
|
124
|
+
imageUrl: z.string().trim().url().optional(),
|
|
125
|
+
videoId: metaIdSchema("video id").optional()
|
|
126
|
+
}).strict().superRefine((value, ctx) => {
|
|
127
|
+
if (!value.imageHash && !value.imageUrl && !value.videoId) {
|
|
128
|
+
ctx.addIssue({
|
|
129
|
+
code: z.ZodIssueCode.custom,
|
|
130
|
+
message: "Platform customization must include imageHash, imageUrl, or videoId."
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
export const creativeFormatOverrideSchema = z.object({
|
|
135
|
+
captionIds: z.array(nonEmptyStringSchema).optional(),
|
|
136
|
+
imageCrops: imageCropsSchema.optional(),
|
|
137
|
+
imageHash: nonEmptyStringSchema.optional(),
|
|
138
|
+
imageUrl: z.string().trim().url().optional(),
|
|
139
|
+
videoId: metaIdSchema("video id").optional()
|
|
140
|
+
}).strict().superRefine((value, ctx) => {
|
|
141
|
+
if (!value.imageHash && !value.imageUrl && !value.videoId) {
|
|
142
|
+
ctx.addIssue({
|
|
143
|
+
code: z.ZodIssueCode.custom,
|
|
144
|
+
message: "Format override must include imageHash, imageUrl, or videoId."
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
export const creativeFormatsSchema = z.object({
|
|
149
|
+
feed4x5: creativeFormatOverrideSchema.optional(),
|
|
150
|
+
square1x1: creativeFormatOverrideSchema.optional(),
|
|
151
|
+
story9x16: creativeFormatOverrideSchema.optional()
|
|
152
|
+
}).strict().superRefine((value, ctx) => {
|
|
153
|
+
if (!launchCreativeFormatKeys.some((key) => value[key])) {
|
|
154
|
+
ctx.addIssue({
|
|
155
|
+
code: z.ZodIssueCode.custom,
|
|
156
|
+
message: "formats must include at least one supported format override."
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
export const creativePlatformCustomizationsSchema = z.object({
|
|
161
|
+
instagram: creativePlatformCustomizationOverrideSchema.optional()
|
|
162
|
+
}).strict().superRefine((value, ctx) => {
|
|
163
|
+
if (!value.instagram) {
|
|
164
|
+
ctx.addIssue({
|
|
165
|
+
code: z.ZodIssueCode.custom,
|
|
166
|
+
message: "platformCustomizations must include at least one supported platform override."
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
export const campaignCreateSpecSchema = z.object({
|
|
171
|
+
bidStrategy: bidStrategySchema.optional(),
|
|
172
|
+
dailyBudget: z.number().int().positive().optional(),
|
|
173
|
+
lifetimeBudget: z.number().int().positive().optional(),
|
|
174
|
+
name: nonEmptyStringSchema,
|
|
175
|
+
objective: campaignObjectiveSchema,
|
|
176
|
+
specialAdCategories: z.array(nonEmptyStringSchema).default([]),
|
|
177
|
+
status: pausedStatusSchema.optional()
|
|
178
|
+
}).strict().superRefine((value, ctx) => {
|
|
179
|
+
if (value.dailyBudget !== undefined && value.lifetimeBudget !== undefined) {
|
|
180
|
+
ctx.addIssue({
|
|
181
|
+
code: z.ZodIssueCode.custom,
|
|
182
|
+
message: "Provide either dailyBudget or lifetimeBudget for campaign budget optimization, not both."
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (value.bidStrategy !== undefined && !hasCampaignBudget(value)) {
|
|
186
|
+
ctx.addIssue({
|
|
187
|
+
code: z.ZodIssueCode.custom,
|
|
188
|
+
message: "Campaign bidStrategy requires dailyBudget or lifetimeBudget."
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
export const adSetCreateSpecSchema = z.object({
|
|
193
|
+
bidAmount: z.number().int().positive().optional(),
|
|
194
|
+
bidStrategy: bidStrategySchema.optional(),
|
|
195
|
+
billingEvent: z.enum(["IMPRESSIONS", "CLICKS"]),
|
|
196
|
+
campaignId: metaIdSchema("campaign id"),
|
|
197
|
+
dailyBudget: z.number().int().positive().optional(),
|
|
198
|
+
endTime: nonEmptyStringSchema.optional(),
|
|
199
|
+
lifetimeBudget: z.number().int().positive().optional(),
|
|
200
|
+
name: nonEmptyStringSchema,
|
|
201
|
+
optimizationGoal: optimizationGoalSchema,
|
|
202
|
+
promotedObject: promotedObjectSchema,
|
|
203
|
+
startTime: nonEmptyStringSchema.optional(),
|
|
204
|
+
status: pausedStatusSchema.optional(),
|
|
205
|
+
targeting: targetingSpecSchema
|
|
206
|
+
}).strict().superRefine((value, ctx) => {
|
|
207
|
+
if (value.dailyBudget === undefined && value.lifetimeBudget === undefined) {
|
|
208
|
+
ctx.addIssue({
|
|
209
|
+
code: z.ZodIssueCode.custom,
|
|
210
|
+
message: "Either dailyBudget or lifetimeBudget is required."
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (value.dailyBudget !== undefined && value.lifetimeBudget !== undefined) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: z.ZodIssueCode.custom,
|
|
216
|
+
message: "Provide either dailyBudget or lifetimeBudget, not both."
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (value.bidStrategy === "LOWEST_COST_WITHOUT_CAP" && value.bidAmount !== undefined) {
|
|
220
|
+
ctx.addIssue({
|
|
221
|
+
code: z.ZodIssueCode.custom,
|
|
222
|
+
message: "bidAmount cannot be combined with LOWEST_COST_WITHOUT_CAP."
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if ((value.bidStrategy === "LOWEST_COST_WITH_BID_CAP" || value.bidStrategy === "COST_CAP")
|
|
226
|
+
&& value.bidAmount === undefined) {
|
|
227
|
+
ctx.addIssue({
|
|
228
|
+
code: z.ZodIssueCode.custom,
|
|
229
|
+
message: `${value.bidStrategy} requires bidAmount.`
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if ((value.optimizationGoal === "OFFSITE_CONVERSIONS" || value.optimizationGoal === "VALUE")
|
|
233
|
+
&& (!value.promotedObject.pixelId || !value.promotedObject.customEventType)) {
|
|
234
|
+
ctx.addIssue({
|
|
235
|
+
code: z.ZodIssueCode.custom,
|
|
236
|
+
message: `${value.optimizationGoal} requires promotedObject.pixelId and promotedObject.customEventType.`
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if ((value.optimizationGoal === "CONVERSATIONS"
|
|
240
|
+
|| value.optimizationGoal === "LEAD_GENERATION"
|
|
241
|
+
|| value.optimizationGoal === "QUALITY_CALL"
|
|
242
|
+
|| value.optimizationGoal === "QUALITY_LEAD")
|
|
243
|
+
&& !value.promotedObject.pageId) {
|
|
244
|
+
ctx.addIssue({
|
|
245
|
+
code: z.ZodIssueCode.custom,
|
|
246
|
+
message: `${value.optimizationGoal} requires promotedObject.pageId.`
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const imageLinkCreativeSpecSchema = z.object({
|
|
251
|
+
formats: creativeFormatsSchema.optional(),
|
|
252
|
+
imageHash: nonEmptyStringSchema.optional(),
|
|
253
|
+
kind: z.literal("link-image"),
|
|
254
|
+
linkData: z.object({
|
|
255
|
+
callToAction: callToActionSchema.optional(),
|
|
256
|
+
description: nonEmptyStringSchema.optional(),
|
|
257
|
+
headline: nonEmptyStringSchema.optional(),
|
|
258
|
+
link: z.string().trim().url(),
|
|
259
|
+
message: nonEmptyStringSchema
|
|
260
|
+
}).strict(),
|
|
261
|
+
name: nonEmptyStringSchema.optional(),
|
|
262
|
+
pageId: metaIdSchema("page id"),
|
|
263
|
+
platformCustomizations: creativePlatformCustomizationsSchema.optional(),
|
|
264
|
+
status: pausedStatusSchema.optional()
|
|
265
|
+
}).strict().superRefine((value, ctx) => {
|
|
266
|
+
if (!value.imageHash && !hasPlacementFormats(value.formats)) {
|
|
267
|
+
ctx.addIssue({
|
|
268
|
+
code: z.ZodIssueCode.custom,
|
|
269
|
+
message: "Creative must include imageHash or formats."
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (value.platformCustomizations && value.formats) {
|
|
273
|
+
ctx.addIssue({
|
|
274
|
+
code: z.ZodIssueCode.custom,
|
|
275
|
+
message: "formats cannot be combined with legacy platformCustomizations."
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (value.platformCustomizations?.instagram?.videoId) {
|
|
279
|
+
ctx.addIssue({
|
|
280
|
+
code: z.ZodIssueCode.custom,
|
|
281
|
+
message: "platformCustomizations.instagram cannot use videoId for link-image creatives."
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
launchCreativeFormatKeys.forEach((formatKey) => {
|
|
285
|
+
const format = value.formats?.[formatKey];
|
|
286
|
+
if (format?.videoId) {
|
|
287
|
+
ctx.addIssue({
|
|
288
|
+
code: z.ZodIssueCode.custom,
|
|
289
|
+
message: `formats.${formatKey} cannot use videoId for link-image creatives.`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
const videoLinkCreativeSpecSchema = z.object({
|
|
295
|
+
formats: creativeFormatsSchema.optional(),
|
|
296
|
+
kind: z.literal("video-link"),
|
|
297
|
+
name: nonEmptyStringSchema.optional(),
|
|
298
|
+
pageId: metaIdSchema("page id"),
|
|
299
|
+
platformCustomizations: creativePlatformCustomizationsSchema.optional(),
|
|
300
|
+
status: pausedStatusSchema.optional(),
|
|
301
|
+
videoData: z.object({
|
|
302
|
+
callToAction: callToActionSchema.optional(),
|
|
303
|
+
description: nonEmptyStringSchema.optional(),
|
|
304
|
+
link: z.string().trim().url(),
|
|
305
|
+
message: nonEmptyStringSchema,
|
|
306
|
+
title: nonEmptyStringSchema.optional()
|
|
307
|
+
}).strict(),
|
|
308
|
+
videoId: metaIdSchema("video id").optional()
|
|
309
|
+
}).strict().superRefine((value, ctx) => {
|
|
310
|
+
if (!value.videoId && !hasPlacementFormats(value.formats)) {
|
|
311
|
+
ctx.addIssue({
|
|
312
|
+
code: z.ZodIssueCode.custom,
|
|
313
|
+
message: "Creative must include videoId or formats."
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (value.platformCustomizations && value.formats) {
|
|
317
|
+
ctx.addIssue({
|
|
318
|
+
code: z.ZodIssueCode.custom,
|
|
319
|
+
message: "formats cannot be combined with legacy platformCustomizations."
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (value.platformCustomizations?.instagram
|
|
323
|
+
&& (value.platformCustomizations.instagram.imageHash
|
|
324
|
+
|| value.platformCustomizations.instagram.imageUrl
|
|
325
|
+
|| value.platformCustomizations.instagram.imageCrops)) {
|
|
326
|
+
ctx.addIssue({
|
|
327
|
+
code: z.ZodIssueCode.custom,
|
|
328
|
+
message: "platformCustomizations.instagram cannot use imageHash, imageUrl, or imageCrops for video-link creatives."
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
launchCreativeFormatKeys.forEach((formatKey) => {
|
|
332
|
+
const format = value.formats?.[formatKey];
|
|
333
|
+
if (format && (format.imageHash || format.imageUrl || format.imageCrops)) {
|
|
334
|
+
ctx.addIssue({
|
|
335
|
+
code: z.ZodIssueCode.custom,
|
|
336
|
+
message: `formats.${formatKey} cannot use imageHash, imageUrl, or imageCrops for video-link creatives.`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
export const creativeCreateSpecSchema = z.discriminatedUnion("kind", [
|
|
342
|
+
imageLinkCreativeSpecSchema,
|
|
343
|
+
videoLinkCreativeSpecSchema
|
|
344
|
+
]);
|
|
345
|
+
export const adCreateSpecSchema = z.object({
|
|
346
|
+
adSetId: metaIdSchema("ad set id"),
|
|
347
|
+
creativeId: metaIdSchema("creative id"),
|
|
348
|
+
name: nonEmptyStringSchema,
|
|
349
|
+
status: pausedStatusSchema.optional()
|
|
350
|
+
}).strict();
|
|
351
|
+
async function ensureFileExists(filePath, label) {
|
|
352
|
+
const absolutePath = path.resolve(filePath);
|
|
353
|
+
try {
|
|
354
|
+
await access(absolutePath);
|
|
355
|
+
return absolutePath;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
throw new AppError(`${label} does not exist: ${filePath}`, ExitCode.Usage, {
|
|
359
|
+
filePath: absolutePath
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export async function resolveExistingFilePath(filePath, label, baseDir) {
|
|
364
|
+
if (!filePath.trim()) {
|
|
365
|
+
throw new AppError(`${label} is required.`, ExitCode.Usage);
|
|
366
|
+
}
|
|
367
|
+
return ensureFileExists(path.resolve(baseDir ?? process.cwd(), filePath), label);
|
|
368
|
+
}
|
|
369
|
+
export async function readSpecFile(specPath, schema, label) {
|
|
370
|
+
const absolutePath = await ensureFileExists(specPath, `${label} spec`);
|
|
371
|
+
let parsed;
|
|
372
|
+
try {
|
|
373
|
+
parsed = JSON.parse(await readFile(absolutePath, "utf8"));
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (error instanceof SyntaxError) {
|
|
377
|
+
throw new AppError(`${label} spec must be valid JSON.`, ExitCode.Usage, {
|
|
378
|
+
specPath: absolutePath
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
const result = schema.safeParse(parsed);
|
|
384
|
+
if (!result.success) {
|
|
385
|
+
throw new AppError(`${label} spec is invalid.`, ExitCode.Usage, {
|
|
386
|
+
issues: result.error.issues,
|
|
387
|
+
specPath: absolutePath
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
spec: result.data,
|
|
392
|
+
specPath: absolutePath
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const launchSpecSchema: z.ZodObject<{
|
|
3
|
+
version: z.ZodLiteral<1>;
|
|
4
|
+
campaign: z.ZodObject<{
|
|
5
|
+
bidStrategy: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
LOWEST_COST_WITHOUT_CAP: "LOWEST_COST_WITHOUT_CAP";
|
|
7
|
+
LOWEST_COST_WITH_BID_CAP: "LOWEST_COST_WITH_BID_CAP";
|
|
8
|
+
COST_CAP: "COST_CAP";
|
|
9
|
+
}>>;
|
|
10
|
+
dailyBudget: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
lifetimeBudget: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
ref: z.ZodString;
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
objective: z.ZodEnum<{
|
|
15
|
+
APP_INSTALLS: "APP_INSTALLS";
|
|
16
|
+
BRAND_AWARENESS: "BRAND_AWARENESS";
|
|
17
|
+
CONVERSIONS: "CONVERSIONS";
|
|
18
|
+
EVENT_RESPONSES: "EVENT_RESPONSES";
|
|
19
|
+
LEAD_GENERATION: "LEAD_GENERATION";
|
|
20
|
+
LINK_CLICKS: "LINK_CLICKS";
|
|
21
|
+
LOCAL_AWARENESS: "LOCAL_AWARENESS";
|
|
22
|
+
MESSAGES: "MESSAGES";
|
|
23
|
+
OFFER_CLAIMS: "OFFER_CLAIMS";
|
|
24
|
+
OUTCOME_APP_PROMOTION: "OUTCOME_APP_PROMOTION";
|
|
25
|
+
OUTCOME_AWARENESS: "OUTCOME_AWARENESS";
|
|
26
|
+
OUTCOME_ENGAGEMENT: "OUTCOME_ENGAGEMENT";
|
|
27
|
+
OUTCOME_LEADS: "OUTCOME_LEADS";
|
|
28
|
+
OUTCOME_SALES: "OUTCOME_SALES";
|
|
29
|
+
OUTCOME_TRAFFIC: "OUTCOME_TRAFFIC";
|
|
30
|
+
PAGE_LIKES: "PAGE_LIKES";
|
|
31
|
+
POST_ENGAGEMENT: "POST_ENGAGEMENT";
|
|
32
|
+
PRODUCT_CATALOG_SALES: "PRODUCT_CATALOG_SALES";
|
|
33
|
+
REACH: "REACH";
|
|
34
|
+
STORE_VISITS: "STORE_VISITS";
|
|
35
|
+
VIDEO_VIEWS: "VIDEO_VIEWS";
|
|
36
|
+
}>;
|
|
37
|
+
specialAdCategories: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
38
|
+
status: z.ZodOptional<z.ZodLiteral<"PAUSED">>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
adSets: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
41
|
+
ref: z.ZodString;
|
|
42
|
+
campaignRef: z.ZodString;
|
|
43
|
+
bidAmount: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
bidStrategy: z.ZodOptional<z.ZodEnum<{
|
|
45
|
+
LOWEST_COST_WITHOUT_CAP: "LOWEST_COST_WITHOUT_CAP";
|
|
46
|
+
LOWEST_COST_WITH_BID_CAP: "LOWEST_COST_WITH_BID_CAP";
|
|
47
|
+
COST_CAP: "COST_CAP";
|
|
48
|
+
}>>;
|
|
49
|
+
billingEvent: z.ZodEnum<{
|
|
50
|
+
IMPRESSIONS: "IMPRESSIONS";
|
|
51
|
+
CLICKS: "CLICKS";
|
|
52
|
+
}>;
|
|
53
|
+
dailyBudget: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
endTime: z.ZodOptional<z.ZodString>;
|
|
55
|
+
lifetimeBudget: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
name: z.ZodString;
|
|
57
|
+
optimizationGoal: z.ZodEnum<{
|
|
58
|
+
APP_INSTALLS: "APP_INSTALLS";
|
|
59
|
+
EVENT_RESPONSES: "EVENT_RESPONSES";
|
|
60
|
+
LEAD_GENERATION: "LEAD_GENERATION";
|
|
61
|
+
LINK_CLICKS: "LINK_CLICKS";
|
|
62
|
+
PAGE_LIKES: "PAGE_LIKES";
|
|
63
|
+
POST_ENGAGEMENT: "POST_ENGAGEMENT";
|
|
64
|
+
REACH: "REACH";
|
|
65
|
+
APP_INSTALLS_AND_OFFSITE_CONVERSIONS: "APP_INSTALLS_AND_OFFSITE_CONVERSIONS";
|
|
66
|
+
CONVERSATIONS: "CONVERSATIONS";
|
|
67
|
+
ENGAGED_PAGE_VIEWS: "ENGAGED_PAGE_VIEWS";
|
|
68
|
+
ENGAGED_USERS: "ENGAGED_USERS";
|
|
69
|
+
IMPRESSIONS: "IMPRESSIONS";
|
|
70
|
+
LANDING_PAGE_VIEWS: "LANDING_PAGE_VIEWS";
|
|
71
|
+
OFFSITE_CONVERSIONS: "OFFSITE_CONVERSIONS";
|
|
72
|
+
QUALITY_CALL: "QUALITY_CALL";
|
|
73
|
+
QUALITY_LEAD: "QUALITY_LEAD";
|
|
74
|
+
THRUPLAY: "THRUPLAY";
|
|
75
|
+
VALUE: "VALUE";
|
|
76
|
+
VISIT_INSTAGRAM_PROFILE: "VISIT_INSTAGRAM_PROFILE";
|
|
77
|
+
}>;
|
|
78
|
+
promotedObject: z.ZodObject<{
|
|
79
|
+
applicationId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
80
|
+
customConversionId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
81
|
+
customEventType: z.ZodOptional<z.ZodString>;
|
|
82
|
+
eventId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
83
|
+
instagramActorId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
84
|
+
objectStoreUrl: z.ZodOptional<z.ZodString>;
|
|
85
|
+
offlineConversionDataSetId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
86
|
+
pageId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
87
|
+
pixelId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
88
|
+
productSetId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
89
|
+
whatsappPhoneNumber: z.ZodOptional<z.ZodString>;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
startTime: z.ZodOptional<z.ZodString>;
|
|
92
|
+
status: z.ZodOptional<z.ZodLiteral<"PAUSED">>;
|
|
93
|
+
targeting: z.ZodPipe<z.ZodObject<{
|
|
94
|
+
age_max: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
age_min: z.ZodOptional<z.ZodNumber>;
|
|
96
|
+
targeting_automation: z.ZodOptional<z.ZodObject<{
|
|
97
|
+
advantage_audience: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>>;
|
|
98
|
+
individual_setting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>>>;
|
|
99
|
+
}, z.core.$catchall<z.ZodUnknown>>>;
|
|
100
|
+
}, z.core.$catchall<z.ZodUnknown>>, z.ZodTransform<{
|
|
101
|
+
[x: string]: unknown;
|
|
102
|
+
age_max?: number | undefined;
|
|
103
|
+
age_min?: number | undefined;
|
|
104
|
+
targeting_automation?: {
|
|
105
|
+
[x: string]: unknown;
|
|
106
|
+
advantage_audience?: 0 | 1 | undefined;
|
|
107
|
+
individual_setting?: Record<string, 0 | 1> | undefined;
|
|
108
|
+
} | undefined;
|
|
109
|
+
}, {
|
|
110
|
+
[x: string]: unknown;
|
|
111
|
+
age_max?: number | undefined;
|
|
112
|
+
age_min?: number | undefined;
|
|
113
|
+
targeting_automation?: {
|
|
114
|
+
[x: string]: unknown;
|
|
115
|
+
advantage_audience?: 0 | 1 | undefined;
|
|
116
|
+
individual_setting?: Record<string, 0 | 1> | undefined;
|
|
117
|
+
} | undefined;
|
|
118
|
+
}>>;
|
|
119
|
+
}, z.core.$strict>>>;
|
|
120
|
+
assets: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
121
|
+
ref: z.ZodString;
|
|
122
|
+
kind: z.ZodLiteral<"image">;
|
|
123
|
+
file: z.ZodString;
|
|
124
|
+
name: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
126
|
+
ref: z.ZodString;
|
|
127
|
+
kind: z.ZodLiteral<"video">;
|
|
128
|
+
file: z.ZodString;
|
|
129
|
+
name: z.ZodOptional<z.ZodString>;
|
|
130
|
+
}, z.core.$strict>], "kind">>>;
|
|
131
|
+
creatives: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
132
|
+
ref: z.ZodString;
|
|
133
|
+
kind: z.ZodLiteral<"link-image">;
|
|
134
|
+
assetRef: z.ZodOptional<z.ZodString>;
|
|
135
|
+
formats: z.ZodOptional<z.ZodObject<{
|
|
136
|
+
feed4x5: z.ZodOptional<z.ZodObject<{
|
|
137
|
+
assetRef: z.ZodString;
|
|
138
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
139
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
140
|
+
}, z.core.$strict>>;
|
|
141
|
+
square1x1: z.ZodOptional<z.ZodObject<{
|
|
142
|
+
assetRef: z.ZodString;
|
|
143
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
144
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
145
|
+
}, z.core.$strict>>;
|
|
146
|
+
story9x16: z.ZodOptional<z.ZodObject<{
|
|
147
|
+
assetRef: z.ZodString;
|
|
148
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
149
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
150
|
+
}, z.core.$strict>>;
|
|
151
|
+
}, z.core.$strict>>;
|
|
152
|
+
linkData: z.ZodObject<{
|
|
153
|
+
callToAction: z.ZodOptional<z.ZodEnum<{
|
|
154
|
+
SHOP_NOW: "SHOP_NOW";
|
|
155
|
+
LEARN_MORE: "LEARN_MORE";
|
|
156
|
+
SIGN_UP: "SIGN_UP";
|
|
157
|
+
}>>;
|
|
158
|
+
description: z.ZodOptional<z.ZodString>;
|
|
159
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
160
|
+
link: z.ZodString;
|
|
161
|
+
message: z.ZodString;
|
|
162
|
+
}, z.core.$strict>;
|
|
163
|
+
name: z.ZodOptional<z.ZodString>;
|
|
164
|
+
pageId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
165
|
+
platformCustomizations: z.ZodOptional<z.ZodObject<{
|
|
166
|
+
instagram: z.ZodOptional<z.ZodObject<{
|
|
167
|
+
assetRef: z.ZodString;
|
|
168
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
169
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
170
|
+
}, z.core.$strict>>;
|
|
171
|
+
}, z.core.$strict>>;
|
|
172
|
+
status: z.ZodOptional<z.ZodLiteral<"PAUSED">>;
|
|
173
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
174
|
+
ref: z.ZodString;
|
|
175
|
+
kind: z.ZodLiteral<"video-link">;
|
|
176
|
+
assetRef: z.ZodOptional<z.ZodString>;
|
|
177
|
+
formats: z.ZodOptional<z.ZodObject<{
|
|
178
|
+
feed4x5: z.ZodOptional<z.ZodObject<{
|
|
179
|
+
assetRef: z.ZodString;
|
|
180
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
181
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
182
|
+
}, z.core.$strict>>;
|
|
183
|
+
square1x1: z.ZodOptional<z.ZodObject<{
|
|
184
|
+
assetRef: z.ZodString;
|
|
185
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
186
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
187
|
+
}, z.core.$strict>>;
|
|
188
|
+
story9x16: z.ZodOptional<z.ZodObject<{
|
|
189
|
+
assetRef: z.ZodString;
|
|
190
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
191
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
192
|
+
}, z.core.$strict>>;
|
|
193
|
+
}, z.core.$strict>>;
|
|
194
|
+
name: z.ZodOptional<z.ZodString>;
|
|
195
|
+
pageId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
196
|
+
platformCustomizations: z.ZodOptional<z.ZodObject<{
|
|
197
|
+
instagram: z.ZodOptional<z.ZodObject<{
|
|
198
|
+
assetRef: z.ZodString;
|
|
199
|
+
captionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
200
|
+
imageCrops: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>>;
|
|
201
|
+
}, z.core.$strict>>;
|
|
202
|
+
}, z.core.$strict>>;
|
|
203
|
+
status: z.ZodOptional<z.ZodLiteral<"PAUSED">>;
|
|
204
|
+
videoData: z.ZodObject<{
|
|
205
|
+
callToAction: z.ZodOptional<z.ZodEnum<{
|
|
206
|
+
SHOP_NOW: "SHOP_NOW";
|
|
207
|
+
LEARN_MORE: "LEARN_MORE";
|
|
208
|
+
SIGN_UP: "SIGN_UP";
|
|
209
|
+
}>>;
|
|
210
|
+
description: z.ZodOptional<z.ZodString>;
|
|
211
|
+
link: z.ZodString;
|
|
212
|
+
message: z.ZodString;
|
|
213
|
+
title: z.ZodOptional<z.ZodString>;
|
|
214
|
+
}, z.core.$strict>;
|
|
215
|
+
}, z.core.$strict>], "kind">>>;
|
|
216
|
+
ads: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
217
|
+
ref: z.ZodString;
|
|
218
|
+
name: z.ZodString;
|
|
219
|
+
adSetRef: z.ZodString;
|
|
220
|
+
creativeRef: z.ZodString;
|
|
221
|
+
status: z.ZodOptional<z.ZodLiteral<"PAUSED">>;
|
|
222
|
+
}, z.core.$strict>>>;
|
|
223
|
+
}, z.core.$strict>;
|
|
224
|
+
export type LaunchSpec = z.infer<typeof launchSpecSchema>;
|
|
225
|
+
export declare function readLaunchSpecFile(specPath: string): Promise<{
|
|
226
|
+
spec: LaunchSpec;
|
|
227
|
+
specPath: string;
|
|
228
|
+
}>;
|
|
229
|
+
export declare function requireReceiptPath(value: string | undefined): string;
|