cli-meta-ads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +188 -0
  2. package/AI_CONTEXT.md +144 -0
  3. package/CLAUDE.md +183 -0
  4. package/README.md +590 -0
  5. package/REQUIREMENTS.md +148 -0
  6. package/dist/auth/constants.d.ts +1 -0
  7. package/dist/auth/constants.js +1 -0
  8. package/dist/auth/guards.d.ts +5 -0
  9. package/dist/auth/guards.js +16 -0
  10. package/dist/auth/login.d.ts +28 -0
  11. package/dist/auth/login.js +222 -0
  12. package/dist/cli/action.d.ts +11 -0
  13. package/dist/cli/action.js +77 -0
  14. package/dist/cli/build-cli.d.ts +2 -0
  15. package/dist/cli/build-cli.js +110 -0
  16. package/dist/cli/context.d.ts +24 -0
  17. package/dist/cli/context.js +19 -0
  18. package/dist/client/meta-api-client.d.ts +50 -0
  19. package/dist/client/meta-api-client.js +258 -0
  20. package/dist/client/meta-discovery.d.ts +13 -0
  21. package/dist/client/meta-discovery.js +88 -0
  22. package/dist/commands/accounts.d.ts +4 -0
  23. package/dist/commands/accounts.js +42 -0
  24. package/dist/commands/ads.d.ts +4 -0
  25. package/dist/commands/ads.js +148 -0
  26. package/dist/commands/adsets.d.ts +4 -0
  27. package/dist/commands/adsets.js +49 -0
  28. package/dist/commands/anomalies.d.ts +4 -0
  29. package/dist/commands/anomalies.js +44 -0
  30. package/dist/commands/assets.d.ts +4 -0
  31. package/dist/commands/assets.js +116 -0
  32. package/dist/commands/audiences.d.ts +4 -0
  33. package/dist/commands/audiences.js +40 -0
  34. package/dist/commands/auth.d.ts +4 -0
  35. package/dist/commands/auth.js +139 -0
  36. package/dist/commands/campaigns.d.ts +4 -0
  37. package/dist/commands/campaigns.js +273 -0
  38. package/dist/commands/capi.d.ts +4 -0
  39. package/dist/commands/capi.js +64 -0
  40. package/dist/commands/creatives.d.ts +4 -0
  41. package/dist/commands/creatives.js +49 -0
  42. package/dist/commands/diagnostics.d.ts +4 -0
  43. package/dist/commands/diagnostics.js +88 -0
  44. package/dist/commands/helpers.d.ts +13 -0
  45. package/dist/commands/helpers.js +50 -0
  46. package/dist/commands/launch.d.ts +4 -0
  47. package/dist/commands/launch.js +109 -0
  48. package/dist/commands/performance.d.ts +4 -0
  49. package/dist/commands/performance.js +55 -0
  50. package/dist/commands/pixel.d.ts +4 -0
  51. package/dist/commands/pixel.js +68 -0
  52. package/dist/commands/report.d.ts +4 -0
  53. package/dist/commands/report.js +30 -0
  54. package/dist/config/file-config.d.ts +6 -0
  55. package/dist/config/file-config.js +174 -0
  56. package/dist/config/types.d.ts +32 -0
  57. package/dist/config/types.js +1 -0
  58. package/dist/domain/account-scope.d.ts +7 -0
  59. package/dist/domain/account-scope.js +28 -0
  60. package/dist/domain/analytics.d.ts +52 -0
  61. package/dist/domain/analytics.js +125 -0
  62. package/dist/domain/approval-service.d.ts +10 -0
  63. package/dist/domain/approval-service.js +48 -0
  64. package/dist/domain/asset-feed-compiler.d.ts +43 -0
  65. package/dist/domain/asset-feed-compiler.js +104 -0
  66. package/dist/domain/launch-service.d.ts +200 -0
  67. package/dist/domain/launch-service.js +558 -0
  68. package/dist/domain/meta-ads-service.d.ts +620 -0
  69. package/dist/domain/meta-ads-service.js +841 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +9 -0
  72. package/dist/output/render.d.ts +3 -0
  73. package/dist/output/render.js +103 -0
  74. package/dist/types.d.ts +42 -0
  75. package/dist/types.js +1 -0
  76. package/dist/utils/currency.d.ts +4 -0
  77. package/dist/utils/currency.js +40 -0
  78. package/dist/utils/date-range.d.ts +20 -0
  79. package/dist/utils/date-range.js +115 -0
  80. package/dist/utils/errors.d.ts +35 -0
  81. package/dist/utils/errors.js +68 -0
  82. package/dist/utils/ids.d.ts +4 -0
  83. package/dist/utils/ids.js +23 -0
  84. package/dist/utils/meta-placement-assets.d.ts +44 -0
  85. package/dist/utils/meta-placement-assets.js +315 -0
  86. package/dist/utils/security.d.ts +5 -0
  87. package/dist/utils/security.js +104 -0
  88. package/dist/validators/common.d.ts +10 -0
  89. package/dist/validators/common.js +56 -0
  90. package/dist/validators/create-spec.d.ts +373 -0
  91. package/dist/validators/create-spec.js +394 -0
  92. package/dist/validators/launch-spec.d.ts +229 -0
  93. package/dist/validators/launch-spec.js +371 -0
  94. package/docs/TECHNICAL.md +480 -0
  95. package/examples/README.md +29 -0
  96. package/examples/launch/assets/feed4x5.png +0 -0
  97. package/examples/launch/assets/story9x16.png +0 -0
  98. package/examples/launch/multi-format-launch.json +90 -0
  99. package/examples/single-object/ad.json +6 -0
  100. package/examples/single-object/adset.json +30 -0
  101. package/examples/single-object/campaign.json +6 -0
  102. package/examples/single-object/creative.json +19 -0
  103. package/package.json +62 -0
  104. package/skills/meta-cli-operator/SKILL.md +105 -0
  105. package/skills/meta-cli-operator/agents/openai.yaml +4 -0
  106. package/skills/meta-cli-operator/references/update-matrix.md +117 -0
