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.
- package/AGENTS.md +188 -0
- package/AI_CONTEXT.md +144 -0
- package/CLAUDE.md +183 -0
- package/README.md +590 -0
- package/REQUIREMENTS.md +148 -0
- package/dist/auth/constants.d.ts +1 -0
- package/dist/auth/constants.js +1 -0
- package/dist/auth/guards.d.ts +5 -0
- package/dist/auth/guards.js +16 -0
- package/dist/auth/login.d.ts +28 -0
- package/dist/auth/login.js +222 -0
- package/dist/cli/action.d.ts +11 -0
- package/dist/cli/action.js +77 -0
- package/dist/cli/build-cli.d.ts +2 -0
- package/dist/cli/build-cli.js +110 -0
- package/dist/cli/context.d.ts +24 -0
- package/dist/cli/context.js +19 -0
- package/dist/client/meta-api-client.d.ts +50 -0
- package/dist/client/meta-api-client.js +258 -0
- package/dist/client/meta-discovery.d.ts +13 -0
- package/dist/client/meta-discovery.js +88 -0
- package/dist/commands/accounts.d.ts +4 -0
- package/dist/commands/accounts.js +42 -0
- package/dist/commands/ads.d.ts +4 -0
- package/dist/commands/ads.js +148 -0
- package/dist/commands/adsets.d.ts +4 -0
- package/dist/commands/adsets.js +49 -0
- package/dist/commands/anomalies.d.ts +4 -0
- package/dist/commands/anomalies.js +44 -0
- package/dist/commands/assets.d.ts +4 -0
- package/dist/commands/assets.js +116 -0
- package/dist/commands/audiences.d.ts +4 -0
- package/dist/commands/audiences.js +40 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +139 -0
- package/dist/commands/campaigns.d.ts +4 -0
- package/dist/commands/campaigns.js +273 -0
- package/dist/commands/capi.d.ts +4 -0
- package/dist/commands/capi.js +64 -0
- package/dist/commands/creatives.d.ts +4 -0
- package/dist/commands/creatives.js +49 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.js +88 -0
- package/dist/commands/helpers.d.ts +13 -0
- package/dist/commands/helpers.js +50 -0
- package/dist/commands/launch.d.ts +4 -0
- package/dist/commands/launch.js +109 -0
- package/dist/commands/performance.d.ts +4 -0
- package/dist/commands/performance.js +55 -0
- package/dist/commands/pixel.d.ts +4 -0
- package/dist/commands/pixel.js +68 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +30 -0
- package/dist/config/file-config.d.ts +6 -0
- package/dist/config/file-config.js +174 -0
- package/dist/config/types.d.ts +32 -0
- package/dist/config/types.js +1 -0
- package/dist/domain/account-scope.d.ts +7 -0
- package/dist/domain/account-scope.js +28 -0
- package/dist/domain/analytics.d.ts +52 -0
- package/dist/domain/analytics.js +125 -0
- package/dist/domain/approval-service.d.ts +10 -0
- package/dist/domain/approval-service.js +48 -0
- package/dist/domain/asset-feed-compiler.d.ts +43 -0
- package/dist/domain/asset-feed-compiler.js +104 -0
- package/dist/domain/launch-service.d.ts +200 -0
- package/dist/domain/launch-service.js +558 -0
- package/dist/domain/meta-ads-service.d.ts +620 -0
- package/dist/domain/meta-ads-service.js +841 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/output/render.d.ts +3 -0
- package/dist/output/render.js +103 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/dist/utils/currency.d.ts +4 -0
- package/dist/utils/currency.js +40 -0
- package/dist/utils/date-range.d.ts +20 -0
- package/dist/utils/date-range.js +115 -0
- package/dist/utils/errors.d.ts +35 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +23 -0
- package/dist/utils/meta-placement-assets.d.ts +44 -0
- package/dist/utils/meta-placement-assets.js +315 -0
- package/dist/utils/security.d.ts +5 -0
- package/dist/utils/security.js +104 -0
- package/dist/validators/common.d.ts +10 -0
- package/dist/validators/common.js +56 -0
- package/dist/validators/create-spec.d.ts +373 -0
- package/dist/validators/create-spec.js +394 -0
- package/dist/validators/launch-spec.d.ts +229 -0
- package/dist/validators/launch-spec.js +371 -0
- package/docs/TECHNICAL.md +480 -0
- package/examples/README.md +29 -0
- package/examples/launch/assets/feed4x5.png +0 -0
- package/examples/launch/assets/story9x16.png +0 -0
- package/examples/launch/multi-format-launch.json +90 -0
- package/examples/single-object/ad.json +6 -0
- package/examples/single-object/adset.json +30 -0
- package/examples/single-object/campaign.json +6 -0
- package/examples/single-object/creative.json +19 -0
- package/package.json +62 -0
- package/skills/meta-cli-operator/SKILL.md +105 -0
- package/skills/meta-cli-operator/agents/openai.yaml +4 -0
- package/skills/meta-cli-operator/references/update-matrix.md +117 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createAction } from "../cli/action.js";
|
|
2
|
+
import { resolveSingleAccount } from "./helpers.js";
|
|
3
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
4
|
+
import { parseTimeWindow } from "../utils/date-range.js";
|
|
5
|
+
export function registerCapiCommands(program, deps, state) {
|
|
6
|
+
const capi = program.command("capi").description("Best-effort Conversions API visibility.");
|
|
7
|
+
capi
|
|
8
|
+
.command("status")
|
|
9
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
10
|
+
.description("Infer CAPI connectivity from pixel/server event fields.")
|
|
11
|
+
.action(createAction("capi status", deps, state, async (context, options) => {
|
|
12
|
+
const account = await resolveSingleAccount(context, options.account, "capi status");
|
|
13
|
+
const service = new MetaAdsService(context.client);
|
|
14
|
+
const pixels = await service.listPixels(account.id);
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
command: "capi status",
|
|
18
|
+
data: {
|
|
19
|
+
accountId: account.id,
|
|
20
|
+
bestEffort: true,
|
|
21
|
+
pixels: pixels.map((entry) => ({
|
|
22
|
+
capiConnected: entry.serverEventsBusinessIds.length > 0,
|
|
23
|
+
id: entry.id,
|
|
24
|
+
lastFiredTime: entry.lastFiredTime,
|
|
25
|
+
name: entry.name,
|
|
26
|
+
serverEventsBusinessIds: entry.serverEventsBusinessIds
|
|
27
|
+
}))
|
|
28
|
+
},
|
|
29
|
+
warnings: [
|
|
30
|
+
"Meta does not expose a single dedicated CAPI health endpoint here; this command infers status from officially documented Ads Pixel fields."
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
}));
|
|
34
|
+
capi
|
|
35
|
+
.command("events")
|
|
36
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
37
|
+
.option("--last <window>", "Relative time window like 24h.", "24h")
|
|
38
|
+
.description("Show recent server-side event hints from pixel fields.")
|
|
39
|
+
.action(createAction("capi events", deps, state, async (context, options) => {
|
|
40
|
+
const account = await resolveSingleAccount(context, options.account, "capi events");
|
|
41
|
+
const window = parseTimeWindow(options.last ?? "24h");
|
|
42
|
+
const service = new MetaAdsService(context.client);
|
|
43
|
+
const pixels = await service.listPixels(account.id);
|
|
44
|
+
const rows = pixels
|
|
45
|
+
.filter((entry) => entry.serverEventsBusinessIds.length > 0)
|
|
46
|
+
.filter((entry) => {
|
|
47
|
+
const marker = entry.lastFiredTime ?? entry.eventTimeMax;
|
|
48
|
+
return marker ? new Date(marker) >= window.since : false;
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
command: "capi events",
|
|
53
|
+
data: {
|
|
54
|
+
accountId: account.id,
|
|
55
|
+
bestEffort: true,
|
|
56
|
+
pixels: rows,
|
|
57
|
+
window
|
|
58
|
+
},
|
|
59
|
+
warnings: [
|
|
60
|
+
"This view is based on Ads Pixel/server-event fields and is not a dedicated Conversions API event log."
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -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 { creativeCreateSpecSchema, readSpecFile } from "../validators/create-spec.js";
|
|
5
|
+
import { resolveSingleAccount } from "./helpers.js";
|
|
6
|
+
export function registerCreativeCommands(program, deps, state) {
|
|
7
|
+
const creatives = program.command("creatives").description("Creative creation workflows.");
|
|
8
|
+
creatives
|
|
9
|
+
.command("create")
|
|
10
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
11
|
+
.requiredOption("--spec <path>", "Path to a JSON creative create spec.")
|
|
12
|
+
.description("Draft or create a supported creative from a JSON spec.")
|
|
13
|
+
.action(createAction("creatives create", deps, state, async (context, options) => {
|
|
14
|
+
const account = await resolveSingleAccount(context, options.account, "creatives create");
|
|
15
|
+
const { spec, specPath } = await readSpecFile(options.spec, creativeCreateSpecSchema, "Creative create");
|
|
16
|
+
const service = new MetaAdsService(context.client);
|
|
17
|
+
const plan = {
|
|
18
|
+
accountId: account.id,
|
|
19
|
+
action: "create-creative",
|
|
20
|
+
spec,
|
|
21
|
+
specPath
|
|
22
|
+
};
|
|
23
|
+
if (!context.apply) {
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
command: "creatives create",
|
|
27
|
+
data: plan,
|
|
28
|
+
meta: {
|
|
29
|
+
mode: "draft"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
requirePermission(context.config, "write", "Creative create");
|
|
34
|
+
const creative = await service.createCreative(account.id, spec);
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
command: "creatives create",
|
|
38
|
+
data: {
|
|
39
|
+
accountId: account.id,
|
|
40
|
+
applied: true,
|
|
41
|
+
creative,
|
|
42
|
+
specPath
|
|
43
|
+
},
|
|
44
|
+
meta: {
|
|
45
|
+
mode: "write"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createAction } from "../cli/action.js";
|
|
2
|
+
import { requireAccessToken } from "../auth/guards.js";
|
|
3
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
4
|
+
import { ExitCode } from "../utils/errors.js";
|
|
5
|
+
import { resolveSingleAccount } from "./helpers.js";
|
|
6
|
+
export function registerDiagnosticCommands(program, deps, state) {
|
|
7
|
+
program
|
|
8
|
+
.command("whoami")
|
|
9
|
+
.description("Resolve /me against the current token.")
|
|
10
|
+
.action(createAction("whoami", deps, state, async (context) => {
|
|
11
|
+
requireAccessToken(context.config);
|
|
12
|
+
const service = new MetaAdsService(context.client);
|
|
13
|
+
const identity = await service.whoAmI();
|
|
14
|
+
return {
|
|
15
|
+
ok: true,
|
|
16
|
+
command: "whoami",
|
|
17
|
+
data: identity
|
|
18
|
+
};
|
|
19
|
+
}));
|
|
20
|
+
program
|
|
21
|
+
.command("doctor")
|
|
22
|
+
.option("--account <id>", "Optional account id to verify account-level reachability.")
|
|
23
|
+
.description("Run local and remote health checks for config, auth and API reachability.")
|
|
24
|
+
.action(createAction("doctor", deps, state, async (context, options) => {
|
|
25
|
+
const checks = [];
|
|
26
|
+
checks.push({
|
|
27
|
+
check: "config",
|
|
28
|
+
detail: context.config.configPath,
|
|
29
|
+
status: "ok"
|
|
30
|
+
});
|
|
31
|
+
checks.push({
|
|
32
|
+
check: "meta_access_token",
|
|
33
|
+
status: context.config.accessToken ? "ok" : "fail"
|
|
34
|
+
});
|
|
35
|
+
checks.push({
|
|
36
|
+
check: "approval_webhook",
|
|
37
|
+
status: context.config.approvalWebhook ? "ok" : "warn"
|
|
38
|
+
});
|
|
39
|
+
if (context.config.accessToken) {
|
|
40
|
+
const service = new MetaAdsService(context.client);
|
|
41
|
+
const whoAmI = await service.whoAmI();
|
|
42
|
+
checks.push({
|
|
43
|
+
check: "whoami",
|
|
44
|
+
detail: whoAmI.name ?? whoAmI.id,
|
|
45
|
+
status: "ok"
|
|
46
|
+
});
|
|
47
|
+
const selectedAccount = options.account ?? context.config.defaultAccountId;
|
|
48
|
+
if (selectedAccount) {
|
|
49
|
+
const accountScope = await resolveSingleAccount(context, selectedAccount, "doctor");
|
|
50
|
+
const account = await service.getAccount(accountScope.id);
|
|
51
|
+
checks.push({
|
|
52
|
+
check: "account",
|
|
53
|
+
detail: account.name ?? accountScope.id,
|
|
54
|
+
status: "ok"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const hasFailure = checks.some((entry) => entry.status === "fail");
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
command: "doctor",
|
|
62
|
+
data: {
|
|
63
|
+
checks
|
|
64
|
+
},
|
|
65
|
+
meta: {
|
|
66
|
+
exitCode: hasFailure ? ExitCode.VerificationFailed : ExitCode.Ok
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}));
|
|
70
|
+
program
|
|
71
|
+
.command("verify-api")
|
|
72
|
+
.option("--account <id>", "Optional account id to verify campaigns and insights.")
|
|
73
|
+
.description("Verify auth, account reachability, campaign reads and insights compatibility.")
|
|
74
|
+
.action(createAction("verify-api", deps, state, async (context, options) => {
|
|
75
|
+
requireAccessToken(context.config);
|
|
76
|
+
const service = new MetaAdsService(context.client);
|
|
77
|
+
const selectedAccount = options.account ?? context.config.defaultAccountId;
|
|
78
|
+
const account = selectedAccount
|
|
79
|
+
? (await resolveSingleAccount(context, selectedAccount, "verify-api")).id
|
|
80
|
+
: undefined;
|
|
81
|
+
const verification = await service.verifyApi(account);
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
command: "verify-api",
|
|
85
|
+
data: verification
|
|
86
|
+
};
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CommandContext } from "../cli/context.js";
|
|
2
|
+
import type { ManagedAccount } from "../client/meta-discovery.js";
|
|
3
|
+
import type { DateWindow } from "../utils/date-range.js";
|
|
4
|
+
export declare function toMetaSort(metric?: string | undefined): string | undefined;
|
|
5
|
+
export declare function collectAcrossAccounts<T>(accounts: ManagedAccount[], run: (account: ManagedAccount) => Promise<T>): Promise<{
|
|
6
|
+
failures: Array<{
|
|
7
|
+
accountId: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}>;
|
|
10
|
+
results: T[];
|
|
11
|
+
}>;
|
|
12
|
+
export declare function buildFixedWindow(period: "daily" | "weekly" | "monthly"): DateWindow;
|
|
13
|
+
export declare function resolveSingleAccount(context: CommandContext, selection: string | undefined, operation: string): Promise<ManagedAccount>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolveAccountScope } from "../domain/account-scope.js";
|
|
2
|
+
import { buildRecentDateWindow } from "../utils/date-range.js";
|
|
3
|
+
import { AppError, ExitCode } from "../utils/errors.js";
|
|
4
|
+
export function toMetaSort(metric) {
|
|
5
|
+
if (!metric) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (metric.endsWith("_ascending") || metric.endsWith("_descending")) {
|
|
9
|
+
return metric;
|
|
10
|
+
}
|
|
11
|
+
return `${metric}_descending`;
|
|
12
|
+
}
|
|
13
|
+
export async function collectAcrossAccounts(accounts, run) {
|
|
14
|
+
const settled = await Promise.allSettled(accounts.map((account) => run(account)));
|
|
15
|
+
const results = [];
|
|
16
|
+
const failures = [];
|
|
17
|
+
settled.forEach((entry, index) => {
|
|
18
|
+
const account = accounts[index];
|
|
19
|
+
if (!account) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (entry.status === "fulfilled") {
|
|
23
|
+
results.push(entry.value);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
failures.push({
|
|
27
|
+
accountId: account.id,
|
|
28
|
+
message: entry.reason instanceof Error ? entry.reason.message : "Unknown error"
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return { failures, results };
|
|
32
|
+
}
|
|
33
|
+
export function buildFixedWindow(period) {
|
|
34
|
+
const days = period === "daily" ? 1 : period === "weekly" ? 7 : 30;
|
|
35
|
+
return buildRecentDateWindow(days);
|
|
36
|
+
}
|
|
37
|
+
export async function resolveSingleAccount(context, selection, operation) {
|
|
38
|
+
if (selection?.trim().toLowerCase() === "all") {
|
|
39
|
+
throw new AppError(`${operation} requires a single ad account. --account all is not supported for this command.`, ExitCode.Usage);
|
|
40
|
+
}
|
|
41
|
+
const scope = await resolveAccountScope(context, selection);
|
|
42
|
+
const account = scope.accounts[0];
|
|
43
|
+
if (!account) {
|
|
44
|
+
throw new AppError(`${operation} could not resolve a single ad account.`, ExitCode.VerificationFailed);
|
|
45
|
+
}
|
|
46
|
+
if (scope.accounts.length > 1) {
|
|
47
|
+
throw new AppError(`${operation} resolved multiple accounts unexpectedly.`, ExitCode.VerificationFailed);
|
|
48
|
+
}
|
|
49
|
+
return account;
|
|
50
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { requirePermission } from "../auth/guards.js";
|
|
2
|
+
import { createAction } from "../cli/action.js";
|
|
3
|
+
import { LaunchService } from "../domain/launch-service.js";
|
|
4
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
5
|
+
import { readLaunchSpecFile, requireReceiptPath } from "../validators/launch-spec.js";
|
|
6
|
+
import { resolveSingleAccount } from "./helpers.js";
|
|
7
|
+
export function registerLaunchCommands(program, deps, state) {
|
|
8
|
+
const launch = program.command("launch").description("Validated, resumable launch execution from a single spec.");
|
|
9
|
+
launch
|
|
10
|
+
.command("validate")
|
|
11
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
12
|
+
.requiredOption("--spec <path>", "Path to a JSON launch spec.")
|
|
13
|
+
.description("Validate a launch spec, refs, and referenced asset files.")
|
|
14
|
+
.action(createAction("launch validate", deps, state, async (context, options) => {
|
|
15
|
+
const account = await resolveSingleAccount(context, options.account, "launch validate");
|
|
16
|
+
const { spec, specPath } = await readLaunchSpecFile(options.spec);
|
|
17
|
+
const service = new LaunchService(new MetaAdsService(context.client));
|
|
18
|
+
const result = service.validate(account.id, specPath, spec);
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
command: "launch validate",
|
|
22
|
+
data: result.data,
|
|
23
|
+
warnings: result.warnings
|
|
24
|
+
};
|
|
25
|
+
}));
|
|
26
|
+
launch
|
|
27
|
+
.command("plan")
|
|
28
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
29
|
+
.requiredOption("--spec <path>", "Path to a JSON launch spec.")
|
|
30
|
+
.description("Render the ordered launch plan without creating anything.")
|
|
31
|
+
.action(createAction("launch plan", deps, state, async (context, options) => {
|
|
32
|
+
const account = await resolveSingleAccount(context, options.account, "launch plan");
|
|
33
|
+
const { spec, specPath } = await readLaunchSpecFile(options.spec);
|
|
34
|
+
const service = new LaunchService(new MetaAdsService(context.client));
|
|
35
|
+
const result = service.buildPlan(account.id, specPath, spec);
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
command: "launch plan",
|
|
39
|
+
data: result.data,
|
|
40
|
+
warnings: result.warnings
|
|
41
|
+
};
|
|
42
|
+
}));
|
|
43
|
+
launch
|
|
44
|
+
.command("apply")
|
|
45
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
46
|
+
.requiredOption("--spec <path>", "Path to a JSON launch spec.")
|
|
47
|
+
.option("--receipt <path>", "Optional receipt output path.")
|
|
48
|
+
.description("Draft or execute the full launch spec and persist a resumable receipt.")
|
|
49
|
+
.action(createAction("launch apply", deps, state, async (context, options) => {
|
|
50
|
+
const account = await resolveSingleAccount(context, options.account, "launch apply");
|
|
51
|
+
const { spec, specPath } = await readLaunchSpecFile(options.spec);
|
|
52
|
+
const service = new LaunchService(new MetaAdsService(context.client));
|
|
53
|
+
if (!context.apply) {
|
|
54
|
+
const result = service.buildPlan(account.id, specPath, spec);
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
command: "launch apply",
|
|
58
|
+
data: result.data,
|
|
59
|
+
warnings: result.warnings,
|
|
60
|
+
meta: {
|
|
61
|
+
mode: "draft"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
requirePermission(context.config, "write", "Launch apply");
|
|
66
|
+
const result = await service.apply({
|
|
67
|
+
accountId: account.id,
|
|
68
|
+
receiptPath: options.receipt,
|
|
69
|
+
spec,
|
|
70
|
+
specPath
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
command: "launch apply",
|
|
75
|
+
data: result.data,
|
|
76
|
+
warnings: result.warnings,
|
|
77
|
+
meta: {
|
|
78
|
+
mode: "write"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}));
|
|
82
|
+
launch
|
|
83
|
+
.command("resume")
|
|
84
|
+
.requiredOption("--receipt <path>", "Path to an existing launch receipt.")
|
|
85
|
+
.description("Resume a previously interrupted launch receipt.")
|
|
86
|
+
.action(createAction("launch resume", deps, state, async (context, options) => {
|
|
87
|
+
const service = new LaunchService(new MetaAdsService(context.client));
|
|
88
|
+
if (!context.apply) {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
command: "launch resume",
|
|
92
|
+
data: await service.previewResume(requireReceiptPath(options.receipt)),
|
|
93
|
+
meta: {
|
|
94
|
+
mode: "draft"
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
requirePermission(context.config, "write", "Launch resume");
|
|
99
|
+
const result = await service.resume(requireReceiptPath(options.receipt));
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
command: "launch resume",
|
|
103
|
+
data: result,
|
|
104
|
+
meta: {
|
|
105
|
+
mode: "write"
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createAction } from "../cli/action.js";
|
|
2
|
+
import { collectAcrossAccounts, toMetaSort } from "./helpers.js";
|
|
3
|
+
import { summarizePerformance } from "../domain/analytics.js";
|
|
4
|
+
import { resolveAccountScope } from "../domain/account-scope.js";
|
|
5
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
6
|
+
import { parseDateWindow } from "../utils/date-range.js";
|
|
7
|
+
import { parseBreakdowns } from "../validators/common.js";
|
|
8
|
+
export function registerPerformanceCommand(program, deps, state) {
|
|
9
|
+
program
|
|
10
|
+
.command("performance")
|
|
11
|
+
.description("Campaign performance from the Meta Insights API.")
|
|
12
|
+
.option("--last <window>", "Relative time window like 7d or 30d, unless --from/--to is used.")
|
|
13
|
+
.option("--account <id|all>", "Ad account id or all.")
|
|
14
|
+
.option("--campaign <name>", "Case-insensitive campaign name filter.")
|
|
15
|
+
.option("--from <date>", "Start date in YYYY-MM-DD.")
|
|
16
|
+
.option("--to <date>", "End date in YYYY-MM-DD.")
|
|
17
|
+
.option("--breakdown <values>", "Comma-separated breakdowns, e.g. age,gender.")
|
|
18
|
+
.option("--sort <metric>", "Sort metric, e.g. ctr or spend.")
|
|
19
|
+
.action(createAction("performance", deps, state, async (context, options) => {
|
|
20
|
+
const window = parseDateWindow({
|
|
21
|
+
from: options.from,
|
|
22
|
+
last: options.last,
|
|
23
|
+
to: options.to
|
|
24
|
+
});
|
|
25
|
+
const scope = await resolveAccountScope(context, options.account);
|
|
26
|
+
const breakdowns = parseBreakdowns(options.breakdown);
|
|
27
|
+
const sort = toMetaSort(options.sort);
|
|
28
|
+
const service = new MetaAdsService(context.client);
|
|
29
|
+
const collected = await collectAcrossAccounts(scope.accounts, async (account) => {
|
|
30
|
+
const rows = await service.getPerformanceByAccount(account.id, window, breakdowns, options.campaign, sort);
|
|
31
|
+
return {
|
|
32
|
+
accountId: account.id,
|
|
33
|
+
accountName: account.name,
|
|
34
|
+
rows,
|
|
35
|
+
summary: summarizePerformance(rows)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
command: "performance",
|
|
41
|
+
data: {
|
|
42
|
+
accounts: collected.results,
|
|
43
|
+
window
|
|
44
|
+
},
|
|
45
|
+
meta: {
|
|
46
|
+
accountSelection: scope.selection,
|
|
47
|
+
count: collected.results.length
|
|
48
|
+
},
|
|
49
|
+
partialFailures: collected.failures.map((failure) => ({
|
|
50
|
+
message: failure.message,
|
|
51
|
+
scope: failure.accountId
|
|
52
|
+
}))
|
|
53
|
+
};
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createAction } from "../cli/action.js";
|
|
2
|
+
import { resolveSingleAccount } from "./helpers.js";
|
|
3
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
4
|
+
import { parseTimeWindow } from "../utils/date-range.js";
|
|
5
|
+
function derivePixelHealth(pixel) {
|
|
6
|
+
if (pixel.isUnavailable) {
|
|
7
|
+
return "unavailable";
|
|
8
|
+
}
|
|
9
|
+
if (!pixel.lastFiredTime) {
|
|
10
|
+
return "unknown";
|
|
11
|
+
}
|
|
12
|
+
const ageMs = Date.now() - new Date(pixel.lastFiredTime).getTime();
|
|
13
|
+
return ageMs <= 48 * 60 * 60 * 1000 ? "healthy" : "stale";
|
|
14
|
+
}
|
|
15
|
+
export function registerPixelCommands(program, deps, state) {
|
|
16
|
+
const pixel = program.command("pixel").description("Pixel health and event visibility.");
|
|
17
|
+
pixel
|
|
18
|
+
.command("status")
|
|
19
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
20
|
+
.description("List pixel health for an account.")
|
|
21
|
+
.action(createAction("pixel status", deps, state, async (context, options) => {
|
|
22
|
+
const account = await resolveSingleAccount(context, options.account, "pixel status");
|
|
23
|
+
const service = new MetaAdsService(context.client);
|
|
24
|
+
const pixels = await service.listPixels(account.id);
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
command: "pixel status",
|
|
28
|
+
data: {
|
|
29
|
+
accountId: account.id,
|
|
30
|
+
pixels: pixels.map((entry) => ({
|
|
31
|
+
...entry,
|
|
32
|
+
health: derivePixelHealth(entry)
|
|
33
|
+
}))
|
|
34
|
+
},
|
|
35
|
+
meta: {
|
|
36
|
+
count: pixels.length
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}));
|
|
40
|
+
pixel
|
|
41
|
+
.command("events")
|
|
42
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
43
|
+
.option("--last <window>", "Relative time window like 24h.", "24h")
|
|
44
|
+
.description("Show recent pixel event activity using provider-exposed pixel stats.")
|
|
45
|
+
.action(createAction("pixel events", deps, state, async (context, options) => {
|
|
46
|
+
const account = await resolveSingleAccount(context, options.account, "pixel events");
|
|
47
|
+
const window = parseTimeWindow(options.last ?? "24h");
|
|
48
|
+
const service = new MetaAdsService(context.client);
|
|
49
|
+
const pixels = await service.listPixels(account.id);
|
|
50
|
+
const recent = pixels.filter((entry) => {
|
|
51
|
+
const marker = entry.lastFiredTime ?? entry.eventTimeMax;
|
|
52
|
+
return marker ? new Date(marker) >= window.since : false;
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
command: "pixel events",
|
|
57
|
+
data: {
|
|
58
|
+
accountId: account.id,
|
|
59
|
+
note: "Event visibility is derived from Ads Pixel reference fields such as event_stats and last_fired_time.",
|
|
60
|
+
pixels: recent,
|
|
61
|
+
window
|
|
62
|
+
},
|
|
63
|
+
meta: {
|
|
64
|
+
count: recent.length
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createAction } from "../cli/action.js";
|
|
2
|
+
import { buildFixedWindow, resolveSingleAccount } from "./helpers.js";
|
|
3
|
+
import { buildReport } from "../domain/analytics.js";
|
|
4
|
+
import { MetaAdsService } from "../domain/meta-ads-service.js";
|
|
5
|
+
function registerReportVariant(parent, name, deps, state) {
|
|
6
|
+
parent
|
|
7
|
+
.command(name)
|
|
8
|
+
.requiredOption("--account <id>", "Ad account id.")
|
|
9
|
+
.description(`Generate a ${name} performance report.`)
|
|
10
|
+
.action(createAction(`report ${name}`, deps, state, async (context, options) => {
|
|
11
|
+
const account = await resolveSingleAccount(context, options.account, `report ${name}`);
|
|
12
|
+
const service = new MetaAdsService(context.client);
|
|
13
|
+
const window = buildFixedWindow(name);
|
|
14
|
+
const rows = await service.getPerformanceByAccount(account.id, window, [], undefined, undefined);
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
command: `report ${name}`,
|
|
18
|
+
data: {
|
|
19
|
+
accountId: account.id,
|
|
20
|
+
report: buildReport(name, window, rows)
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function registerReportCommands(program, deps, state) {
|
|
26
|
+
const report = program.command("report").description("Predefined reporting views.");
|
|
27
|
+
registerReportVariant(report, "daily", deps, state);
|
|
28
|
+
registerReportVariant(report, "weekly", deps, state);
|
|
29
|
+
registerReportVariant(report, "monthly", deps, state);
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OutputFormat } from "../types.js";
|
|
2
|
+
import type { ConfigOverrides, FileConfig, ResolvedConfig } from "./types.js";
|
|
3
|
+
export declare function resolveDefaultConfigPath(): string;
|
|
4
|
+
export declare function detectConfiguredOutputFormat(configPath?: string): Promise<OutputFormat | undefined>;
|
|
5
|
+
export declare function loadResolvedConfig(overrides?: ConfigOverrides): Promise<ResolvedConfig>;
|
|
6
|
+
export declare function writeFileConfig(configPath: string, partial: Partial<FileConfig>): Promise<void>;
|