@adkit/cli 1.13.24 → 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 +268 -200
- 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
|
|
|
@@ -162,7 +336,7 @@ function parseArgs(argv, options) {
|
|
|
162
336
|
} else {
|
|
163
337
|
key = arg.slice(2);
|
|
164
338
|
const next = argv[i + 1];
|
|
165
|
-
if (next && !next.startsWith("--")) {
|
|
339
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
166
340
|
value = next;
|
|
167
341
|
i++;
|
|
168
342
|
} else value = true;
|
|
@@ -1227,6 +1401,12 @@ async function createProject(client, options) {
|
|
|
1227
1401
|
current: true
|
|
1228
1402
|
};
|
|
1229
1403
|
}
|
|
1404
|
+
async function updateProject(client, projectId, update) {
|
|
1405
|
+
const encodedProjectId = encodeURIComponent(projectId);
|
|
1406
|
+
const response = await client.patch(`/manage/projects/${encodedProjectId}`, update);
|
|
1407
|
+
if (!isUpdateProjectResponse(response)) throw new Error("Unexpected response from AdKit project update");
|
|
1408
|
+
return response.project;
|
|
1409
|
+
}
|
|
1230
1410
|
async function listProjects(client, options = {}) {
|
|
1231
1411
|
const qs = queryString({ query: options.query, limit: options.limit });
|
|
1232
1412
|
const response = await client.get(`/manage/projects${qs}`);
|
|
@@ -1257,6 +1437,13 @@ function isCreateProjectResponse(value) {
|
|
|
1257
1437
|
if (!("name" in value) || typeof value.name !== "string") return false;
|
|
1258
1438
|
return !("website" in value) || typeof value.website === "string";
|
|
1259
1439
|
}
|
|
1440
|
+
function isUpdateProjectResponse(value) {
|
|
1441
|
+
if (!value || typeof value !== "object" || !("project" in value)) return false;
|
|
1442
|
+
const project = value.project;
|
|
1443
|
+
if (!project || typeof project !== "object") return false;
|
|
1444
|
+
if (!("projectId" in project) || typeof project.projectId !== "string") return false;
|
|
1445
|
+
return "name" in project && typeof project.name === "string";
|
|
1446
|
+
}
|
|
1260
1447
|
function mapProjectEntry(project, selectedProject) {
|
|
1261
1448
|
const entry = {
|
|
1262
1449
|
id: project.projectId,
|
|
@@ -1807,174 +1994,6 @@ function getAd(client, args, flags) {
|
|
|
1807
1994
|
return getMetaEntity(client, args, flags, { path: "ads", usageHint: "Run: adkit manage meta ads <ad-id>" });
|
|
1808
1995
|
}
|
|
1809
1996
|
|
|
1810
|
-
// ../shared/dist/manage/google/asset-utils.js
|
|
1811
|
-
var SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS = ["Amenities", "Brands", "Courses", "Degree programs", "Destinations", "Featured hotels", "Insurance coverage", "Models", "Neighborhoods", "Service catalog", "Shows", "Styles", "Types"];
|
|
1812
|
-
var STRUCTURED_SNIPPET_HEADER_LOOKUP = new Map(SUPPORTED_GOOGLE_STRUCTURED_SNIPPET_HEADERS.map((header) => [header.toLowerCase(), header]));
|
|
1813
|
-
var DAY_TO_GOOGLE_MAP = {
|
|
1814
|
-
monday: "MONDAY",
|
|
1815
|
-
tuesday: "TUESDAY",
|
|
1816
|
-
wednesday: "WEDNESDAY",
|
|
1817
|
-
thursday: "THURSDAY",
|
|
1818
|
-
friday: "FRIDAY",
|
|
1819
|
-
saturday: "SATURDAY",
|
|
1820
|
-
sunday: "SUNDAY"
|
|
1821
|
-
};
|
|
1822
|
-
var MINUTE_TO_GOOGLE_MAP = {
|
|
1823
|
-
0: "ZERO",
|
|
1824
|
-
15: "FIFTEEN",
|
|
1825
|
-
30: "THIRTY",
|
|
1826
|
-
45: "FORTY_FIVE"
|
|
1827
|
-
};
|
|
1828
|
-
function normalizeWhitespace(value) {
|
|
1829
|
-
return value.trim().replace(/\s+/g, " ");
|
|
1830
|
-
}
|
|
1831
|
-
function normalizeOptionalText(value) {
|
|
1832
|
-
if (typeof value !== "string")
|
|
1833
|
-
return void 0;
|
|
1834
|
-
const normalized = normalizeWhitespace(value);
|
|
1835
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
1836
|
-
}
|
|
1837
|
-
function normalizeDate(value) {
|
|
1838
|
-
const normalized = normalizeOptionalText(value);
|
|
1839
|
-
if (!normalized)
|
|
1840
|
-
return void 0;
|
|
1841
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
|
|
1842
|
-
throw new Error(`Invalid date "${normalized}". Use YYYY-MM-DD.`);
|
|
1843
|
-
const [yearRaw, monthRaw, dayRaw] = normalized.split("-");
|
|
1844
|
-
const year = Number.parseInt(yearRaw ?? "", 10);
|
|
1845
|
-
const month = Number.parseInt(monthRaw ?? "", 10);
|
|
1846
|
-
const day = Number.parseInt(dayRaw ?? "", 10);
|
|
1847
|
-
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
1848
|
-
if (Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day)
|
|
1849
|
-
throw new Error(`Invalid calendar date "${normalized}". Use a real YYYY-MM-DD date.`);
|
|
1850
|
-
return normalized;
|
|
1851
|
-
}
|
|
1852
|
-
function validateDateRange(startDate, endDate) {
|
|
1853
|
-
if (!startDate || !endDate)
|
|
1854
|
-
return;
|
|
1855
|
-
if (Date.parse(`${startDate}T00:00:00Z`) > Date.parse(`${endDate}T00:00:00Z`))
|
|
1856
|
-
throw new Error("startDate must be on or before endDate");
|
|
1857
|
-
}
|
|
1858
|
-
function normalizeUrl(value) {
|
|
1859
|
-
const raw = value.trim();
|
|
1860
|
-
if (!raw)
|
|
1861
|
-
throw new Error("URL cannot be empty");
|
|
1862
|
-
try {
|
|
1863
|
-
const url = new URL(raw);
|
|
1864
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
1865
|
-
throw new Error("Only http:// and https:// URLs are allowed");
|
|
1866
|
-
return url.toString();
|
|
1867
|
-
} catch (error) {
|
|
1868
|
-
if (error instanceof Error && error.message === "Only http:// and https:// URLs are allowed")
|
|
1869
|
-
throw error;
|
|
1870
|
-
throw new Error(`Invalid URL "${value}"`);
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
function normalizeScheduleEntry(entry) {
|
|
1874
|
-
const { dayOfWeek, startHour, startMinute, endHour, endMinute } = entry;
|
|
1875
|
-
if (!(dayOfWeek in DAY_TO_GOOGLE_MAP))
|
|
1876
|
-
throw new Error(`Invalid schedule day "${String(dayOfWeek)}"`);
|
|
1877
|
-
if (!Number.isInteger(startHour) || startHour < 0 || startHour > 23)
|
|
1878
|
-
throw new Error("Schedule startHour must be an integer between 0 and 23");
|
|
1879
|
-
if (!Number.isInteger(endHour) || endHour < 0 || endHour > 24)
|
|
1880
|
-
throw new Error("Schedule endHour must be an integer between 0 and 24");
|
|
1881
|
-
if (!(startMinute in MINUTE_TO_GOOGLE_MAP))
|
|
1882
|
-
throw new Error("Schedule startMinute must be one of 0, 15, 30, 45");
|
|
1883
|
-
if (!(endMinute in MINUTE_TO_GOOGLE_MAP))
|
|
1884
|
-
throw new Error("Schedule endMinute must be one of 0, 15, 30, 45");
|
|
1885
|
-
if (endHour === 24 && endMinute !== 0)
|
|
1886
|
-
throw new Error("Schedule endMinute must be 0 when endHour is 24");
|
|
1887
|
-
const startTotalMinutes = startHour * 60 + startMinute;
|
|
1888
|
-
const endTotalMinutes = endHour * 60 + endMinute;
|
|
1889
|
-
if (endTotalMinutes <= startTotalMinutes)
|
|
1890
|
-
throw new Error("Schedule end must be after start");
|
|
1891
|
-
return { dayOfWeek, startHour, startMinute, endHour, endMinute };
|
|
1892
|
-
}
|
|
1893
|
-
function normalizeSchedule(schedule) {
|
|
1894
|
-
if (!schedule)
|
|
1895
|
-
return void 0;
|
|
1896
|
-
if (!Array.isArray(schedule))
|
|
1897
|
-
throw new Error("schedule must be an array");
|
|
1898
|
-
if (schedule.length === 0)
|
|
1899
|
-
return void 0;
|
|
1900
|
-
const normalized = schedule.map(normalizeScheduleEntry).sort((left, right) => {
|
|
1901
|
-
const leftKey = `${left.dayOfWeek}:${String(left.startHour).padStart(2, "0")}:${left.startMinute}:${String(left.endHour).padStart(2, "0")}:${left.endMinute}`;
|
|
1902
|
-
const rightKey = `${right.dayOfWeek}:${String(right.startHour).padStart(2, "0")}:${right.startMinute}:${String(right.endHour).padStart(2, "0")}:${right.endMinute}`;
|
|
1903
|
-
return leftKey.localeCompare(rightKey);
|
|
1904
|
-
});
|
|
1905
|
-
const perDay = /* @__PURE__ */ new Map();
|
|
1906
|
-
for (const entry of normalized) {
|
|
1907
|
-
const current = perDay.get(entry.dayOfWeek) ?? 0;
|
|
1908
|
-
const next = current + 1;
|
|
1909
|
-
if (next > 6)
|
|
1910
|
-
throw new Error(`Schedule exceeds Google limit of 6 entries on ${entry.dayOfWeek}`);
|
|
1911
|
-
perDay.set(entry.dayOfWeek, next);
|
|
1912
|
-
}
|
|
1913
|
-
if (normalized.length > 42)
|
|
1914
|
-
throw new Error("Schedule exceeds Google limit of 42 total entries");
|
|
1915
|
-
return normalized;
|
|
1916
|
-
}
|
|
1917
|
-
function normalizeSitelinkAsset(asset) {
|
|
1918
|
-
const linkText = normalizeOptionalText(asset.linkText);
|
|
1919
|
-
if (!linkText)
|
|
1920
|
-
throw new Error("Sitelink linkText is required");
|
|
1921
|
-
if (linkText.length > 25)
|
|
1922
|
-
throw new Error("Sitelink linkText must be between 1 and 25 characters");
|
|
1923
|
-
if (!Array.isArray(asset.finalUrls) || asset.finalUrls.length === 0)
|
|
1924
|
-
throw new Error("Sitelink finalUrls must contain at least one URL");
|
|
1925
|
-
const finalUrls = asset.finalUrls.map(normalizeUrl);
|
|
1926
|
-
const description1 = normalizeOptionalText(asset.description1);
|
|
1927
|
-
const description2 = normalizeOptionalText(asset.description2);
|
|
1928
|
-
if (description1 && !description2 || !description1 && description2)
|
|
1929
|
-
throw new Error("Sitelink description1 and description2 must either both be set or both be omitted");
|
|
1930
|
-
if (description1 && description1.length > 35)
|
|
1931
|
-
throw new Error("Sitelink description1 must be between 1 and 35 characters");
|
|
1932
|
-
if (description2 && description2.length > 35)
|
|
1933
|
-
throw new Error("Sitelink description2 must be between 1 and 35 characters");
|
|
1934
|
-
const startDate = normalizeDate(asset.startDate);
|
|
1935
|
-
const endDate = normalizeDate(asset.endDate);
|
|
1936
|
-
validateDateRange(startDate, endDate);
|
|
1937
|
-
const urlCustomParameters = asset.urlCustomParameters?.map((parameter) => {
|
|
1938
|
-
const key = normalizeOptionalText(parameter.key);
|
|
1939
|
-
const value = normalizeOptionalText(parameter.value);
|
|
1940
|
-
if (!key || !value)
|
|
1941
|
-
throw new Error("urlCustomParameters require non-empty key and value");
|
|
1942
|
-
return { key, value };
|
|
1943
|
-
}) ?? void 0;
|
|
1944
|
-
return {
|
|
1945
|
-
type: "sitelink",
|
|
1946
|
-
linkText,
|
|
1947
|
-
finalUrls,
|
|
1948
|
-
description1,
|
|
1949
|
-
description2,
|
|
1950
|
-
startDate,
|
|
1951
|
-
endDate,
|
|
1952
|
-
schedule: normalizeSchedule(asset.schedule),
|
|
1953
|
-
trackingUrlTemplate: normalizeOptionalText(asset.trackingUrlTemplate),
|
|
1954
|
-
finalUrlSuffix: normalizeOptionalText(asset.finalUrlSuffix),
|
|
1955
|
-
urlCustomParameters: urlCustomParameters && urlCustomParameters.length > 0 ? urlCustomParameters : void 0
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
function parseGoogleAssetReference(value) {
|
|
1959
|
-
if (!value.startsWith("asset:"))
|
|
1960
|
-
return null;
|
|
1961
|
-
const assetId = value.slice("asset:".length).trim();
|
|
1962
|
-
if (!/^\d+$/.test(assetId))
|
|
1963
|
-
throw new Error(`Invalid asset reference "${value}". Expected asset:<numeric-id>.`);
|
|
1964
|
-
return assetId;
|
|
1965
|
-
}
|
|
1966
|
-
function parseGoogleSitelinkShorthand(value) {
|
|
1967
|
-
const separatorCount = value.split("|").length - 1;
|
|
1968
|
-
if (separatorCount !== 1)
|
|
1969
|
-
throw new Error('Invalid sitelink shorthand. Expected exactly one "|" in "text|url".');
|
|
1970
|
-
const [rawText, rawUrl] = value.split("|");
|
|
1971
|
-
const linkText = normalizeOptionalText(rawText);
|
|
1972
|
-
if (!linkText)
|
|
1973
|
-
throw new Error("Invalid sitelink shorthand. Text cannot be empty.");
|
|
1974
|
-
const finalUrl = normalizeUrl(rawUrl ?? "");
|
|
1975
|
-
return normalizeSitelinkAsset({ type: "sitelink", linkText, finalUrls: [finalUrl] });
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
1997
|
// src/commands/google.ts
|
|
1979
1998
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1980
1999
|
import { basename, extname, resolve } from "node:path";
|
|
@@ -2756,14 +2775,16 @@ async function getAd2(client, args, flags) {
|
|
|
2756
2775
|
async function createAd2(client, _args, flags) {
|
|
2757
2776
|
validateFlags(flags, AD_FLAGS2, "manage google ads create");
|
|
2758
2777
|
if (typeof flags.data === "string") {
|
|
2759
|
-
const
|
|
2760
|
-
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"]);
|
|
2761
2781
|
const path4 = `/manage/google/ads${qs2}`;
|
|
2762
2782
|
return client.post(path4, body2);
|
|
2763
2783
|
}
|
|
2764
2784
|
const payload = buildAdPayload(flags);
|
|
2765
|
-
const
|
|
2766
|
-
const qs = buildGoogleMutationQuery(flags,
|
|
2785
|
+
const rawBody = { ads: [payload] };
|
|
2786
|
+
const qs = buildGoogleMutationQuery(flags, rawBody);
|
|
2787
|
+
const body = removeQueryOnlyBodyKeys(rawBody, ["accountId"]);
|
|
2767
2788
|
const path3 = `/manage/google/ads${qs}`;
|
|
2768
2789
|
return client.post(path3, body);
|
|
2769
2790
|
}
|
|
@@ -2771,19 +2792,21 @@ async function updateAd2(client, args, flags) {
|
|
|
2771
2792
|
const id = requireArg(args, 0, "ad-id", "Run: adkit manage google ads update <ad-id> --status paused --ad-group 123456");
|
|
2772
2793
|
validateFlags(flags, AD_FLAGS2, "manage google ads update");
|
|
2773
2794
|
if (typeof flags.data === "string") {
|
|
2774
|
-
const
|
|
2775
|
-
const adGroupId2 = resolveGoogleAdGroupId(
|
|
2795
|
+
const rawBody = parseDataFlag(flags, "Check JSON syntax in --data");
|
|
2796
|
+
const adGroupId2 = resolveGoogleAdGroupId(rawBody, flags);
|
|
2776
2797
|
if (!adGroupId2) throw new CliError("MISSING_FLAG", "Missing required flag: `--ad-group`", "Provide --ad-group <id> or include adGroupId in --data");
|
|
2777
|
-
const qs2 = buildGoogleMutationQuery(flags,
|
|
2798
|
+
const qs2 = buildGoogleMutationQuery(flags, rawBody, { adGroupId: adGroupId2 });
|
|
2799
|
+
const body2 = removeQueryOnlyBodyKeys(rawBody, ["accountId", "adGroupId"]);
|
|
2778
2800
|
const path4 = `/manage/google/ads/${id}${qs2}`;
|
|
2779
|
-
return client.patch(path4,
|
|
2801
|
+
return client.patch(path4, body2);
|
|
2780
2802
|
}
|
|
2781
2803
|
const payload = buildAdPayload(flags);
|
|
2782
2804
|
const adGroupId = resolveGoogleAdGroupId(payload, flags);
|
|
2783
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> ...");
|
|
2784
2806
|
const qs = buildGoogleMutationQuery(flags, payload, { adGroupId });
|
|
2807
|
+
const body = removeQueryOnlyBodyKeys(payload, ["accountId", "adGroupId"]);
|
|
2785
2808
|
const path3 = `/manage/google/ads/${id}${qs}`;
|
|
2786
|
-
return client.patch(path3,
|
|
2809
|
+
return client.patch(path3, body);
|
|
2787
2810
|
}
|
|
2788
2811
|
async function deleteAd2(client, args, flags) {
|
|
2789
2812
|
validateFlags(flags, ["ad-group"], "manage google ads delete");
|
|
@@ -4822,7 +4845,7 @@ Commands:
|
|
|
4822
4845
|
library Browse ads and advertisers
|
|
4823
4846
|
studio Create and manage Studio ads
|
|
4824
4847
|
manage Manage ad platforms and drafts
|
|
4825
|
-
projects Manage projects (list, create, use, current)
|
|
4848
|
+
projects Manage projects (list, create, update, use, current)
|
|
4826
4849
|
|
|
4827
4850
|
Advanced:
|
|
4828
4851
|
logout Remove stored API key
|
|
@@ -5486,7 +5509,7 @@ Flags (create):
|
|
|
5486
5509
|
--name <name> Campaign name (required)
|
|
5487
5510
|
--status <s> enabled, paused (default: paused)
|
|
5488
5511
|
--budget-daily <n> Daily budget in account currency
|
|
5489
|
-
--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)
|
|
5490
5513
|
--bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
|
|
5491
5514
|
--target-cpa <n> Target CPA in account currency (with --bid-strategy maximize_conversions)
|
|
5492
5515
|
--target-roas <n> Target ROAS ratio, e.g. 3.5 = 350% (with --bid-strategy maximize_conversion_value)
|
|
@@ -5514,6 +5537,7 @@ Note:
|
|
|
5514
5537
|
For cities, regions, postal codes, and metros, run google geo-locations search and use the returned bare code.
|
|
5515
5538
|
Advanced geo examples: adkit manage google campaigns --help full
|
|
5516
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.
|
|
5517
5541
|
Performance Max: campaignType "performance_max" with asset groups \u2014 build it with --data. See --help full.
|
|
5518
5542
|
conversion-goals chooses what to optimize toward; bid-strategy controls bidding. It works on Search, Display, and Performance Max.
|
|
5519
5543
|
On create, omit conversion-goals to inherit account defaults. On update, omit it to leave goals unchanged or pass null to restore defaults.
|
|
@@ -5525,6 +5549,7 @@ Examples:
|
|
|
5525
5549
|
adkit manage google campaigns list --account 1234567890
|
|
5526
5550
|
adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
|
|
5527
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
|
|
5528
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
|
|
5529
5554
|
adkit manage google campaigns update 987654321 --data '{"targeting":{"geoLocations":{"match":"presence_or_interest"}}}' --account 1234567890
|
|
5530
5555
|
adkit manage google campaigns update 987654321 --status paused --account 1234567890
|
|
@@ -5544,7 +5569,7 @@ Flags (create):
|
|
|
5544
5569
|
--name <name> Campaign name (required)
|
|
5545
5570
|
--status <s> enabled, paused (default: paused)
|
|
5546
5571
|
--budget-daily <n> Daily budget in account currency
|
|
5547
|
-
--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)
|
|
5548
5573
|
--bid-strategy <s> manual_cpc, manual_cpm, maximize_clicks, maximize_conversions, maximize_conversion_value, target_spend, target_impression_share
|
|
5549
5574
|
--conversion-goals <list> Comma-separated goal categories or conversion-action IDs; never mix them
|
|
5550
5575
|
${FLAG.account}
|
|
@@ -5575,6 +5600,7 @@ Notes:
|
|
|
5575
5600
|
platformLocation accepts exact Google geo objects for advanced criteria and existing raw values.
|
|
5576
5601
|
Google exclusions support named locations. Proximity circles belong in include.
|
|
5577
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.
|
|
5578
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.
|
|
5579
5605
|
conversion-goals works on Search, Display, and Performance Max. It chooses what to optimize toward; bid-strategy controls bidding.
|
|
5580
5606
|
Valid goal categories: ${GOOGLE_CAMPAIGN_CONVERSION_GOAL_CATEGORY_LIST}
|
|
@@ -5587,6 +5613,7 @@ Examples:
|
|
|
5587
5613
|
adkit manage google campaigns list --account 1234567890
|
|
5588
5614
|
adkit manage google campaigns create --name "Display Prospecting" --campaign-type display --budget-daily 50 --account 1234567890
|
|
5589
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
|
|
5590
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
|
|
5591
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
|
|
5592
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
|
|
@@ -5670,6 +5697,7 @@ Flags (list):
|
|
|
5670
5697
|
|
|
5671
5698
|
Note:
|
|
5672
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.
|
|
5673
5701
|
Topic and interest search return IDs used in Display targeting: adkit manage google research topics <query>, adkit manage google research interests <query>.
|
|
5674
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.
|
|
5675
5703
|
list returns ad group configuration only (name, status, bid).
|
|
@@ -5679,6 +5707,7 @@ Examples:
|
|
|
5679
5707
|
adkit manage google ad-groups list --account 1234567890 --campaign 987654321
|
|
5680
5708
|
adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
|
|
5681
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
|
|
5682
5711
|
adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890
|
|
5683
5712
|
adkit manage google ad-groups update 555666777 --status paused --account 1234567890
|
|
5684
5713
|
adkit manage google ad-groups delete 555666777 --account 1234567890
|
|
@@ -5714,6 +5743,7 @@ Notes:
|
|
|
5714
5743
|
Topic and interest search return IDs used in targeting: research topics -> targeting.content.topics; research interests -> targeting.audience.interests.
|
|
5715
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.
|
|
5716
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.
|
|
5717
5747
|
list returns ad group configuration only (name, status, bid).
|
|
5718
5748
|
For spend/clicks/conversions: adkit manage google results
|
|
5719
5749
|
|
|
@@ -5722,6 +5752,7 @@ Examples:
|
|
|
5722
5752
|
adkit manage google ad-groups create --campaign 987654321 --name "Brand Terms" --cpc-bid 2 --account 1234567890
|
|
5723
5753
|
adkit manage google ad-groups create --data '{"adGroups":[{"campaignId":"987654321","name":"Brand Terms","cpcBid":2}]}' --account 1234567890
|
|
5724
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
|
|
5725
5756
|
adkit manage google ad-groups update 555666777 --data '{"targeting":{"content":{"websites":{"include":["https://example.com","https://nytimes.com"]}}}}' --account 1234567890 --publish
|
|
5726
5757
|
adkit manage google ad-groups update 555666777 --status paused --account 1234567890
|
|
5727
5758
|
adkit manage google ad-groups delete 555666777 --account 1234567890 --publish`;
|
|
@@ -5741,7 +5772,7 @@ Flags (create/update):
|
|
|
5741
5772
|
--sitelink <value> Repeatable. Use asset:<id> or "text|url"
|
|
5742
5773
|
--final-url <url> Landing page URL
|
|
5743
5774
|
--path <text> Optional display URL path as segment-one[/segment-two]
|
|
5744
|
-
--status <s> enabled, paused, removed
|
|
5775
|
+
--status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
|
|
5745
5776
|
|
|
5746
5777
|
Flags (list):
|
|
5747
5778
|
--campaign <ids> Filter by Google campaign IDs (platformId, comma-separated)
|
|
@@ -5752,8 +5783,10 @@ Flags (list):
|
|
|
5752
5783
|
--offset <n> Pagination offset
|
|
5753
5784
|
|
|
5754
5785
|
Notes:
|
|
5755
|
-
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.
|
|
5756
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.
|
|
5757
5790
|
Display ads use --data with type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
|
|
5758
5791
|
Display media roles: marketing_image, square_marketing_image, logo, square_logo.
|
|
5759
5792
|
Upload images first with google media upload, then use returned Google asset id/resourceName in media[].id.
|
|
@@ -5766,6 +5799,7 @@ Notes:
|
|
|
5766
5799
|
Examples:
|
|
5767
5800
|
adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
|
|
5768
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
|
|
5769
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
|
|
5770
5804
|
adkit manage google ads create --ad-group 555666777 \\
|
|
5771
5805
|
--headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
|
|
@@ -5794,7 +5828,7 @@ Flags (create/update):
|
|
|
5794
5828
|
--sitelink <value> Repeatable. Use asset:<id> or "text|url"
|
|
5795
5829
|
--final-url <url> Landing page URL
|
|
5796
5830
|
--path <text> Optional display URL path as segment-one[/segment-two]
|
|
5797
|
-
--status <s> enabled, paused, removed
|
|
5831
|
+
--status <s> Legacy Search/Display: enabled, paused, removed. Demand Gen JSON: active, paused
|
|
5798
5832
|
${FLAG.account}
|
|
5799
5833
|
${FLAG.publish}
|
|
5800
5834
|
${FLAG.data}
|
|
@@ -5808,8 +5842,10 @@ Flags (list):
|
|
|
5808
5842
|
--offset <n> Pagination offset
|
|
5809
5843
|
|
|
5810
5844
|
Notes:
|
|
5811
|
-
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.
|
|
5812
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.
|
|
5813
5849
|
Display ads use type:"responsive_display", headlines, longHeadline, descriptions, businessName, finalUrls, and media.
|
|
5814
5850
|
Display media roles: marketing_image, square_marketing_image, logo, square_logo.
|
|
5815
5851
|
For Display updates, omitted media is preserved. If --data includes media, that media array replaces all Display media.
|
|
@@ -5824,6 +5860,7 @@ Notes:
|
|
|
5824
5860
|
Examples:
|
|
5825
5861
|
adkit manage google ads list --account 1234567890 --campaign 987654321,987654322
|
|
5826
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
|
|
5827
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
|
|
5828
5865
|
adkit manage google ads create --ad-group 555666777 \\
|
|
5829
5866
|
--headline-1 "CRM for B2B SaaS" --headline-2 "Close More Deals" --headline "Book a Demo" \\
|
|
@@ -7036,23 +7073,28 @@ Examples:
|
|
|
7036
7073
|
|
|
7037
7074
|
list List accessible projects
|
|
7038
7075
|
create Create a project and make it active
|
|
7076
|
+
update <id> Update a project
|
|
7039
7077
|
use <id> Switch active project
|
|
7040
7078
|
current Show active project
|
|
7041
7079
|
|
|
7042
7080
|
Flags:
|
|
7043
7081
|
--query <text> Filter projects by name or website
|
|
7044
7082
|
--limit <n> Max results (default: 20, max: 50)
|
|
7045
|
-
--name <text> Project or business name (create)
|
|
7046
|
-
--website <url> Project website URL or domain (create)
|
|
7083
|
+
--name <text> Project or business name (create/update)
|
|
7084
|
+
--website <url> Project website URL or domain (create/update)
|
|
7047
7085
|
--workspace <id> Target workspace ID (create)
|
|
7048
|
-
--description <t>
|
|
7049
|
-
--industry <text>
|
|
7050
|
-
--category <text>
|
|
7051
|
-
--tags <a,b>
|
|
7086
|
+
--description <t> Business description (create/update)
|
|
7087
|
+
--industry <text> Business industry (create/update)
|
|
7088
|
+
--category <text> Business category (create/update)
|
|
7089
|
+
--tags <a,b> Tags; update replaces all tags
|
|
7090
|
+
--primary-color <hex> Primary brand color (update)
|
|
7091
|
+
--accent-color <hex> Accent brand color (update)
|
|
7092
|
+
--background-color <hex> Background brand color (update)
|
|
7052
7093
|
|
|
7053
7094
|
Examples:
|
|
7054
7095
|
adkit projects list
|
|
7055
7096
|
adkit projects create --name "Acme" --website acme.com
|
|
7097
|
+
adkit projects update proj_abc123 --name "Acme Inc." --primary-color "#7c3aed"
|
|
7056
7098
|
adkit projects use proj_abc123`.trim()
|
|
7057
7099
|
};
|
|
7058
7100
|
var STUDIO_GENERATE_HELP_FULL = `adkit studio generate \u2014 Create ad images with AI
|
|
@@ -9086,6 +9128,32 @@ ${pageInfo}`);
|
|
|
9086
9128
|
else console.log(`Created and selected project: ${project.name} (${project.id})`);
|
|
9087
9129
|
break;
|
|
9088
9130
|
}
|
|
9131
|
+
case "update": {
|
|
9132
|
+
validateFlags(flags, ["name", "website", "description", "industry", "category", "tags", "primary-color", "accent-color", "background-color"], "projects update");
|
|
9133
|
+
const projectId = args[2];
|
|
9134
|
+
if (!projectId) throw new CliError("MISSING_ARGUMENT", "Missing required argument: `<project-id>`", "Run: adkit projects update <project-id> [flags]");
|
|
9135
|
+
const update = {};
|
|
9136
|
+
if (typeof flags.name === "string") update.name = flags.name;
|
|
9137
|
+
if (typeof flags.website === "string") update.website = flags.website;
|
|
9138
|
+
const descriptionValues = collectFlagValues(flags, "description");
|
|
9139
|
+
if (descriptionValues.length > 0) update.description = descriptionValues.join(" ");
|
|
9140
|
+
if (typeof flags.industry === "string") update.industry = flags.industry;
|
|
9141
|
+
if (typeof flags.category === "string") update.category = flags.category;
|
|
9142
|
+
if (typeof flags.tags === "string") {
|
|
9143
|
+
const tagParts = flags.tags.split(",");
|
|
9144
|
+
const trimmedTags = tagParts.map((tag) => tag.trim());
|
|
9145
|
+
update.tags = trimmedTags.filter(Boolean);
|
|
9146
|
+
}
|
|
9147
|
+
const colors = {};
|
|
9148
|
+
if (typeof flags["primary-color"] === "string") colors.primary = flags["primary-color"];
|
|
9149
|
+
if (typeof flags["accent-color"] === "string") colors.accent = flags["accent-color"];
|
|
9150
|
+
if (typeof flags["background-color"] === "string") colors.background = flags["background-color"];
|
|
9151
|
+
if (Object.keys(colors).length > 0) update.brand = { colors };
|
|
9152
|
+
const project = await updateProject(client, projectId, update);
|
|
9153
|
+
if (wantsJson(flags)) printResult(project, flags);
|
|
9154
|
+
else console.log(`Updated project: ${project.name} (${project.projectId})`);
|
|
9155
|
+
break;
|
|
9156
|
+
}
|
|
9089
9157
|
case "use": {
|
|
9090
9158
|
const projectId = args[2];
|
|
9091
9159
|
if (!projectId) throw new CliError("MISSING_ARGUMENT", "Missing required argument: `<project-id>`", "Run: adkit projects use <project-id>");
|
|
@@ -9094,7 +9162,7 @@ ${pageInfo}`);
|
|
|
9094
9162
|
break;
|
|
9095
9163
|
}
|
|
9096
9164
|
default:
|
|
9097
|
-
throw new CliError("UNKNOWN_COMMAND", `Unknown action: projects ${action}`, "Available: list, create, use, current");
|
|
9165
|
+
throw new CliError("UNKNOWN_COMMAND", `Unknown action: projects ${action}`, "Available: list, create, update, use, current");
|
|
9098
9166
|
}
|
|
9099
9167
|
return;
|
|
9100
9168
|
}
|
package/package.json
CHANGED