@adkit.so/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-utils.d.ts +11 -0
- package/dist/cli-utils.js +58 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +841 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.js +84 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +184 -0
- package/dist/commands/drafts.d.ts +6 -0
- package/dist/commands/drafts.js +36 -0
- package/dist/commands/meta.d.ts +28 -0
- package/dist/commands/meta.js +534 -0
- package/dist/commands/projects.d.ts +25 -0
- package/dist/commands/projects.js +30 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +36 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +70 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +9 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +48 -0
- package/package.json +20 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { validateFlags } from '../cli-utils.js';
|
|
2
|
+
import { getDefaultAccount } from '../config.js';
|
|
3
|
+
import { CliError } from '../errors.js';
|
|
4
|
+
import { IMAGE_HASH_RE, VIDEO_ID_RE } from '@adkit/shared/manage/meta/media-utils';
|
|
5
|
+
/** Parse a JSON string and validate it's a plain object. Returns unknown — callers cast to the target type. */
|
|
6
|
+
function parseJsonObject(raw, label) {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new CliError('INVALID_VALUE', `Invalid JSON in \`--${label}\``, `Check JSON syntax in --${label}`);
|
|
13
|
+
}
|
|
14
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
15
|
+
throw new CliError('INVALID_VALUE', `Expected a JSON object in \`--${label}\``, `Check JSON syntax in --${label}`);
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
function dateStamp() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
function generateCampaignName() {
|
|
22
|
+
return `campaign ${dateStamp()}`;
|
|
23
|
+
}
|
|
24
|
+
function generateAdSetName() {
|
|
25
|
+
return `adset ${dateStamp()}`;
|
|
26
|
+
}
|
|
27
|
+
function generateAdName() {
|
|
28
|
+
return `ad ${dateStamp()}`;
|
|
29
|
+
}
|
|
30
|
+
function generateCreativeName() {
|
|
31
|
+
return `creative ${dateStamp()}`;
|
|
32
|
+
}
|
|
33
|
+
function requireArg(args, index, label, hint) {
|
|
34
|
+
const val = args[index];
|
|
35
|
+
if (!val)
|
|
36
|
+
throw new CliError('MISSING_ARGUMENT', `Missing required argument: \`<${label}>\``, hint);
|
|
37
|
+
return val;
|
|
38
|
+
}
|
|
39
|
+
function requireFlag(flags, key, hint) {
|
|
40
|
+
const val = flags[key];
|
|
41
|
+
if (typeof val !== 'string')
|
|
42
|
+
throw new CliError('MISSING_FLAG', `Missing required flag: \`--${key}\``, hint);
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
function parseDataFlag(flags, hint) {
|
|
46
|
+
const raw = requireFlag(flags, 'data', hint);
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
const truncated = raw.length > 80 ? raw.slice(0, 80) + '...' : raw;
|
|
52
|
+
throw new CliError('INVALID_VALUE', `Invalid JSON in \`--data\` flag: ${truncated}`, 'Check JSON syntax in --data');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mergeAccountId(body, flags) {
|
|
56
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
57
|
+
return body;
|
|
58
|
+
const obj = body;
|
|
59
|
+
if (!obj.accountId) {
|
|
60
|
+
if (typeof flags.account === 'string')
|
|
61
|
+
obj.accountId = flags.account;
|
|
62
|
+
else {
|
|
63
|
+
const defaultAccount = getDefaultAccount('meta');
|
|
64
|
+
if (defaultAccount)
|
|
65
|
+
obj.accountId = defaultAccount;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
function flagStr(flags, key) {
|
|
71
|
+
const val = flags[key];
|
|
72
|
+
if (typeof val === 'string')
|
|
73
|
+
return val;
|
|
74
|
+
if (Array.isArray(val))
|
|
75
|
+
return val[0];
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function flagArr(flags, key) {
|
|
79
|
+
const val = flags[key];
|
|
80
|
+
if (Array.isArray(val))
|
|
81
|
+
return val;
|
|
82
|
+
if (typeof val === 'string')
|
|
83
|
+
return [val];
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
function queryString(params) {
|
|
87
|
+
const entries = Object.entries(params).filter((e) => e[1] !== undefined);
|
|
88
|
+
if (entries.length === 0)
|
|
89
|
+
return '';
|
|
90
|
+
return '?' + entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
|
|
91
|
+
}
|
|
92
|
+
/** Type guard for MediaUploadResult — checks discriminated union tag and payload fields. */
|
|
93
|
+
function isMediaUploadResult(value) {
|
|
94
|
+
if (typeof value !== 'object' || value === null)
|
|
95
|
+
return false;
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- narrowing after null check
|
|
97
|
+
const obj = value;
|
|
98
|
+
return (obj.type === 'image' && typeof obj.imageHash === 'string') || (obj.type === 'video' && typeof obj.videoId === 'string');
|
|
99
|
+
}
|
|
100
|
+
// --- Accounts ---
|
|
101
|
+
export async function listAccounts(client, _args, _flags) {
|
|
102
|
+
return client.get('/manage/meta/accounts');
|
|
103
|
+
}
|
|
104
|
+
export async function connectAccount(client, args, _flags) {
|
|
105
|
+
const id = requireArg(args, 0, 'account-id', 'Run: adkit manage meta accounts connect <account-id>');
|
|
106
|
+
return client.post('/manage/meta/accounts/connect', { id });
|
|
107
|
+
}
|
|
108
|
+
export async function disconnectAccount(client, args, _flags) {
|
|
109
|
+
const id = requireArg(args, 0, 'account-id', 'Run: adkit manage meta accounts disconnect <account-id>');
|
|
110
|
+
return client.delete(`/manage/meta/accounts/${id}`);
|
|
111
|
+
}
|
|
112
|
+
export async function listPages(client, args, _flags) {
|
|
113
|
+
const id = requireArg(args, 0, 'account-id', 'Run: adkit manage meta accounts <account-id> pages');
|
|
114
|
+
return client.get(`/manage/meta/accounts/${id}/pages`);
|
|
115
|
+
}
|
|
116
|
+
export async function listPixels(client, args, _flags) {
|
|
117
|
+
const id = requireArg(args, 0, 'account-id', 'Run: adkit manage meta accounts <account-id> pixels');
|
|
118
|
+
return client.get(`/manage/meta/accounts/${id}/pixels`);
|
|
119
|
+
}
|
|
120
|
+
// --- Campaigns ---
|
|
121
|
+
function buildCampaignPayload(flags) {
|
|
122
|
+
const payload = {};
|
|
123
|
+
if (typeof flags.name === 'string')
|
|
124
|
+
payload.name = flags.name;
|
|
125
|
+
// Server validates enum values — CLI forwards raw input
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
127
|
+
if (typeof flags.objective === 'string')
|
|
128
|
+
payload.objective = flags.objective;
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
130
|
+
if (typeof flags.status === 'string')
|
|
131
|
+
payload.status = flags.status;
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
133
|
+
if (typeof flags['bid-strategy'] === 'string')
|
|
134
|
+
payload.bidStrategy = flags['bid-strategy'];
|
|
135
|
+
// CBO is ON by default (matches Meta UI). Use --abo for adset-level budget.
|
|
136
|
+
if (flags.abo === true)
|
|
137
|
+
payload.advantageCampaignBudget = false;
|
|
138
|
+
else
|
|
139
|
+
payload.advantageCampaignBudget = true;
|
|
140
|
+
// Budget
|
|
141
|
+
if (typeof flags['budget-daily'] === 'string')
|
|
142
|
+
payload.budget = { daily: parseFloat(flags['budget-daily']) };
|
|
143
|
+
if (typeof flags['budget-total'] === 'string')
|
|
144
|
+
payload.budget = { ...payload.budget, lifetime: parseFloat(flags['budget-total']) };
|
|
145
|
+
// JSON flags
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- raw JSON from user flag, server validates shape
|
|
147
|
+
if (typeof flags['platform-overrides'] === 'string')
|
|
148
|
+
payload.platformOverrides = parseJsonObject(flags['platform-overrides'], 'platform-overrides');
|
|
149
|
+
return payload;
|
|
150
|
+
}
|
|
151
|
+
export async function listCampaigns(client, _args, flags) {
|
|
152
|
+
validateFlags(flags, [], 'manage meta campaigns list');
|
|
153
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
154
|
+
const qs = queryString({ accountId });
|
|
155
|
+
return client.get(`/manage/meta/campaigns${qs}`);
|
|
156
|
+
}
|
|
157
|
+
const CAMPAIGN_FLAGS = ['name', 'objective', 'status', 'budget-daily', 'budget-total', 'abo', 'bid-strategy'];
|
|
158
|
+
const ADSET_FLAGS = ['campaign', 'name', 'status', 'optimization', 'budget-daily', 'budget-total', 'countries', 'genders', 'targeting', 'pixel', 'event-type', 'interest'];
|
|
159
|
+
const AD_FLAGS = ['creative', 'adset', 'name', 'status', 'media', 'primary-text', 'headline', 'description', 'cta', 'url', 'page'];
|
|
160
|
+
const CREATIVE_FLAGS = ['page-id', 'headline', 'primary-text', 'link-description', 'link-url', 'cta', 'name', 'image-hash', 'image-url', 'video-id', 'force'];
|
|
161
|
+
const MEDIA_FLAGS = ['file', 'url', 'account'];
|
|
162
|
+
export async function createCampaign(client, _args, flags) {
|
|
163
|
+
if (typeof flags.data === 'string') {
|
|
164
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
165
|
+
mergeAccountId(body, flags);
|
|
166
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
167
|
+
return client.post(`/manage/meta/campaigns${publish}`, body);
|
|
168
|
+
}
|
|
169
|
+
validateFlags(flags, CAMPAIGN_FLAGS, 'manage meta campaigns create');
|
|
170
|
+
const payload = buildCampaignPayload(flags);
|
|
171
|
+
if (!payload.name)
|
|
172
|
+
payload.name = generateCampaignName();
|
|
173
|
+
const body = { campaigns: [payload] };
|
|
174
|
+
mergeAccountId(body, flags);
|
|
175
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
176
|
+
return client.post(`/manage/meta/campaigns${publish}`, body);
|
|
177
|
+
}
|
|
178
|
+
export async function updateCampaign(client, args, flags) {
|
|
179
|
+
const id = requireArg(args, 0, 'campaign-id', `Run: adkit manage meta campaigns update <campaign-id> --status paused`);
|
|
180
|
+
if (typeof flags.data === 'string') {
|
|
181
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
182
|
+
mergeAccountId(body, flags);
|
|
183
|
+
return client.patch(`/manage/meta/campaigns/${id}`, body);
|
|
184
|
+
}
|
|
185
|
+
validateFlags(flags, CAMPAIGN_FLAGS, 'manage meta campaigns update');
|
|
186
|
+
const payload = buildCampaignPayload(flags);
|
|
187
|
+
mergeAccountId(payload, flags);
|
|
188
|
+
return client.patch(`/manage/meta/campaigns/${id}`, payload);
|
|
189
|
+
}
|
|
190
|
+
export async function deleteCampaign(client, args, _flags) {
|
|
191
|
+
const id = requireArg(args, 0, 'campaign-id', 'Run: adkit manage meta campaigns delete <campaign-id>');
|
|
192
|
+
return client.delete(`/manage/meta/campaigns/${id}`);
|
|
193
|
+
}
|
|
194
|
+
// --- AdSets ---
|
|
195
|
+
function buildAdSetPayload(flags) {
|
|
196
|
+
const payload = {};
|
|
197
|
+
if (typeof flags.campaign === 'string')
|
|
198
|
+
payload.campaignId = flags.campaign;
|
|
199
|
+
if (typeof flags.name === 'string')
|
|
200
|
+
payload.name = flags.name;
|
|
201
|
+
// Server validates enum values — CLI forwards raw input
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
203
|
+
if (typeof flags.status === 'string')
|
|
204
|
+
payload.status = flags.status;
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
206
|
+
if (typeof flags.optimization === 'string')
|
|
207
|
+
payload.optimization = flags.optimization;
|
|
208
|
+
// Budget
|
|
209
|
+
if (typeof flags['budget-daily'] === 'string')
|
|
210
|
+
payload.budget = { daily: parseFloat(flags['budget-daily']) };
|
|
211
|
+
if (typeof flags['budget-total'] === 'string')
|
|
212
|
+
payload.budget = { ...payload.budget, lifetime: parseFloat(flags['budget-total']) };
|
|
213
|
+
// Targeting (simple flags)
|
|
214
|
+
const targeting = {};
|
|
215
|
+
if (typeof flags.countries === 'string')
|
|
216
|
+
targeting.countries = flags.countries.split(',');
|
|
217
|
+
// --interest (repeatable) → targeting.interests
|
|
218
|
+
if (Array.isArray(flags.interest))
|
|
219
|
+
targeting.interests = flags.interest;
|
|
220
|
+
else if (typeof flags.interest === 'string')
|
|
221
|
+
targeting.interests = [flags.interest];
|
|
222
|
+
// Deep-merge --targeting JSON (takes precedence for nested keys)
|
|
223
|
+
if (typeof flags.targeting === 'string')
|
|
224
|
+
Object.assign(targeting, parseJsonObject(flags.targeting, 'targeting'));
|
|
225
|
+
if (Object.keys(targeting).length)
|
|
226
|
+
payload.targeting = targeting;
|
|
227
|
+
// Conversion tracking convenience flags
|
|
228
|
+
if (typeof flags.pixel === 'string')
|
|
229
|
+
payload.pixelId = flags.pixel;
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
231
|
+
if (typeof flags['event-type'] === 'string')
|
|
232
|
+
payload.eventType = flags['event-type'];
|
|
233
|
+
// JSON flags
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- raw JSON from user flag, server validates shape
|
|
235
|
+
if (typeof flags['platform-overrides'] === 'string')
|
|
236
|
+
payload.platformOverrides = parseJsonObject(flags['platform-overrides'], 'platform-overrides');
|
|
237
|
+
return payload;
|
|
238
|
+
}
|
|
239
|
+
export async function listAdSets(client, _args, flags) {
|
|
240
|
+
validateFlags(flags, ['campaign'], 'manage meta adsets list');
|
|
241
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
242
|
+
const campaignId = typeof flags.campaign === 'string' ? flags.campaign : undefined;
|
|
243
|
+
const qs = queryString({ accountId, campaignId });
|
|
244
|
+
return client.get(`/manage/meta/adsets${qs}`);
|
|
245
|
+
}
|
|
246
|
+
export async function createAdSet(client, _args, flags) {
|
|
247
|
+
if (typeof flags.data === 'string') {
|
|
248
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
249
|
+
mergeAccountId(body, flags);
|
|
250
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
251
|
+
return client.post(`/manage/meta/adsets${publish}`, body);
|
|
252
|
+
}
|
|
253
|
+
validateFlags(flags, ADSET_FLAGS, 'manage meta adsets create');
|
|
254
|
+
const payload = buildAdSetPayload(flags);
|
|
255
|
+
if (!payload.campaignId)
|
|
256
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--campaign`', 'Run: adkit manage meta adsets create --campaign cmp_abc --name "US 25-44" --budget-daily 20 --optimization link_clicks');
|
|
257
|
+
if (!payload.name)
|
|
258
|
+
payload.name = generateAdSetName();
|
|
259
|
+
const body = { adsets: [payload] };
|
|
260
|
+
mergeAccountId(body, flags);
|
|
261
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
262
|
+
return client.post(`/manage/meta/adsets${publish}`, body);
|
|
263
|
+
}
|
|
264
|
+
export async function updateAdSet(client, args, flags) {
|
|
265
|
+
const id = requireArg(args, 0, 'adset-id', `Run: adkit manage meta adsets update <adset-id> --budget-daily 50`);
|
|
266
|
+
if (typeof flags.data === 'string') {
|
|
267
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
268
|
+
mergeAccountId(body, flags);
|
|
269
|
+
return client.patch(`/manage/meta/adsets/${id}`, body);
|
|
270
|
+
}
|
|
271
|
+
validateFlags(flags, ADSET_FLAGS, 'manage meta adsets update');
|
|
272
|
+
const payload = buildAdSetPayload(flags);
|
|
273
|
+
mergeAccountId(payload, flags);
|
|
274
|
+
return client.patch(`/manage/meta/adsets/${id}`, payload);
|
|
275
|
+
}
|
|
276
|
+
export async function deleteAdSet(client, args, _flags) {
|
|
277
|
+
const id = requireArg(args, 0, 'adset-id', 'Run: adkit manage meta adsets delete <adset-id>');
|
|
278
|
+
return client.delete(`/manage/meta/adsets/${id}`);
|
|
279
|
+
}
|
|
280
|
+
// --- Ads ---
|
|
281
|
+
export async function listAds(client, _args, flags) {
|
|
282
|
+
validateFlags(flags, ['adset'], 'manage meta ads list');
|
|
283
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
284
|
+
const adsetId = typeof flags.adset === 'string' ? flags.adset : undefined;
|
|
285
|
+
const qs = queryString({ accountId, adsetId });
|
|
286
|
+
return client.get(`/manage/meta/ads${qs}`);
|
|
287
|
+
}
|
|
288
|
+
export async function createAd(client, _args, flags) {
|
|
289
|
+
// Low-level flow: --data bypasses the rich flow
|
|
290
|
+
if (typeof flags.data === 'string') {
|
|
291
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
292
|
+
mergeAccountId(body, flags);
|
|
293
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
294
|
+
return client.post(`/manage/meta/ads${publish}`, body);
|
|
295
|
+
}
|
|
296
|
+
validateFlags(flags, AD_FLAGS, 'manage meta ads create');
|
|
297
|
+
const creativeId = typeof flags.creative === 'string' ? flags.creative : undefined;
|
|
298
|
+
const mediaFlags = ['media', 'primary-text', 'headline', 'description', 'cta'];
|
|
299
|
+
const hasMediaFlags = mediaFlags.some((f) => flags[f] !== undefined);
|
|
300
|
+
if (creativeId && hasMediaFlags)
|
|
301
|
+
throw new CliError('INVALID_VALUE', '`--creative` cannot be combined with --media, --primary-text, --headline, --description, or --cta', 'Use --creative to reference an existing creative, OR use --media/--primary-text/etc. to create a new one');
|
|
302
|
+
// Existing creative flow: --creative <id> → uses ads[] low-level API
|
|
303
|
+
if (creativeId) {
|
|
304
|
+
const adsetId = requireFlag(flags, 'adset', 'Run: adkit manage meta ads create --creative cr_abc --adset as_xyz --publish');
|
|
305
|
+
const ad = {
|
|
306
|
+
adsetId,
|
|
307
|
+
name: typeof flags.name === 'string' ? flags.name : generateAdName(),
|
|
308
|
+
creativeId,
|
|
309
|
+
};
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
311
|
+
if (typeof flags.status === 'string')
|
|
312
|
+
ad.status = flags.status;
|
|
313
|
+
const body = { ads: [ad] };
|
|
314
|
+
mergeAccountId(body, flags);
|
|
315
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
316
|
+
return client.post(`/manage/meta/ads${publish}`, body);
|
|
317
|
+
}
|
|
318
|
+
// New creative flow: --media, --primary-text, --headline, --cta, --adset
|
|
319
|
+
const mediaInputs = flagArr(flags, 'media');
|
|
320
|
+
const primaryTexts = flagArr(flags, 'primary-text');
|
|
321
|
+
const headlines = flagArr(flags, 'headline');
|
|
322
|
+
const descriptions = flagArr(flags, 'description');
|
|
323
|
+
if (mediaInputs.length === 0)
|
|
324
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--media` (or use `--creative` to reference an existing creative)', 'Run: adkit manage meta ads create --media ./hero.mp4 --primary-text "Hello" --adset as_xyz --publish');
|
|
325
|
+
const adsetId = typeof flags.adset === 'string' ? flags.adset : undefined;
|
|
326
|
+
if (!adsetId)
|
|
327
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--adset`', 'Run: adkit manage meta ads create --media ./hero.mp4 --adset as_789 --publish');
|
|
328
|
+
// Auto-detect: media ID (image hash or video ID) vs file path vs URL
|
|
329
|
+
function isMediaId(input) {
|
|
330
|
+
return IMAGE_HASH_RE.test(input) || VIDEO_ID_RE.test(input);
|
|
331
|
+
}
|
|
332
|
+
const mediaIds = [];
|
|
333
|
+
for (const input of mediaInputs) {
|
|
334
|
+
if (isMediaId(input)) {
|
|
335
|
+
// Already a resolved media ID — pass through directly
|
|
336
|
+
mediaIds.push(input);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// File path or URL — upload first, extract returned ID
|
|
340
|
+
let uploadBody;
|
|
341
|
+
if (input.startsWith('http://') || input.startsWith('https://'))
|
|
342
|
+
uploadBody = { imageUrl: input };
|
|
343
|
+
else {
|
|
344
|
+
const fs = await import('node:fs');
|
|
345
|
+
const path = await import('node:path');
|
|
346
|
+
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.avi', '.webm', '.mkv']);
|
|
347
|
+
const resolved = path.resolve(input);
|
|
348
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
349
|
+
const filename = path.basename(resolved);
|
|
350
|
+
const base64 = fs.readFileSync(resolved).toString('base64');
|
|
351
|
+
if (VIDEO_EXTENSIONS.has(ext))
|
|
352
|
+
uploadBody = { videoBase64: base64, filename };
|
|
353
|
+
else
|
|
354
|
+
uploadBody = { imageBase64: base64, filename };
|
|
355
|
+
}
|
|
356
|
+
const result = await client.post('/manage/meta/media', uploadBody);
|
|
357
|
+
if (!isMediaUploadResult(result))
|
|
358
|
+
throw new CliError('SERVER_ERROR', 'Media upload did not return an image hash or video ID');
|
|
359
|
+
const id = result.type === 'image' ? result.imageHash : result.videoId;
|
|
360
|
+
mediaIds.push(id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const ad = { adsetId, mediaIds };
|
|
364
|
+
if (primaryTexts.length)
|
|
365
|
+
ad.primaryTexts = primaryTexts;
|
|
366
|
+
if (headlines.length)
|
|
367
|
+
ad.headlines = headlines;
|
|
368
|
+
if (descriptions.length)
|
|
369
|
+
ad.descriptions = descriptions;
|
|
370
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
371
|
+
if (typeof flags.cta === 'string')
|
|
372
|
+
ad.cta = flags.cta;
|
|
373
|
+
if (typeof flags.name === 'string')
|
|
374
|
+
ad.name = flags.name;
|
|
375
|
+
if (typeof flags.url === 'string')
|
|
376
|
+
ad.url = flags.url;
|
|
377
|
+
if (typeof flags.page === 'string')
|
|
378
|
+
ad.pageId = flags.page;
|
|
379
|
+
const body = { ads: [ad] };
|
|
380
|
+
mergeAccountId(body, flags);
|
|
381
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
382
|
+
return client.post(`/manage/meta/ads${publish}`, body);
|
|
383
|
+
}
|
|
384
|
+
export async function updateAd(client, args, flags) {
|
|
385
|
+
const id = requireArg(args, 0, 'ad-id', `Run: adkit manage meta ads update <ad-id> --data '{"status":"paused"}'`);
|
|
386
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
387
|
+
mergeAccountId(body, flags);
|
|
388
|
+
return client.patch(`/manage/meta/ads/${id}`, body);
|
|
389
|
+
}
|
|
390
|
+
export async function deleteAd(client, args, _flags) {
|
|
391
|
+
const id = requireArg(args, 0, 'ad-id', 'Run: adkit manage meta ads delete <ad-id>');
|
|
392
|
+
return client.delete(`/manage/meta/ads/${id}`);
|
|
393
|
+
}
|
|
394
|
+
// --- Creatives ---
|
|
395
|
+
function buildCreativePayload(flags) {
|
|
396
|
+
const payload = {};
|
|
397
|
+
const pageId = flagStr(flags, 'page-id');
|
|
398
|
+
const linkUrl = flagStr(flags, 'link-url');
|
|
399
|
+
const cta = flagStr(flags, 'cta');
|
|
400
|
+
const name = flagStr(flags, 'name');
|
|
401
|
+
const imageHash = flagStr(flags, 'image-hash');
|
|
402
|
+
const imageUrl = flagStr(flags, 'image-url');
|
|
403
|
+
const videoId = flagStr(flags, 'video-id');
|
|
404
|
+
if (pageId)
|
|
405
|
+
payload.pageId = pageId;
|
|
406
|
+
if (linkUrl)
|
|
407
|
+
payload.linkUrl = linkUrl;
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server validates enum, CLI forwards raw string
|
|
409
|
+
if (cta)
|
|
410
|
+
payload.cta = cta;
|
|
411
|
+
if (name)
|
|
412
|
+
payload.name = name;
|
|
413
|
+
if (imageHash)
|
|
414
|
+
payload.imageHash = imageHash;
|
|
415
|
+
if (imageUrl)
|
|
416
|
+
payload.imageUrl = imageUrl;
|
|
417
|
+
if (videoId)
|
|
418
|
+
payload.videoId = videoId;
|
|
419
|
+
// Array fields (repeatable flags)
|
|
420
|
+
const headlines = flagArr(flags, 'headline');
|
|
421
|
+
if (headlines.length)
|
|
422
|
+
payload.headlines = headlines;
|
|
423
|
+
const primaryTexts = flagArr(flags, 'primary-text');
|
|
424
|
+
if (primaryTexts.length)
|
|
425
|
+
payload.primaryTexts = primaryTexts;
|
|
426
|
+
const linkDescriptions = flagArr(flags, 'link-description');
|
|
427
|
+
if (linkDescriptions.length)
|
|
428
|
+
payload.linkDescriptions = linkDescriptions;
|
|
429
|
+
// JSON flags
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- raw JSON from user flag, server validates shape
|
|
431
|
+
if (typeof flags['platform-overrides'] === 'string')
|
|
432
|
+
payload.platformOverrides = parseJsonObject(flags['platform-overrides'], 'platform-overrides');
|
|
433
|
+
return payload;
|
|
434
|
+
}
|
|
435
|
+
export async function createCreative(client, _args, flags) {
|
|
436
|
+
if (typeof flags.data === 'string') {
|
|
437
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
438
|
+
mergeAccountId(body, flags);
|
|
439
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
440
|
+
return client.post(`/manage/meta/creatives${publish}`, body);
|
|
441
|
+
}
|
|
442
|
+
validateFlags(flags, CREATIVE_FLAGS, 'manage meta creatives create');
|
|
443
|
+
const payload = buildCreativePayload(flags);
|
|
444
|
+
if (!payload.pageId)
|
|
445
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--page-id`', 'Run: adkit manage meta creatives create --page-id pg_123 --headline "Get Started" --primary-text "Try it free" --link-url https://example.com');
|
|
446
|
+
if (!payload.headlines?.length)
|
|
447
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--headline`', 'Run: adkit manage meta creatives create --page-id pg_123 --headline "Get Started" --primary-text "Try it free" --link-url https://example.com');
|
|
448
|
+
if (!payload.primaryTexts?.length)
|
|
449
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--primary-text`', 'Run: adkit manage meta creatives create --page-id pg_123 --headline "Get Started" --primary-text "Try it free" --link-url https://example.com');
|
|
450
|
+
if (!payload.linkUrl)
|
|
451
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--link-url`', 'Run: adkit manage meta creatives create --page-id pg_123 --headline "Get Started" --primary-text "Try it free" --link-url https://example.com');
|
|
452
|
+
// Multi-asset detection: standalone creatives with multiple text variants
|
|
453
|
+
// use Dynamic Creative format, which Meta doesn't support for all objectives.
|
|
454
|
+
const isMultiAsset = (payload.headlines?.length ?? 0) > 1 || (payload.primaryTexts?.length ?? 0) > 1 || (payload.linkDescriptions?.length ?? 0) > 1;
|
|
455
|
+
if (isMultiAsset && flags.force !== true)
|
|
456
|
+
throw new CliError('INVALID_VALUE', "Standalone creatives with multiple assets use Dynamic Creative format, which Meta doesn't support for sales/app_promotion. Use `ads create` instead (auto-detects the right format). Add --force to proceed anyway.");
|
|
457
|
+
if (!payload.name)
|
|
458
|
+
payload.name = generateCreativeName();
|
|
459
|
+
mergeAccountId(payload, flags);
|
|
460
|
+
const publish = flags.publish === true ? '?publish=true' : '';
|
|
461
|
+
return client.post(`/manage/meta/creatives${publish}`, payload);
|
|
462
|
+
}
|
|
463
|
+
export async function updateCreative(client, args, flags) {
|
|
464
|
+
const id = requireArg(args, 0, 'creative-id', `Run: adkit manage meta creatives update <creative-id> --primary-text "Start free trial"`);
|
|
465
|
+
if (typeof flags.data === 'string') {
|
|
466
|
+
const body = parseDataFlag(flags, 'Check JSON syntax in --data');
|
|
467
|
+
mergeAccountId(body, flags);
|
|
468
|
+
return client.patch(`/manage/meta/creatives/${id}`, body);
|
|
469
|
+
}
|
|
470
|
+
validateFlags(flags, CREATIVE_FLAGS, 'manage meta creatives update');
|
|
471
|
+
const payload = buildCreativePayload(flags);
|
|
472
|
+
mergeAccountId(payload, flags);
|
|
473
|
+
return client.patch(`/manage/meta/creatives/${id}`, payload);
|
|
474
|
+
}
|
|
475
|
+
export async function deleteCreative(client, args, _flags) {
|
|
476
|
+
const id = requireArg(args, 0, 'creative-id', 'Run: adkit manage meta creatives delete <creative-id>');
|
|
477
|
+
return client.delete(`/manage/meta/creatives/${id}`);
|
|
478
|
+
}
|
|
479
|
+
// --- Interests ---
|
|
480
|
+
export async function searchInterests(client, args, flags) {
|
|
481
|
+
if (!args.length)
|
|
482
|
+
throw new CliError('MISSING_ARGUMENT', 'Missing query', 'Run: adkit manage meta interests search <query> [query2] [query3]');
|
|
483
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
484
|
+
const allResults = [];
|
|
485
|
+
for (const query of args) {
|
|
486
|
+
const qs = queryString({ accountId, q: query });
|
|
487
|
+
const res = await client.get(`/manage/meta/interests/search${qs}`);
|
|
488
|
+
const obj = res && typeof res === 'object' && 'results' in res ? res : null;
|
|
489
|
+
const arr = obj && Array.isArray(obj.results) ? obj.results : [];
|
|
490
|
+
for (const item of arr)
|
|
491
|
+
allResults.push(item);
|
|
492
|
+
}
|
|
493
|
+
return { results: allResults };
|
|
494
|
+
}
|
|
495
|
+
// --- Media ---
|
|
496
|
+
export async function uploadMedia(client, _args, flags) {
|
|
497
|
+
validateFlags(flags, MEDIA_FLAGS, 'manage meta media upload');
|
|
498
|
+
const filePath = typeof flags.file === 'string' ? flags.file : undefined;
|
|
499
|
+
const url = typeof flags.url === 'string' ? flags.url : undefined;
|
|
500
|
+
if (!filePath && !url)
|
|
501
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--file` or `--url`', 'Run: adkit manage meta media upload --file ./video.mp4');
|
|
502
|
+
if (filePath) {
|
|
503
|
+
const fs = await import('node:fs');
|
|
504
|
+
const path = await import('node:path');
|
|
505
|
+
const resolved = path.resolve(filePath);
|
|
506
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
507
|
+
const isVideo = ['.mp4', '.mov', '.avi', '.webm', '.mkv'].includes(ext);
|
|
508
|
+
const base64 = fs.readFileSync(resolved).toString('base64');
|
|
509
|
+
const filename = path.basename(resolved);
|
|
510
|
+
if (isVideo)
|
|
511
|
+
return client.post('/manage/meta/media', { videoBase64: base64, filename });
|
|
512
|
+
else
|
|
513
|
+
return client.post('/manage/meta/media', { imageBase64: base64, filename });
|
|
514
|
+
}
|
|
515
|
+
// URL-based upload — detect video vs image from URL extension
|
|
516
|
+
if (!url)
|
|
517
|
+
throw new CliError('MISSING_FLAG', 'Missing required flag: `--file` or `--url`');
|
|
518
|
+
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm']);
|
|
519
|
+
const urlExt = new URL(url).pathname.match(/\.[^.]+$/)?.[0]?.toLowerCase() ?? '';
|
|
520
|
+
if (VIDEO_EXTENSIONS.has(urlExt))
|
|
521
|
+
return client.post('/manage/meta/media', { videoUrl: url });
|
|
522
|
+
return client.post('/manage/meta/media', { imageUrl: url });
|
|
523
|
+
}
|
|
524
|
+
export async function listMedia(client, _args, flags) {
|
|
525
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
526
|
+
const qs = queryString({ accountId });
|
|
527
|
+
return client.get(`/manage/meta/media${qs}`);
|
|
528
|
+
}
|
|
529
|
+
export async function deleteMedia(client, args, flags) {
|
|
530
|
+
const id = requireArg(args, 0, 'media-id', 'Run: adkit manage meta media delete <media-id>');
|
|
531
|
+
const accountId = typeof flags.account === 'string' ? flags.account : undefined;
|
|
532
|
+
const qs = queryString({ accountId });
|
|
533
|
+
return client.delete(`/manage/meta/media/${id}${qs}`);
|
|
534
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface ProjectConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
selectedProject?: string;
|
|
4
|
+
projects?: Record<string, {
|
|
5
|
+
name: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
current?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Returns a formatted list of all configured projects. */
|
|
16
|
+
export declare function listProjects(config: ProjectConfig): ProjectEntry[];
|
|
17
|
+
/** Writes selectedProject to config, making projectId the active project. */
|
|
18
|
+
export declare function useProject(projectId: string): void;
|
|
19
|
+
/** Returns info about the current default project, or null if none is set. */
|
|
20
|
+
export declare function currentProject(config: ProjectConfig): {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
} | null;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../config.js';
|
|
2
|
+
/** Returns a formatted list of all configured projects. */
|
|
3
|
+
export function listProjects(config) {
|
|
4
|
+
if (!config.projects)
|
|
5
|
+
return [];
|
|
6
|
+
return Object.entries(config.projects).map(([id, value]) => {
|
|
7
|
+
const entry = { id, name: value.name, apiKey: value.apiKey };
|
|
8
|
+
if (config.selectedProject === id) {
|
|
9
|
+
entry.current = true;
|
|
10
|
+
}
|
|
11
|
+
return entry;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Writes selectedProject to config, making projectId the active project. */
|
|
15
|
+
export function useProject(projectId) {
|
|
16
|
+
const config = readConfig() ?? {};
|
|
17
|
+
if (!config.projects?.[projectId]) {
|
|
18
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
19
|
+
}
|
|
20
|
+
writeConfig({ selectedProject: projectId });
|
|
21
|
+
}
|
|
22
|
+
/** Returns info about the current default project, or null if none is set. */
|
|
23
|
+
export function currentProject(config) {
|
|
24
|
+
if (!config.selectedProject)
|
|
25
|
+
return null;
|
|
26
|
+
const entry = config.projects?.[config.selectedProject];
|
|
27
|
+
if (!entry)
|
|
28
|
+
return null;
|
|
29
|
+
return { id: config.selectedProject, name: entry.name, apiKey: entry.apiKey };
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export async function status(client, json) {
|
|
2
|
+
const data = (await client.get('/manage/status'));
|
|
3
|
+
if (json) {
|
|
4
|
+
console.log(JSON.stringify(data));
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const { project, platforms } = data;
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`Project: ${project.name || 'unnamed'} (${project.id})`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
// Meta
|
|
12
|
+
if (platforms.meta.connected) {
|
|
13
|
+
lines.push('Meta: connected');
|
|
14
|
+
for (const a of platforms.meta.accounts) {
|
|
15
|
+
lines.push(` ${a.id} — ${a.name} (${a.currency}, ${a.status})`);
|
|
16
|
+
if (a.defaults.pageId)
|
|
17
|
+
lines.push(` Page: ${a.defaults.pageId}`);
|
|
18
|
+
if (a.defaults.pixelId)
|
|
19
|
+
lines.push(` Pixel: ${a.defaults.pixelId}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
lines.push('Meta: not connected');
|
|
24
|
+
}
|
|
25
|
+
// Google
|
|
26
|
+
if (platforms.google.connected) {
|
|
27
|
+
lines.push('Google: connected');
|
|
28
|
+
for (const a of platforms.google.accounts) {
|
|
29
|
+
lines.push(` ${a.id} — ${a.name} (${a.currency})`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push('Google: not connected');
|
|
34
|
+
}
|
|
35
|
+
console.log(lines.join('\n'));
|
|
36
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const CONFIG_DIR: string;
|
|
2
|
+
export declare const CONFIG_PATH: string;
|
|
3
|
+
interface ConfigFile {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
selectedProject?: string;
|
|
6
|
+
projects?: Record<string, {
|
|
7
|
+
name: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
defaultMetaAccount?: string;
|
|
10
|
+
defaultGoogleAccount?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
interface ResolvedConfig {
|
|
14
|
+
apiKey: string | null;
|
|
15
|
+
selectedProject?: string;
|
|
16
|
+
}
|
|
17
|
+
interface ResolveFlags {
|
|
18
|
+
project?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function readConfig(): ConfigFile | null;
|
|
21
|
+
export declare function writeConfig(updates: Partial<ConfigFile>): void;
|
|
22
|
+
export declare function resolveConfig(flags?: ResolveFlags): ResolvedConfig;
|
|
23
|
+
export declare function getDefaultAccount(platform: 'meta' | 'google'): string | null;
|
|
24
|
+
export {};
|