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,148 @@
1
+ # CLI-meta-ads Requirements Snapshot
2
+
3
+ This file is a compact product and scope snapshot for the CLI. It is intentionally shorter than the README and more stable than day-to-day implementation notes.
4
+
5
+ ## Product intent
6
+
7
+ - Tool: Meta Ads CLI for Facebook and Instagram surfaces
8
+ - API: Meta Marketing API
9
+ - Runtime: Node.js CLI
10
+ - Primary human auth model: Facebook Login for Business user token stored in local config
11
+ - Primary automation auth model: pre-generated Meta access token, typically a system-user token
12
+ - Safety model: read-first, draft-first, approval-gated for sensitive writes
13
+
14
+ ## Target users
15
+
16
+ | User or agent | Typical mode | Typical use cases |
17
+ | --- | --- | --- |
18
+ | Ad operations | write | Performance review, budget changes, launch execution, creative diagnostics |
19
+ | Designers or creative teams | read | Creative performance and fatigue visibility |
20
+ | Client-facing reporting users | read | Account, campaign, and report visibility |
21
+ | Tracking / QA owners | read | Pixel and best-effort CAPI checks |
22
+
23
+ ## Current command scope
24
+
25
+ ### Read flows
26
+
27
+ ```bash
28
+ meta performance --account <ad-account-id> --last 7d
29
+ meta performance --account <ad-account-id> --campaign "Retargeting" --last 30d
30
+ meta performance --account all --last 7d
31
+ meta performance --account <id> --breakdown age,gender --last 30d
32
+
33
+ meta campaigns list --account <id>
34
+ meta campaigns get <campaign-id>
35
+
36
+ meta ads list --campaign <id>
37
+ meta ads performance --campaign <id> --last 30d --sort ctr
38
+ meta ads fatigue --campaign <id> --last 30d
39
+ meta ads preview <ad-id>
40
+
41
+ meta anomalies --account <id> --threshold 20
42
+ meta anomalies --account all --threshold 30
43
+
44
+ meta pixel status --account <id>
45
+ meta pixel events --account <id> --last 24h
46
+ meta capi status --account <id>
47
+ meta capi events --account <id> --last 24h
48
+
49
+ meta audiences list --account <id>
50
+ meta audiences size --audience <id>
51
+
52
+ meta report daily --account <id>
53
+ meta report weekly --account <id>
54
+ meta report monthly --account <id>
55
+
56
+ meta auth login
57
+ meta auth status
58
+ meta auth logout
59
+ meta whoami
60
+ meta doctor --account <id>
61
+ meta verify-api --account <id>
62
+ ```
63
+
64
+ ### Create, upload, and launch flows
65
+
66
+ ```bash
67
+ meta assets images upload --account <id> --file ./hero.png
68
+ meta assets videos upload --account <id> --file ./spot.mp4 --wait
69
+
70
+ meta campaigns create --account <id> --spec ./campaign.json
71
+ meta adsets create --account <id> --spec ./adset.json
72
+ meta creatives create --account <id> --spec ./creative.json
73
+ meta ads create --account <id> --spec ./ad.json
74
+
75
+ meta launch validate --account <id> --spec ./launch.json
76
+ meta launch plan --account <id> --spec ./launch.json
77
+ meta launch apply --account <id> --spec ./launch.json
78
+ meta launch resume --receipt ./.meta-launch/<execution-id>.json
79
+ ```
80
+
81
+ ### Approval-gated changes
82
+
83
+ ```bash
84
+ meta campaigns pause <campaign-id>
85
+ meta campaigns enable <campaign-id>
86
+ meta campaigns budget <campaign-id> --daily 50
87
+ ```
88
+
89
+ ## Permission model
90
+
91
+ ```yaml
92
+ modes:
93
+ read:
94
+ - performance
95
+ - campaigns list/get
96
+ - ads list/performance/fatigue/preview
97
+ - anomalies
98
+ - pixel/capi status/events
99
+ - audiences list/size
100
+ - reports
101
+ - launch validate/plan
102
+
103
+ write:
104
+ - everything in read
105
+ - assets upload
106
+ - campaigns/adsets/creatives/ads create
107
+ - launch apply/resume
108
+ - campaigns budget direct write only when the change is <= 20%
109
+
110
+ admin:
111
+ - reserved for future expansion
112
+ - not intended as the default agent mode
113
+ ```
114
+
115
+ ## Multi-account behavior
116
+
117
+ The CLI supports multiple Meta ad accounts through Business Manager discovery:
118
+
119
+ ```bash
120
+ meta accounts list
121
+ meta accounts set-default <ad-account-id>
122
+ meta performance --account all
123
+ ```
124
+
125
+ Only explicit multi-account read surfaces support `--account all`. Single-account commands reject it.
126
+
127
+ ## Environment variables
128
+
129
+ ```bash
130
+ META_ACCESS_TOKEN=
131
+ META_APP_ID=
132
+ META_AUTH_CONFIG_ID=
133
+ META_AUTH_REDIRECT_URI=https://www.facebook.com/connect/login_success.html
134
+ META_APP_SECRET=
135
+ META_CLI_MODE=read
136
+ META_APPROVAL_WEBHOOK=
137
+ META_API_VERSION=v25.0
138
+ META_DEFAULT_ACCOUNT_ID=
139
+ META_OUTPUT_FORMAT=text
140
+ ```
141
+
142
+ ## Delivery constraints
143
+
144
+ - No live credentials are stored in the repository.
145
+ - No hidden host-local env files should be auto-loaded.
146
+ - Approval webhooks must remain HTTPS-only.
147
+ - Draft mode must remain the default for mutations.
148
+ - The npm artifact should stay free of repo-local artifacts and accidental internal files.
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_AUTH_REDIRECT_URI = "https://www.facebook.com/connect/login_success.html";
@@ -0,0 +1 @@
1
+ export const DEFAULT_AUTH_REDIRECT_URI = "https://www.facebook.com/connect/login_success.html";
@@ -0,0 +1,5 @@
1
+ import type { PermissionMode } from "../types.js";
2
+ import type { ResolvedConfig } from "../config/types.js";
3
+ export declare function hasPermission(current: PermissionMode, required: PermissionMode): boolean;
4
+ export declare function requirePermission(config: ResolvedConfig, required: PermissionMode, operation: string): void;
5
+ export declare function requireAccessToken(config: ResolvedConfig): string;
@@ -0,0 +1,16 @@
1
+ import { AppError, ExitCode } from "../utils/errors.js";
2
+ const PERMISSION_ORDER = ["read", "write", "admin"];
3
+ export function hasPermission(current, required) {
4
+ return PERMISSION_ORDER.indexOf(current) >= PERMISSION_ORDER.indexOf(required);
5
+ }
6
+ export function requirePermission(config, required, operation) {
7
+ if (!hasPermission(config.permissionMode, required)) {
8
+ throw new AppError(`${operation} requires ${required} mode. Current mode is ${config.permissionMode}.`, ExitCode.Permission);
9
+ }
10
+ }
11
+ export function requireAccessToken(config) {
12
+ if (!config.accessToken) {
13
+ throw new AppError("A Meta access token is required for Meta API operations. Set META_ACCESS_TOKEN or run auth login.", ExitCode.Auth);
14
+ }
15
+ return config.accessToken;
16
+ }
@@ -0,0 +1,28 @@
1
+ import { type FetchLike } from "../client/meta-api-client.js";
2
+ import type { ResolvedConfig } from "../config/types.js";
3
+ export interface InteractiveAuthLoginOptions {
4
+ appId?: string | undefined;
5
+ configId?: string | undefined;
6
+ openBrowser?: boolean | undefined;
7
+ redirectUri?: string | undefined;
8
+ }
9
+ export interface InteractiveAuthLoginResult {
10
+ accessToken: string;
11
+ accessTokenExpiresAt?: string | undefined;
12
+ appId: string;
13
+ authConfigId: string;
14
+ authRedirectUri: string;
15
+ browserOpened: boolean;
16
+ codeExchangeUsed: boolean;
17
+ loginUrl: string;
18
+ }
19
+ export interface AuthLoginDeps {
20
+ fetchImpl?: FetchLike | undefined;
21
+ now?: (() => Date) | undefined;
22
+ openUrl?: ((url: string) => Promise<boolean>) | undefined;
23
+ promptForCallback?: ((loginUrl: string) => Promise<string>) | undefined;
24
+ randomBytes?: ((size: number) => Buffer) | undefined;
25
+ }
26
+ export declare function defaultPromptForCallback(loginUrl: string): Promise<string>;
27
+ export declare function openExternalBrowser(url: string): Promise<boolean>;
28
+ export declare function runInteractiveAuthLogin(config: ResolvedConfig, options?: InteractiveAuthLoginOptions, deps?: AuthLoginDeps): Promise<InteractiveAuthLoginResult>;
@@ -0,0 +1,222 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomBytes as defaultRandomBytes } from "node:crypto";
3
+ import { createInterface } from "node:readline/promises";
4
+ import process from "node:process";
5
+ import { promisify } from "node:util";
6
+ import { AppError, ExitCode } from "../utils/errors.js";
7
+ import { normalizeAuthRedirectUri } from "../utils/security.js";
8
+ import { DEFAULT_AUTH_REDIRECT_URI } from "./constants.js";
9
+ const execFileAsync = promisify(execFile);
10
+ function normalizeOptionalString(value) {
11
+ if (value === undefined) {
12
+ return undefined;
13
+ }
14
+ const normalized = value.trim();
15
+ return normalized.length > 0 ? normalized : undefined;
16
+ }
17
+ function resolveLoginOptions(config, options) {
18
+ const appId = normalizeOptionalString(options.appId) ?? normalizeOptionalString(config.appId);
19
+ if (!appId) {
20
+ throw new AppError("auth login requires a Meta app ID. Set META_APP_ID, store appId in config, or pass --app-id.", ExitCode.Config);
21
+ }
22
+ const authConfigId = normalizeOptionalString(options.configId) ?? normalizeOptionalString(config.authConfigId);
23
+ if (!authConfigId) {
24
+ throw new AppError("auth login requires a Facebook Login for Business configuration ID. Set META_AUTH_CONFIG_ID, store authConfigId in config, or pass --config-id.", ExitCode.Config);
25
+ }
26
+ const authRedirectUri = normalizeOptionalString(options.redirectUri)
27
+ ? normalizeAuthRedirectUri(options.redirectUri ?? "", "--redirect-uri")
28
+ : normalizeOptionalString(config.authRedirectUri)
29
+ ?? DEFAULT_AUTH_REDIRECT_URI;
30
+ return {
31
+ appId,
32
+ authConfigId,
33
+ authRedirectUri
34
+ };
35
+ }
36
+ function buildLoginUrl(config, options, state) {
37
+ const url = new URL(`https://www.facebook.com/${config.apiVersion}/dialog/oauth`);
38
+ url.searchParams.set("client_id", options.appId);
39
+ url.searchParams.set("redirect_uri", options.authRedirectUri);
40
+ url.searchParams.set("state", state);
41
+ url.searchParams.set("config_id", options.authConfigId);
42
+ return url.toString();
43
+ }
44
+ function promptLabel(loginUrl) {
45
+ return [
46
+ "",
47
+ "Complete the Meta login in your browser.",
48
+ "After the flow finishes, copy the final redirect URL from the browser address bar and paste it below.",
49
+ "",
50
+ `Login URL: ${loginUrl}`,
51
+ "",
52
+ "Paste final redirect URL: "
53
+ ].join("\n");
54
+ }
55
+ export async function defaultPromptForCallback(loginUrl) {
56
+ if (!process.stdin.isTTY) {
57
+ throw new AppError("auth login requires interactive stdin so the browser callback URL can be pasted back into the CLI.", ExitCode.Auth);
58
+ }
59
+ const prompt = createInterface({
60
+ input: process.stdin,
61
+ output: process.stderr
62
+ });
63
+ try {
64
+ const response = await prompt.question(promptLabel(loginUrl));
65
+ return response.trim();
66
+ }
67
+ finally {
68
+ prompt.close();
69
+ }
70
+ }
71
+ export async function openExternalBrowser(url) {
72
+ try {
73
+ if (process.platform === "darwin") {
74
+ await execFileAsync("open", [url]);
75
+ return true;
76
+ }
77
+ if (process.platform === "win32") {
78
+ await execFileAsync("rundll32", ["url.dll,FileProtocolHandler", url]);
79
+ return true;
80
+ }
81
+ await execFileAsync("xdg-open", [url]);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function readResponseParams(callbackUrl) {
89
+ if (callbackUrl.hash.startsWith("#")) {
90
+ return new URLSearchParams(callbackUrl.hash.slice(1));
91
+ }
92
+ return callbackUrl.searchParams;
93
+ }
94
+ function parsePositiveInteger(value) {
95
+ if (!value) {
96
+ return undefined;
97
+ }
98
+ const parsed = Number.parseInt(value, 10);
99
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
100
+ }
101
+ function parseCallbackUrl(rawValue) {
102
+ const trimmed = rawValue.trim();
103
+ try {
104
+ return new URL(trimmed);
105
+ }
106
+ catch {
107
+ throw new AppError("Could not parse the login callback. Paste the full final redirect URL from the browser address bar.", ExitCode.Auth);
108
+ }
109
+ }
110
+ function hasExpectedQueryParams(callbackUrl, expectedRedirectUrl) {
111
+ for (const [key, expectedValue] of expectedRedirectUrl.searchParams.entries()) {
112
+ const callbackValues = callbackUrl.searchParams.getAll(key);
113
+ if (!callbackValues.includes(expectedValue)) {
114
+ return false;
115
+ }
116
+ }
117
+ return true;
118
+ }
119
+ function assertCallbackMatchesRedirectUri(callbackUrl, expectedRedirectUri) {
120
+ const expectedRedirectUrl = new URL(expectedRedirectUri);
121
+ const matchesTarget = callbackUrl.origin === expectedRedirectUrl.origin
122
+ && callbackUrl.pathname === expectedRedirectUrl.pathname
123
+ && callbackUrl.username === expectedRedirectUrl.username
124
+ && callbackUrl.password === expectedRedirectUrl.password
125
+ && hasExpectedQueryParams(callbackUrl, expectedRedirectUrl);
126
+ if (!matchesTarget) {
127
+ throw new AppError(`The pasted callback URL did not match the configured redirect URI (${expectedRedirectUri}).`, ExitCode.Auth);
128
+ }
129
+ }
130
+ function parseAuthCallback(rawValue, expectedState, expectedRedirectUri) {
131
+ const callbackUrl = parseCallbackUrl(rawValue);
132
+ assertCallbackMatchesRedirectUri(callbackUrl, expectedRedirectUri);
133
+ const responseParams = readResponseParams(callbackUrl);
134
+ const queryParams = callbackUrl.searchParams;
135
+ const returnedState = responseParams.get("state") ?? queryParams.get("state");
136
+ if (!returnedState || returnedState !== expectedState) {
137
+ throw new AppError("The login callback state did not match. Cancel the flow and run auth login again.", ExitCode.Auth);
138
+ }
139
+ const errorReason = responseParams.get("error_reason") ?? queryParams.get("error_reason");
140
+ const errorDescription = responseParams.get("error_description") ?? queryParams.get("error_description");
141
+ if (errorReason || errorDescription) {
142
+ throw new AppError(`Meta login failed: ${errorDescription ?? errorReason ?? "unknown error"}.`, ExitCode.Auth);
143
+ }
144
+ const accessToken = responseParams.get("access_token") ?? queryParams.get("access_token");
145
+ if (accessToken) {
146
+ return {
147
+ accessToken,
148
+ expiresInSeconds: parsePositiveInteger(responseParams.get("expires_in") ?? queryParams.get("expires_in"))
149
+ };
150
+ }
151
+ const code = queryParams.get("code") ?? responseParams.get("code");
152
+ if (code) {
153
+ return {
154
+ code
155
+ };
156
+ }
157
+ throw new AppError("Meta login did not return an access token or authorization code. Paste the full final redirect URL from the browser.", ExitCode.Auth);
158
+ }
159
+ async function exchangeAuthorizationCode(config, options, code, fetchImpl) {
160
+ const appSecret = normalizeOptionalString(config.appSecret);
161
+ if (!appSecret) {
162
+ throw new AppError("This login flow returned an authorization code. Configure META_APP_SECRET to exchange it, or switch the Meta login configuration back to the default user-token response flow.", ExitCode.Auth);
163
+ }
164
+ const exchangeUrl = new URL(`https://graph.facebook.com/${config.apiVersion}/oauth/access_token`);
165
+ exchangeUrl.searchParams.set("client_id", options.appId);
166
+ exchangeUrl.searchParams.set("redirect_uri", options.authRedirectUri);
167
+ exchangeUrl.searchParams.set("client_secret", appSecret);
168
+ exchangeUrl.searchParams.set("code", code);
169
+ const response = await fetchImpl(exchangeUrl.toString(), {
170
+ method: "GET"
171
+ });
172
+ const body = await response.json().catch(() => null);
173
+ if (!response.ok) {
174
+ const providerMessage = body && "error" in body ? body.error?.message : undefined;
175
+ throw new AppError(providerMessage
176
+ ? `Meta auth code exchange failed: ${providerMessage}`
177
+ : `Meta auth code exchange failed with HTTP ${response.status}.`, ExitCode.Auth);
178
+ }
179
+ if (!body || !("access_token" in body) || !body.access_token) {
180
+ throw new AppError("Meta auth code exchange completed without returning an access token.", ExitCode.Auth);
181
+ }
182
+ return {
183
+ accessToken: body.access_token,
184
+ expiresInSeconds: typeof body.expires_in === "number" ? body.expires_in : undefined
185
+ };
186
+ }
187
+ function toExpiresAt(now, expiresInSeconds) {
188
+ if (!expiresInSeconds) {
189
+ return undefined;
190
+ }
191
+ return new Date(now.getTime() + (expiresInSeconds * 1000)).toISOString();
192
+ }
193
+ export async function runInteractiveAuthLogin(config, options = {}, deps = {}) {
194
+ const resolvedOptions = resolveLoginOptions(config, options);
195
+ const randomBytes = deps.randomBytes ?? defaultRandomBytes;
196
+ const state = randomBytes(16).toString("hex");
197
+ const loginUrl = buildLoginUrl(config, resolvedOptions, state);
198
+ const openBrowser = options.openBrowser ?? true;
199
+ const browserOpened = openBrowser
200
+ ? await (deps.openUrl ?? openExternalBrowser)(loginUrl)
201
+ : false;
202
+ const callbackValue = await (deps.promptForCallback ?? defaultPromptForCallback)(loginUrl);
203
+ let authCallback = parseAuthCallback(callbackValue, state, resolvedOptions.authRedirectUri);
204
+ const usedAuthorizationCode = Boolean(authCallback.code);
205
+ if (authCallback.code) {
206
+ authCallback = await exchangeAuthorizationCode(config, resolvedOptions, authCallback.code, deps.fetchImpl ?? fetch);
207
+ }
208
+ if (!authCallback.accessToken) {
209
+ throw new AppError("Meta login did not return an access token.", ExitCode.Auth);
210
+ }
211
+ const now = deps.now ? deps.now() : new Date();
212
+ return {
213
+ accessToken: authCallback.accessToken,
214
+ accessTokenExpiresAt: toExpiresAt(now, authCallback.expiresInSeconds),
215
+ appId: resolvedOptions.appId,
216
+ authConfigId: resolvedOptions.authConfigId,
217
+ authRedirectUri: resolvedOptions.authRedirectUri,
218
+ browserOpened,
219
+ codeExchangeUsed: usedAuthorizationCode,
220
+ loginUrl
221
+ };
222
+ }
@@ -0,0 +1,11 @@
1
+ import type { Command } from "commander";
2
+ import { createCommandContext, type CliDeps } from "./context.js";
3
+ import type { CommandResult } from "../types.js";
4
+ export interface CliRuntimeState {
5
+ argv: string[];
6
+ exitCode: number;
7
+ preferJson: boolean;
8
+ }
9
+ type ActionHandler<TArgs extends unknown[]> = (context: Awaited<ReturnType<typeof createCommandContext>>, ...args: TArgs) => Promise<CommandResult<unknown>>;
10
+ export declare function createAction<TArgs extends unknown[]>(commandName: string, deps: CliDeps, state: CliRuntimeState, handler: ActionHandler<TArgs>): (...actionArgs: [...TArgs, Command]) => Promise<void>;
11
+ export {};
@@ -0,0 +1,77 @@
1
+ import { createCommandContext } from "./context.js";
2
+ import { renderFailure, renderSuccess } from "../output/render.js";
3
+ import { ExitCode, isAppError, toFailureShape } from "../utils/errors.js";
4
+ import { sanitizeArgvForLogs } from "../utils/security.js";
5
+ function buildDebugPayload(commandName, state, context) {
6
+ return {
7
+ apply: context.apply,
8
+ argv: sanitizeArgvForLogs(state.argv),
9
+ command: commandName,
10
+ config: {
11
+ accessTokenPresent: Boolean(context.config.accessToken),
12
+ apiVersion: context.config.apiVersion,
13
+ approvalWebhookPresent: Boolean(context.config.approvalWebhook),
14
+ configPath: context.config.configPath,
15
+ defaultAccountId: context.config.defaultAccountId,
16
+ outputFormat: context.config.outputFormat,
17
+ permissionMode: context.config.permissionMode
18
+ },
19
+ json: context.json
20
+ };
21
+ }
22
+ function renderDebugMessage(payload) {
23
+ return [
24
+ "debug:",
25
+ ` command: ${payload.command}`,
26
+ ` argv: ${payload.argv.join(" ")}`,
27
+ ` apply: ${payload.apply}`,
28
+ ` json: ${payload.json}`,
29
+ ` apiVersion: ${payload.config.apiVersion}`,
30
+ ` permissionMode: ${payload.config.permissionMode}`,
31
+ ` outputFormat: ${payload.config.outputFormat}`,
32
+ ` defaultAccountId: ${payload.config.defaultAccountId ?? "-"}`,
33
+ ` configPath: ${payload.config.configPath}`,
34
+ ` accessTokenPresent: ${payload.config.accessTokenPresent}`,
35
+ ` approvalWebhookPresent: ${payload.config.approvalWebhookPresent}`
36
+ ].join("\n") + "\n";
37
+ }
38
+ export function createAction(commandName, deps, state, handler) {
39
+ return async (...actionArgs) => {
40
+ const command = actionArgs[actionArgs.length - 1];
41
+ const args = actionArgs.slice(0, -1);
42
+ const options = command.optsWithGlobals();
43
+ let asJson = Boolean(options.json);
44
+ let debugPayload;
45
+ try {
46
+ const context = await createCommandContext(options, deps);
47
+ asJson = context.json;
48
+ if (context.config.debug) {
49
+ debugPayload = buildDebugPayload(commandName, state, context);
50
+ if (!context.json) {
51
+ deps.io.stderr.write(renderDebugMessage(debugPayload));
52
+ }
53
+ }
54
+ const result = await handler(context, ...args);
55
+ const success = context.json && debugPayload
56
+ ? {
57
+ ...result,
58
+ meta: {
59
+ ...(result.meta ?? {}),
60
+ debug: debugPayload
61
+ }
62
+ }
63
+ : result;
64
+ deps.io.stdout.write(renderSuccess(success, context.json));
65
+ const requestedExitCode = typeof result.meta?.exitCode === "number"
66
+ ? Number(result.meta.exitCode)
67
+ : undefined;
68
+ state.exitCode = requestedExitCode
69
+ ?? (result.partialFailures?.length ? ExitCode.PartialFailure : ExitCode.Ok);
70
+ }
71
+ catch (error) {
72
+ const failure = toFailureShape(error, commandName, asJson && debugPayload ? { debug: debugPayload } : undefined);
73
+ deps.io.stderr.write(renderFailure(failure, asJson));
74
+ state.exitCode = isAppError(error) ? error.exitCode : 1;
75
+ }
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ import type { CliDeps } from "./context.js";
2
+ export declare function runCli(argv: string[], deps: CliDeps): Promise<number>;
@@ -0,0 +1,110 @@
1
+ import { Command, CommanderError } from "commander";
2
+ import { detectConfiguredOutputFormat } from "../config/file-config.js";
3
+ import { registerAccountsCommands } from "../commands/accounts.js";
4
+ import { registerAdSetCommands } from "../commands/adsets.js";
5
+ import { registerAdsCommands } from "../commands/ads.js";
6
+ import { registerAnomaliesCommand } from "../commands/anomalies.js";
7
+ import { registerAuthCommands } from "../commands/auth.js";
8
+ import { registerAssetCommands } from "../commands/assets.js";
9
+ import { registerAudienceCommands } from "../commands/audiences.js";
10
+ import { registerCampaignCommands } from "../commands/campaigns.js";
11
+ import { registerCapiCommands } from "../commands/capi.js";
12
+ import { registerCreativeCommands } from "../commands/creatives.js";
13
+ import { registerDiagnosticCommands } from "../commands/diagnostics.js";
14
+ import { registerLaunchCommands } from "../commands/launch.js";
15
+ import { registerPerformanceCommand } from "../commands/performance.js";
16
+ import { registerPixelCommands } from "../commands/pixel.js";
17
+ import { registerReportCommands } from "../commands/report.js";
18
+ import { renderFailure } from "../output/render.js";
19
+ import { AppError, ExitCode } from "../utils/errors.js";
20
+ import { toFailureShape } from "../utils/errors.js";
21
+ function readOptionValue(argv, optionName) {
22
+ for (let index = 0; index < argv.length; index += 1) {
23
+ const entry = argv[index];
24
+ if (!entry) {
25
+ continue;
26
+ }
27
+ if (entry === optionName) {
28
+ return argv[index + 1];
29
+ }
30
+ if (entry.startsWith(`${optionName}=`)) {
31
+ return entry.slice(optionName.length + 1);
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ async function resolveJsonPreference(argv) {
37
+ if (argv.includes("--json")) {
38
+ return true;
39
+ }
40
+ if (process.env.META_OUTPUT_FORMAT) {
41
+ return process.env.META_OUTPUT_FORMAT === "json";
42
+ }
43
+ const configuredOutputFormat = await detectConfiguredOutputFormat(readOptionValue(argv, "--config"));
44
+ return configuredOutputFormat === "json";
45
+ }
46
+ function buildProgram(deps, state) {
47
+ const program = new Command();
48
+ program
49
+ .name("meta")
50
+ .description("Agent-ready CLI for Meta Ads reads, drafts and approval-gated writes.")
51
+ .showHelpAfterError()
52
+ .exitOverride()
53
+ .configureOutput({
54
+ writeErr: (message) => {
55
+ if (!state.preferJson) {
56
+ deps.io.stderr.write(message);
57
+ }
58
+ },
59
+ writeOut: (message) => {
60
+ deps.io.stdout.write(message);
61
+ }
62
+ })
63
+ .allowExcessArguments(false)
64
+ .option("--json", "Render machine-readable JSON output.")
65
+ .option("--config <path>", "Override the local CLI config path.")
66
+ .option("--api-version <version>", "Override the Graph/Marketing API version, e.g. v25.0.")
67
+ .option("--mode <mode>", "Override permission mode: read, write or admin.")
68
+ .option("--apply", "Apply a write. Without this flag, mutation commands stay in draft mode.")
69
+ .option("--debug", "Enable debug-oriented config resolution.");
70
+ registerAccountsCommands(program, deps, state);
71
+ registerAuthCommands(program, deps, state);
72
+ registerAssetCommands(program, deps, state);
73
+ registerPerformanceCommand(program, deps, state);
74
+ registerCampaignCommands(program, deps, state);
75
+ registerAdSetCommands(program, deps, state);
76
+ registerCreativeCommands(program, deps, state);
77
+ registerAdsCommands(program, deps, state);
78
+ registerAnomaliesCommand(program, deps, state);
79
+ registerPixelCommands(program, deps, state);
80
+ registerCapiCommands(program, deps, state);
81
+ registerAudienceCommands(program, deps, state);
82
+ registerReportCommands(program, deps, state);
83
+ registerLaunchCommands(program, deps, state);
84
+ registerDiagnosticCommands(program, deps, state);
85
+ return program;
86
+ }
87
+ export async function runCli(argv, deps) {
88
+ const state = {
89
+ argv: [...argv],
90
+ exitCode: 0,
91
+ preferJson: await resolveJsonPreference(argv)
92
+ };
93
+ const program = buildProgram(deps, state);
94
+ try {
95
+ await program.parseAsync(argv, { from: "user" });
96
+ }
97
+ catch (error) {
98
+ if (error instanceof CommanderError) {
99
+ state.exitCode = error.code === "commander.helpDisplayed" ? ExitCode.Ok : ExitCode.Usage;
100
+ if (state.preferJson && state.exitCode !== ExitCode.Ok) {
101
+ deps.io.stderr.write(renderFailure(toFailureShape(new AppError(error.message, state.exitCode), "meta"), true));
102
+ }
103
+ }
104
+ else {
105
+ deps.io.stderr.write(renderFailure(toFailureShape(error, "meta"), state.preferJson));
106
+ state.exitCode = 1;
107
+ }
108
+ }
109
+ return state.exitCode;
110
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResolvedConfig } from "../config/types.js";
2
+ import { MetaApiClient, type FetchLike } from "../client/meta-api-client.js";
3
+ import type { GlobalOptions } from "../types.js";
4
+ export interface IoLike {
5
+ stderr: {
6
+ write(message: string): void;
7
+ };
8
+ stdout: {
9
+ write(message: string): void;
10
+ };
11
+ }
12
+ export interface CliDeps {
13
+ fetchImpl?: FetchLike | undefined;
14
+ io: IoLike;
15
+ }
16
+ export interface CommandContext {
17
+ apply: boolean;
18
+ client: MetaApiClient;
19
+ config: ResolvedConfig;
20
+ fetchImpl?: FetchLike | undefined;
21
+ io: IoLike;
22
+ json: boolean;
23
+ }
24
+ export declare function createCommandContext(options: GlobalOptions, deps: CliDeps): Promise<CommandContext>;
@@ -0,0 +1,19 @@
1
+ import { loadResolvedConfig } from "../config/file-config.js";
2
+ import { MetaApiClient } from "../client/meta-api-client.js";
3
+ export async function createCommandContext(options, deps) {
4
+ const config = await loadResolvedConfig({
5
+ apiVersion: options.apiVersion,
6
+ configPath: options.config,
7
+ debug: options.debug,
8
+ outputFormat: options.json ? "json" : undefined,
9
+ permissionMode: options.mode
10
+ });
11
+ return {
12
+ apply: Boolean(options.apply),
13
+ client: new MetaApiClient(config, deps.fetchImpl),
14
+ config,
15
+ fetchImpl: deps.fetchImpl,
16
+ io: deps.io,
17
+ json: options.json ?? config.outputFormat === "json"
18
+ };
19
+ }