@adkit/cli 1.13.25 → 1.13.26
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/dist/cli.js +215 -191
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,14 +25,6 @@ function readStringProperty(value, key) {
|
|
|
25
25
|
var GOOGLE_LIMITED_AD_APPROVAL_STATUSES = ["APPROVED_LIMITED", "AREA_OF_INTEREST_ONLY"];
|
|
26
26
|
var GOOGLE_PROBLEM_AD_APPROVAL_STATUSES = ["DISAPPROVED", ...GOOGLE_LIMITED_AD_APPROVAL_STATUSES];
|
|
27
27
|
|
|
28
|
-
// ../shared/dist/manage/google/campaign-validation.js
|
|
29
|
-
var VALID_GOOGLE_BID_STRATEGIES = ["manual_cpc", "manual_cpm", "maximize_clicks", "maximize_conversions", "maximize_conversion_value", "target_spend", "target_impression_share"];
|
|
30
|
-
var SUPPORTED_GOOGLE_CAMPAIGN_TYPES = ["search", "display"];
|
|
31
|
-
var validGoogleBidStrategySet = new Set(VALID_GOOGLE_BID_STRATEGIES);
|
|
32
|
-
var validGoogleBidStrategyList = VALID_GOOGLE_BID_STRATEGIES.join(", ");
|
|
33
|
-
var supportedGoogleCampaignTypeSet = new Set(SUPPORTED_GOOGLE_CAMPAIGN_TYPES);
|
|
34
|
-
var supportedGoogleCampaignTypeList = SUPPORTED_GOOGLE_CAMPAIGN_TYPES.join(", ");
|
|
35
|
-
|
|
36
28
|
// ../shared/dist/manage/google/validators/conversion-goals.js
|
|
37
29
|
var GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES = [
|
|
38
30
|
"add_to_cart",
|
|
@@ -60,6 +52,188 @@ var GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES = [
|
|
|
60
52
|
];
|
|
61
53
|
var googleCampaignConversionGoalCategorySet = new Set(GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORIES);
|
|
62
54
|
|
|
55
|
+
// ../shared/dist/manage/google/validators/campaigns.js
|
|
56
|
+
var VALID_GOOGLE_BID_STRATEGIES = ["manual_cpc", "manual_cpm", "maximize_clicks", "maximize_conversions", "maximize_conversion_value", "target_spend", "target_impression_share"];
|
|
57
|
+
var SUPPORTED_GOOGLE_CAMPAIGN_TYPES = ["search", "display", "demand_gen"];
|
|
58
|
+
var GOOGLE_DEMAND_GEN_BID_STRATEGIES = ["maximize_clicks", "maximize_conversions", "maximize_conversion_value"];
|
|
59
|
+
var GOOGLE_DEMAND_GEN_BID_STRATEGY_LIST = GOOGLE_DEMAND_GEN_BID_STRATEGIES.join(", ");
|
|
60
|
+
var validGoogleBidStrategySet = new Set(VALID_GOOGLE_BID_STRATEGIES);
|
|
61
|
+
var validGoogleBidStrategyList = VALID_GOOGLE_BID_STRATEGIES.join(", ");
|
|
62
|
+
var supportedGoogleCampaignTypeSet = new Set(SUPPORTED_GOOGLE_CAMPAIGN_TYPES);
|
|
63
|
+
var supportedGoogleCampaignTypeList = SUPPORTED_GOOGLE_CAMPAIGN_TYPES.join(", ");
|
|
64
|
+
|
|
65
|
+
// ../shared/dist/manage/google/validators/ad-groups.js
|
|
66
|
+
var GOOGLE_DEMAND_GEN_NETWORKS = ["youtube_in_feed", "youtube_in_stream", "youtube_shorts"];
|
|
67
|
+
var GOOGLE_DEMAND_GEN_NETWORK_LIST = GOOGLE_DEMAND_GEN_NETWORKS.join(", ");
|
|
68
|
+
|
|
69
|
+
// ../shared/dist/manage/google/asset-utils.js
|
|
70
|
+
var SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS = ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
|
|
71
|
+
var STRUCTURED_SNIPPET_HEADER_LOOKUP = new Map(SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS.map((header) => [header.toLowerCase(), header]));
|
|
72
|
+
var DAY_TO_GOOGLE_MAP = {
|
|
73
|
+
monday: "MONDAY",
|
|
74
|
+
tuesday: "TUESDAY",
|
|
75
|
+
wednesday: "WEDNESDAY",
|
|
76
|
+
thursday: "THURSDAY",
|
|
77
|
+
friday: "FRIDAY",
|
|
78
|
+
saturday: "SATURDAY",
|
|
79
|
+
sunday: "SUNDAY"
|
|
80
|
+
};
|
|
81
|
+
var MINUTE_TO_GOOGLE_MAP = {
|
|
82
|
+
0: "ZERO",
|
|
83
|
+
15: "FIFTEEN",
|
|
84
|
+
30: "THIRTY",
|
|
85
|
+
45: "FORTY_FIVE"
|
|
86
|
+
};
|
|
87
|
+
function normalizeWhitespace(value) {
|
|
88
|
+
return value.trim().replace(/\s+/g, " ");
|
|
89
|
+
}
|
|
90
|
+
function normalizeOptionalText(value) {
|
|
91
|
+
if (typeof value !== "string")
|
|
92
|
+
return void 0;
|
|
93
|
+
const normalized = normalizeWhitespace(value);
|
|
94
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
95
|
+
}
|
|
96
|
+
function normalizeDate(value) {
|
|
97
|
+
const normalized = normalizeOptionalText(value);
|
|
98
|
+
if (!normalized)
|
|
99
|
+
return void 0;
|
|
100
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
|
|
101
|
+
throw new Error(`Invalid date "${normalized}". Use YYYY-MM-DD.`);
|
|
102
|
+
const [yearRaw, monthRaw, dayRaw] = normalized.split("-");
|
|
103
|
+
const year = Number.parseInt(yearRaw ?? "", 10);
|
|
104
|
+
const month = Number.parseInt(monthRaw ?? "", 10);
|
|
105
|
+
const day = Number.parseInt(dayRaw ?? "", 10);
|
|
106
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
107
|
+
if (Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day)
|
|
108
|
+
throw new Error(`Invalid calendar date "${normalized}". Use a real YYYY-MM-DD date.`);
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
function validateDateRange(startDate, endDate) {
|
|
112
|
+
if (!startDate || !endDate)
|
|
113
|
+
return;
|
|
114
|
+
if (Date.parse(`${startDate}T00:00:00Z`) > Date.parse(`${endDate}T00:00:00Z`))
|
|
115
|
+
throw new Error("startDate must be on or before endDate");
|
|
116
|
+
}
|
|
117
|
+
function normalizeUrl(value) {
|
|
118
|
+
const raw = value.trim();
|
|
119
|
+
if (!raw)
|
|
120
|
+
throw new Error("URL cannot be empty");
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(raw);
|
|
123
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
124
|
+
throw new Error("Only http:// and https:// URLs are allowed");
|
|
125
|
+
return url.toString();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof Error && error.message === "Only http:// and https:// URLs are allowed")
|
|
128
|
+
throw error;
|
|
129
|
+
throw new Error(`Invalid URL "${value}"`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function normalizeScheduleEntry(entry) {
|
|
133
|
+
const { dayOfWeek, startHour, startMinute, endHour, endMinute } = entry;
|
|
134
|
+
if (!(dayOfWeek in DAY_TO_GOOGLE_MAP))
|
|
135
|
+
throw new Error(`Invalid schedule day "${String(dayOfWeek)}"`);
|
|
136
|
+
if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23)
|
|
137
|
+
throw new Error("Schedule startHour must be an integer between 0 and 23");
|
|
138
|
+
if (!Number.isInteger(endHour) || endHour < 0 || endHour > 24)
|
|
139
|
+
throw new Error("Schedule endHour must be an integer between 0 and 24");
|
|
140
|
+
if (!(startMinute in MINUTE_TO_GOOGLE_MAP))
|
|
141
|
+
throw new Error("Schedule startMinute must be one of 0, 15, 30, 45");
|
|
142
|
+
if (!(endMinute in MINUTE_TO_GOOGLE_MAP))
|
|
143
|
+
throw new Error("Schedule endMinute must be one of 0, 15, 30, 45");
|
|
144
|
+
if (endHour === 24 && endMinute !== 0)
|
|
145
|
+
throw new Error("Schedule endMinute must be 0 when endHour is 24");
|
|
146
|
+
const startTotalMinutes = startHour * 60 + startMinute;
|
|
147
|
+
const endTotalMinutes = endHour * 60 + endMinute;
|
|
148
|
+
if (endTotalMinutes <= startTotalMinutes)
|
|
149
|
+
throw new Error("Schedule end must be after start");
|
|
150
|
+
return { dayOfWeek, startHour, startMinute, endHour, endMinute };
|
|
151
|
+
}
|
|
152
|
+
function normalizeSchedule(schedule) {
|
|
153
|
+
if (!schedule)
|
|
154
|
+
return void 0;
|
|
155
|
+
if (!Array.isArray(schedule))
|
|
156
|
+
throw new Error("schedule must be an array");
|
|
157
|
+
if (schedule.length === 0)
|
|
158
|
+
return void 0;
|
|
159
|
+
const normalized = schedule.map(normalizeScheduleEntry).sort((left, right) => {
|
|
160
|
+
const leftKey = `${left.dayOfWeek}:${String(left.startHour).padStart(2, "0")}:${left.startMinute}:${String(left.endHour).padStart(2, "0")}:${left.endMinute}`;
|
|
161
|
+
const rightKey = `${right.dayOfWeek}:${String(right.startHour).padStart(2, "0")}:${right.startMinute}:${String(right.endHour).padStart(2, "0")}:${right.endMinute}`;
|
|
162
|
+
return leftKey.localeCompare(rightKey);
|
|
163
|
+
});
|
|
164
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const entry of normalized) {
|
|
166
|
+
const current = perDay.get(entry.dayOfWeek) ?? 0;
|
|
167
|
+
const next = current + 1;
|
|
168
|
+
if (next > 6)
|
|
169
|
+
throw new Error(`Schedule exceeds Google limit of 6 entries on ${entry.dayOfWeek}`);
|
|
170
|
+
perDay.set(entry.dayOfWeek, next);
|
|
171
|
+
}
|
|
172
|
+
if (normalized.length > 42)
|
|
173
|
+
throw new Error("Schedule exceeds Google limit of 42 total entries");
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
function normalizeSitelinkAsset(asset) {
|
|
177
|
+
const linkText = normalizeOptionalText(asset.linkText);
|
|
178
|
+
if (!linkText)
|
|
179
|
+
throw new Error("Sitelink linkText is required");
|
|
180
|
+
if (linkText.length > 25)
|
|
181
|
+
throw new Error("Sitelink linkText must be between 1 and 25 characters");
|
|
182
|
+
if (!Array.isArray(asset.finalUrls) || asset.finalUrls.length === 0)
|
|
183
|
+
throw new Error("Sitelink finalUrls must contain at least one URL");
|
|
184
|
+
const finalUrls = asset.finalUrls.map(normalizeUrl);
|
|
185
|
+
const description1 = normalizeOptionalText(asset.description1);
|
|
186
|
+
const description2 = normalizeOptionalText(asset.description2);
|
|
187
|
+
if (description1 && !description2 || !description1 && description2)
|
|
188
|
+
throw new Error("Sitelink description1 and description2 must either both be set or both be omitted");
|
|
189
|
+
if (description1 && description1.length > 35)
|
|
190
|
+
throw new Error("Sitelink description1 must be between 1 and 35 characters");
|
|
191
|
+
if (description2 && description2.length > 35)
|
|
192
|
+
throw new Error("Sitelink description2 must be between 1 and 35 characters");
|
|
193
|
+
const startDate = normalizeDate(asset.startDate);
|
|
194
|
+
const endDate = normalizeDate(asset.endDate);
|
|
195
|
+
validateDateRange(startDate, endDate);
|
|
196
|
+
const urlCustomParameters = asset.urlCustomParameters?.map((parameter) => {
|
|
197
|
+
const key = normalizeOptionalText(parameter.key);
|
|
198
|
+
const value = normalizeOptionalText(parameter.value);
|
|
199
|
+
if (!key || !value)
|
|
200
|
+
throw new Error("urlCustomParameters require non-empty key and value");
|
|
201
|
+
return { key, value };
|
|
202
|
+
}) ?? void 0;
|
|
203
|
+
return {
|
|
204
|
+
type: "sitelink",
|
|
205
|
+
linkText,
|
|
206
|
+
finalUrls,
|
|
207
|
+
description1,
|
|
208
|
+
description2,
|
|
209
|
+
startDate,
|
|
210
|
+
endDate,
|
|
211
|
+
schedule: normalizeSchedule(asset.schedule),
|
|
212
|
+
trackingUrlTemplate: normalizeOptionalText(asset.trackingUrlTemplate),
|
|
213
|
+
finalUrlSuffix: normalizeOptionalText(asset.finalUrlSuffix),
|
|
214
|
+
urlCustomParameters: urlCustomParameters && urlCustomParameters.length > 0 ? urlCustomParameters : void 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function parseGoogleAssetReference(value) {
|
|
218
|
+
if (!value.startsWith("asset:"))
|
|
219
|
+
return null;
|
|
220
|
+
const assetId = value.slice("asset:".length).trim();
|
|
221
|
+
if (!/^\d+$/.test(assetId))
|
|
222
|
+
throw new Error(`Invalid asset reference "${value}". Expected asset:<numeric-id>.`);
|
|
223
|
+
return assetId;
|
|
224
|
+
}
|
|
225
|
+
function parseGoogleSitelinkShorthand(value) {
|
|
226
|
+
const separatorCount = value.split("|").length - 1;
|
|
227
|
+
if (separatorCount !== 1)
|
|
228
|
+
throw new Error('Invalid sitelink shorthand. Expected exactly one "|" in "text|url".');
|
|
229
|
+
const [rawText, rawUrl] = value.split("|");
|
|
230
|
+
const linkText = normalizeOptionalText(rawText);
|
|
231
|
+
if (!linkText)
|
|
232
|
+
throw new Error("Invalid sitelink shorthand. Text cannot be empty.");
|
|
233
|
+
const finalUrl = normalizeUrl(rawUrl ?? "");
|
|
234
|
+
return normalizeSitelinkAsset({ type: "sitelink", linkText, finalUrls: [finalUrl] });
|
|
235
|
+
}
|
|
236
|
+
|
|
63
237
|
// ../shared/dist/manage/google/types/google-adkit.js
|
|
64
238
|
var GOOGLE_GEO_LOCATION_SEARCH_TYPES = ["country", "region", "city", "postal_code", "metro"];
|
|
65
239
|
|
|
@@ -1820,174 +1994,6 @@ function getAd(client, args, flags) {
|
|
|
1820
1994
|
return getMetaEntity(client, args, flags, { path: "ads", usageHint: "Run: adkit manage meta ads <ad-id>" });
|
|
1821
1995
|
}
|
|
1822
1996
|
|
|
1823
|
-
// ../shared/dist/manage/google/asset-utils.js
|
|
1824
|
-
var SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS = ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
|
|
1825
|
-
var STRUCTURED_SNIPPET_HEADER_LOOKUP = new Map(SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS.map((header) => [header.toLowerCase(), header]));
|
|
1826
|
-
var DAY_TO_GOOGLE_MAP = {
|
|
1827
|
-
monday: "MONDAY",
|
|
1828
|
-
tuesday: "TUESDAY",
|
|
1829
|
-
wednesday: "WEDNESDAY",
|
|
1830
|
-
thursday: "THURSDAY",
|
|
1831
|
-
friday: "FRIDAY",
|
|
1832
|
-
saturday: "SATURDAY",
|
|
1833
|
-
sunday: "SUNDAY"
|
|
1834
|
-
};
|
|
1835
|
-
var MINUTE_TO_GOOGLE_MAP = {
|
|
1836
|
-
0: "ZERO",
|
|
1837
|
-
15: "FIFTEEN",
|
|
1838
|
-
30: "THIRTY",
|
|
1839
|
-
45: "FORTY_FIVE"
|
|
1840
|
-
};
|
|
1841
|
-
function normalizeWhitespace(value) {
|
|
1842
|
-
return value.trim().replace(/\s+/g, " ");
|
|
1843
|
-
}
|
|
1844
|
-
function normalizeOptionalText(value) {
|
|
1845
|
-
if (typeof value !== "string")
|
|
1846
|
-
return void 0;
|
|
1847
|
-
const normalized = normalizeWhitespace(value);
|
|
1848
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
1849
|
-
}
|
|
1850
|
-
function normalizeDate(value) {
|
|
1851
|
-
const normalized = normalizeOptionalText(value);
|
|
1852
|
-
if (!normalized)
|
|
1853
|
-
return void 0;
|
|
1854
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
|
|
1855
|
-
throw new Error(`Invalid date "${normalized}". Use YYYY-MM-DD.`);
|
|
1856
|
-
const [yearRaw, monthRaw, dayRaw] = normalized.split("-");
|
|
1857
|
-
const year = Number.parseInt(yearRaw ?? "", 10);
|
|
1858
|
-
const month = Number.parseInt(monthRaw ?? "", 10);
|
|
1859
|
-
const day = Number.parseInt(dayRaw ?? "", 10);
|
|
1860
|
-
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
1861
|
-
if (Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day)
|
|
1862
|
-
throw new Error(`Invalid calendar date "${normalized}". Use a real YYYY-MM-DD date.`);
|
|
1863
|
-
return normalized;
|
|
1864
|
-
}
|
|
1865
|
-
function validateDateRange(startDate, endDate) {
|
|
1866
|
-
if (!startDate || !endDate)
|
|
1867
|
-
return;
|
|
1868
|
-
if (Date.parse(`${startDate}T00:00:00Z`) > Date.parse(`${endDate}T00:00:00Z`))
|
|
1869
|
-
throw new Error("startDate must be on or before endDate");
|
|
1870
|
-
}
|
|
1871
|
-
function normalizeUrl(value) {
|
|
1872
|
-
const raw = value.trim();
|
|
1873
|
-
if (!raw)
|
|
1874
|
-
throw new Error("URL cannot be empty");
|
|
1875
|
-
try {
|
|
1876
|
-
const url = new URL(raw);
|
|
1877
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
1878
|
-
throw new Error("Only http:// and https:// URLs are allowed");
|
|
1879
|
-
return url.toString();
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
if (error instanceof Error && error.message === "Only http:// and https:// URLs are allowed")
|
|
1882
|
-
throw error;
|
|
1883
|
-
throw new Error(`Invalid URL "${value}"`);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
function normalizeScheduleEntry(entry) {
|
|
1887
|
-
const { dayOfWeek, startHour, startMinute, endHour, endMinute } = entry;
|
|
1888
|
-
if (!(dayOfWeek in DAY_TO_GOOGLE_MAP))
|
|
1889
|
-
throw new Error(`Invalid schedule day "${String(dayOfWeek)}"`);
|
|
1890
|
-
if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23)
|
|
1891
|
-
throw new Error("Schedule startHour must be an integer between 0 and 23");
|
|
1892
|
-
if (!Number.isInteger(endHour) || endHour < 0 || endHour > 24)
|
|
1893
|
-
throw new Error("Schedule endHour must be an integer between 0 and 24");
|
|
1894
|
-
if (!(startMinute in MINUTE_TO_GOOGLE_MAP))
|
|
1895
|
-
throw new Error("Schedule startMinute must be one of 0, 15, 30, 45");
|
|
1896
|
-
if (!(endMinute in MINUTE_TO_GOOGLE_MAP))
|
|
1897
|
-
throw new Error("Schedule endMinute must be one of 0, 15, 30, 45");
|
|
1898
|
-
if (endHour === 24 && endMinute !== 0)
|
|
1899
|
-
throw new Error("Schedule endMinute must be 0 when endHour is 24");
|
|
1900
|
-
const startTotalMinutes = startHour * 60 + startMinute;
|
|
1901
|
-
const endTotalMinutes = endHour * 60 + endMinute;
|
|
1902
|
-
if (endTotalMinutes <= startTotalMinutes)
|
|
1903
|
-
throw new Error("Schedule end must be after start");
|
|
1904
|
-
return { dayOfWeek, startHour, startMinute, endHour, endMinute };
|
|
1905
|
-
}
|
|
1906
|
-
function normalizeSchedule(schedule) {
|
|
1907
|
-
if (!schedule)
|
|
1908
|
-
return void 0;
|
|
1909
|
-
if (!Array.isArray(schedule))
|
|
1910
|
-
throw new Error("schedule must be an array");
|
|
1911
|
-
if (schedule.length === 0)
|
|
1912
|
-
return void 0;
|
|
1913
|
-
const normalized = schedule.map(normalizeScheduleEntry).sort((left, right) => {
|
|
1914
|
-
const leftKey = `${left.dayOfWeek}:${String(left.startHour).padStart(2, "0")}:${left.startMinute}:${String(left.endHour).padStart(2, "0")}:${left.endMinute}`;
|
|
1915
|
-
const rightKey = `${right.dayOfWeek}:${String(right.startHour).padStart(2, "0")}:${right.startMinute}:${String(right.endHour).padStart(2, "0")}:${right.endMinute}`;
|
|
1916
|
-
return leftKey.localeCompare(rightKey);
|
|
1917
|
-
});
|
|
1918
|
-
const perDay = /* @__PURE__ */ new Map();
|
|
1919
|
-
for (const entry of normalized) {
|
|
1920
|
-
const current = perDay.get(entry.dayOfWeek) ?? 0;
|
|
1921
|
-
const next = current + 1;
|
|
1922
|
-
if (next > 6)
|
|
1923
|
-
throw new Error(`Schedule exceeds Google limit of 6 entries on ${entry.dayOfWeek}`);
|
|
1924
|
-
perDay.set(entry.dayOfWeek, next);
|
|
1925
|
-
}
|
|
1926
|
-
if (normalized.length > 42)
|
|
1927
|
-
throw new Error("Schedule exceeds Google limit of 42 total entries");
|
|
1928
|
-
return normalized;
|
|
1929
|
-
}
|
|
1930
|
-
function normalizeSitelinkAsset(asset) {
|
|
1931
|
-
const linkText = normalizeOptionalText(asset.linkText);
|
|
1932
|
-
if (!linkText)
|
|
1933
|
-
throw new Error("Sitelink linkText is required");
|
|
1934
|
-
if (linkText.length > 25)
|
|
1935
|
-
throw new Error("Sitelink linkText must be between 1 and 25 characters");
|
|
1936
|
-
if (!Array.isArray(asset.finalUrls) || asset.finalUrls.length === 0)
|
|
1937
|
-
throw new Error("Sitelink finalUrls must contain at least one URL");
|
|
1938
|
-
const finalUrls = asset.finalUrls.map(normalizeUrl);
|
|
1939
|
-
const description1 = normalizeOptionalText(asset.description1);
|
|
1940
|
-
const description2 = normalizeOptionalText(asset.description2);
|
|
1941
|
-
if (description1 && !description2 || !description1 && description2)
|
|
1942
|
-
throw new Error("Sitelink description1 and description2 must either both be set or both be omitted");
|
|
1943
|
-
if (description1 && description1.length > 35)
|
|
1944
|
-
throw new Error("Sitelink description1 must be between 1 and 35 characters");
|
|
1945
|
-
if (description2 && description2.length > 35)
|
|
1946
|
-
throw new Error("Sitelink description2 must be between 1 and 35 characters");
|
|
1947
|
-
const startDate = normalizeDate(asset.startDate);
|
|
1948
|
-
const endDate = normalizeDate(asset.endDate);
|
|
1949
|
-
validateDateRange(startDate, endDate);
|
|
1950
|
-
const urlCustomParameters = asset.urlCustomParameters?.map((parameter) => {
|
|
1951
|
-
const key = normalizeOptionalText(parameter.key);
|
|
1952
|
-
const value = normalizeOptionalText(parameter.value);
|
|
1953
|
-
if (!key || !value)
|
|
1954
|
-
throw new Error("urlCustomParameters require non-empty key and value");
|
|
1955
|
-
return { key, value };
|
|
1956
|
-
}) ?? void 0;
|
|
1957
|
-
return {
|
|
1958
|
-
type: "sitelink",
|
|
1959
|
-
linkText,
|
|
1960
|
-
finalUrls,
|
|
1961
|
-
description1,
|
|
1962
|
-
description2,
|
|
1963
|
-
startDate,
|
|
1964
|
-
endDate,
|
|
1965
|
-
schedule: normalizeSchedule(asset.schedule),
|
|
1966
|
-
trackingUrlTemplate: normalizeOptionalText(asset.trackingUrlTemplate),
|
|
1967
|
-
finalUrlSuffix: normalizeOptionalText(asset.finalUrlSuffix),
|
|
1968
|
-
urlCustomParameters: urlCustomParameters && urlCustomParameters.length > 0 ? urlCustomParameters : void 0
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
|
-
function parseGoogleAssetReference(value) {
|
|
1972
|
-
if (!value.startsWith("asset:"))
|
|
1973
|
-
return null;
|
|
1974
|
-
const assetId = value.slice("asset:".length).trim();
|
|
1975
|
-
if (!/^\d+$/.test(assetId))
|
|
1976
|
-
throw new Error(`Invalid asset reference "${value}". Expected asset:<numeric-id>.`);
|
|
1977
|
-
return assetId;
|
|
1978
|
-
}
|
|
1979
|
-
function parseGoogleSitelinkShorthand(value) {
|
|
1980
|
-
const separatorCount = value.split("|").length - 1;
|
|
1981
|
-
if (separatorCount !== 1)
|
|
1982
|
-
throw new Error('Invalid sitelink shorthand. Expected exactly one "|" in "text|url".');
|
|
1983
|
-
const [rawText, rawUrl] = value.split("|");
|
|
1984
|
-
const linkText = normalizeOptionalText(rawText);
|
|
1985
|
-
if (!linkText)
|
|
1986
|
-
throw new Error("Invalid sitelink shorthand. Text cannot be empty.");
|
|
1987
|
-
const finalUrl = normalizeUrl(rawUrl ?? "");
|
|
1988
|
-
return normalizeSitelinkAsset({ type: "sitelink", linkText, finalUrls: [finalUrl] });
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
1997
|
// src/commands/google.ts
|
|
1992
1998
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1993
1999
|
import { basename, extname, resolve } from "node:path";
|
|
@@ -2769,14 +2775,16 @@ async function getAd2(client, args, flags) {
|
|
|
2769
2775
|
async function createAd2(client, _args, flags) {
|
|
2770
2776
|
validateFlags(flags, AD_FLAGS2, "manage google ads create");
|
|
2771
2777
|
if (typeof flags.data === "string") {
|
|
2772
|
-
const
|
|
2773
|
-
const qs2 = buildGoogleMutationQuery(flags,
|
|
2778
|
+
const rawBody2 = parseDataFlag(flags, "Check JSON syntax in --data");
|
|
2779
|
+
const qs2 = buildGoogleMutationQuery(flags, rawBody2);
|
|
2780
|
+
const body2 = removeQueryOnlyBodyKeys(rawBody2, ["accountId"]);
|
|
2774
2781
|
const path4 = `/manage/google/ads${qs2}`;
|
|
2775
2782
|
return client.post(path4, body2);
|
|
2776
2783
|
}
|
|
2777
2784
|
const payload = buildAdPayload(flags);
|
|
2778
|
-
const
|
|
2779
|
-
const qs = buildGoogleMutationQuery(flags,
|
|
2785
|
+
const rawBody = { ads: [payload] };
|
|
2786
|
+
const qs = buildGoogleMutationQuery(flags, rawBody);
|
|
2787
|
+
const body = removeQueryOnlyBodyKeys(rawBody, ["accountId"]);
|
|
2780
2788
|
const path3 = `/manage/google/ads${qs}`;
|
|
2781
2789
|
return client.post(path3, body);
|
|
2782
2790
|
}
|
|
@@ -2784,19 +2792,21 @@ async function updateAd2(client, args, flags) {
|
|
|
2784
2792
|
const id = requireArg(args, 0, "ad-id", "Run: adkit manage google ads update <ad-id> --status paused --ad-group 123456");
|
|
2785
2793
|
validateFlags(flags, AD_FLAGS2, "manage google ads update");
|
|
2786
2794
|
if (typeof flags.data === "string") {
|
|
2787
|
-
const
|
|
2788
|
-
const adGroupId2 = resolveGoogleAdGroupId(
|
|
2795
|
+
const rawBody = parseDataFlag(flags, "Check JSON syntax in --data");
|
|
2796
|
+
const adGroupId2 = resolveGoogleAdGroupId(rawBody, flags);
|
|
2789
2797
|
if (!adGroupId2) throw new CliError("MISSING_FLAG", "Missing required flag: `--ad-group`", "Provide --ad-group <id> or include adGroupId in --data");
|
|
2790
|
-
const qs2 = buildGoogleMutationQuery(flags,
|
|
2798
|
+
const qs2 = buildGoogleMutationQuery(flags, rawBody, { adGroupId: adGroupId2 });
|
|
2799
|
+
const body2 = removeQueryOnlyBodyKeys(rawBody, ["accountId", "adGroupId"]);
|
|
2791
2800
|
const path4 = `/manage/google/ads/${id}${qs2}`;
|
|
2792
|
-
return client.patch(path4,
|
|
2801
|
+
return client.patch(path4, body2);
|
|
2793
2802
|
}
|
|
2794
2803
|
const payload = buildAdPayload(flags);
|
|
2795
2804
|
const adGroupId = resolveGoogleAdGroupId(payload, flags);
|
|
2796
2805
|
if (!adGroupId) throw new CliError("MISSING_FLAG", "Missing required flag: `--ad-group`", "Run: adkit manage google ads update <ad-id> --ad-group <ad-group-id> ...");
|
|
2797
2806
|
const qs = buildGoogleMutationQuery(flags, payload, { adGroupId });
|
|
2807
|
+
const body = removeQueryOnlyBodyKeys(payload, ["accountId", "adGroupId"]);
|
|
2798
2808
|
const path3 = `/manage/google/ads/${id}${qs}`;
|
|
2799
|
-
return client.patch(path3,
|
|
2809
|
+
return client.patch(path3, body);
|
|
2800
2810
|
}
|
|
2801
2811
|
async function deleteAd2(client, args, flags) {
|
|
2802
2812
|
validateFlags(flags, ["ad-group"], "manage google ads delete");
|
|
@@ -5499,7 +5509,7 @@ Flags (create):
|
|
|
5499
5509
|
--name <name> Campaign name (required)
|
|
5500
5510
|
--status <s> enabled, paused (default: paused)
|
|
5501
5511
|
--budget-daily <n> Daily budget in account currency
|
|
5502
|
-
--campaign-type <type> search, display, performance_max (performance_max needs --data for asset groups)
|
|
5512
|
+
--campaign-type <type> search, display, demand_gen, performance_max (performance_max needs --data for asset groups)
|
|
5503
5513
|
--bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
|
|
5504
5514
|
--target-cpa <n> Target CPA in account currency (with --bid-strategy maximize_conversions)
|
|
5505
5515
|
--target-roas <n> Target ROAS ratio, e.g. 3.5 = 350% (with --bid-strategy maximize_conversion_value)
|
|
@@ -5527,6 +5537,7 @@ Note:
|
|
|
5527
5537
|
For cities, regions, postal codes, and metros, run google geo-locations search and use the returned bare code.
|
|
5528
5538
|
Advanced geo examples: adkit manage google campaigns --help full
|
|
5529
5539
|
Display creation starts here: campaigns create --campaign-type display, then create Display ad groups and responsive_display ads.
|
|
5540
|
+
Demand Gen uses two calls: create the campaign, then create its ad group with the returned campaign ID. Geo and YouTube networks belong on the ad group.
|
|
5530
5541
|
Performance Max: campaignType "performance_max" with asset groups \u2014 build it with --data. See --help full.
|
|
5531
5542
|
conversion-goals chooses what to optimize toward; bid-strategy controls bidding. It works on Search, Display, and Performance Max.
|
|
5532
5543
|
On create, omit conversion-goals to inherit account defaults. On update, omit it to leave goals unchanged or pass null to restore defaults.
|
|
@@ -5538,6 +5549,7 @@ Examples:
|
|
|
5538
5549
|
adkit manage google campaigns list --account 1234567890
|
|
5539
5550
|
adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
|
|
5540
5551
|
adkit manage google campaigns create --name "Purchase Search" --campaign-type search --budget-daily 40 --bid-strategy maximize_conversion_value --conversion-goals purchase --account 1234567890
|
|
5552
|
+
adkit manage google campaigns create --name "YouTube Launch" --campaign-type demand_gen --budget-daily 40 --bid-strategy maximize_conversions --account 1234567890
|
|
5541
5553
|
adkit manage google campaigns create --data '{"campaigns":[{"name":"Display US","campaignType":"display","budget":{"daily":50},"targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]}}}]}' --account 1234567890
|
|
5542
5554
|
adkit manage google campaigns update 987654321 --data '{"targeting":{"geoLocations":{"match":"presence_or_interest"}}}' --account 1234567890
|
|
5543
5555
|
adkit manage google campaigns update 987654321 --status paused --account 1234567890
|
|
@@ -5557,7 +5569,7 @@ Flags (create):
|
|
|
5557
5569
|
--name <name> Campaign name (required)
|
|
5558
5570
|
--status <s> enabled, paused (default: paused)
|
|
5559
5571
|
--budget-daily <n> Daily budget in account currency
|
|
5560
|
-
--campaign-type <type> search, display, performance_max (performance_max needs --data for asset groups)
|
|
5572
|
+
--campaign-type <type> search, display, demand_gen, performance_max (performance_max needs --data for asset groups)
|
|
5561
5573
|
--bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
|
|
5562
5574
|
--conversion-goals <list> Comma-separated goal categories or conversion-action IDs; never mix them
|
|
5563
5575
|
${FLAG.account}
|
|
@@ -5588,6 +5600,7 @@ Notes:
|
|
|
5588
5600
|
platformLocation accepts exact Google geo objects for advanced criteria and existing raw values.
|
|
5589
5601
|
Google exclusions support named locations. Proximity circles belong in include.
|
|
5590
5602
|
Display campaign creation uses campaignType:"display" or --campaign-type display. Videos are not supported for Display media in v1.
|
|
5603
|
+
Demand Gen: create campaignType:"demand_gen" first. Then use the returned campaign ID in a separate ad-groups create call with targeting.geoLocations and targeting.inventory.networks.
|
|
5591
5604
|
Performance Max: campaignType "performance_max" requires at least one assetGroups[] entry and supports multiple groups. Text assets are inline ({role,text}); upload images with google media first, then reference their id/resourceName. Needs an enabled conversion (publish is blocked without one). Non-retail only. Build it with --data.
|
|
5592
5605
|
conversion-goals works on Search, Display, and Performance Max. It chooses what to optimize toward; bid-strategy controls bidding.
|
|
5593
5606
|
Valid goal categories: ${GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORY_LIST}
|
|
@@ -5600,6 +5613,7 @@ Examples:
|
|
|
5600
5613
|
adkit manage google campaigns list --account 1234567890
|
|
5601
5614
|
adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
|
|
5602
5615
|
adkit manage google campaigns create --name "Purchase Search" --campaign-type search --budget-daily 40 --bid-strategy maximize_conversion_value --conversion-goals purchase --account 1234567890
|
|
5616
|
+
adkit manage google campaigns create --name "YouTube Launch" --campaign-type demand_gen --budget-daily 40 --bid-strategy maximize_conversions --account 1234567890
|
|
5603
5617
|
adkit manage google campaigns create --data '{"campaigns":[{"name":"Display US","campaignType":"display","budget":{"daily":50},"targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]}}}]}' --account 1234567890
|
|
5604
5618
|
adkit manage google campaigns create --data '{"campaigns":[{"name":"Austin Search","campaignType":"search","budget":{"daily":5},"targeting":{"geoLocations":{"include":[{"type":"city","code":"1026201"}]}}}]}' --account 1234567890
|
|
5605
5619
|
adkit manage google campaigns create --data '{"campaigns":[{"name":"SF Radius","campaignType":"search","budget":{"daily":5},"targeting":{"geoLocations":{"include":[{"type":"platformLocation","platform":"google","value":{"proximity":{"geo_point":{"latitude_in_micro_degrees":37774900,"longitude_in_micro_degrees":-122419400},"radius":5,"radius_units":"MILES"}}}]}}}]}' --account 1234567890
|
|
@@ -5683,6 +5697,7 @@ Flags (list):
|
|
|
5683
5697
|
|
|
5684
5698
|
Note:
|
|
5685
5699
|
Display targeting is created or updated with --data using targeting.audience.interests, targeting.audience.customAudiences, targeting.content.topics, and targeting.content.websites.
|
|
5700
|
+
Demand Gen creation uses --data with targeting.geoLocations and targeting.inventory.networks (youtube_in_feed, youtube_in_stream, youtube_shorts). These fields are create-only.
|
|
5686
5701
|
Topic and interest search return IDs used in Display targeting: adkit manage google research topics <query>, adkit manage google research interests <query>.
|
|
5687
5702
|
WARNING: On update, targeting replaces the full Display targeting set. If you send only one new website, old websites/topics/audiences can be removed.
|
|
5688
5703
|
list returns ad group configuration only (name, status, bid).
|
|
@@ -5692,6 +5707,7 @@ Examples:
|
|
|
5692
5707
|
adkit manage google ad-groups list --account 1234567890 --campaign 987654321
|
|
5693
5708
|
adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
|
|
5694
5709
|
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Display SaaS","targeting":{"audience":{"interests":{"include":["804"]},"customAudiences":{"include":["123456789"]}},"content":{"topics":{"include":["3"]},"websites":{"include":["https://example.com"],"exclude":["https://bad.example.com"]}}}}]}' --account 1234567890
|
|
5710
|
+
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"YouTube US","targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]},"inventory":{"networks":["youtube_in_feed","youtube_in_stream","youtube_shorts"]}}}]}' --account 1234567890
|
|
5695
5711
|
adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890
|
|
5696
5712
|
adkit manage google ad-groups update 555666777 --status paused --account 1234567890
|
|
5697
5713
|
adkit manage google ad-groups delete 555666777 --account 1234567890
|
|
@@ -5727,6 +5743,7 @@ Notes:
|
|
|
5727
5743
|
Topic and interest search return IDs used in targeting: research topics -> targeting.content.topics; research interests -> targeting.audience.interests.
|
|
5728
5744
|
Websites use direct URLs in targeting.content.websites.include/exclude. Bare numeric customAudiences IDs are treated as Google user lists (remarketing/Customer Match); for custom segments, pass a customAudiences/{id} or customers/{accountId}/customAudiences/{id} resource path.
|
|
5729
5745
|
WARNING: On update, targeting replaces the full Display targeting set. If you send only one new website, old websites/topics/audiences can be removed.
|
|
5746
|
+
Demand Gen creation uses targeting.geoLocations plus targeting.inventory.networks (youtube_in_feed, youtube_in_stream, youtube_shorts). Omit targeting.inventory for Google's default channels. Demand Gen geo/inventory updates are not supported yet.
|
|
5730
5747
|
list returns ad group configuration only (name, status, bid).
|
|
5731
5748
|
For spend/clicks/conversions: adkit manage google results
|
|
5732
5749
|
|
|
@@ -5735,6 +5752,7 @@ Examples:
|
|
|
5735
5752
|
adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
|
|
5736
5753
|
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Brand Terms","cpcBid":2}]}' --account 1234567890
|
|
5737
5754
|
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Display SaaS","targeting":{"audience":{"interests":{"include":["804"]},"customAudiences":{"include":["123456789"]}},"content":{"topics":{"include":["3"]},"websites":{"include":["https://example.com"],"exclude":["https://bad.example.com"]}}}}]}' --account 1234567890
|
|
5755
|
+
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"YouTube US","targeting":{"geoLocations":{"include":[{"type":"country","country":"US"}]},"inventory":{"networks":["youtube_in_feed","youtube_in_stream","youtube_shorts"]}}}]}' --account 1234567890
|
|
5738
5756
|
adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890 --publish
|
|
5739
5757
|
adkit manage google ad-groups update 555666777 --status paused --account 1234567890
|
|
5740
5758
|
adkit manage google ad-groups delete 555666777 --account 1234567890 --publish`;
|
|
@@ -5754,7 +5772,7 @@ Flags (create/update):
|
|
|
5754
5772
|
--sitelink <value> Repeatable. Use asset:<id> or "text|url"
|
|
5755
5773
|
--final-url <url> Landing page URL
|
|
5756
5774
|
--path <text> Optional display URL path as segment-one[/segment-two]
|
|
5757
|
-
--status <s> enabled, paused, removed
|
|
5775
|
+
--status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
|
|
5758
5776
|
|
|
5759
5777
|
Flags (list):
|
|
5760
5778
|
--campaign <ids> Filter by Google campaign IDs (platformId, comma-separated)
|
|
@@ -5765,8 +5783,10 @@ Flags (list):
|
|
|
5765
5783
|
--offset <n> Pagination offset
|
|
5766
5784
|
|
|
5767
5785
|
Notes:
|
|
5768
|
-
Google enforces strict character limits:
|
|
5786
|
+
Google enforces strict character limits: Search/Display headlines max 30 chars, Demand Gen video headlines max 40 chars, descriptions and long headlines max 90 chars, business name max 25 chars, and each path segment max 15 chars.
|
|
5769
5787
|
update is a partial object update. If --data sends an array field, it replaces that whole array; omitted arrays stay unchanged.
|
|
5788
|
+
Demand Gen video ads use --data with adType:"demand_gen_video_responsive" and creative:{ headlines, longHeadlines, descriptions, businessName, finalUrls, media }.
|
|
5789
|
+
First run google media upload with the videoId from the YouTube URL. Then run google ads create with the returned video asset ID and an existing Google logo asset ID.
|
|
5770
5790
|
Display ads use --data with type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
|
|
5771
5791
|
Display media roles: marketing_image, square_marketing_image, logo, square_logo.
|
|
5772
5792
|
Upload images first with google media upload, then use returned Google asset id/resourceName in media[].id.
|
|
@@ -5779,6 +5799,7 @@ Notes:
|
|
|
5779
5799
|
Examples:
|
|
5780
5800
|
adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
|
|
5781
5801
|
adkit manage google ads list --account 1234567890 --ad-group 555666777
|
|
5802
|
+
adkit manage google ads create --data '{"ads":[{"adType":"demand_gen_video_responsive","adGroupId":"555666777","creative":{"headlines":[{"text":"Make every launch count"}],"longHeadlines":[{"text":"Turn your next product launch into lasting demand"}],"descriptions":[{"text":"Reach the right buyers with a focused product story."}],"businessName":"AdKit","finalUrls":["https://example.com/demand-gen"],"media":[{"role":"video","id":"customers/1234567890/assets/501"},{"role":"logo","id":"customers/1234567890/assets/502"}]},"status":"paused"}]}' --account 1234567890
|
|
5782
5803
|
adkit manage google ads create --data '{"ads":[{"type":"responsive_display","adGroupId":"555666777","headlines":[{"text":"Display headline"}],"longHeadline":{"text":"A longer Display headline"},"descriptions":[{"text":"Display description"}],"businessName":"AdKit","finalUrls":["https://example.com"],"media":[{"role":"marketing_image","id":"customers/123/assets/111"},{"role":"square_marketing_image","id":"customers/123/assets/222"},{"role":"logo","id":"customers/123/assets/333"},{"role":"square_logo","id":"customers/123/assets/444"}]}]}' --account 1234567890
|
|
5783
5804
|
adkit manage google ads create --ad-group 555666777 \\
|
|
5784
5805
|
--headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
|
|
@@ -5807,7 +5828,7 @@ Flags (create/update):
|
|
|
5807
5828
|
--sitelink <value> Repeatable. Use asset:<id> or "text|url"
|
|
5808
5829
|
--final-url <url> Landing page URL
|
|
5809
5830
|
--path <text> Optional display URL path as segment-one[/segment-two]
|
|
5810
|
-
--status <s> enabled, paused, removed
|
|
5831
|
+
--status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
|
|
5811
5832
|
${FLAG.account}
|
|
5812
5833
|
${FLAG.publish}
|
|
5813
5834
|
${FLAG.data}
|
|
@@ -5821,8 +5842,10 @@ Flags (list):
|
|
|
5821
5842
|
--offset <n> Pagination offset
|
|
5822
5843
|
|
|
5823
5844
|
Notes:
|
|
5824
|
-
Google enforces strict character limits:
|
|
5845
|
+
Google enforces strict character limits: Search/Display headlines max 30 chars, Demand Gen video headlines max 40 chars, descriptions and long headlines max 90 chars, business name max 25 chars, and each path segment max 15 chars.
|
|
5825
5846
|
update is a partial object update. If --data sends an array field, it replaces that whole array; omitted arrays stay unchanged.
|
|
5847
|
+
Demand Gen video ads use --data with adType:"demand_gen_video_responsive" and creative:{ headlines, longHeadlines, descriptions, businessName, finalUrls, media }.
|
|
5848
|
+
First run google media upload with the videoId from the YouTube URL. Then run google ads create with the returned video asset ID and an existing Google logo asset ID.
|
|
5826
5849
|
Display ads use type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
|
|
5827
5850
|
Display media roles: marketing_image, square_marketing_image, logo, square_logo.
|
|
5828
5851
|
For Display updates, omitted media is preserved. If --data includes media, that media array replaces all Display media.
|
|
@@ -5837,6 +5860,7 @@ Notes:
|
|
|
5837
5860
|
Examples:
|
|
5838
5861
|
adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
|
|
5839
5862
|
adkit manage google ads list --account 1234567890 --ad-group 555666777
|
|
5863
|
+
adkit manage google ads create --data '{"ads":[{"adType":"demand_gen_video_responsive","adGroupId":"555666777","creative":{"headlines":[{"text":"Make every launch count"}],"longHeadlines":[{"text":"Turn your next product launch into lasting demand"}],"descriptions":[{"text":"Reach the right buyers with a focused product story."}],"businessName":"AdKit","finalUrls":["https://example.com/demand-gen"],"media":[{"role":"video","id":"customers/1234567890/assets/501"},{"role":"logo","id":"customers/1234567890/assets/502"}]},"status":"paused"}]}' --account 1234567890
|
|
5840
5864
|
adkit manage google ads create --data '{"ads":[{"type":"responsive_display","adGroupId":"555666777","headlines":[{"text":"Display headline"}],"longHeadline":{"text":"A longer Display headline"},"descriptions":[{"text":"Display description"}],"businessName":"AdKit","finalUrls":["https://example.com"],"media":[{"role":"marketing_image","id":"customers/123/assets/111"},{"role":"square_marketing_image","id":"customers/123/assets/222"},{"role":"logo","id":"customers/123/assets/333"},{"role":"square_logo","id":"customers/123/assets/444"}]}]}' --account 1234567890
|
|
5841
5865
|
adkit manage google ads create --ad-group 555666777 \\
|
|
5842
5866
|
--headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
|
package/package.json
CHANGED