@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.
Files changed (2) hide show
  1. package/dist/cli.js +268 -200
  2. 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 body2 = parseDataFlag(flags, "Check JSON syntax in --data");
2760
- const qs2 = buildGoogleMutationQuery(flags, body2);
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 body = { ads: [payload] };
2766
- const qs = buildGoogleMutationQuery(flags, body);
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 body = parseDataFlag(flags, "Check JSON syntax in --data");
2775
- const adGroupId2 = resolveGoogleAdGroupId(body, flags);
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, body, { adGroupId: adGroupId2 });
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, body);
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, payload);
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: each headline max 30 chars, each description max 90 chars, long headline max 90 chars, each path segment max 15 chars.
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: each headline max 30 chars, each description max 90 chars, long headline max 90 chars, each path segment max 15 chars.
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> Optional business description (create)
7049
- --industry <text> Optional business industry (create)
7050
- --category <text> Optional business category (create)
7051
- --tags <a,b> Optional comma-separated tags (create)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adkit/cli",
3
- "version": "1.13.24",
3
+ "version": "1.13.26",
4
4
  "description": "The Ads CLI for AI agents — manage Meta & Google ad campaigns, browse the ad library, and generate creatives from your terminal.",
5
5
  "keywords": [
6
6
  "ads cli",