@@ -0,0 +1,50 @@
1
+ import type { ResolvedConfig } from "../config/types.js";
2
+ export interface MetaRateLimitUsage {
3
+ app?: Record<string, unknown> | undefined;
4
+ adAccount?: Record<string, unknown> | undefined;
5
+ businessUseCase?: Record<string, unknown> | undefined;
6
+ }
7
+ export interface MetaPagedResponse<T> {
8
+ data: T[];
9
+ paging?: {
10
+ cursors?: {
11
+ after?: string | undefined;
12
+ before?: string | undefined;
13
+ } | undefined;
14
+ next?: string | undefined;
15
+ } | undefined;
16
+ summary?: Record<string, unknown> | undefined;
17
+ }
18
+ export interface MetaRequestOptions {
19
+ body?: Record<string, string | number | boolean | undefined> | undefined;
20
+ multipart?: Record<string, MetaMultipartValue | undefined> | undefined;
21
+ method?: "GET" | "POST";
22
+ path: string;
23
+ query?: Record<string, string | number | boolean | undefined> | undefined;
24
+ retryable?: boolean | undefined;
25
+ }
26
+ export interface MetaRequestResult<T> {
27
+ data: T;
28
+ usage: MetaRateLimitUsage;
29
+ headers: Headers;
30
+ }
31
+ export interface FetchLike {
32
+ (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
33
+ }
34
+ export type MetaMultipartValue = string | number | boolean | Blob | {
35
+ blob: Blob;
36
+ filename: string;
37
+ };
38
+ export declare class MetaApiClient {
39
+ private readonly fetchImpl;
40
+ private readonly config;
41
+ constructor(config: ResolvedConfig, fetchImpl?: FetchLike);
42
+ get apiVersion(): string;
43
+ request<T>(options: MetaRequestOptions): Promise<MetaRequestResult<T>>;
44
+ get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<MetaRequestResult<T>>;
45
+ post<T>(path: string, body?: Record<string, string | number | boolean | undefined>): Promise<MetaRequestResult<T>>;
46
+ postMultipart<T>(path: string, multipart: Record<string, MetaMultipartValue | undefined>): Promise<MetaRequestResult<T>>;
47
+ paginate<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<MetaRequestResult<MetaPagedResponse<T>>>;
48
+ private executeRequest;
49
+ private extractError;
50
+ }
@@ -0,0 +1,258 @@
1
+ import { createHmac } from "node:crypto";
2
+ import { createRequire } from "node:module";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import { requireAccessToken } from "../auth/guards.js";
5
+ import { AppError, ExitCode, MetaApiError } from "../utils/errors.js";
6
+ import { sanitizeText, sanitizeUrl } from "../utils/security.js";
7
+ const require = createRequire(import.meta.url);
8
+ const packageJson = require("../../package.json");
9
+ const USER_AGENT = `cli-meta-ads/${packageJson.version ?? "0.0.0"}`;
10
+ const RETRIABLE_META_CODES = new Set([4, 17, 32, 341, 613, 80001, 80002, 80004]);
11
+ const MAX_PAGINATION_PAGES = 100;
12
+ function buildFormData(params, accessToken, appSecret) {
13
+ const formData = new FormData();
14
+ for (const [key, value] of Object.entries(params)) {
15
+ if (value === undefined) {
16
+ continue;
17
+ }
18
+ if (value instanceof Blob) {
19
+ formData.append(key, value);
20
+ continue;
21
+ }
22
+ if (typeof value === "object" && "blob" in value && "filename" in value) {
23
+ formData.append(key, value.blob, value.filename);
24
+ continue;
25
+ }
26
+ formData.append(key, String(value));
27
+ }
28
+ formData.append("access_token", accessToken);
29
+ if (appSecret) {
30
+ const proof = createHmac("sha256", appSecret).update(accessToken).digest("hex");
31
+ formData.append("appsecret_proof", proof);
32
+ }
33
+ return formData;
34
+ }
35
+ function parseUsageHeader(value) {
36
+ if (!value) {
37
+ return undefined;
38
+ }
39
+ try {
40
+ return JSON.parse(value);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ function isRetriableError(status, metaCode) {
47
+ return status === 429 || status >= 500 || (metaCode !== undefined && RETRIABLE_META_CODES.has(metaCode));
48
+ }
49
+ function classifyExitCode(status, metaCode) {
50
+ if (metaCode === 190) {
51
+ return ExitCode.Auth;
52
+ }
53
+ if (metaCode === 200 || metaCode === 10 || metaCode === 270) {
54
+ return ExitCode.Permission;
55
+ }
56
+ if (isRetriableError(status, metaCode)) {
57
+ return ExitCode.RateLimit;
58
+ }
59
+ if (metaCode === 2635) {
60
+ return ExitCode.VerificationFailed;
61
+ }
62
+ return ExitCode.Provider;
63
+ }
64
+ function buildSearchParams(params, accessToken, appSecret) {
65
+ const searchParams = new URLSearchParams();
66
+ for (const [key, value] of Object.entries(params)) {
67
+ if (value !== undefined) {
68
+ searchParams.set(key, String(value));
69
+ }
70
+ }
71
+ searchParams.set("access_token", accessToken);
72
+ if (appSecret) {
73
+ const proof = createHmac("sha256", appSecret).update(accessToken).digest("hex");
74
+ searchParams.set("appsecret_proof", proof);
75
+ }
76
+ return searchParams;
77
+ }
78
+ export class MetaApiClient {
79
+ fetchImpl;
80
+ config;
81
+ constructor(config, fetchImpl = fetch) {
82
+ this.config = config;
83
+ this.fetchImpl = fetchImpl;
84
+ }
85
+ get apiVersion() {
86
+ return this.config.apiVersion;
87
+ }
88
+ async request(options) {
89
+ const maxAttempts = options.retryable === false ? 1 : 4;
90
+ const accessToken = requireAccessToken(this.config);
91
+ let lastError;
92
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
93
+ try {
94
+ return await this.executeRequest(options, accessToken);
95
+ }
96
+ catch (error) {
97
+ if (!(error instanceof MetaApiError)) {
98
+ throw error;
99
+ }
100
+ lastError = error;
101
+ const retriable = options.retryable !== false && isRetriableError(error.status, error.metaCode);
102
+ if (!retriable || attempt === maxAttempts) {
103
+ throw error;
104
+ }
105
+ const backoffMs = 400 * (2 ** (attempt - 1)) + Math.floor(Math.random() * 250);
106
+ await sleep(backoffMs);
107
+ }
108
+ }
109
+ throw lastError ?? new MetaApiError("Meta request failed.", ExitCode.Provider, { status: 500 });
110
+ }
111
+ async get(path, query) {
112
+ return this.request({
113
+ method: "GET",
114
+ path,
115
+ query,
116
+ retryable: true
117
+ });
118
+ }
119
+ async post(path, body) {
120
+ return this.request({
121
+ method: "POST",
122
+ path,
123
+ body,
124
+ retryable: false
125
+ });
126
+ }
127
+ async postMultipart(path, multipart) {
128
+ return this.request({
129
+ method: "POST",
130
+ multipart,
131
+ path,
132
+ retryable: false
133
+ });
134
+ }
135
+ async paginate(path, query) {
136
+ const collected = [];
137
+ let after;
138
+ let lastUsage = {};
139
+ let summary;
140
+ let pageCount = 0;
141
+ do {
142
+ const pageQuery = {
143
+ ...(query ?? {}),
144
+ ...(after ? { after } : {})
145
+ };
146
+ const page = await this.get(path, pageQuery);
147
+ pageCount += 1;
148
+ collected.push(...page.data.data);
149
+ lastUsage = page.usage;
150
+ summary = page.data.summary ?? summary;
151
+ after = page.data.paging?.cursors?.after;
152
+ if (!page.data.paging?.next) {
153
+ after = undefined;
154
+ }
155
+ if (after && pageCount >= MAX_PAGINATION_PAGES) {
156
+ throw new AppError(`Pagination exceeded the safety limit of ${MAX_PAGINATION_PAGES} pages. Refine the query before retrying.`, ExitCode.Provider, {
157
+ pageLimit: MAX_PAGINATION_PAGES,
158
+ path
159
+ });
160
+ }
161
+ } while (after);
162
+ return {
163
+ data: {
164
+ data: collected,
165
+ summary
166
+ },
167
+ usage: lastUsage,
168
+ headers: new Headers()
169
+ };
170
+ }
171
+ async executeRequest(options, accessToken) {
172
+ const baseUrl = `https://graph.facebook.com/${this.config.apiVersion}${options.path}`;
173
+ const method = options.method ?? "GET";
174
+ const init = {
175
+ method,
176
+ headers: {
177
+ Accept: "application/json",
178
+ "User-Agent": USER_AGENT
179
+ }
180
+ };
181
+ let url = baseUrl;
182
+ if (method === "GET") {
183
+ const searchParams = buildSearchParams(options.query ?? {}, accessToken, this.config.appSecret);
184
+ url = `${baseUrl}?${searchParams.toString()}`;
185
+ }
186
+ else if (options.multipart) {
187
+ init.body = buildFormData(options.multipart, accessToken, this.config.appSecret);
188
+ }
189
+ else {
190
+ const bodyParams = buildSearchParams(options.body ?? {}, accessToken, this.config.appSecret);
191
+ init.headers = {
192
+ ...init.headers,
193
+ "Content-Type": "application/x-www-form-urlencoded"
194
+ };
195
+ init.body = bodyParams.toString();
196
+ }
197
+ const sanitizedRequestUrl = sanitizeUrl(url);
198
+ const response = await this.fetchImpl(url, init);
199
+ const usage = {
200
+ app: parseUsageHeader(response.headers.get("x-app-usage")),
201
+ adAccount: parseUsageHeader(response.headers.get("x-ad-account-usage")),
202
+ businessUseCase: parseUsageHeader(response.headers.get("x-business-use-case-usage"))
203
+ };
204
+ const text = await response.text();
205
+ let payload = {};
206
+ if (text) {
207
+ try {
208
+ payload = JSON.parse(text);
209
+ }
210
+ catch {
211
+ throw new MetaApiError(`Meta returned a non-JSON response with status ${response.status}.`, classifyExitCode(response.status), {
212
+ status: response.status,
213
+ details: {
214
+ contentType: response.headers.get("content-type") ?? undefined,
215
+ path: options.path,
216
+ requestUrl: sanitizedRequestUrl,
217
+ responsePreview: sanitizeText(text.slice(0, 200))
218
+ }
219
+ });
220
+ }
221
+ }
222
+ const errorPayload = this.extractError(payload);
223
+ if (!response.ok || errorPayload) {
224
+ const status = response.status;
225
+ const message = errorPayload?.message ?? `Meta request failed with status ${status}.`;
226
+ const metaCode = errorPayload?.code;
227
+ const metaSubcode = errorPayload?.error_subcode;
228
+ const fbtraceId = errorPayload?.fbtrace_id;
229
+ throw new MetaApiError(message, classifyExitCode(status, metaCode), {
230
+ status,
231
+ metaCode,
232
+ metaSubcode,
233
+ fbtraceId,
234
+ details: {
235
+ path: options.path,
236
+ requestUrl: sanitizedRequestUrl,
237
+ usage,
238
+ response: payload
239
+ }
240
+ });
241
+ }
242
+ return {
243
+ data: payload,
244
+ headers: response.headers,
245
+ usage
246
+ };
247
+ }
248
+ extractError(payload) {
249
+ if (typeof payload !== "object" || payload === null || !("error" in payload)) {
250
+ return null;
251
+ }
252
+ const errorValue = payload.error;
253
+ if (typeof errorValue !== "object" || errorValue === null) {
254
+ return null;
255
+ }
256
+ return errorValue;
257
+ }
258
+ }
@@ -0,0 +1,13 @@
1
+ import type { ResolvedConfig } from "../config/types.js";
2
+ import { MetaApiClient } from "./meta-api-client.js";
3
+ export interface ManagedAccount {
4
+ id: string;
5
+ name?: string | undefined;
6
+ accountStatus?: number | string | undefined;
7
+ currency?: string | undefined;
8
+ timezoneName?: string | undefined;
9
+ source: string;
10
+ businessId?: string | undefined;
11
+ businessName?: string | undefined;
12
+ }
13
+ export declare function discoverManagedAccounts(config: ResolvedConfig, client?: MetaApiClient): Promise<ManagedAccount[]>;
@@ -0,0 +1,88 @@
1
+ import { AppError, ExitCode, MetaApiError } from "../utils/errors.js";
2
+ import { normalizeAdAccountId } from "../utils/ids.js";
3
+ import { MetaApiClient } from "./meta-api-client.js";
4
+ function normalizeAccountNode(node, source, business) {
5
+ const rawId = node.account_id ?? node.id;
6
+ if (!rawId) {
7
+ throw new AppError("Meta account discovery returned an account without an id.", ExitCode.Provider);
8
+ }
9
+ return {
10
+ id: normalizeAdAccountId(rawId),
11
+ name: node.name,
12
+ accountStatus: node.account_status,
13
+ businessId: business?.id,
14
+ businessName: business?.name,
15
+ currency: node.currency,
16
+ source,
17
+ timezoneName: node.timezone_name
18
+ };
19
+ }
20
+ function dedupeAccounts(accounts) {
21
+ const byId = new Map();
22
+ for (const account of accounts) {
23
+ if (!byId.has(account.id)) {
24
+ byId.set(account.id, account);
25
+ }
26
+ }
27
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
28
+ }
29
+ async function swallowDiscoveryError(promise) {
30
+ try {
31
+ return await promise;
32
+ }
33
+ catch (error) {
34
+ if (error instanceof MetaApiError) {
35
+ if (error.exitCode === ExitCode.Auth || error.exitCode === ExitCode.RateLimit) {
36
+ throw error;
37
+ }
38
+ return null;
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ export async function discoverManagedAccounts(config, client = new MetaApiClient(config)) {
44
+ const discovered = [];
45
+ const [meAccounts, meAssigned, businesses] = await Promise.all([
46
+ swallowDiscoveryError(client.paginate("/me/adaccounts", {
47
+ fields: "id,account_id,name,account_status,currency,timezone_name",
48
+ limit: 500
49
+ })),
50
+ swallowDiscoveryError(client.get("/me", {
51
+ fields: "assigned_ad_accounts{id,account_id,name,account_status,currency,timezone_name}"
52
+ })),
53
+ swallowDiscoveryError(client.paginate("/me/businesses", {
54
+ fields: "id,name",
55
+ limit: 200
56
+ }))
57
+ ]);
58
+ if (meAccounts) {
59
+ discovered.push(...meAccounts.data.data.map((account) => normalizeAccountNode(account, "/me/adaccounts")));
60
+ }
61
+ if (meAssigned?.data.assigned_ad_accounts?.data) {
62
+ discovered.push(...meAssigned.data.assigned_ad_accounts.data.map((account) => normalizeAccountNode(account, "/me?fields=assigned_ad_accounts")));
63
+ }
64
+ if (businesses) {
65
+ const expandedBusinesses = [];
66
+ for (const business of businesses.data.data) {
67
+ const expanded = await swallowDiscoveryError(client.get(`/${business.id}`, {
68
+ fields: [
69
+ "id",
70
+ "name",
71
+ "owned_ad_accounts.limit(500){id,account_id,name,account_status,currency,timezone_name}",
72
+ "client_ad_accounts.limit(500){id,account_id,name,account_status,currency,timezone_name}"
73
+ ].join(",")
74
+ }));
75
+ expandedBusinesses.push(expanded);
76
+ }
77
+ for (const expanded of expandedBusinesses) {
78
+ if (!expanded) {
79
+ continue;
80
+ }
81
+ const businessNode = expanded.data;
82
+ const owned = businessNode.owned_ad_accounts?.data ?? [];
83
+ const clientAccounts = businessNode.client_ad_accounts?.data ?? [];
84
+ discovered.push(...owned.map((account) => normalizeAccountNode(account, "business.owned_ad_accounts", businessNode)), ...clientAccounts.map((account) => normalizeAccountNode(account, "business.client_ad_accounts", businessNode)));
85
+ }
86
+ }
87
+ return dedupeAccounts(discovered);
88
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAccountsCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,42 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { writeFileConfig } from "../config/file-config.js";
3
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
4
+ import { normalizeAdAccountId } from "../utils/ids.js";
5
+ export function registerAccountsCommands(program, deps, state) {
6
+ const accounts = program.command("accounts").description("Managed account discovery and local defaults.");
7
+ accounts
8
+ .command("list")
9
+ .description("List all managed ad accounts accessible to the current token.")
10
+ .action(createAction("accounts list", deps, state, async (context) => {
11
+ const service = new MetaAdsService(context.client);
12
+ const accountsList = await service.listManagedAccounts(context.config);
13
+ return {
14
+ ok: true,
15
+ command: "accounts list",
16
+ data: {
17
+ accounts: accountsList
18
+ },
19
+ meta: {
20
+ count: accountsList.length
21
+ }
22
+ };
23
+ }));
24
+ accounts
25
+ .command("set-default")
26
+ .argument("<ad-account-id>", "Default ad account id.")
27
+ .description("Persist the default ad account id in the local CLI config.")
28
+ .action(createAction("accounts set-default", deps, state, async (context, accountId) => {
29
+ const normalized = normalizeAdAccountId(accountId);
30
+ await writeFileConfig(context.config.configPath, {
31
+ defaultAccountId: normalized
32
+ });
33
+ return {
34
+ ok: true,
35
+ command: "accounts set-default",
36
+ data: {
37
+ defaultAccountId: normalized,
38
+ configPath: context.config.configPath
39
+ }
40
+ };
41
+ }));
42
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAdsCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,148 @@
1
+ import { requirePermission } from "../auth/guards.js";
2
+ import { createAction } from "../cli/action.js";
3
+ import { analyzeCreativeFatigue } from "../domain/analytics.js";
4
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
5
+ import { parseDateWindow } from "../utils/date-range.js";
6
+ import { adCreateSpecSchema, readSpecFile } from "../validators/create-spec.js";
7
+ import { parseAdId, parseCampaignId } from "../validators/common.js";
8
+ import { resolveSingleAccount, toMetaSort } from "./helpers.js";
9
+ export function registerAdsCommands(program, deps, state) {
10
+ const ads = program.command("ads").description("Ad reads, creation, performance and fatigue analysis.");
11
+ ads
12
+ .command("create")
13
+ .requiredOption("--account <id>", "Ad account id.")
14
+ .requiredOption("--spec <path>", "Path to a JSON ad create spec.")
15
+ .description("Draft or create an ad from a JSON spec.")
16
+ .action(createAction("ads create", deps, state, async (context, options) => {
17
+ const account = await resolveSingleAccount(context, options.account, "ads create");
18
+ const { spec, specPath } = await readSpecFile(options.spec, adCreateSpecSchema, "Ad create");
19
+ const service = new MetaAdsService(context.client);
20
+ const plan = {
21
+ accountId: account.id,
22
+ action: "create-ad",
23
+ spec,
24
+ specPath
25
+ };
26
+ if (!context.apply) {
27
+ return {
28
+ ok: true,
29
+ command: "ads create",
30
+ data: plan,
31
+ meta: {
32
+ mode: "draft"
33
+ }
34
+ };
35
+ }
36
+ requirePermission(context.config, "write", "Ad create");
37
+ const ad = await service.createAd(account.id, spec);
38
+ return {
39
+ ok: true,
40
+ command: "ads create",
41
+ data: {
42
+ accountId: account.id,
43
+ ad,
44
+ applied: true,
45
+ specPath
46
+ },
47
+ meta: {
48
+ mode: "write"
49
+ }
50
+ };
51
+ }));
52
+ ads
53
+ .command("list")
54
+ .requiredOption("--campaign <id>", "Campaign id.")
55
+ .description("List ads under a campaign.")
56
+ .action(createAction("ads list", deps, state, async (context, options) => {
57
+ const service = new MetaAdsService(context.client);
58
+ const campaignId = parseCampaignId(options.campaign);
59
+ const rows = await service.listAdsByCampaign(campaignId);
60
+ return {
61
+ ok: true,
62
+ command: "ads list",
63
+ data: {
64
+ ads: rows,
65
+ campaignId
66
+ },
67
+ meta: {
68
+ count: rows.length
69
+ }
70
+ };
71
+ }));
72
+ ads
73
+ .command("performance")
74
+ .requiredOption("--campaign <id>", "Campaign id.")
75
+ .option("--last <window>", "Relative time window like 7d or 30d, unless --from/--to is used.")
76
+ .option("--from <date>", "Start date in YYYY-MM-DD.")
77
+ .option("--to <date>", "End date in YYYY-MM-DD.")
78
+ .option("--sort <metric>", "Sort metric such as ctr or spend.")
79
+ .description("Ad-level performance for a campaign.")
80
+ .action(createAction("ads performance", deps, state, async (context, options) => {
81
+ const service = new MetaAdsService(context.client);
82
+ const campaignId = parseCampaignId(options.campaign);
83
+ const window = parseDateWindow({
84
+ from: options.from,
85
+ last: options.last,
86
+ to: options.to
87
+ });
88
+ const rows = await service.getAdPerformanceByCampaign(campaignId, window, toMetaSort(options.sort));
89
+ return {
90
+ ok: true,
91
+ command: "ads performance",
92
+ data: {
93
+ campaignId,
94
+ rows,
95
+ window
96
+ },
97
+ meta: {
98
+ count: rows.length
99
+ }
100
+ };
101
+ }));
102
+ ads
103
+ .command("fatigue")
104
+ .requiredOption("--campaign <id>", "Campaign id.")
105
+ .option("--last <window>", "Relative time window like 30d.", "30d")
106
+ .description("Local creative fatigue analysis on ad-level trends.")
107
+ .action(createAction("ads fatigue", deps, state, async (context, options) => {
108
+ const service = new MetaAdsService(context.client);
109
+ const campaignId = parseCampaignId(options.campaign);
110
+ const window = parseDateWindow({
111
+ last: options.last
112
+ });
113
+ const rows = await service.getDailyAdInsights(campaignId, window);
114
+ const fatigue = analyzeCreativeFatigue(rows);
115
+ return {
116
+ ok: true,
117
+ command: "ads fatigue",
118
+ data: {
119
+ campaignId,
120
+ fatigue,
121
+ window
122
+ },
123
+ meta: {
124
+ count: fatigue.length
125
+ }
126
+ };
127
+ }));
128
+ ads
129
+ .command("preview")
130
+ .argument("<ad-id>", "Ad id.")
131
+ .option("--ad-format <format>", "Optional ad_format override for previews.")
132
+ .description("Fetch preview payloads for an ad.")
133
+ .action(createAction("ads preview", deps, state, async (context, adId, options) => {
134
+ const service = new MetaAdsService(context.client);
135
+ const previews = await service.getAdPreview(parseAdId(adId), options.adFormat);
136
+ return {
137
+ ok: true,
138
+ command: "ads preview",
139
+ data: {
140
+ adId,
141
+ previews
142
+ },
143
+ meta: {
144
+ count: previews.length
145
+ }
146
+ };
147
+ }));
148
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAdSetCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,49 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { requirePermission } from "../auth/guards.js";
3
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
4
+ import { adSetCreateSpecSchema, readSpecFile } from "../validators/create-spec.js";
5
+ import { resolveSingleAccount } from "./helpers.js";
6
+ export function registerAdSetCommands(program, deps, state) {
7
+ const adSets = program.command("adsets").description("Ad set creation workflows.");
8
+ adSets
9
+ .command("create")
10
+ .requiredOption("--account <id>", "Ad account id.")
11
+ .requiredOption("--spec <path>", "Path to a JSON ad set create spec.")
12
+ .description("Draft or create an ad set from a JSON spec.")
13
+ .action(createAction("adsets create", deps, state, async (context, options) => {
14
+ const account = await resolveSingleAccount(context, options.account, "adsets create");
15
+ const { spec, specPath } = await readSpecFile(options.spec, adSetCreateSpecSchema, "Ad set create");
16
+ const service = new MetaAdsService(context.client);
17
+ const plan = {
18
+ accountId: account.id,
19
+ action: "create-adset",
20
+ spec,
21
+ specPath
22
+ };
23
+ if (!context.apply) {
24
+ return {
25
+ ok: true,
26
+ command: "adsets create",
27
+ data: plan,
28
+ meta: {
29
+ mode: "draft"
30
+ }
31
+ };
32
+ }
33
+ requirePermission(context.config, "write", "Ad set create");
34
+ const adSet = await service.createAdSet(account.id, spec);
35
+ return {
36
+ ok: true,
37
+ command: "adsets create",
38
+ data: {
39
+ accountId: account.id,
40
+ adSet,
41
+ applied: true,
42
+ specPath
43
+ },
44
+ meta: {
45
+ mode: "write"
46
+ }
47
+ };
48
+ }));
49
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAnomaliesCommand(program: Command, deps: CliDeps, state: CliRuntimeState): void;