@a8techads/cli 0.4.2 → 0.4.4
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/a8techads.js +895 -26
- package/package.json +1 -1
package/dist/a8techads.js
CHANGED
|
@@ -4096,7 +4096,8 @@ function printSectionedCampaignDetail(campaign) {
|
|
|
4096
4096
|
supplyProductCount: campaign.supplyProductCount ?? (Array.isArray(campaign.supplyProductIds) ? campaign.supplyProductIds.length : 0),
|
|
4097
4097
|
supplyProducts: formatSupplyProducts(campaign.supplyProducts),
|
|
4098
4098
|
sspPartnerIds: formatScalar(campaign.sspPartnerIds),
|
|
4099
|
-
sourceMasking: formatScalar(campaign.sourceMasking)
|
|
4099
|
+
sourceMasking: formatScalar(campaign.sourceMasking),
|
|
4100
|
+
bidderAlgorithmId: campaign.bidderAlgorithmId
|
|
4100
4101
|
}
|
|
4101
4102
|
},
|
|
4102
4103
|
{
|
|
@@ -4217,7 +4218,7 @@ Examples:
|
|
|
4217
4218
|
}
|
|
4218
4219
|
console.log(`Campaign created: ${json.data?.id ?? json.id}`);
|
|
4219
4220
|
});
|
|
4220
|
-
cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4221
|
+
cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--bidder-algorithm <id>", "Bind a bidder algorithm").option("--bidder-algorithm-id <id>", "Bind a bidder algorithm").option("--clear-bidder-algorithm", "Remove bidder algorithm binding").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4221
4222
|
let body;
|
|
4222
4223
|
if (opts.fromJson) {
|
|
4223
4224
|
const { readFileSync: readFileSync4 } = await import("fs");
|
|
@@ -4238,14 +4239,22 @@ Examples:
|
|
|
4238
4239
|
console.error("Error: use either --supply-products or --clear-supply-products, not both.");
|
|
4239
4240
|
process.exit(1);
|
|
4240
4241
|
}
|
|
4242
|
+
const bidderAlgorithmId = opts.bidderAlgorithm ?? opts.bidderAlgorithmId;
|
|
4243
|
+
if (bidderAlgorithmId && opts.clearBidderAlgorithm) {
|
|
4244
|
+
console.error("Error: use either --bidder-algorithm or --clear-bidder-algorithm, not both.");
|
|
4245
|
+
process.exit(1);
|
|
4246
|
+
}
|
|
4241
4247
|
const updatesSupplyBindings = Boolean(supplyProducts || clearSupplyProducts);
|
|
4242
|
-
|
|
4248
|
+
const updatesBidderAlgorithm = Boolean(bidderAlgorithmId || opts.clearBidderAlgorithm);
|
|
4249
|
+
if (updatesSupplyBindings || updatesBidderAlgorithm) {
|
|
4243
4250
|
const campaign = await fetchCampaign(id);
|
|
4244
4251
|
if (String(campaign.status || "").toUpperCase() === "ACTIVE") {
|
|
4245
|
-
|
|
4252
|
+
const field = updatesSupplyBindings ? "supply product bindings" : "bidder algorithm binding";
|
|
4253
|
+
const updateArg = updatesSupplyBindings ? supplyProducts ? `--supply-products ${supplyProducts}` : "--clear-supply-products" : bidderAlgorithmId ? `--bidder-algorithm ${bidderAlgorithmId}` : "--clear-bidder-algorithm";
|
|
4254
|
+
console.error(`Error: active campaigns cannot change ${field} via update.`);
|
|
4246
4255
|
console.error(`Next steps:`);
|
|
4247
4256
|
console.error(` a8techads campaigns pause ${id}`);
|
|
4248
|
-
console.error(` a8techads campaigns update ${id} ${
|
|
4257
|
+
console.error(` a8techads campaigns update ${id} ${updateArg}`);
|
|
4249
4258
|
console.error(` a8techads campaigns resume ${id}`);
|
|
4250
4259
|
process.exit(1);
|
|
4251
4260
|
}
|
|
@@ -4263,6 +4272,10 @@ Examples:
|
|
|
4263
4272
|
body.supplyProductIds = parseCsvList(supplyProducts);
|
|
4264
4273
|
if (clearSupplyProducts)
|
|
4265
4274
|
body.supplyProductIds = [];
|
|
4275
|
+
if (bidderAlgorithmId)
|
|
4276
|
+
body.bidderAlgorithmId = bidderAlgorithmId;
|
|
4277
|
+
if (opts.clearBidderAlgorithm)
|
|
4278
|
+
body.bidderAlgorithmId = null;
|
|
4266
4279
|
}
|
|
4267
4280
|
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
|
|
4268
4281
|
if (!resp.ok) {
|
|
@@ -5129,7 +5142,16 @@ Examples:
|
|
|
5129
5142
|
}
|
|
5130
5143
|
console.log(`Zone ${id} deleted.`);
|
|
5131
5144
|
});
|
|
5132
|
-
cmd.command("tag").description("Get the ad tag code for a zone (
|
|
5145
|
+
cmd.command("tag").description("Get or generate the ad tag code for a zone.").argument("<id>", "Zone ID").option("--local", "Generate tag locally without calling the API").option("--variant <variant>", "Tag variant: js or iframe", "js").option("--format <format>", "Zone format: banner, popunder, offerwall, rewarded_video, native, video").option("--ad-server <url>", "Bidder/ad server URL", "https://bidder.a8.tech").option("--loader-url <url>", "Loader script URL", "https://assets.a8.tech/tags/v1/loader.js").option("--sizes <sizes>", "Comma-separated sizes for display tags, e.g. 728x90,300x250").option("--theme <theme>", "Theme for offerwall/rewarded tags, e.g. light or dark").option("--sub <sub>", "Publisher player/user identifier macro", "{YOUR_PLAYER_ID}").option("--keywords <keywords>", "Comma-separated targeting keywords").option("--limit <n>", "Offerwall max offers", "20").option("--button-text <text>", "Rewarded video button text", "Watch and earn reward").addHelpText("after", `
|
|
5146
|
+
Examples:
|
|
5147
|
+
$ a8techads zones tag <id>
|
|
5148
|
+
$ a8techads zones tag <id> --local --format offerwall --theme dark --sub player-123
|
|
5149
|
+
$ a8techads zones tag <id> --local --format rewarded_video --button-text "Watch ad"
|
|
5150
|
+
$ a8techads zones tag <id> --local --variant iframe --format banner --sizes 728x90`).action(async (id, opts) => {
|
|
5151
|
+
if (shouldGenerateLocalTag(opts)) {
|
|
5152
|
+
console.log(buildZoneTag(id, opts));
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5133
5155
|
const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}/tag` });
|
|
5134
5156
|
const json = await resp.json();
|
|
5135
5157
|
if (!resp.ok) {
|
|
@@ -5140,6 +5162,111 @@ Examples:
|
|
|
5140
5162
|
});
|
|
5141
5163
|
return cmd;
|
|
5142
5164
|
}
|
|
5165
|
+
function shouldGenerateLocalTag(opts) {
|
|
5166
|
+
return Boolean(opts.local || opts.variant !== "js" || opts.format || opts.theme || opts.sub !== "{YOUR_PLAYER_ID}" || opts.keywords || opts.sizes || opts.limit !== "20" || opts.buttonText !== "Watch and earn reward");
|
|
5167
|
+
}
|
|
5168
|
+
function buildZoneTag(zoneId, opts) {
|
|
5169
|
+
const format = normalizeFormat(opts.format);
|
|
5170
|
+
if (!format) {
|
|
5171
|
+
console.error("Error: --format is required when generating a local tag.");
|
|
5172
|
+
process.exit(1);
|
|
5173
|
+
}
|
|
5174
|
+
const variant = String(opts.variant).trim().toLowerCase();
|
|
5175
|
+
if (variant === "iframe")
|
|
5176
|
+
return buildIframeTag(zoneId, format, opts);
|
|
5177
|
+
if (variant !== "js") {
|
|
5178
|
+
console.error("Error: --variant must be js or iframe.");
|
|
5179
|
+
process.exit(1);
|
|
5180
|
+
}
|
|
5181
|
+
return buildJavascriptTag(zoneId, format, opts);
|
|
5182
|
+
}
|
|
5183
|
+
function buildJavascriptTag(zoneId, format, opts) {
|
|
5184
|
+
const divId = `alpineads-${zoneId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 20)}`;
|
|
5185
|
+
const attrs = {
|
|
5186
|
+
id: divId,
|
|
5187
|
+
"data-zone": zoneId,
|
|
5188
|
+
"data-format": format,
|
|
5189
|
+
"data-ad-server": cleanUrl(opts.adServer)
|
|
5190
|
+
};
|
|
5191
|
+
if (opts.sizes)
|
|
5192
|
+
attrs["data-sizes"] = opts.sizes;
|
|
5193
|
+
if (format === "offerwall") {
|
|
5194
|
+
attrs["data-limit"] = String(opts.limit ?? "20");
|
|
5195
|
+
attrs["data-theme"] = opts.theme ?? "dark";
|
|
5196
|
+
attrs["data-sub"] = opts.sub;
|
|
5197
|
+
}
|
|
5198
|
+
if (format === "rewarded_video") {
|
|
5199
|
+
attrs["data-theme"] = opts.theme ?? "dark";
|
|
5200
|
+
attrs["data-sub"] = opts.sub;
|
|
5201
|
+
attrs["data-button-text"] = opts.buttonText;
|
|
5202
|
+
}
|
|
5203
|
+
if (opts.keywords)
|
|
5204
|
+
attrs["data-keywords"] = opts.keywords;
|
|
5205
|
+
const attrLines = Object.entries(attrs).map(([key, value], index) => `${index === 0 ? "" : " "}${key}="${escapeHtml(String(value ?? ""))}"`).join(`
|
|
5206
|
+
`);
|
|
5207
|
+
return `<!-- AlpineAds Zone: ${zoneId} -->
|
|
5208
|
+
<div ${attrLines}>
|
|
5209
|
+
</div>
|
|
5210
|
+
<script async src="${escapeHtml(cleanUrl(opts.loaderUrl))}"></script>`;
|
|
5211
|
+
}
|
|
5212
|
+
function buildIframeTag(zoneId, format, opts) {
|
|
5213
|
+
const params = new URLSearchParams;
|
|
5214
|
+
params.set("format", format);
|
|
5215
|
+
if (opts.sizes)
|
|
5216
|
+
params.set("sizes", opts.sizes);
|
|
5217
|
+
if (shouldIncludeSub(format, opts.sub))
|
|
5218
|
+
params.set("sub", opts.sub);
|
|
5219
|
+
if (opts.keywords)
|
|
5220
|
+
params.set("keywords", opts.keywords);
|
|
5221
|
+
if (format === "offerwall") {
|
|
5222
|
+
params.set("limit", String(opts.limit ?? "20"));
|
|
5223
|
+
params.set("theme", opts.theme ?? "dark");
|
|
5224
|
+
}
|
|
5225
|
+
if (format === "rewarded_video") {
|
|
5226
|
+
params.set("theme", opts.theme ?? "dark");
|
|
5227
|
+
}
|
|
5228
|
+
const size = iframeSize(format, opts.sizes);
|
|
5229
|
+
const src = `${cleanUrl(opts.adServer)}/ad/${encodeURIComponent(zoneId)}?${params.toString()}`;
|
|
5230
|
+
return `<!-- AlpineAds Zone: ${zoneId} -->
|
|
5231
|
+
<iframe src="${escapeHtml(src)}"
|
|
5232
|
+
width="${size.width}" height="${size.height}"
|
|
5233
|
+
frameborder="0" scrolling="no"
|
|
5234
|
+
style="border:none;overflow:hidden"></iframe>`;
|
|
5235
|
+
}
|
|
5236
|
+
function normalizeFormat(value) {
|
|
5237
|
+
if (!value)
|
|
5238
|
+
return null;
|
|
5239
|
+
const normalized = String(value).trim().toLowerCase().replace(/-/g, "_");
|
|
5240
|
+
if (normalized === "rewarded" || normalized === "rewardedvideo")
|
|
5241
|
+
return "rewarded_video";
|
|
5242
|
+
if (normalized === "offer_wall")
|
|
5243
|
+
return "offerwall";
|
|
5244
|
+
return normalized;
|
|
5245
|
+
}
|
|
5246
|
+
function shouldIncludeSub(format, sub) {
|
|
5247
|
+
if (!sub)
|
|
5248
|
+
return false;
|
|
5249
|
+
if (format === "offerwall" || format === "rewarded_video")
|
|
5250
|
+
return true;
|
|
5251
|
+
return String(sub) !== "{YOUR_PLAYER_ID}";
|
|
5252
|
+
}
|
|
5253
|
+
function iframeSize(format, sizes) {
|
|
5254
|
+
const firstSize = String(sizes ?? "").split(",")[0]?.trim();
|
|
5255
|
+
const match = firstSize?.match(/^(\d+)x(\d+)$/);
|
|
5256
|
+
if (match)
|
|
5257
|
+
return { width: match[1], height: match[2] };
|
|
5258
|
+
if (format === "offerwall")
|
|
5259
|
+
return { width: "320", height: "480" };
|
|
5260
|
+
if (format === "rewarded_video")
|
|
5261
|
+
return { width: "640", height: "360" };
|
|
5262
|
+
return { width: "300", height: "250" };
|
|
5263
|
+
}
|
|
5264
|
+
function cleanUrl(value) {
|
|
5265
|
+
return String(value).replace(/\/+$/, "");
|
|
5266
|
+
}
|
|
5267
|
+
function escapeHtml(value) {
|
|
5268
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
5269
|
+
}
|
|
5143
5270
|
|
|
5144
5271
|
// src/commands/reports.ts
|
|
5145
5272
|
function createReportsCommand() {
|
|
@@ -5148,6 +5275,7 @@ function createReportsCommand() {
|
|
|
5148
5275
|
Routes based on current capability (DSP or SSP).`).addHelpText("after", `
|
|
5149
5276
|
Examples:
|
|
5150
5277
|
$ a8techads reports query --metrics spend,impressions --dimensions date
|
|
5278
|
+
$ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country
|
|
5151
5279
|
$ a8techads reports list
|
|
5152
5280
|
$ a8techads reports templates`);
|
|
5153
5281
|
addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
|
|
@@ -5195,6 +5323,54 @@ Summary:`);
|
|
|
5195
5323
|
}
|
|
5196
5324
|
}
|
|
5197
5325
|
});
|
|
5326
|
+
addFormatOption(cmd.command("ssp-pull").description(`Pull authoritative SSP reporting data for reconciliation.
|
|
5327
|
+
|
|
5328
|
+
Requires: PUBLISHER capability and an SSP tenant context.`).requiredOption("--from <date>", "Start date/time, e.g. 2026-06-14 or 2026-06-14T00:00:00Z").requiredOption("--to <date>", "End date/time, e.g. 2026-06-15 or 2026-06-15T00:00:00Z").option("--group-by <list>", "Comma-separated group dimensions", "zone,day").option("--metrics <list>", "Comma-separated metrics", "requests,fills,impressions,clicks,revenue").option("--timezone <tz>", "Report timezone", "UTC").option("--zone <id>", "Filter by zone ID").option("--site <id>", "Filter by site ID").option("--country <code>", "Filter by ISO country code").option("--device <type>", "Filter by device type").option("--campaign <id>", "Filter by campaign ID").option("--variation <id>", "Filter by variation ID").option("--format-type <format>", "Filter by ad format/zone type").option("--sub-id1 <value>", "Filter by sub_id1").option("--limit <n>", "Max rows", "500").option("--offset <n>", "Result offset", "0").addHelpText("after", `
|
|
5329
|
+
Examples:
|
|
5330
|
+
$ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country
|
|
5331
|
+
$ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --zone <zone-id> --metrics impressions,clicks,revenue --format json`)).action(async (opts) => {
|
|
5332
|
+
const params = new URLSearchParams;
|
|
5333
|
+
params.set("from", opts.from);
|
|
5334
|
+
params.set("to", opts.to);
|
|
5335
|
+
params.set("group_by", opts.groupBy);
|
|
5336
|
+
params.set("metrics", opts.metrics);
|
|
5337
|
+
params.set("timezone", opts.timezone);
|
|
5338
|
+
params.set("limit", opts.limit);
|
|
5339
|
+
params.set("offset", opts.offset);
|
|
5340
|
+
const filters = [
|
|
5341
|
+
["zone_id", opts.zone],
|
|
5342
|
+
["site_id", opts.site],
|
|
5343
|
+
["country", opts.country],
|
|
5344
|
+
["device", opts.device],
|
|
5345
|
+
["campaign_id", opts.campaign],
|
|
5346
|
+
["variation_id", opts.variation],
|
|
5347
|
+
["format", opts.formatType],
|
|
5348
|
+
["sub_id1", opts.subId1]
|
|
5349
|
+
];
|
|
5350
|
+
for (const [key, value] of filters) {
|
|
5351
|
+
if (value !== undefined && value !== null && String(value) !== "")
|
|
5352
|
+
params.set(key, String(value));
|
|
5353
|
+
}
|
|
5354
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/reports?${params}` });
|
|
5355
|
+
const json = await resp.json();
|
|
5356
|
+
if (!resp.ok) {
|
|
5357
|
+
console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
|
|
5358
|
+
process.exit(1);
|
|
5359
|
+
}
|
|
5360
|
+
const data = json.data ?? json;
|
|
5361
|
+
const rows = data.rows ?? data.results ?? data;
|
|
5362
|
+
if (opts.format === "json") {
|
|
5363
|
+
console.log(JSON.stringify(data, null, 2));
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
5367
|
+
console.log("No results.");
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
const keys = Object.keys(rows[0]);
|
|
5371
|
+
const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
|
|
5372
|
+
printData(rows, columns, opts.format);
|
|
5373
|
+
});
|
|
5198
5374
|
addFormatOption(cmd.command("list").description("List saved reports.")).action(async (opts) => {
|
|
5199
5375
|
const resp = await apiRequest({ path: `${getApiPrefix()}/reports/saved` });
|
|
5200
5376
|
const json = await resp.json();
|
|
@@ -5614,7 +5790,7 @@ Examples:
|
|
|
5614
5790
|
}
|
|
5615
5791
|
printDetail(json.data ?? json, opts.format);
|
|
5616
5792
|
});
|
|
5617
|
-
tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required)").option("--admin-name <name>", "Admin user name").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
|
|
5793
|
+
tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required unless --tenant-type is INTERNAL_SSP or EXTERNAL_SSP)").option("--admin-name <name>", "Admin user name").option("--tenant-type <type>", "Tenant type: ADVERTISER, PUBLISHER, INTERNAL_SSP, EXTERNAL_SSP").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
|
|
5618
5794
|
Examples:
|
|
5619
5795
|
$ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
|
|
5620
5796
|
$ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
|
|
@@ -5622,17 +5798,20 @@ Examples:
|
|
|
5622
5798
|
console.error("Error: --name is required.");
|
|
5623
5799
|
process.exit(1);
|
|
5624
5800
|
}
|
|
5625
|
-
|
|
5801
|
+
const tenantType = opts.tenantType ? String(opts.tenantType).trim().toUpperCase() : undefined;
|
|
5802
|
+
const isSupplyPartner = tenantType === "INTERNAL_SSP" || tenantType === "EXTERNAL_SSP";
|
|
5803
|
+
if (!opts.email && !isSupplyPartner) {
|
|
5626
5804
|
console.error("Error: --email is required.");
|
|
5627
5805
|
process.exit(1);
|
|
5628
5806
|
}
|
|
5629
|
-
const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
|
|
5807
|
+
const capabilities = isSupplyPartner ? [] : opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
|
|
5630
5808
|
const body = {
|
|
5631
5809
|
tenant: {
|
|
5632
5810
|
companyName: opts.name,
|
|
5811
|
+
tenantType,
|
|
5633
5812
|
capabilities
|
|
5634
5813
|
},
|
|
5635
|
-
admin: {
|
|
5814
|
+
admin: isSupplyPartner ? undefined : {
|
|
5636
5815
|
email: opts.email,
|
|
5637
5816
|
name: opts.adminName ?? undefined
|
|
5638
5817
|
}
|
|
@@ -5764,11 +5943,104 @@ Examples:
|
|
|
5764
5943
|
{ key: "time", header: "TIME", width: 22 }
|
|
5765
5944
|
], opts.format);
|
|
5766
5945
|
});
|
|
5946
|
+
const rateLimits = cmd.command("rate-limits").description("Business rate-limit configuration.").addHelpText("after", `
|
|
5947
|
+
Examples:
|
|
5948
|
+
$ a8techads admin rate-limits list --format table
|
|
5949
|
+
$ a8techads admin rate-limits upsert-partner PROPELLERADS --qps 500 --yes
|
|
5950
|
+
$ a8techads admin rate-limits upsert-partner HILLTOPADS --qps 100 --yes`);
|
|
5951
|
+
function normalizeRateLimit(config) {
|
|
5952
|
+
return {
|
|
5953
|
+
id: config.id,
|
|
5954
|
+
name: config.name ?? config.targetName ?? config.target_name,
|
|
5955
|
+
configType: config.configType ?? config.config_type,
|
|
5956
|
+
targetId: config.targetId ?? config.target_id,
|
|
5957
|
+
qpsLimit: config.qpsLimit ?? config.qps_limit,
|
|
5958
|
+
windowSizeMs: config.windowSizeMs ?? config.window_size_ms,
|
|
5959
|
+
burstAllowance: config.burstAllowance ?? config.burst_allowance,
|
|
5960
|
+
strategy: config.strategy,
|
|
5961
|
+
isEnabled: config.isEnabled ?? config.is_enabled,
|
|
5962
|
+
updatedAt: config.updatedAt ?? config.updated_at
|
|
5963
|
+
};
|
|
5964
|
+
}
|
|
5965
|
+
addFormatOption(rateLimits.command("list").description("List business rate-limit configs.").option("--type <type>", "Filter by config type, e.g. global or partner").option("--limit <n>", "Max results", "100")).action(async (opts) => {
|
|
5966
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5967
|
+
if (opts.type)
|
|
5968
|
+
params.set("configType", opts.type);
|
|
5969
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/rate-limits/config?${params}` });
|
|
5970
|
+
const json = await resp.json();
|
|
5971
|
+
if (!resp.ok) {
|
|
5972
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5973
|
+
process.exit(1);
|
|
5974
|
+
}
|
|
5975
|
+
const rows = (json.data ?? json).map(normalizeRateLimit);
|
|
5976
|
+
printData(rows, [
|
|
5977
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5978
|
+
{ key: "configType", header: "TYPE", width: 10 },
|
|
5979
|
+
{ key: "name", header: "NAME", width: 20 },
|
|
5980
|
+
{ key: "qpsLimit", header: "QPS", width: 8 },
|
|
5981
|
+
{ key: "windowSizeMs", header: "WINDOW", width: 8 },
|
|
5982
|
+
{ key: "burstAllowance", header: "BURST", width: 8 },
|
|
5983
|
+
{ key: "strategy", header: "STRATEGY", width: 10 },
|
|
5984
|
+
{ key: "isEnabled", header: "ENABLED", width: 8 }
|
|
5985
|
+
], opts.format);
|
|
5986
|
+
});
|
|
5987
|
+
addFormatOption(rateLimits.command("upsert-partner").description("Create or update a partner business QPS limit by partner code.").argument("<partner-code>", "Partner code, e.g. PROPELLERADS or HILLTOPADS").requiredOption("--qps <n>", "QPS limit").option("--window-ms <n>", "Window size in milliseconds", "1000").option("--burst <n>", "Burst allowance multiplier", "1.5").option("--strategy <strategy>", "Limit strategy", "drop").option("--disable", "Create/update as disabled", false).option("--yes", "Skip confirmation", false)).action(async (partnerCode, opts) => {
|
|
5988
|
+
const normalizedPartner = String(partnerCode).trim().toUpperCase();
|
|
5989
|
+
const qps = Number.parseInt(opts.qps, 10);
|
|
5990
|
+
const windowMs = Number.parseInt(opts.windowMs, 10);
|
|
5991
|
+
const burst = Number.parseFloat(opts.burst);
|
|
5992
|
+
if (!normalizedPartner) {
|
|
5993
|
+
console.error("Error: partner code is required.");
|
|
5994
|
+
process.exit(1);
|
|
5995
|
+
}
|
|
5996
|
+
if (!Number.isFinite(qps) || qps < 0) {
|
|
5997
|
+
console.error("Error: --qps must be a non-negative integer.");
|
|
5998
|
+
process.exit(1);
|
|
5999
|
+
}
|
|
6000
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
6001
|
+
console.error("Error: --window-ms must be a positive integer.");
|
|
6002
|
+
process.exit(1);
|
|
6003
|
+
}
|
|
6004
|
+
if (!Number.isFinite(burst) || burst <= 0) {
|
|
6005
|
+
console.error("Error: --burst must be a positive number.");
|
|
6006
|
+
process.exit(1);
|
|
6007
|
+
}
|
|
6008
|
+
await confirmAction(`Set business rate limit for ${normalizedPartner} to ${qps} QPS?`, opts.yes);
|
|
6009
|
+
const listResp = await apiRequest({ path: `${consolePrefix()}/rate-limits/config?configType=partner&limit=100` });
|
|
6010
|
+
const listJson = await listResp.json();
|
|
6011
|
+
if (!listResp.ok) {
|
|
6012
|
+
console.error(`Error: ${listJson.error ?? listJson.errors ?? listResp.statusText}`);
|
|
6013
|
+
process.exit(1);
|
|
6014
|
+
}
|
|
6015
|
+
const existing = (listJson.data ?? listJson).find((config) => {
|
|
6016
|
+
const name = String(config.name ?? config.targetName ?? config.target_name ?? "").trim().toUpperCase();
|
|
6017
|
+
return name === normalizedPartner;
|
|
6018
|
+
});
|
|
6019
|
+
const body = {
|
|
6020
|
+
configType: "partner",
|
|
6021
|
+
name: normalizedPartner,
|
|
6022
|
+
targetName: normalizedPartner,
|
|
6023
|
+
qpsLimit: qps,
|
|
6024
|
+
windowSizeMs: windowMs,
|
|
6025
|
+
burstAllowance: burst,
|
|
6026
|
+
strategy: opts.strategy,
|
|
6027
|
+
dailyRequestLimit: 0,
|
|
6028
|
+
isEnabled: !opts.disable
|
|
6029
|
+
};
|
|
6030
|
+
const resp = existing ? await apiRequest({ method: "PATCH", path: `${consolePrefix()}/rate-limits/${existing.id}`, body }) : await apiRequest({ method: "POST", path: `${consolePrefix()}/rate-limits`, body });
|
|
6031
|
+
const json = await resp.json();
|
|
6032
|
+
if (!resp.ok) {
|
|
6033
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6034
|
+
process.exit(1);
|
|
6035
|
+
}
|
|
6036
|
+
printDetail(normalizeRateLimit(json.data ?? json), opts.format);
|
|
6037
|
+
});
|
|
5767
6038
|
const campaignReviews = cmd.command("campaign-reviews").description("Campaign review operations (Console).").addHelpText("after", `
|
|
5768
6039
|
Examples:
|
|
5769
6040
|
$ a8techads admin campaign-reviews pending
|
|
5770
6041
|
$ a8techads admin campaign-reviews show <campaign-id>
|
|
5771
6042
|
$ a8techads admin campaign-reviews approve-all <campaign-id> --yes
|
|
6043
|
+
$ a8techads admin campaign-reviews approve-variation <variation-id> --yes
|
|
5772
6044
|
$ a8techads admin campaign-reviews decline <campaign-id> --reason-code policy --feedback "Needs changes" --yes`);
|
|
5773
6045
|
const reviewColumns = [
|
|
5774
6046
|
{ key: "id", header: "ID", width: 36 },
|
|
@@ -5845,6 +6117,16 @@ Variations`);
|
|
|
5845
6117
|
}
|
|
5846
6118
|
printDetail(json.data ?? json, opts.format);
|
|
5847
6119
|
});
|
|
6120
|
+
addFormatOption(campaignReviews.command("approve-variation").description("Approve one pending variation without changing the parent campaign status.").argument("<id>", "Variation ID").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
|
|
6121
|
+
await confirmAction(`Approve variation ${id}?`, opts.yes);
|
|
6122
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/variations/${id}/approve` });
|
|
6123
|
+
const json = await resp.json();
|
|
6124
|
+
if (!resp.ok) {
|
|
6125
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6126
|
+
process.exit(1);
|
|
6127
|
+
}
|
|
6128
|
+
printDetail(json.data ?? json, opts.format);
|
|
6129
|
+
});
|
|
5848
6130
|
addFormatOption(campaignReviews.command("decline").description("Decline all pending variations for a campaign atomically.").argument("<id>", "Campaign ID").option("--feedback <text>", "Review feedback").option("--reason-code <code>", "Review reason code").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
|
|
5849
6131
|
await confirmAction(`Decline campaign ${id}?`, opts.yes);
|
|
5850
6132
|
const body = {};
|
|
@@ -6910,9 +7192,11 @@ Examples:
|
|
|
6910
7192
|
var COLUMNS9 = [
|
|
6911
7193
|
{ key: "id", header: "ID", width: 36 },
|
|
6912
7194
|
{ key: "name", header: "NAME", width: 24 },
|
|
7195
|
+
{ key: "codes", header: "CODES", width: 24 },
|
|
6913
7196
|
{ key: "goalOrder", header: "G#", width: 4 },
|
|
6914
7197
|
{ key: "conversionType", header: "TYPE", width: 20 },
|
|
6915
7198
|
{ key: "valueType", header: "VALUE", width: 10 },
|
|
7199
|
+
{ key: "signatureRequired", header: "HMAC", width: 8 },
|
|
6916
7200
|
{ key: "status", header: "STATUS", width: 10 }
|
|
6917
7201
|
];
|
|
6918
7202
|
function createConversionGoalsCommand() {
|
|
@@ -6923,6 +7207,7 @@ Examples:
|
|
|
6923
7207
|
$ a8techads conversion-goals list
|
|
6924
7208
|
$ a8techads conversion-goals get <id>
|
|
6925
7209
|
$ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
|
|
7210
|
+
$ a8techads conversion-goals integration <id> --status install
|
|
6926
7211
|
$ a8techads conversion-goals pause <id>`);
|
|
6927
7212
|
addFormatOption(cmd.command("list").description("List conversion goals.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6928
7213
|
const params = new URLSearchParams;
|
|
@@ -6939,8 +7224,10 @@ Examples:
|
|
|
6939
7224
|
id: g.id,
|
|
6940
7225
|
name: g.name,
|
|
6941
7226
|
goalOrder: `G${g.goalOrder ?? g.goal_order}`,
|
|
7227
|
+
codes: Array.isArray(g.codes) ? g.codes.join(",") : "",
|
|
6942
7228
|
conversionType: g.conversionType ?? g.conversion_type,
|
|
6943
7229
|
valueType: g.valueType ?? g.value_type,
|
|
7230
|
+
signatureRequired: g.signatureRequired ?? g.signature_required ? "required" : "optional",
|
|
6944
7231
|
status: g.status
|
|
6945
7232
|
}));
|
|
6946
7233
|
printData(rows, COLUMNS9, opts.format);
|
|
@@ -6954,10 +7241,45 @@ Examples:
|
|
|
6954
7241
|
}
|
|
6955
7242
|
printDetail(json.data ?? json, opts.format);
|
|
6956
7243
|
});
|
|
7244
|
+
cmd.command("integration").description("Print S2S postback and pixel integration examples for a conversion goal.").argument("<id>", "Goal ID").option("--base-url <url>", "Bidder/conversion base URL", "https://bidder.a8.tech").option("--click-id <macro>", "Click ID macro or test click ID", "{click_id}").option("--status <code>", "External code/status sample, e.g. install or registration").option("--value <amount>", "Conversion value sample").action(async (id, opts) => {
|
|
7245
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
|
|
7246
|
+
const json = await resp.json();
|
|
7247
|
+
if (!resp.ok) {
|
|
7248
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
7249
|
+
process.exit(1);
|
|
7250
|
+
}
|
|
7251
|
+
const goal = json.data ?? json;
|
|
7252
|
+
const goalId = goal.goalId ?? goal.goal_id ?? goal.id ?? id;
|
|
7253
|
+
const codes = Array.isArray(goal.codes) ? goal.codes : [];
|
|
7254
|
+
const base = cleanBaseUrl(opts.baseUrl);
|
|
7255
|
+
const baseUrl = buildConversionUrl(base, goalId, opts.clickId, undefined, opts.value);
|
|
7256
|
+
const statusUrl = opts.status ? buildConversionUrl(base, goalId, opts.clickId, opts.status, opts.value) : null;
|
|
7257
|
+
const pixelUrl = `${base}/conversion.gif?${conversionParams(goalId, opts.clickId, opts.status, opts.value)}`;
|
|
7258
|
+
console.log(`Goal: ${goal.name ?? id}`);
|
|
7259
|
+
console.log(`Goal ID: ${goalId}`);
|
|
7260
|
+
console.log(`External codes: ${codes.length > 0 ? codes.join(", ") : "(none)"}`);
|
|
7261
|
+
console.log("");
|
|
7262
|
+
console.log("S2S postback:");
|
|
7263
|
+
console.log(baseUrl);
|
|
7264
|
+
if (statusUrl) {
|
|
7265
|
+
console.log("");
|
|
7266
|
+
console.log(`Status/code postback sample (${opts.status}):`);
|
|
7267
|
+
console.log(statusUrl);
|
|
7268
|
+
}
|
|
7269
|
+
console.log("");
|
|
7270
|
+
console.log("Browser pixel sample:");
|
|
7271
|
+
console.log(`<!-- AlpineAds Conversion Pixel -->`);
|
|
7272
|
+
console.log(`<img src="${pixelUrl}" width="1" height="1" style="display:none" alt="" />`);
|
|
7273
|
+
console.log("");
|
|
7274
|
+
console.log("Notes:");
|
|
7275
|
+
console.log("- Replace {click_id} with the click ID captured from the campaign tracking URL.");
|
|
7276
|
+
console.log("- status=<code> is matched against this goal external codes, for tracker integrations such as install or registration.");
|
|
7277
|
+
console.log("- DYNAMIC value goals should use signed server-to-server postbacks; browser pixels are only for validation or lightweight fixed/no-value goals.");
|
|
7278
|
+
});
|
|
6957
7279
|
cmd.command("create").description(`Create a conversion goal.
|
|
6958
7280
|
|
|
6959
7281
|
Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
6960
|
-
PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
7282
|
+
PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--code <code>", "Single external tracker alias/status for this goal").option("--codes <codes>", "Comma-separated external tracker aliases/statuses for this goal").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--signature-required <bool>", "Require HMAC signature for DYNAMIC goals (true/false)", "false").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6961
7283
|
let body;
|
|
6962
7284
|
if (opts.fromJson) {
|
|
6963
7285
|
const { readFileSync: readFileSync5 } = await import("fs");
|
|
@@ -6977,9 +7299,11 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
6977
7299
|
}
|
|
6978
7300
|
body = {
|
|
6979
7301
|
name: opts.name,
|
|
7302
|
+
codes: parseCodes(opts.codes ?? opts.code),
|
|
6980
7303
|
conversionType: opts.conversionType,
|
|
6981
7304
|
goalOrder: Number(opts.goalOrder),
|
|
6982
7305
|
valueType: opts.valueType,
|
|
7306
|
+
signatureRequired: parseBoolOption(opts.signatureRequired),
|
|
6983
7307
|
countType: opts.countType,
|
|
6984
7308
|
conversionWindowHours: Number(opts.window)
|
|
6985
7309
|
};
|
|
@@ -6999,7 +7323,7 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
6999
7323
|
console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
|
|
7000
7324
|
console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
|
|
7001
7325
|
});
|
|
7002
|
-
cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
7326
|
+
cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--code <code>", "Replace external tracker aliases with one code").option("--codes <codes>", "Replace external tracker aliases with comma-separated codes").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--signature-required <bool>", "Require HMAC signature for DYNAMIC goals (true/false)").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
7003
7327
|
let body;
|
|
7004
7328
|
if (opts.fromJson) {
|
|
7005
7329
|
const { readFileSync: readFileSync5 } = await import("fs");
|
|
@@ -7008,12 +7332,16 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
7008
7332
|
body = {};
|
|
7009
7333
|
if (opts.name)
|
|
7010
7334
|
body.name = opts.name;
|
|
7335
|
+
if (opts.codes !== undefined || opts.code !== undefined)
|
|
7336
|
+
body.codes = parseCodes(opts.codes ?? opts.code);
|
|
7011
7337
|
if (opts.description)
|
|
7012
7338
|
body.description = opts.description;
|
|
7013
7339
|
if (opts.valueType)
|
|
7014
7340
|
body.valueType = opts.valueType;
|
|
7015
7341
|
if (opts.fixedValue)
|
|
7016
7342
|
body.fixedValue = Number(opts.fixedValue);
|
|
7343
|
+
if (opts.signatureRequired !== undefined)
|
|
7344
|
+
body.signatureRequired = parseBoolOption(opts.signatureRequired);
|
|
7017
7345
|
if (opts.countType)
|
|
7018
7346
|
body.countType = opts.countType;
|
|
7019
7347
|
if (opts.window)
|
|
@@ -7063,6 +7391,40 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
7063
7391
|
});
|
|
7064
7392
|
return cmd;
|
|
7065
7393
|
}
|
|
7394
|
+
function parseBoolOption(value) {
|
|
7395
|
+
if (typeof value === "boolean")
|
|
7396
|
+
return value;
|
|
7397
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
7398
|
+
if (["true", "1", "yes", "y"].includes(normalized))
|
|
7399
|
+
return true;
|
|
7400
|
+
if (["false", "0", "no", "n", ""].includes(normalized))
|
|
7401
|
+
return false;
|
|
7402
|
+
console.error(`Error: expected boolean value, got ${value}`);
|
|
7403
|
+
process.exit(1);
|
|
7404
|
+
}
|
|
7405
|
+
function parseCodes(value) {
|
|
7406
|
+
if (value === undefined || value === null)
|
|
7407
|
+
return [];
|
|
7408
|
+
const values = Array.isArray(value) ? value : String(value).split(",");
|
|
7409
|
+
const normalized = values.map((item) => String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "")).filter(Boolean);
|
|
7410
|
+
return Array.from(new Set(normalized));
|
|
7411
|
+
}
|
|
7412
|
+
function cleanBaseUrl(value) {
|
|
7413
|
+
return String(value).replace(/\/+$/, "");
|
|
7414
|
+
}
|
|
7415
|
+
function buildConversionUrl(baseUrl, goalId, clickId, status, value) {
|
|
7416
|
+
return `${baseUrl}/conversion?${conversionParams(goalId, clickId, status, value)}`;
|
|
7417
|
+
}
|
|
7418
|
+
function conversionParams(goalId, clickId, status, value) {
|
|
7419
|
+
const params = new URLSearchParams;
|
|
7420
|
+
params.set("goal", goalId);
|
|
7421
|
+
params.set("click_id", clickId);
|
|
7422
|
+
if (status)
|
|
7423
|
+
params.set("status", status);
|
|
7424
|
+
if (value)
|
|
7425
|
+
params.set("value", value);
|
|
7426
|
+
return params.toString().replace(/%7B/g, "{").replace(/%7D/g, "}");
|
|
7427
|
+
}
|
|
7066
7428
|
|
|
7067
7429
|
// src/commands/algorithms.ts
|
|
7068
7430
|
var COLUMNS10 = [
|
|
@@ -7073,6 +7435,18 @@ var COLUMNS10 = [
|
|
|
7073
7435
|
{ key: "status", header: "STATUS", width: 10 },
|
|
7074
7436
|
{ key: "campaignCount", header: "CAMPAIGNS", width: 10 }
|
|
7075
7437
|
];
|
|
7438
|
+
var RULE_COLUMNS = [
|
|
7439
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7440
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7441
|
+
{ key: "dimension", header: "DIMENSION", width: 14 },
|
|
7442
|
+
{ key: "metric", header: "METRIC", width: 12 },
|
|
7443
|
+
{ key: "threshold", header: "THRESHOLD", width: 10 },
|
|
7444
|
+
{ key: "check", header: "CHECK", width: 24 },
|
|
7445
|
+
{ key: "action", header: "ACTION", width: 12 },
|
|
7446
|
+
{ key: "bidModifier", header: "MOD", width: 8 },
|
|
7447
|
+
{ key: "isActive", header: "ACTIVE", width: 8 },
|
|
7448
|
+
{ key: "priority", header: "PRI", width: 6 }
|
|
7449
|
+
];
|
|
7076
7450
|
function createAlgorithmsCommand() {
|
|
7077
7451
|
const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
|
|
7078
7452
|
|
|
@@ -7081,7 +7455,9 @@ Examples:
|
|
|
7081
7455
|
$ a8techads algorithms list
|
|
7082
7456
|
$ a8techads algorithms get <id>
|
|
7083
7457
|
$ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
|
|
7084
|
-
$ a8techads algorithms update <id> --conversion-goal-id <goal-id>
|
|
7458
|
+
$ a8techads algorithms update <id> --conversion-goal-id <goal-id> --auto-adjust-bids true --auto-block-enabled false
|
|
7459
|
+
$ a8techads algorithms rules create <id> --name "Propeller source cut" --dimension source_site --metric clicks --threshold 50 --condition equals --check-metric conversions --check-value 0 --action reduce_bid --bid-modifier 0.6
|
|
7460
|
+
$ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1,quarter_hour --target-cpa 5
|
|
7085
7461
|
$ a8techads algorithms pause <id>`);
|
|
7086
7462
|
addFormatOption(cmd.command("list").description("List bidder algorithms.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
7087
7463
|
const params = new URLSearchParams;
|
|
@@ -7091,7 +7467,7 @@ Examples:
|
|
|
7091
7467
|
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
|
|
7092
7468
|
const json = await resp.json();
|
|
7093
7469
|
if (!resp.ok) {
|
|
7094
|
-
console.error(`Error: ${json
|
|
7470
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7095
7471
|
process.exit(1);
|
|
7096
7472
|
}
|
|
7097
7473
|
const rows = (json.data ?? json).map((algo) => ({
|
|
@@ -7108,27 +7484,27 @@ Examples:
|
|
|
7108
7484
|
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
|
|
7109
7485
|
const json = await resp.json();
|
|
7110
7486
|
if (!resp.ok) {
|
|
7111
|
-
console.error(`Error: ${json
|
|
7487
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7112
7488
|
process.exit(1);
|
|
7113
7489
|
}
|
|
7114
7490
|
printDetail(json.data ?? json, opts.format);
|
|
7115
7491
|
});
|
|
7116
|
-
cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
7492
|
+
cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--data-period-days <n>", "Data lookback period in days").option("--max-test-budget <amount>", "Maximum test budget before reducing/blocking weak traffic").option("--min-impressions <n>", "Minimum impressions before evaluating").option("--min-clicks <n>", "Minimum clicks before evaluating").option("--auto-adjust-bids <bool>", "Enable automatic bid modifiers").option("--auto-block-enabled <bool>", "Enable automatic block actions").option("--min-bid-modifier <number>", "Minimum bid modifier, e.g. 0.6").option("--max-bid-modifier <number>", "Maximum bid modifier, e.g. 1.3").option("--block-after-impressions <n>", "Block threshold by impressions").option("--block-after-spend <amount>", "Block threshold by spend").option("--block-if-no-conversions <bool>", "Block when no conversions after thresholds").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
7117
7493
|
const body = await buildAlgorithmBody(opts, true);
|
|
7118
7494
|
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
|
|
7119
7495
|
const json = await resp.json();
|
|
7120
7496
|
if (!resp.ok) {
|
|
7121
|
-
console.error(`Error: ${json
|
|
7497
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7122
7498
|
process.exit(1);
|
|
7123
7499
|
}
|
|
7124
7500
|
console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
|
|
7125
7501
|
});
|
|
7126
|
-
cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
7502
|
+
cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--data-period-days <n>", "Data lookback period in days").option("--max-test-budget <amount>", "Maximum test budget before reducing/blocking weak traffic").option("--min-impressions <n>", "Minimum impressions before evaluating").option("--min-clicks <n>", "Minimum clicks before evaluating").option("--auto-adjust-bids <bool>", "Enable automatic bid modifiers").option("--auto-block-enabled <bool>", "Enable automatic block actions").option("--min-bid-modifier <number>", "Minimum bid modifier, e.g. 0.6").option("--max-bid-modifier <number>", "Maximum bid modifier, e.g. 1.3").option("--block-after-impressions <n>", "Block threshold by impressions").option("--block-after-spend <amount>", "Block threshold by spend").option("--block-if-no-conversions <bool>", "Block when no conversions after thresholds").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
7127
7503
|
const body = await buildAlgorithmBody(opts, false);
|
|
7128
7504
|
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
|
|
7129
7505
|
const json = await resp.json().catch(() => ({}));
|
|
7130
7506
|
if (!resp.ok) {
|
|
7131
|
-
console.error(`Error: ${json
|
|
7507
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7132
7508
|
process.exit(1);
|
|
7133
7509
|
}
|
|
7134
7510
|
console.log(`Algorithm ${id} updated.`);
|
|
@@ -7145,7 +7521,7 @@ Examples:
|
|
|
7145
7521
|
});
|
|
7146
7522
|
const json = await resp.json().catch(() => ({}));
|
|
7147
7523
|
if (!resp.ok) {
|
|
7148
|
-
console.error(`Error: ${json
|
|
7524
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7149
7525
|
process.exit(1);
|
|
7150
7526
|
}
|
|
7151
7527
|
console.log(`Algorithm ${id} archived.`);
|
|
@@ -7158,12 +7534,14 @@ Examples:
|
|
|
7158
7534
|
});
|
|
7159
7535
|
const json = await resp.json().catch(() => ({}));
|
|
7160
7536
|
if (!resp.ok) {
|
|
7161
|
-
console.error(`Error: ${json
|
|
7537
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7162
7538
|
process.exit(1);
|
|
7163
7539
|
}
|
|
7164
7540
|
console.log(`Algorithm ${id} ${action}d.`);
|
|
7165
7541
|
});
|
|
7166
7542
|
}
|
|
7543
|
+
addRulesCommand(cmd);
|
|
7544
|
+
addProgressCommand(cmd);
|
|
7167
7545
|
return cmd;
|
|
7168
7546
|
}
|
|
7169
7547
|
async function buildAlgorithmBody(opts, creating) {
|
|
@@ -7177,11 +7555,22 @@ async function buildAlgorithmBody(opts, creating) {
|
|
|
7177
7555
|
if (opts.description)
|
|
7178
7556
|
body.description = opts.description;
|
|
7179
7557
|
if (opts.optimizationGoal)
|
|
7180
|
-
body.optimizationGoal = String(opts.optimizationGoal).
|
|
7558
|
+
body.optimizationGoal = String(opts.optimizationGoal).toLowerCase();
|
|
7181
7559
|
if (opts.targetValue !== undefined)
|
|
7182
7560
|
body.targetValue = Number(opts.targetValue);
|
|
7183
7561
|
if (opts.conversionGoalId)
|
|
7184
7562
|
body.conversionGoalId = opts.conversionGoalId;
|
|
7563
|
+
putNumber(body, "dataPeriodDays", opts.dataPeriodDays);
|
|
7564
|
+
putNumber(body, "maxTestBudget", opts.maxTestBudget);
|
|
7565
|
+
putNumber(body, "minImpressions", opts.minImpressions);
|
|
7566
|
+
putNumber(body, "minClicks", opts.minClicks);
|
|
7567
|
+
putBoolean(body, "autoAdjustBids", opts.autoAdjustBids);
|
|
7568
|
+
putBoolean(body, "autoBlockEnabled", opts.autoBlockEnabled);
|
|
7569
|
+
putNumber(body, "minBidModifier", opts.minBidModifier);
|
|
7570
|
+
putNumber(body, "maxBidModifier", opts.maxBidModifier);
|
|
7571
|
+
putNumber(body, "blockAfterImpressions", opts.blockAfterImpressions);
|
|
7572
|
+
putNumber(body, "blockAfterSpend", opts.blockAfterSpend);
|
|
7573
|
+
putBoolean(body, "blockIfNoConversions", opts.blockIfNoConversions);
|
|
7185
7574
|
if (creating) {
|
|
7186
7575
|
if (!body.name) {
|
|
7187
7576
|
console.error("Error: --name is required.");
|
|
@@ -7198,6 +7587,245 @@ async function buildAlgorithmBody(opts, creating) {
|
|
|
7198
7587
|
}
|
|
7199
7588
|
return body;
|
|
7200
7589
|
}
|
|
7590
|
+
function addRulesCommand(cmd) {
|
|
7591
|
+
const rules = new Command("rules").description("Manage bidder optimization rules.").addHelpText("after", `
|
|
7592
|
+
Examples:
|
|
7593
|
+
$ a8techads algorithms rules list <algorithm-id>
|
|
7594
|
+
$ a8techads algorithms rules create <algorithm-id> --name "No conversion after 50 clicks" --dimension source_site --metric clicks --threshold 50 --condition equals --check-metric conversions --check-value 0 --action reduce_bid --bid-modifier 0.6
|
|
7595
|
+
$ a8techads algorithms rules update <rule-id> --bid-modifier 0.7
|
|
7596
|
+
$ a8techads algorithms rules delete <rule-id> --yes`);
|
|
7597
|
+
addFormatOption(rules.command("list").description("List rules for a bidder algorithm.").argument("<algorithm-id>", "Algorithm ID")).action(async (algorithmId, opts) => {
|
|
7598
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${algorithmId}/blocking-rules` });
|
|
7599
|
+
const json = await resp.json();
|
|
7600
|
+
if (!resp.ok) {
|
|
7601
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7602
|
+
process.exit(1);
|
|
7603
|
+
}
|
|
7604
|
+
const rows = (json.data ?? json).map(formatRuleRow);
|
|
7605
|
+
printData(rows, RULE_COLUMNS, opts.format);
|
|
7606
|
+
});
|
|
7607
|
+
rules.command("create").description("Create an optimization rule for a bidder algorithm.").argument("<algorithm-id>", "Algorithm ID").option("--name <name>", "Rule name (required)").option("--dimension <dimension>", "zone, site, source_site, country, region, device, os, browser, language, mobile_carrier").option("--metric <metric>", "impressions, clicks, or spend").option("--threshold <number>", "Trigger threshold").option("--condition <type>", "greater_than, greater_than_or_equal, less_than, less_than_or_equal, or equals").option("--check-metric <metric>", "ctr, clicks, conversions, ecpa, roi, or profit").option("--check-value <number>", "Performance metric value to compare").option("--action <action>", "block, reduce_bid, or alert").option("--bid-modifier <number>", "Bid modifier for reduce_bid action").option("--priority <n>", "Rule priority, lower runs first").option("--active <bool>", "Whether the rule is active", "true").option("--from-json <file>", "Create from JSON file").action(async (algorithmId, opts) => {
|
|
7608
|
+
const body = await buildRuleBody(opts, true);
|
|
7609
|
+
const resp = await apiRequest({
|
|
7610
|
+
method: "POST",
|
|
7611
|
+
path: `${dspPrefix()}/bidder-algorithms/${algorithmId}/blocking-rules`,
|
|
7612
|
+
body
|
|
7613
|
+
});
|
|
7614
|
+
const json = await resp.json().catch(() => ({}));
|
|
7615
|
+
if (!resp.ok) {
|
|
7616
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7617
|
+
process.exit(1);
|
|
7618
|
+
}
|
|
7619
|
+
console.log(`Rule created: ${json.data?.id ?? json.id}`);
|
|
7620
|
+
});
|
|
7621
|
+
rules.command("update").description("Update an optimization rule.").argument("<rule-id>", "Rule ID").option("--name <name>", "Rule name").option("--dimension <dimension>", "zone, site, source_site, country, region, device, os, browser, language, mobile_carrier").option("--metric <metric>", "impressions, clicks, or spend").option("--threshold <number>", "Trigger threshold").option("--condition <type>", "greater_than, greater_than_or_equal, less_than, less_than_or_equal, or equals").option("--check-metric <metric>", "ctr, clicks, conversions, ecpa, roi, or profit").option("--check-value <number>", "Performance metric value to compare").option("--action <action>", "block, reduce_bid, or alert").option("--bid-modifier <number>", "Bid modifier for reduce_bid action").option("--priority <n>", "Rule priority, lower runs first").option("--active <bool>", "Whether the rule is active").option("--from-json <file>", "Update from JSON file").action(async (ruleId, opts) => {
|
|
7622
|
+
const body = await buildRuleBody(opts, false);
|
|
7623
|
+
const resp = await apiRequest({
|
|
7624
|
+
method: "PATCH",
|
|
7625
|
+
path: `${dspPrefix()}/blocking-rules/${ruleId}`,
|
|
7626
|
+
body
|
|
7627
|
+
});
|
|
7628
|
+
const json = await resp.json().catch(() => ({}));
|
|
7629
|
+
if (!resp.ok) {
|
|
7630
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7631
|
+
process.exit(1);
|
|
7632
|
+
}
|
|
7633
|
+
console.log(`Rule ${ruleId} updated.`);
|
|
7634
|
+
});
|
|
7635
|
+
rules.command("delete").description("Delete an optimization rule.").argument("<rule-id>", "Rule ID").option("--yes", "Skip confirmation").action(async (ruleId, opts) => {
|
|
7636
|
+
if (!opts.yes) {
|
|
7637
|
+
console.error("Add --yes to confirm deletion.");
|
|
7638
|
+
process.exit(1);
|
|
7639
|
+
}
|
|
7640
|
+
const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/blocking-rules/${ruleId}` });
|
|
7641
|
+
if (!resp.ok && resp.status !== 204) {
|
|
7642
|
+
const json = await resp.json().catch(() => ({}));
|
|
7643
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7644
|
+
process.exit(1);
|
|
7645
|
+
}
|
|
7646
|
+
console.log(`Rule ${ruleId} deleted.`);
|
|
7647
|
+
});
|
|
7648
|
+
cmd.addCommand(rules);
|
|
7649
|
+
}
|
|
7650
|
+
function addProgressCommand(cmd) {
|
|
7651
|
+
addFormatOption(cmd.command("progress").description("Monitor optimization progress for a campaign through DSP reports.").requiredOption("--campaign <id>", "Campaign ID to filter").option("--dimensions <list>", "Comma-separated dimensions, e.g. sub_id1 or sub_id1,quarter_hour", "sub_id1").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--target-cpa <amount>", "Target CPA used for suggestions").option("--limit <n>", "Max rows", "100").option("--semantic-mode <mode>", "event_time or attributed", "event_time").addHelpText("after", `
|
|
7652
|
+
Examples:
|
|
7653
|
+
$ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1 --from 2026-06-01 --to 2026-06-02 --target-cpa 5
|
|
7654
|
+
$ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1,quarter_hour --semantic-mode event_time --limit 200`)).action(async (opts) => {
|
|
7655
|
+
const dimensions = parseCsvList3(opts.dimensions);
|
|
7656
|
+
const targetCpa = opts.targetCpa === undefined ? undefined : Number(opts.targetCpa);
|
|
7657
|
+
const body = {
|
|
7658
|
+
dimensions,
|
|
7659
|
+
metrics: ["requests", "bids", "wins", "impressions", "clicks", "conversions", "spend", "ctr", "cvr", "cpa"],
|
|
7660
|
+
filters: { campaign: opts.campaign },
|
|
7661
|
+
limit: Number(opts.limit),
|
|
7662
|
+
sortBy: "clicks",
|
|
7663
|
+
sortOrder: "desc",
|
|
7664
|
+
semanticMode: opts.semanticMode
|
|
7665
|
+
};
|
|
7666
|
+
if (opts.from)
|
|
7667
|
+
body.dateRange = { ...body.dateRange ?? {}, start: opts.from };
|
|
7668
|
+
if (opts.to)
|
|
7669
|
+
body.dateRange = { ...body.dateRange ?? {}, end: opts.to };
|
|
7670
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/reports/query`, body });
|
|
7671
|
+
const json = await resp.json().catch(() => ({}));
|
|
7672
|
+
if (!resp.ok) {
|
|
7673
|
+
console.error(`Error: ${formatApiError(json, resp.statusText)}`);
|
|
7674
|
+
process.exit(1);
|
|
7675
|
+
}
|
|
7676
|
+
const data = json.data ?? json;
|
|
7677
|
+
const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
|
|
7678
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
7679
|
+
console.log("No results.");
|
|
7680
|
+
return;
|
|
7681
|
+
}
|
|
7682
|
+
const formatted = rows.map((row) => ({
|
|
7683
|
+
...row,
|
|
7684
|
+
suggestion: suggestOptimization(row, targetCpa)
|
|
7685
|
+
}));
|
|
7686
|
+
if (opts.format === "json") {
|
|
7687
|
+
console.log(JSON.stringify({ ...data, results: formatted }, null, 2));
|
|
7688
|
+
return;
|
|
7689
|
+
}
|
|
7690
|
+
const columns = [
|
|
7691
|
+
...dimensions.map((dim) => ({ key: dim, header: dim.toUpperCase(), width: Math.max(dim.length, 14) })),
|
|
7692
|
+
{ key: "requests", header: "REQ", width: 10 },
|
|
7693
|
+
{ key: "bids", header: "BID", width: 10 },
|
|
7694
|
+
{ key: "wins", header: "WIN", width: 10 },
|
|
7695
|
+
{ key: "impressions", header: "IMP", width: 10 },
|
|
7696
|
+
{ key: "clicks", header: "CLICK", width: 10 },
|
|
7697
|
+
{ key: "conversions", header: "CONV", width: 8 },
|
|
7698
|
+
{ key: "spend", header: "SPEND", width: 10, format: money },
|
|
7699
|
+
{ key: "ctr", header: "CTR%", width: 8, format: pct },
|
|
7700
|
+
{ key: "cvr", header: "CVR%", width: 8, format: pct },
|
|
7701
|
+
{ key: "cpa", header: "CPA", width: 10, format: money },
|
|
7702
|
+
{ key: "suggestion", header: "SUGGESTION", width: 22 }
|
|
7703
|
+
];
|
|
7704
|
+
printData(formatted, columns, opts.format);
|
|
7705
|
+
const summary = data.summary ?? json.summary;
|
|
7706
|
+
if (summary && opts.format === "table") {
|
|
7707
|
+
console.log(`
|
|
7708
|
+
Summary:`);
|
|
7709
|
+
printDetail(summary, "table");
|
|
7710
|
+
}
|
|
7711
|
+
});
|
|
7712
|
+
}
|
|
7713
|
+
async function buildRuleBody(opts, creating) {
|
|
7714
|
+
if (opts.fromJson) {
|
|
7715
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
7716
|
+
return JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
7717
|
+
}
|
|
7718
|
+
const body = {};
|
|
7719
|
+
if (opts.name)
|
|
7720
|
+
body.name = opts.name;
|
|
7721
|
+
if (opts.dimension)
|
|
7722
|
+
body.dimension = opts.dimension;
|
|
7723
|
+
if (opts.metric)
|
|
7724
|
+
body.metric = opts.metric;
|
|
7725
|
+
putNumber(body, "threshold", opts.threshold);
|
|
7726
|
+
if (opts.condition)
|
|
7727
|
+
body.conditionType = opts.condition;
|
|
7728
|
+
if (opts.checkMetric)
|
|
7729
|
+
body.checkMetric = opts.checkMetric;
|
|
7730
|
+
putNumber(body, "checkValue", opts.checkValue);
|
|
7731
|
+
if (opts.action)
|
|
7732
|
+
body.action = opts.action;
|
|
7733
|
+
putNumber(body, "bidModifier", opts.bidModifier);
|
|
7734
|
+
putNumber(body, "priority", opts.priority);
|
|
7735
|
+
putBoolean(body, "isActive", opts.active);
|
|
7736
|
+
if (creating) {
|
|
7737
|
+
for (const [key, option] of [
|
|
7738
|
+
["name", "--name"],
|
|
7739
|
+
["dimension", "--dimension"],
|
|
7740
|
+
["metric", "--metric"],
|
|
7741
|
+
["threshold", "--threshold"],
|
|
7742
|
+
["conditionType", "--condition"],
|
|
7743
|
+
["checkMetric", "--check-metric"],
|
|
7744
|
+
["checkValue", "--check-value"],
|
|
7745
|
+
["action", "--action"]
|
|
7746
|
+
]) {
|
|
7747
|
+
if (body[key] === undefined || body[key] === "") {
|
|
7748
|
+
console.error(`Error: ${option} is required.`);
|
|
7749
|
+
process.exit(1);
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
if (body.action === "reduce_bid" && body.bidModifier === undefined) {
|
|
7754
|
+
console.error("Error: --bid-modifier is required when --action reduce_bid.");
|
|
7755
|
+
process.exit(1);
|
|
7756
|
+
}
|
|
7757
|
+
return body;
|
|
7758
|
+
}
|
|
7759
|
+
function formatRuleRow(rule) {
|
|
7760
|
+
return {
|
|
7761
|
+
id: rule.id,
|
|
7762
|
+
name: rule.name,
|
|
7763
|
+
dimension: rule.dimension,
|
|
7764
|
+
metric: rule.metric,
|
|
7765
|
+
threshold: rule.threshold,
|
|
7766
|
+
check: `${rule.checkMetric} ${rule.conditionType} ${rule.checkValue}`,
|
|
7767
|
+
action: rule.action,
|
|
7768
|
+
bidModifier: rule.bidModifier,
|
|
7769
|
+
isActive: rule.isActive,
|
|
7770
|
+
priority: rule.priority
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
function suggestOptimization(row, targetCpa) {
|
|
7774
|
+
const clicks = asNumber(row.clicks);
|
|
7775
|
+
const conversions = asNumber(row.conversions);
|
|
7776
|
+
const spend = asNumber(row.spend);
|
|
7777
|
+
const cpa = conversions > 0 ? spend / conversions : asNumber(row.cpa);
|
|
7778
|
+
if (clicks < 30)
|
|
7779
|
+
return "hold: new traffic";
|
|
7780
|
+
if (clicks >= 50 && conversions === 0)
|
|
7781
|
+
return "reduce bid 0.6x";
|
|
7782
|
+
if (targetCpa && conversions > 0 && cpa > targetCpa)
|
|
7783
|
+
return "reduce bid 0.7x";
|
|
7784
|
+
if (targetCpa && conversions > 0 && cpa > 0 && cpa < targetCpa)
|
|
7785
|
+
return "raise bid 1.1-1.3x";
|
|
7786
|
+
return "hold";
|
|
7787
|
+
}
|
|
7788
|
+
function putNumber(body, key, value) {
|
|
7789
|
+
if (value !== undefined && value !== null && value !== "")
|
|
7790
|
+
body[key] = Number(value);
|
|
7791
|
+
}
|
|
7792
|
+
function putBoolean(body, key, value) {
|
|
7793
|
+
if (value === undefined || value === null || value === "")
|
|
7794
|
+
return;
|
|
7795
|
+
body[key] = parseBoolean(String(value), key);
|
|
7796
|
+
}
|
|
7797
|
+
function parseBoolean(value, optionName) {
|
|
7798
|
+
const normalized = value.trim().toLowerCase();
|
|
7799
|
+
if (["true", "1", "yes", "y", "on"].includes(normalized))
|
|
7800
|
+
return true;
|
|
7801
|
+
if (["false", "0", "no", "n", "off"].includes(normalized))
|
|
7802
|
+
return false;
|
|
7803
|
+
console.error(`Error: ${optionName} must be true or false.`);
|
|
7804
|
+
process.exit(1);
|
|
7805
|
+
}
|
|
7806
|
+
function parseCsvList3(value) {
|
|
7807
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
7808
|
+
}
|
|
7809
|
+
function asNumber(value) {
|
|
7810
|
+
const n = Number(value ?? 0);
|
|
7811
|
+
return Number.isFinite(n) ? n : 0;
|
|
7812
|
+
}
|
|
7813
|
+
function money(value) {
|
|
7814
|
+
const n = Number(value ?? 0);
|
|
7815
|
+
return Number.isFinite(n) ? `$${n.toFixed(4)}` : "-";
|
|
7816
|
+
}
|
|
7817
|
+
function pct(value) {
|
|
7818
|
+
const n = Number(value ?? 0);
|
|
7819
|
+
return Number.isFinite(n) ? n.toFixed(2) : "-";
|
|
7820
|
+
}
|
|
7821
|
+
function formatApiError(json, fallback) {
|
|
7822
|
+
const value = json?.error ?? json?.errors ?? json?.message ?? json;
|
|
7823
|
+
if (value === undefined || value === null || value === "")
|
|
7824
|
+
return fallback;
|
|
7825
|
+
if (typeof value === "string")
|
|
7826
|
+
return value;
|
|
7827
|
+
return JSON.stringify(value);
|
|
7828
|
+
}
|
|
7201
7829
|
|
|
7202
7830
|
// src/commands/payouts.ts
|
|
7203
7831
|
function createPayoutsCommand() {
|
|
@@ -8117,16 +8745,22 @@ Examples:
|
|
|
8117
8745
|
addFormatOption(contracts.command("summary").description("Summarize supply contracts.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type")).action(async (opts) => {
|
|
8118
8746
|
await summarizeResource(`${consolePrefix()}/supply-contracts`, opts, "contracts");
|
|
8119
8747
|
});
|
|
8120
|
-
contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--
|
|
8748
|
+
contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--supply-product-id <id>", "Traffic package ID").option("--description <text>", "Description").option("--source-type <type>", "INTERNAL or EXTERNAL", "EXTERNAL").option("--supplier-tenant-id <id>", "Supplier tenant ID for EXTERNAL contracts").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION", "OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "OPAQUE").option("--priority <n>", "Contract-level routing priority", "0").option("--match-rules <json>", 'Request match rules JSON, e.g. [{"fieldPath":"ext.alpineads.route_key","operator":"eq","value":"hilltopads-open"}]').option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency", "USD").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
8121
8749
|
let body;
|
|
8122
8750
|
if (opts.fromJson) {
|
|
8123
8751
|
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8124
8752
|
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8125
8753
|
} else {
|
|
8754
|
+
const sourceType = String(opts.sourceType || "EXTERNAL").toUpperCase();
|
|
8755
|
+
if (sourceType === "EXTERNAL" && !opts.supplierTenantId) {
|
|
8756
|
+
console.error("Error: --supplier-tenant-id is required for EXTERNAL contracts.");
|
|
8757
|
+
process.exit(1);
|
|
8758
|
+
}
|
|
8126
8759
|
body = {
|
|
8127
8760
|
name: opts.name,
|
|
8128
8761
|
region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
|
|
8129
|
-
|
|
8762
|
+
sourceType,
|
|
8763
|
+
supplierTenantId: sourceType === "EXTERNAL" ? opts.supplierTenantId : null,
|
|
8130
8764
|
supplyProductId: opts.supplyProductId,
|
|
8131
8765
|
description: opts.description,
|
|
8132
8766
|
contractType: opts.contractType,
|
|
@@ -8149,7 +8783,7 @@ Examples:
|
|
|
8149
8783
|
}
|
|
8150
8784
|
console.log(`Supply contract created: ${json.data?.id ?? json.id}`);
|
|
8151
8785
|
});
|
|
8152
|
-
contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
8786
|
+
contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--source-type <type>", "INTERNAL or EXTERNAL").option("--supplier-tenant-id <id>", "Supplier tenant ID for EXTERNAL contracts").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
8153
8787
|
let body;
|
|
8154
8788
|
if (opts.fromJson) {
|
|
8155
8789
|
const { readFileSync: readFileSync6 } = await import("fs");
|
|
@@ -8159,6 +8793,8 @@ Examples:
|
|
|
8159
8793
|
...opts.name ? { name: opts.name } : {},
|
|
8160
8794
|
...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
|
|
8161
8795
|
...opts.description ? { description: opts.description } : {},
|
|
8796
|
+
...opts.sourceType ? { sourceType: String(opts.sourceType).toUpperCase() } : {},
|
|
8797
|
+
...opts.sourceType && String(opts.sourceType).toUpperCase() === "INTERNAL" ? { supplierTenantId: null } : opts.supplierTenantId ? { supplierTenantId: opts.supplierTenantId } : {},
|
|
8162
8798
|
...opts.contractType ? { contractType: opts.contractType } : {},
|
|
8163
8799
|
...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
|
|
8164
8800
|
...opts.priority ? { priority: Number(opts.priority) } : {},
|
|
@@ -8474,13 +9110,243 @@ Examples:
|
|
|
8474
9110
|
return cmd;
|
|
8475
9111
|
}
|
|
8476
9112
|
|
|
9113
|
+
// src/commands/oauth-clients.ts
|
|
9114
|
+
var COLUMNS11 = [
|
|
9115
|
+
{ key: "id", header: "ID", width: 36 },
|
|
9116
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
9117
|
+
{ key: "clientId", header: "CLIENT ID", width: 36 },
|
|
9118
|
+
{ key: "tenantType", header: "CAPABILITY", width: 12 },
|
|
9119
|
+
{ key: "role", header: "ROLE", width: 18 },
|
|
9120
|
+
{ key: "isActive", header: "ACTIVE", width: 8 },
|
|
9121
|
+
{ key: "createdAt", header: "CREATED", width: 20 }
|
|
9122
|
+
];
|
|
9123
|
+
function createOAuthClientsCommand() {
|
|
9124
|
+
const cmd = new Command("oauth-clients").description(`OAuth client_credentials management.
|
|
9125
|
+
|
|
9126
|
+
Requires: tenant admin or platform admin role.`).addHelpText("after", `
|
|
9127
|
+
Examples:
|
|
9128
|
+
$ a8techads oauth-clients list
|
|
9129
|
+
$ a8techads oauth-clients create --name "monedita ssp" --capability PUBLISHER
|
|
9130
|
+
$ a8techads oauth-clients create --name "ci bot" --capability ADVERTISER --save-profile ci-bot
|
|
9131
|
+
$ a8techads oauth-clients delete <id> --yes`);
|
|
9132
|
+
addFormatOption(cmd.command("list").description("List OAuth clients for the current tenant.")).action(async (opts) => {
|
|
9133
|
+
const resp = await apiRequest({ path: oauthClientPath() });
|
|
9134
|
+
const json = await resp.json();
|
|
9135
|
+
if (!resp.ok) {
|
|
9136
|
+
console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
|
|
9137
|
+
process.exit(1);
|
|
9138
|
+
}
|
|
9139
|
+
const rows = asArray(json.data ?? json).map(normalizeClient);
|
|
9140
|
+
printData(rows, COLUMNS11, opts.format);
|
|
9141
|
+
});
|
|
9142
|
+
addFormatOption(cmd.command("create").description(`Create an OAuth client bound to a tenant user.
|
|
9143
|
+
|
|
9144
|
+
The secret is returned once. Use --save-profile to store it under ~/.alpineads/oauth-client.json.`).requiredOption("--name <name>", "Client display name").option("--user-id <id>", "Bind the client to a specific tenant user; defaults to the current actor").option("--capability <capability>", "ADVERTISER or PUBLISHER. Defaults to server-side tenant role resolution.").option("--save-profile <profile>", "Save returned client_id/client_secret as a local OAuth profile").option("--api-url <url>", "API URL saved with --save-profile", "https://api.a8.tech").option("--auth-url <url>", "Auth URL saved with --save-profile", "https://auth.a8.tech")).action(async (opts) => {
|
|
9145
|
+
const body = { name: opts.name };
|
|
9146
|
+
if (opts.userId)
|
|
9147
|
+
body.user_id = opts.userId;
|
|
9148
|
+
if (opts.capability)
|
|
9149
|
+
body.capability = normalizeCapability(opts.capability);
|
|
9150
|
+
const resp = await apiRequest({ method: "POST", path: oauthClientPath(), body });
|
|
9151
|
+
const json = await resp.json();
|
|
9152
|
+
if (!resp.ok) {
|
|
9153
|
+
console.error(`Error: ${json.error ?? json.message ?? json.errors ?? resp.statusText}`);
|
|
9154
|
+
process.exit(1);
|
|
9155
|
+
}
|
|
9156
|
+
const client = json.data ?? json;
|
|
9157
|
+
const normalized = normalizeClient(client);
|
|
9158
|
+
if (opts.format === "json") {
|
|
9159
|
+
console.log(JSON.stringify(client, null, 2));
|
|
9160
|
+
} else {
|
|
9161
|
+
printDetail(normalized, opts.format);
|
|
9162
|
+
if (client.clientSecret ?? client.client_secret) {
|
|
9163
|
+
console.log(`clientSecret ${client.clientSecret ?? client.client_secret}`);
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
const clientId = client.clientId ?? client.client_id ?? client.hydraClientId ?? client.hydra_client_id;
|
|
9167
|
+
const clientSecret = client.clientSecret ?? client.client_secret;
|
|
9168
|
+
if (opts.saveProfile) {
|
|
9169
|
+
if (!clientId || !clientSecret) {
|
|
9170
|
+
console.error("Error: server response did not include clientId/clientSecret; profile was not saved.");
|
|
9171
|
+
process.exit(1);
|
|
9172
|
+
}
|
|
9173
|
+
const saved = saveOAuthClientConfig({
|
|
9174
|
+
profile: opts.saveProfile,
|
|
9175
|
+
clientId,
|
|
9176
|
+
clientSecret,
|
|
9177
|
+
apiUrl: opts.apiUrl,
|
|
9178
|
+
authUrl: opts.authUrl
|
|
9179
|
+
});
|
|
9180
|
+
console.log(`Saved OAuth profile: ${saved.profile}`);
|
|
9181
|
+
} else {
|
|
9182
|
+
console.log("Secret is shown once. Re-run with --save-profile <name> if this client should be used by CLI automation.");
|
|
9183
|
+
}
|
|
9184
|
+
});
|
|
9185
|
+
cmd.command("delete").alias("revoke").description("Revoke an OAuth client.").argument("<id>", "OAuth client ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
9186
|
+
if (!opts.yes) {
|
|
9187
|
+
console.error("Add --yes to confirm revoking this OAuth client.");
|
|
9188
|
+
process.exit(1);
|
|
9189
|
+
}
|
|
9190
|
+
const resp = await apiRequest({ method: "DELETE", path: `${oauthClientPath()}/${id}` });
|
|
9191
|
+
const json = await resp.json().catch(() => ({}));
|
|
9192
|
+
if (!resp.ok) {
|
|
9193
|
+
console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
|
|
9194
|
+
process.exit(1);
|
|
9195
|
+
}
|
|
9196
|
+
console.log(`OAuth client ${id} revoked.`);
|
|
9197
|
+
});
|
|
9198
|
+
return cmd;
|
|
9199
|
+
}
|
|
9200
|
+
function oauthClientPath() {
|
|
9201
|
+
return `${dspPrefix()}/settings/oauth-clients`;
|
|
9202
|
+
}
|
|
9203
|
+
function normalizeClient(client) {
|
|
9204
|
+
return {
|
|
9205
|
+
id: client.id,
|
|
9206
|
+
name: client.name,
|
|
9207
|
+
clientId: client.clientId ?? client.client_id ?? client.hydraClientId ?? client.hydra_client_id,
|
|
9208
|
+
tenantType: client.tenantType ?? client.tenant_type,
|
|
9209
|
+
role: client.role,
|
|
9210
|
+
isActive: client.isActive ?? client.is_active ?? true,
|
|
9211
|
+
createdAt: client.createdAt ?? client.created_at
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
function normalizeCapability(value) {
|
|
9215
|
+
const normalized = value.trim().toUpperCase();
|
|
9216
|
+
if (!["ADVERTISER", "PUBLISHER"].includes(normalized)) {
|
|
9217
|
+
console.error("Error: --capability must be ADVERTISER or PUBLISHER.");
|
|
9218
|
+
process.exit(1);
|
|
9219
|
+
}
|
|
9220
|
+
return normalized;
|
|
9221
|
+
}
|
|
9222
|
+
function asArray(value) {
|
|
9223
|
+
if (Array.isArray(value))
|
|
9224
|
+
return value;
|
|
9225
|
+
if (value && typeof value === "object") {
|
|
9226
|
+
const maybe = value;
|
|
9227
|
+
if (Array.isArray(maybe.clients))
|
|
9228
|
+
return maybe.clients;
|
|
9229
|
+
}
|
|
9230
|
+
return [];
|
|
9231
|
+
}
|
|
9232
|
+
|
|
9233
|
+
// src/commands/workflows.ts
|
|
9234
|
+
var WORKFLOWS = [
|
|
9235
|
+
{
|
|
9236
|
+
name: "profile-login",
|
|
9237
|
+
area: "Auth",
|
|
9238
|
+
description: "Create, switch, and validate human or client_credentials profiles.",
|
|
9239
|
+
commands: [
|
|
9240
|
+
"a8techads auth login --profile owner",
|
|
9241
|
+
"a8techads profile use owner",
|
|
9242
|
+
"a8techads context show",
|
|
9243
|
+
'a8techads oauth-clients create --name "monedita ssp" --capability PUBLISHER --save-profile monedita-ssp',
|
|
9244
|
+
"a8techads profile use monedita-ssp",
|
|
9245
|
+
"a8techads auth token"
|
|
9246
|
+
]
|
|
9247
|
+
},
|
|
9248
|
+
{
|
|
9249
|
+
name: "dsp-campaign",
|
|
9250
|
+
area: "DSP",
|
|
9251
|
+
description: "Create the assets needed for a campaign and move it through review.",
|
|
9252
|
+
commands: [
|
|
9253
|
+
"a8techads context dsp",
|
|
9254
|
+
"a8techads media-assets upload --file ./creative.png",
|
|
9255
|
+
"a8techads landing-pages list",
|
|
9256
|
+
'a8techads campaigns create --name "Rewarded Offer Demo" --budget 100',
|
|
9257
|
+
'a8techads variations create --campaign <campaign-id> --name "Offer A" --from-json variation.json',
|
|
9258
|
+
"a8techads variations submit <variation-id>",
|
|
9259
|
+
"a8techads campaigns submit <campaign-id>"
|
|
9260
|
+
]
|
|
9261
|
+
},
|
|
9262
|
+
{
|
|
9263
|
+
name: "conversion-goals",
|
|
9264
|
+
area: "DSP",
|
|
9265
|
+
description: "Create tracker-compatible goals and print postback instructions.",
|
|
9266
|
+
commands: [
|
|
9267
|
+
"a8techads context dsp",
|
|
9268
|
+
'a8techads conversion-goals create --name "Install" --code install --conversion-type APP_INSTALL --goal-order 1',
|
|
9269
|
+
'a8techads conversion-goals create --name "Registration" --code registration --conversion-type LEAD_SOI --goal-order 2',
|
|
9270
|
+
"a8techads conversion-goals integration <goal-id> --status install",
|
|
9271
|
+
"a8techads conversion-goals integration <goal-id> --status registration"
|
|
9272
|
+
]
|
|
9273
|
+
},
|
|
9274
|
+
{
|
|
9275
|
+
name: "ssp-inventory",
|
|
9276
|
+
area: "SSP",
|
|
9277
|
+
description: "Create SSP inventory and generate JavaScript or iframe tags.",
|
|
9278
|
+
commands: [
|
|
9279
|
+
"a8techads context ssp",
|
|
9280
|
+
'a8techads sites create --name "Monedita" --domain monedita.jogabox.net',
|
|
9281
|
+
'a8techads zones create --site <site-id> --name "Offerwall" --format offerwall',
|
|
9282
|
+
'a8techads zones create --site <site-id> --name "Rewarded" --format rewarded_video',
|
|
9283
|
+
'a8techads zones tag <offerwall-zone-id> --local --format offerwall --theme dark --sub "{PLAYER_ID}"',
|
|
9284
|
+
'a8techads zones tag <rewarded-zone-id> --local --format rewarded_video --theme dark --sub "{PLAYER_ID}"'
|
|
9285
|
+
]
|
|
9286
|
+
},
|
|
9287
|
+
{
|
|
9288
|
+
name: "ssp-reconcile",
|
|
9289
|
+
area: "Reports",
|
|
9290
|
+
description: "Pull authoritative SSP metrics for revenue reconciliation.",
|
|
9291
|
+
commands: [
|
|
9292
|
+
"a8techads context ssp",
|
|
9293
|
+
"a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country --metrics requests,fills,impressions,clicks,revenue",
|
|
9294
|
+
"a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --zone <zone-id> --format json"
|
|
9295
|
+
]
|
|
9296
|
+
},
|
|
9297
|
+
{
|
|
9298
|
+
name: "supply-ops",
|
|
9299
|
+
area: "Console",
|
|
9300
|
+
description: "Inspect supply products, contracts, leases, and runtime allocation state.",
|
|
9301
|
+
commands: [
|
|
9302
|
+
"a8techads context set-app console",
|
|
9303
|
+
"a8techads supply-ops products list",
|
|
9304
|
+
"a8techads supply-ops contracts list",
|
|
9305
|
+
"a8techads supply-ops leases list",
|
|
9306
|
+
"a8techads admin system bidding-gate show --format json"
|
|
9307
|
+
]
|
|
9308
|
+
}
|
|
9309
|
+
];
|
|
9310
|
+
var COLUMNS12 = [
|
|
9311
|
+
{ key: "name", header: "WORKFLOW", width: 24 },
|
|
9312
|
+
{ key: "area", header: "AREA", width: 10 },
|
|
9313
|
+
{ key: "description", header: "DESCRIPTION", width: 72 }
|
|
9314
|
+
];
|
|
9315
|
+
function createWorkflowsCommand() {
|
|
9316
|
+
const cmd = new Command("workflows").description("Discover page-level operating workflows and the CLI commands behind them.").addHelpText("after", `
|
|
9317
|
+
Examples:
|
|
9318
|
+
$ a8techads workflows list
|
|
9319
|
+
$ a8techads workflows show ssp-inventory
|
|
9320
|
+
$ a8techads workflows show conversion-goals`);
|
|
9321
|
+
cmd.command("list").description("List available workflows.").action(() => {
|
|
9322
|
+
printData(WORKFLOWS.map(({ name, area, description }) => ({ name, area, description })), COLUMNS12);
|
|
9323
|
+
});
|
|
9324
|
+
cmd.command("show").description("Show commands for a workflow.").argument("<name>", "Workflow name").action((name) => {
|
|
9325
|
+
const workflow = WORKFLOWS.find((item) => item.name === name);
|
|
9326
|
+
if (!workflow) {
|
|
9327
|
+
console.error(`Unknown workflow: ${name}`);
|
|
9328
|
+
console.error(`Available workflows: ${WORKFLOWS.map((item) => item.name).join(", ")}`);
|
|
9329
|
+
process.exit(1);
|
|
9330
|
+
}
|
|
9331
|
+
console.log(`${workflow.name} (${workflow.area})`);
|
|
9332
|
+
console.log(workflow.description);
|
|
9333
|
+
console.log("");
|
|
9334
|
+
for (const command of workflow.commands) {
|
|
9335
|
+
console.log(command);
|
|
9336
|
+
}
|
|
9337
|
+
});
|
|
9338
|
+
return cmd;
|
|
9339
|
+
}
|
|
9340
|
+
|
|
8477
9341
|
// src/index.ts
|
|
8478
9342
|
function createProgram() {
|
|
8479
|
-
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.
|
|
9343
|
+
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.4").addHelpText("after", `
|
|
8480
9344
|
Command Groups:
|
|
8481
9345
|
auth Authentication (login, logout, token, status)
|
|
8482
9346
|
profile Multi-profile management
|
|
8483
9347
|
context Workspace context (tenant, capability, app)
|
|
9348
|
+
workflows Page-level operating workflows and command recipes
|
|
9349
|
+
oauth-clients OAuth client_credentials management
|
|
8484
9350
|
audiences Audience management (DSP)
|
|
8485
9351
|
campaigns Campaign management (DSP)
|
|
8486
9352
|
variations Ad variation management (DSP)
|
|
@@ -8505,12 +9371,15 @@ Command Groups:
|
|
|
8505
9371
|
Getting Started:
|
|
8506
9372
|
$ a8techads auth login # Authenticate
|
|
8507
9373
|
$ a8techads context show # Check current context
|
|
9374
|
+
$ a8techads workflows list # Discover end-to-end workflows
|
|
8508
9375
|
$ a8techads campaigns list # List campaigns (DSP)
|
|
8509
9376
|
$ a8techads sites list # List sites (SSP)
|
|
8510
9377
|
$ a8techads reports query --metrics spend # Run analytics query`);
|
|
8511
9378
|
program2.addCommand(createAuthCommand());
|
|
8512
9379
|
program2.addCommand(createProfileCommand());
|
|
8513
9380
|
program2.addCommand(createContextCommand());
|
|
9381
|
+
program2.addCommand(createWorkflowsCommand());
|
|
9382
|
+
program2.addCommand(createOAuthClientsCommand());
|
|
8514
9383
|
program2.addCommand(createAudiencesCommand());
|
|
8515
9384
|
program2.addCommand(createCampaignsCommand());
|
|
8516
9385
|
program2.addCommand(createVariationsCommand());
|