@a8techads/cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/a8techads.js +1959 -149
  2. package/package.json +1 -1
package/dist/a8techads.js CHANGED
@@ -2628,6 +2628,18 @@ async function login(opts = {}) {
2628
2628
  const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL;
2629
2629
  const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
2630
2630
  const authorizationEndpoint = `${authUrl}/.ory/hydra/oauth2/auth`;
2631
+ if (opts.forceLogin) {
2632
+ const existingCreds = loadCredentials();
2633
+ if (existingCreds.profiles[profileName]) {
2634
+ delete existingCreds.profiles[profileName];
2635
+ saveCredentials(existingCreds);
2636
+ }
2637
+ const existingCtx = loadContext();
2638
+ if (existingCtx.profiles[profileName]) {
2639
+ delete existingCtx.profiles[profileName];
2640
+ saveContext(existingCtx);
2641
+ }
2642
+ }
2631
2643
  const codeVerifier = generateRandomCodeVerifier();
2632
2644
  const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
2633
2645
  const state = generateRandomState();
@@ -3202,6 +3214,10 @@ function createProfileCommand() {
3202
3214
 
3203
3215
  // src/utils/http.ts
3204
3216
  async function apiRequest(opts) {
3217
+ const request = await buildAuthenticatedRequest(opts);
3218
+ return fetch(request.url, request.init);
3219
+ }
3220
+ async function buildAuthenticatedRequest(opts) {
3205
3221
  await refreshTokenIfNeeded();
3206
3222
  const creds = loadCredentials();
3207
3223
  const profile = getCurrentProfile(creds);
@@ -3216,21 +3232,23 @@ async function apiRequest(opts) {
3216
3232
  "Content-Type": "application/json",
3217
3233
  ...opts.headers
3218
3234
  };
3219
- if (context?.current_capability) {
3220
- headers["X-Effective-Capability"] = context.current_capability;
3221
- }
3222
3235
  if (context?.impersonation) {
3223
3236
  headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
3224
3237
  if (context.impersonation.effective_capability) {
3225
3238
  headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
3226
3239
  }
3240
+ } else if (context?.current_capability) {
3241
+ headers["X-Effective-Capability"] = context.current_capability;
3227
3242
  }
3228
- const url = `${profile.api_url}${opts.path}`;
3229
- return fetch(url, {
3230
- method: opts.method ?? "GET",
3231
- headers,
3232
- body: opts.body ? JSON.stringify(opts.body) : undefined
3233
- });
3243
+ const url = `${opts.baseUrl ?? profile.api_url}${opts.path}`;
3244
+ return {
3245
+ url,
3246
+ init: {
3247
+ method: opts.method ?? "GET",
3248
+ headers,
3249
+ body: opts.body ? JSON.stringify(opts.body) : undefined
3250
+ }
3251
+ };
3234
3252
  }
3235
3253
  function validateTenantMatch(accessToken, contextTenantId) {
3236
3254
  const claims = decodeJwt(accessToken);
@@ -3538,6 +3556,9 @@ function dspPrefix() {
3538
3556
  function sspPrefix() {
3539
3557
  return "/api/v1/ssp";
3540
3558
  }
3559
+ function consolePrefix() {
3560
+ return "/api/v1/console";
3561
+ }
3541
3562
 
3542
3563
  // src/utils/output.ts
3543
3564
  function printData(data, columns, format = "table") {
@@ -3646,21 +3667,24 @@ Examples:
3646
3667
  printData(rows, COLUMNS, opts.format);
3647
3668
  });
3648
3669
  addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
3649
- const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
3650
- const json = await resp.json();
3651
- if (!resp.ok) {
3652
- console.error(`Error: ${json.error ?? resp.statusText}`);
3653
- process.exit(1);
3654
- }
3655
- printDetail(json.data ?? json, opts.format);
3670
+ const audience = await fetchAudience(id);
3671
+ printDetail(audience, opts.format);
3656
3672
  });
3657
- cmd.command("create").description(`Create a new audience.
3658
-
3659
- Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST or RETARGETING", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3673
+ addFormatOption(cmd.command("size").description("Show the current estimated audience size.").argument("<id>", "Audience ID")).action(async (id, opts) => {
3674
+ const audience = await fetchAudience(id);
3675
+ const detail = {
3676
+ id: audience.id,
3677
+ name: audience.name,
3678
+ status: audience.status,
3679
+ estimatedSize: audience.estimatedSize ?? audience.estimated_size ?? null
3680
+ };
3681
+ printDetail(detail, opts.format);
3682
+ });
3683
+ cmd.command("create").description("Create a new audience.").option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST, RETARGETING, or LOOKALIKE", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--seed <id>", "Seed audience ID (required for LOOKALIKE type)").option("--ratio <n>", "Expansion ratio for LOOKALIKE (default: 0.05)", "0.05").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3660
3684
  Examples:
3661
3685
  $ a8techads audiences create --name "High-Value Customers"
3662
- $ a8techads audiences create --name "Retarget Pool" --ttl 30
3663
- $ a8techads audiences create --from-json audience.json`).action(async (opts) => {
3686
+ $ a8techads audiences create --name "Retarget Pool" --type RETARGETING --goal-id <id>
3687
+ $ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
3664
3688
  let body;
3665
3689
  if (opts.fromJson) {
3666
3690
  const { readFileSync: readFileSync3 } = await import("fs");
@@ -3674,6 +3698,10 @@ Examples:
3674
3698
  console.error("Error: --goal-id is required for RETARGETING type.");
3675
3699
  process.exit(1);
3676
3700
  }
3701
+ if (opts.type === "LOOKALIKE" && !opts.seed) {
3702
+ console.error("Error: --seed is required for LOOKALIKE type.");
3703
+ process.exit(1);
3704
+ }
3677
3705
  body = {
3678
3706
  name: opts.name,
3679
3707
  type: opts.type,
@@ -3684,6 +3712,9 @@ Examples:
3684
3712
  if (opts.goalId) {
3685
3713
  body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
3686
3714
  }
3715
+ if (opts.seed) {
3716
+ body.rules = { seed_audience_id: opts.seed, expansion_ratio: Number(opts.ratio) };
3717
+ }
3687
3718
  }
3688
3719
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
3689
3720
  const json = await resp.json();
@@ -3796,8 +3827,41 @@ File format (one per line):
3796
3827
  }
3797
3828
  console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
3798
3829
  });
3830
+ cmd.command("wait-ready").description("Poll audience status until it becomes READY, ACTIVE, or ERROR.").argument("<id>", "Audience ID").option("--timeout <seconds>", "Timeout in seconds", "120").option("--interval <seconds>", "Polling interval in seconds", "3").action(async (id, opts) => {
3831
+ const timeoutMs = Number(opts.timeout) * 1000;
3832
+ const intervalMs = Number(opts.interval) * 1000;
3833
+ const started = Date.now();
3834
+ while (true) {
3835
+ const audience = await fetchAudience(id);
3836
+ const status = audience.status;
3837
+ const size = audience.estimatedSize ?? audience.estimated_size ?? null;
3838
+ console.log(`Status: ${status}${size != null ? ` | Size: ${Number(size).toLocaleString()}` : ""}`);
3839
+ if (status === "READY" || status === "ACTIVE") {
3840
+ console.log(`Audience ${id} is ready.`);
3841
+ return;
3842
+ }
3843
+ if (status === "ERROR" || status === "ARCHIVED") {
3844
+ console.error(`Audience ${id} reached terminal status: ${status}`);
3845
+ process.exit(1);
3846
+ }
3847
+ if (Date.now() - started >= timeoutMs) {
3848
+ console.error(`Timed out waiting for audience ${id}.`);
3849
+ process.exit(1);
3850
+ }
3851
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
3852
+ }
3853
+ });
3799
3854
  return cmd;
3800
3855
  }
3856
+ async function fetchAudience(id) {
3857
+ const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
3858
+ const json = await resp.json();
3859
+ if (!resp.ok) {
3860
+ console.error(`Error: ${json.error ?? resp.statusText}`);
3861
+ process.exit(1);
3862
+ }
3863
+ return json.data ?? json;
3864
+ }
3801
3865
 
3802
3866
  // src/commands/campaigns.ts
3803
3867
  var COLUMNS2 = [
@@ -3942,8 +4006,178 @@ Examples:
3942
4006
  }
3943
4007
  console.log(`Campaign duplicated. New ID: ${json.data?.id ?? json.id}`);
3944
4008
  });
4009
+ const targetingCmd = new Command("targeting").description("Show or update campaign targeting without editing full JSON.").addHelpText("after", `
4010
+ Examples:
4011
+ $ a8techads campaigns targeting show <campaign-id>
4012
+ $ a8techads campaigns targeting set-geo <campaign-id> --countries US,CA --mode target
4013
+ $ a8techads campaigns targeting add-audience <campaign-id> --audience <id> --mode include
4014
+ $ a8techads campaigns targeting set-day-parting <campaign-id> --timezone UTC --from-json schedule.json`);
4015
+ addFormatOption(targetingCmd.command("show").description("Show campaign targeting, audience include/exclude, and day parting.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
4016
+ const campaign = await fetchCampaign(id);
4017
+ const detail = {
4018
+ campaignId: campaign.id,
4019
+ campaignName: campaign.name,
4020
+ targeting: campaign.targeting ?? {},
4021
+ timezone: campaign.timezone ?? "UTC",
4022
+ dayParting: campaign.dayParting ?? null
4023
+ };
4024
+ printDetail(detail, opts.format);
4025
+ });
4026
+ targetingCmd.command("set-geo").description("Replace campaign geo countries and mode.").argument("<id>", "Campaign ID").requiredOption("--countries <codes>", "Comma-separated country codes, e.g. US,CA").option("--mode <mode>", "target or block", "target").action(async (id, opts) => {
4027
+ const campaign = await fetchCampaign(id);
4028
+ const targeting = normalizeTargeting(campaign.targeting);
4029
+ const countries = parseCsvList(opts.countries);
4030
+ const mode = opts.mode === "block" ? "block" : "target";
4031
+ targeting.geo = {
4032
+ ...targeting.geo ?? {},
4033
+ countries,
4034
+ mode,
4035
+ countriesMode: mode
4036
+ };
4037
+ await patchCampaign(id, { targeting });
4038
+ console.log(`Campaign ${id} geo targeting updated.`);
4039
+ });
4040
+ targetingCmd.command("add-audience").description("Add an audience to include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
4041
+ const mode = normalizeAudienceMode(opts.mode);
4042
+ const campaign = await fetchCampaign(id);
4043
+ const targeting = normalizeTargeting(campaign.targeting);
4044
+ const audiences = normalizeAudiences(targeting.audiences);
4045
+ const list = new Set(mode === "include" ? audiences.include : audiences.exclude);
4046
+ list.add(opts.audience);
4047
+ targeting.audiences = {
4048
+ ...audiences,
4049
+ [mode]: Array.from(list)
4050
+ };
4051
+ await patchCampaign(id, { targeting });
4052
+ console.log(`Campaign ${id} audience ${opts.audience} added to ${mode}.`);
4053
+ });
4054
+ targetingCmd.command("remove-audience").description("Remove an audience from include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
4055
+ const mode = normalizeAudienceMode(opts.mode);
4056
+ const campaign = await fetchCampaign(id);
4057
+ const targeting = normalizeTargeting(campaign.targeting);
4058
+ const audiences = normalizeAudiences(targeting.audiences);
4059
+ targeting.audiences = {
4060
+ ...audiences,
4061
+ [mode]: (mode === "include" ? audiences.include : audiences.exclude).filter((audId) => audId !== opts.audience)
4062
+ };
4063
+ await patchCampaign(id, { targeting });
4064
+ console.log(`Campaign ${id} audience ${opts.audience} removed from ${mode}.`);
4065
+ });
4066
+ targetingCmd.command("set-day-parting").description("Set or disable day parting schedule.").argument("<id>", "Campaign ID").option("--timezone <tz>", "Timezone (default: keep existing or UTC)").option("--from-json <file>", "Schedule JSON file").option("--disable", "Disable day parting").action(async (id, opts) => {
4067
+ if (!opts.disable && !opts.fromJson) {
4068
+ console.error("Error: provide --from-json <file> or --disable.");
4069
+ process.exit(1);
4070
+ }
4071
+ const campaign = await fetchCampaign(id);
4072
+ let dayParting;
4073
+ if (opts.disable) {
4074
+ dayParting = null;
4075
+ } else {
4076
+ const { readFileSync: readFileSync3 } = await import("fs");
4077
+ dayParting = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4078
+ }
4079
+ await patchCampaign(id, {
4080
+ dayParting,
4081
+ timezone: opts.timezone ?? campaign.timezone ?? "UTC"
4082
+ });
4083
+ console.log(`Campaign ${id} day parting updated.`);
4084
+ });
4085
+ for (const [resourceName, field] of [
4086
+ ["domain", "domains"],
4087
+ ["keyword", "keywords"],
4088
+ ["ip-range", "ipRanges"]
4089
+ ]) {
4090
+ targetingCmd.command(`add-${resourceName}`).description(`Add a ${resourceName} to targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to add`).option("--mode <mode>", "target or block (default: keep current or target)", "target").action(async (id, opts) => {
4091
+ const campaign = await fetchCampaign(id);
4092
+ const targeting = normalizeTargeting(campaign.targeting);
4093
+ const existing = normalizeListTargeting(targeting[field]);
4094
+ const value = String(opts[camelizeOption(resourceName)]).trim();
4095
+ if (!value) {
4096
+ console.error(`Error: --${resourceName} is required.`);
4097
+ process.exit(1);
4098
+ }
4099
+ const list = new Set(existing.list);
4100
+ list.add(value);
4101
+ targeting[field] = {
4102
+ mode: opts.mode === "block" ? "block" : existing.mode,
4103
+ list: Array.from(list)
4104
+ };
4105
+ await patchCampaign(id, { targeting });
4106
+ console.log(`Campaign ${id} ${resourceName} added.`);
4107
+ });
4108
+ targetingCmd.command(`remove-${resourceName}`).description(`Remove a ${resourceName} from targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to remove`).action(async (id, opts) => {
4109
+ const campaign = await fetchCampaign(id);
4110
+ const targeting = normalizeTargeting(campaign.targeting);
4111
+ const existing = normalizeListTargeting(targeting[field]);
4112
+ const value = String(opts[camelizeOption(resourceName)]).trim();
4113
+ targeting[field] = {
4114
+ ...existing,
4115
+ list: existing.list.filter((item) => item !== value)
4116
+ };
4117
+ await patchCampaign(id, { targeting });
4118
+ console.log(`Campaign ${id} ${resourceName} removed.`);
4119
+ });
4120
+ targetingCmd.command(`set-${resourceName}-mode`).description(`Set ${resourceName} targeting mode.`).argument("<id>", "Campaign ID").requiredOption("--mode <mode>", "target or block").action(async (id, opts) => {
4121
+ const campaign = await fetchCampaign(id);
4122
+ const targeting = normalizeTargeting(campaign.targeting);
4123
+ const existing = normalizeListTargeting(targeting[field]);
4124
+ targeting[field] = {
4125
+ ...existing,
4126
+ mode: opts.mode === "block" ? "block" : "target"
4127
+ };
4128
+ await patchCampaign(id, { targeting });
4129
+ console.log(`Campaign ${id} ${resourceName} mode updated.`);
4130
+ });
4131
+ }
4132
+ cmd.addCommand(targetingCmd);
3945
4133
  return cmd;
3946
4134
  }
4135
+ async function fetchCampaign(id) {
4136
+ const resp = await apiRequest({ path: `${dspPrefix()}/campaigns/${id}` });
4137
+ const json = await resp.json();
4138
+ if (!resp.ok) {
4139
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4140
+ process.exit(1);
4141
+ }
4142
+ return json.data ?? json;
4143
+ }
4144
+ async function patchCampaign(id, body) {
4145
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
4146
+ const json = await resp.json().catch(() => ({}));
4147
+ if (!resp.ok) {
4148
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4149
+ process.exit(1);
4150
+ }
4151
+ }
4152
+ function parseCsvList(value) {
4153
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
4154
+ }
4155
+ function normalizeAudienceMode(value) {
4156
+ const normalized = String(value).toLowerCase();
4157
+ if (normalized !== "include" && normalized !== "exclude") {
4158
+ console.error("Error: --mode must be include or exclude.");
4159
+ process.exit(1);
4160
+ }
4161
+ return normalized;
4162
+ }
4163
+ function normalizeTargeting(targeting) {
4164
+ return targeting && typeof targeting === "object" ? { ...targeting } : {};
4165
+ }
4166
+ function normalizeAudiences(audiences) {
4167
+ return {
4168
+ include: Array.isArray(audiences?.include) ? audiences.include : [],
4169
+ exclude: Array.isArray(audiences?.exclude) ? audiences.exclude : []
4170
+ };
4171
+ }
4172
+ function normalizeListTargeting(value) {
4173
+ return {
4174
+ mode: value?.mode === "block" ? "block" : "target",
4175
+ list: Array.isArray(value?.list) ? value.list : []
4176
+ };
4177
+ }
4178
+ function camelizeOption(value) {
4179
+ return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
4180
+ }
3947
4181
 
3948
4182
  // src/commands/variations.ts
3949
4183
  var COLUMNS3 = [
@@ -4041,8 +4275,218 @@ Examples:
4041
4275
  return cmd;
4042
4276
  }
4043
4277
 
4044
- // src/commands/sites.ts
4278
+ // src/commands/media-assets.ts
4279
+ import { basename, extname } from "node:path";
4280
+ import { readFileSync as readFileSync3 } from "node:fs";
4045
4281
  var COLUMNS4 = [
4282
+ { key: "id", header: "ID", width: 36 },
4283
+ { key: "name", header: "NAME", width: 24 },
4284
+ { key: "type", header: "TYPE", width: 10 },
4285
+ { key: "status", header: "STATUS", width: 10 },
4286
+ { key: "adFormat", header: "FORMAT", width: 12 },
4287
+ { key: "mimeType", header: "MIME", width: 20 }
4288
+ ];
4289
+ function inferMimeType(filePath) {
4290
+ switch (extname(filePath).toLowerCase()) {
4291
+ case ".png":
4292
+ return "image/png";
4293
+ case ".jpg":
4294
+ case ".jpeg":
4295
+ return "image/jpeg";
4296
+ case ".gif":
4297
+ return "image/gif";
4298
+ case ".webp":
4299
+ return "image/webp";
4300
+ case ".svg":
4301
+ return "image/svg+xml";
4302
+ case ".mp4":
4303
+ return "video/mp4";
4304
+ case ".mov":
4305
+ return "video/quicktime";
4306
+ case ".html":
4307
+ return "text/html";
4308
+ case ".txt":
4309
+ return "text/plain";
4310
+ default:
4311
+ return "application/octet-stream";
4312
+ }
4313
+ }
4314
+ function normalizeAsset(asset) {
4315
+ return {
4316
+ id: asset.id,
4317
+ name: asset.name,
4318
+ type: asset.type,
4319
+ status: asset.status,
4320
+ adFormat: asset.adFormat ?? asset.ad_format,
4321
+ mimeType: asset.mimeType ?? asset.mime_type
4322
+ };
4323
+ }
4324
+ function createMediaAssetsCommand() {
4325
+ const cmd = new Command("media-assets").description(`Media library management (DSP)
4326
+
4327
+ Requires: ADVERTISER capability.`).addHelpText("after", `
4328
+ Examples:
4329
+ $ a8techads media-assets list
4330
+ $ a8techads media-assets get <id>
4331
+ $ a8techads media-assets upload --file ./banner.png --ad-format BANNER
4332
+ $ a8techads media-assets pause <id>`);
4333
+ addFormatOption(cmd.command("list").description("List media assets.").option("--status <status>", "Filter by status").option("--type <type>", "Filter by type").option("--ad-format <format>", "Filter by ad format").option("--search <text>", "Search by name").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
4334
+ const params = new URLSearchParams;
4335
+ params.set("limit", opts.limit);
4336
+ params.set("offset", opts.offset);
4337
+ if (opts.status)
4338
+ params.set("status", opts.status);
4339
+ if (opts.type)
4340
+ params.set("type", opts.type);
4341
+ if (opts.adFormat)
4342
+ params.set("adFormat", opts.adFormat);
4343
+ if (opts.search)
4344
+ params.set("search", opts.search);
4345
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets?${params}` });
4346
+ const json = await resp.json();
4347
+ if (!resp.ok) {
4348
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4349
+ process.exit(1);
4350
+ }
4351
+ const rows = (json.data ?? json).map(normalizeAsset);
4352
+ printData(rows, COLUMNS4, opts.format);
4353
+ });
4354
+ addFormatOption(cmd.command("active").description("List active media assets.").option("--ad-format <format>", "Filter by ad format").option("--width <n>", "Required width").option("--height <n>", "Required height").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
4355
+ const params = new URLSearchParams;
4356
+ params.set("limit", opts.limit);
4357
+ params.set("offset", opts.offset);
4358
+ if (opts.adFormat)
4359
+ params.set("adFormat", opts.adFormat);
4360
+ if (opts.width)
4361
+ params.set("width", opts.width);
4362
+ if (opts.height)
4363
+ params.set("height", opts.height);
4364
+ const resp = await apiRequest({
4365
+ path: `${dspPrefix()}/media-assets/active?${params}`
4366
+ });
4367
+ const json = await resp.json();
4368
+ if (!resp.ok) {
4369
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4370
+ process.exit(1);
4371
+ }
4372
+ const rows = (json.data ?? json).map(normalizeAsset);
4373
+ printData(rows, COLUMNS4, opts.format);
4374
+ });
4375
+ addFormatOption(cmd.command("get").description("Get media asset details.").argument("<id>", "Media asset ID")).action(async (id, opts) => {
4376
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/${id}` });
4377
+ const json = await resp.json();
4378
+ if (!resp.ok) {
4379
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4380
+ process.exit(1);
4381
+ }
4382
+ printDetail(json.data ?? json, opts.format);
4383
+ });
4384
+ addFormatOption(cmd.command("size-limits").description("Get media asset size limits.")).action(async (opts) => {
4385
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/size-limits` });
4386
+ const json = await resp.json();
4387
+ if (!resp.ok) {
4388
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4389
+ process.exit(1);
4390
+ }
4391
+ printDetail(json.data ?? json, opts.format);
4392
+ });
4393
+ cmd.command("upload").description("Upload a media asset file.").requiredOption("--file <path>", "Path to local file").option("--name <name>", "Override asset name").option("--ad-format <format>", "Ad format, e.g. BANNER, NATIVE, VIDEO").action(async (opts) => {
4394
+ const filePath = opts.file;
4395
+ const fileName = basename(filePath);
4396
+ const request = await buildAuthenticatedRequest({
4397
+ method: "POST",
4398
+ path: `${dspPrefix()}/media-assets/upload`,
4399
+ headers: {}
4400
+ });
4401
+ const headers = new Headers(request.init.headers);
4402
+ headers.delete("Content-Type");
4403
+ const body = new FormData;
4404
+ const fileBytes = readFileSync3(filePath);
4405
+ body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
4406
+ if (opts.name)
4407
+ body.append("name", opts.name);
4408
+ if (opts.adFormat)
4409
+ body.append("adFormat", opts.adFormat);
4410
+ const resp = await fetch(request.url, {
4411
+ ...request.init,
4412
+ headers,
4413
+ body
4414
+ });
4415
+ const json = await resp.json();
4416
+ if (!resp.ok) {
4417
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4418
+ process.exit(1);
4419
+ }
4420
+ const asset = json.data ?? json;
4421
+ console.log(`Media asset uploaded: ${asset.id}`);
4422
+ if (asset.name)
4423
+ console.log(` Name: ${asset.name}`);
4424
+ if (asset.cdnUrl ?? asset.cdn_url) {
4425
+ console.log(` CDN URL: ${asset.cdnUrl ?? asset.cdn_url}`);
4426
+ }
4427
+ });
4428
+ cmd.command("update").description("Update media asset metadata.").argument("<id>", "Media asset ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--product-category <value>", "Product category").option("--target-audience <value>", "Target audience").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4429
+ let body = {};
4430
+ if (opts.fromJson) {
4431
+ body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4432
+ } else {
4433
+ if (opts.name)
4434
+ body.name = opts.name;
4435
+ if (opts.description)
4436
+ body.description = opts.description;
4437
+ if (opts.productCategory)
4438
+ body.productCategory = opts.productCategory;
4439
+ if (opts.targetAudience)
4440
+ body.targetAudience = opts.targetAudience;
4441
+ }
4442
+ const resp = await apiRequest({
4443
+ method: "PATCH",
4444
+ path: `${dspPrefix()}/media-assets/${id}`,
4445
+ body
4446
+ });
4447
+ const json = await resp.json();
4448
+ if (!resp.ok) {
4449
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4450
+ process.exit(1);
4451
+ }
4452
+ console.log(`Media asset ${id} updated.`);
4453
+ });
4454
+ cmd.command("delete").description("Delete a media asset.").argument("<id>", "Media asset ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
4455
+ if (!opts.yes) {
4456
+ console.error("Add --yes to confirm deletion.");
4457
+ process.exit(1);
4458
+ }
4459
+ const resp = await apiRequest({
4460
+ method: "DELETE",
4461
+ path: `${dspPrefix()}/media-assets/${id}`
4462
+ });
4463
+ if (!resp.ok && resp.status !== 204) {
4464
+ const json = await resp.json();
4465
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4466
+ process.exit(1);
4467
+ }
4468
+ console.log(`Media asset ${id} deleted.`);
4469
+ });
4470
+ for (const action of ["pause", "resume", "archive", "unarchive"]) {
4471
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a media asset.`).argument("<id>", "Media asset ID").action(async (id) => {
4472
+ const resp = await apiRequest({
4473
+ method: "PATCH",
4474
+ path: `${dspPrefix()}/media-assets/${id}/${action}`,
4475
+ body: {}
4476
+ });
4477
+ const json = await resp.json();
4478
+ if (!resp.ok) {
4479
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4480
+ process.exit(1);
4481
+ }
4482
+ console.log(`Media asset ${id} ${action}d.`);
4483
+ });
4484
+ }
4485
+ return cmd;
4486
+ }
4487
+
4488
+ // src/commands/sites.ts
4489
+ var COLUMNS5 = [
4046
4490
  { key: "id", header: "ID", width: 36 },
4047
4491
  { key: "name", header: "NAME", width: 25 },
4048
4492
  { key: "domain", header: "DOMAIN", width: 25 },
@@ -4075,7 +4519,7 @@ Examples:
4075
4519
  status: s.status,
4076
4520
  zoneCount: s.zoneCount ?? s.zone_count ?? "-"
4077
4521
  }));
4078
- printData(rows, COLUMNS4, opts.format);
4522
+ printData(rows, COLUMNS5, opts.format);
4079
4523
  });
4080
4524
  addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
4081
4525
  const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
@@ -4089,8 +4533,8 @@ Examples:
4089
4533
  cmd.command("create").description("Create a new site.").option("--name <name>", "Site name (required)").option("--domain <domain>", "Site domain (required)").option("--type <type>", "Site type: WEB, APP, CTV", "WEB").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4090
4534
  let body;
4091
4535
  if (opts.fromJson) {
4092
- const { readFileSync: readFileSync3 } = await import("fs");
4093
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4536
+ const { readFileSync: readFileSync4 } = await import("fs");
4537
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4094
4538
  } else {
4095
4539
  if (!opts.name || !opts.domain) {
4096
4540
  console.error("Error: --name and --domain are required.");
@@ -4109,8 +4553,8 @@ Examples:
4109
4553
  cmd.command("update").description("Update a site.").argument("<id>", "Site ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4110
4554
  let body;
4111
4555
  if (opts.fromJson) {
4112
- const { readFileSync: readFileSync3 } = await import("fs");
4113
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4556
+ const { readFileSync: readFileSync4 } = await import("fs");
4557
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4114
4558
  } else {
4115
4559
  body = {};
4116
4560
  if (opts.name)
@@ -4161,7 +4605,7 @@ Examples:
4161
4605
  }
4162
4606
 
4163
4607
  // src/commands/zones.ts
4164
- var COLUMNS5 = [
4608
+ var COLUMNS6 = [
4165
4609
  { key: "id", header: "ID", width: 36 },
4166
4610
  { key: "name", header: "NAME", width: 25 },
4167
4611
  { key: "format", header: "FORMAT", width: 18 },
@@ -4193,7 +4637,7 @@ Examples:
4193
4637
  format: z.adFormat ?? z.ad_format ?? "-",
4194
4638
  status: z.status
4195
4639
  }));
4196
- printData(rows, COLUMNS5, opts.format);
4640
+ printData(rows, COLUMNS6, opts.format);
4197
4641
  });
4198
4642
  addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
4199
4643
  const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
@@ -4207,8 +4651,8 @@ Examples:
4207
4651
  cmd.command("create").description("Create a new ad zone.").option("--site <id>", "Site ID (required)").option("--name <name>", "Zone name (required)").option("--format <format>", "Ad format (e.g., banner_300x250)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4208
4652
  let body;
4209
4653
  if (opts.fromJson) {
4210
- const { readFileSync: readFileSync3 } = await import("fs");
4211
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4654
+ const { readFileSync: readFileSync4 } = await import("fs");
4655
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4212
4656
  } else {
4213
4657
  if (!opts.site || !opts.name) {
4214
4658
  console.error("Error: --site and --name are required.");
@@ -4229,8 +4673,8 @@ Examples:
4229
4673
  cmd.command("update").description("Update a zone.").argument("<id>", "Zone ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4230
4674
  let body;
4231
4675
  if (opts.fromJson) {
4232
- const { readFileSync: readFileSync3 } = await import("fs");
4233
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4676
+ const { readFileSync: readFileSync4 } = await import("fs");
4677
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4234
4678
  } else {
4235
4679
  body = {};
4236
4680
  if (opts.name)
@@ -4279,11 +4723,12 @@ Examples:
4279
4723
  $ a8techads reports templates`);
4280
4724
  addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
4281
4725
 
4282
- Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
4726
+ Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo,goal,goal_id,goal_order)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
4283
4727
  Examples:
4284
4728
  $ a8techads reports query --metrics spend,impressions --dimensions date --from 2026-03-01 --to 2026-03-20
4285
4729
  $ a8techads reports query --metrics revenue --dimensions publisher --format json
4286
- $ a8techads reports query --dimensions geo --limit 10`)).action(async (opts) => {
4730
+ $ a8techads reports query --dimensions geo --limit 10
4731
+ $ a8techads reports query --dimensions goal,date --metrics conversions,spend,cpa --from 2026-03-01`)).action(async (opts) => {
4287
4732
  const body = {
4288
4733
  metrics: opts.metrics.split(","),
4289
4734
  dimensions: opts.dimensions.split(","),
@@ -4299,19 +4744,20 @@ Examples:
4299
4744
  console.error(`Error: ${json.error ?? resp.statusText}`);
4300
4745
  process.exit(1);
4301
4746
  }
4302
- const rows = json.data?.rows ?? json.rows ?? [];
4747
+ const data = json.data ?? json;
4748
+ const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
4303
4749
  if (rows.length === 0) {
4304
4750
  console.log("No results.");
4305
4751
  return;
4306
4752
  }
4307
4753
  if (opts.format === "json") {
4308
- console.log(JSON.stringify(json.data ?? json, null, 2));
4754
+ console.log(JSON.stringify(data, null, 2));
4309
4755
  return;
4310
4756
  }
4311
4757
  const keys = Object.keys(rows[0]);
4312
4758
  const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
4313
4759
  printData(rows, columns, opts.format);
4314
- const summary = json.data?.summary ?? json.summary;
4760
+ const summary = data.summary ?? data.totals ?? json.summary ?? json.totals;
4315
4761
  if (summary && opts.format === "table") {
4316
4762
  console.log(`
4317
4763
  Summary:`);
@@ -4327,7 +4773,9 @@ Summary:`);
4327
4773
  console.error(`Error: ${json.error ?? resp.statusText}`);
4328
4774
  process.exit(1);
4329
4775
  }
4330
- const rows = (json.data ?? json).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
4776
+ const data = json.data ?? json;
4777
+ const reports = data.reports ?? json.reports ?? data;
4778
+ const rows = (Array.isArray(reports) ? reports : []).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
4331
4779
  printData(rows, [
4332
4780
  { key: "id", header: "ID", width: 36 },
4333
4781
  { key: "name", header: "NAME", width: 30 },
@@ -4350,7 +4798,9 @@ Summary:`);
4350
4798
  console.error(`Error: ${json.error ?? resp.statusText}`);
4351
4799
  process.exit(1);
4352
4800
  }
4353
- const rows = (json.data ?? json).map((t) => ({ id: t.id, name: t.name, description: t.description }));
4801
+ const data = json.data ?? json;
4802
+ const templates = data.templates ?? json.templates ?? data;
4803
+ const rows = (Array.isArray(templates) ? templates : []).map((t) => ({ id: t.id, name: t.name, description: t.description }));
4354
4804
  printData(rows, [
4355
4805
  { key: "id", header: "ID", width: 20 },
4356
4806
  { key: "name", header: "NAME", width: 25 },
@@ -4360,8 +4810,8 @@ Summary:`);
4360
4810
  cmd.command("create").description("Create a saved report.").option("--name <name>", "Report name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4361
4811
  let body;
4362
4812
  if (opts.fromJson) {
4363
- const { readFileSync: readFileSync3 } = await import("fs");
4364
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4813
+ const { readFileSync: readFileSync4 } = await import("fs");
4814
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4365
4815
  } else {
4366
4816
  if (!opts.name) {
4367
4817
  console.error("Error: --name is required.");
@@ -4463,8 +4913,8 @@ Examples:
4463
4913
  cmd.command("settings-update").description("Update billing settings.").option("--auto-recharge <bool>", "Enable/disable auto-recharge (true/false)").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4464
4914
  let body;
4465
4915
  if (opts.fromJson) {
4466
- const { readFileSync: readFileSync3 } = await import("fs");
4467
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4916
+ const { readFileSync: readFileSync4 } = await import("fs");
4917
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4468
4918
  } else {
4469
4919
  body = {};
4470
4920
  if (opts.autoRecharge !== undefined)
@@ -4478,6 +4928,60 @@ Examples:
4478
4928
  }
4479
4929
  console.log("Billing settings updated.");
4480
4930
  });
4931
+ addFormatOption(cmd.command("payment-methods").description("List saved payment methods.")).action(async (opts) => {
4932
+ const resp = await apiRequest({ path: `${dspPrefix()}/payments/stripe/payment-methods` });
4933
+ const json = await resp.json();
4934
+ if (!resp.ok) {
4935
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4936
+ process.exit(1);
4937
+ }
4938
+ const columns = [
4939
+ { key: "id", header: "ID", width: 30 },
4940
+ { key: "type", header: "TYPE", width: 10 },
4941
+ { key: "last4", header: "LAST4", width: 6 },
4942
+ { key: "brand", header: "BRAND", width: 12 },
4943
+ { key: "expires", header: "EXPIRES", width: 10 }
4944
+ ];
4945
+ const rows = (json.data ?? json).map((pm) => ({
4946
+ id: pm.id,
4947
+ type: pm.type,
4948
+ last4: pm.last4 ?? pm.card?.last4,
4949
+ brand: pm.brand ?? pm.card?.brand,
4950
+ expires: pm.expires ?? (pm.card ? `${pm.card.exp_month}/${pm.card.exp_year}` : "-")
4951
+ }));
4952
+ printData(rows, columns, opts.format);
4953
+ });
4954
+ cmd.command("usdt-deposit").description("Initiate a USDT deposit.").option("--amount <dollars>", "Deposit amount in dollars (required)").option("--network <network>", "Blockchain network", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
4955
+ if (!opts.amount) {
4956
+ console.error("Error: --amount is required.");
4957
+ process.exit(1);
4958
+ }
4959
+ const amount = Number(opts.amount);
4960
+ if (isNaN(amount) || amount <= 0) {
4961
+ console.error("Error: Amount must be a positive number.");
4962
+ process.exit(1);
4963
+ }
4964
+ if (!opts.yes) {
4965
+ const rl = await import("readline");
4966
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
4967
+ const answer = await new Promise((resolve) => iface.question(`Initiate USDT deposit of $${amount.toFixed(2)}? (y/N) `, resolve));
4968
+ iface.close();
4969
+ if (answer.toLowerCase() !== "y") {
4970
+ console.log("Cancelled.");
4971
+ process.exit(0);
4972
+ }
4973
+ }
4974
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/payments/usdt/checkout`, body: { amount, network: opts.network } });
4975
+ const json = await resp.json();
4976
+ if (!resp.ok) {
4977
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4978
+ process.exit(1);
4979
+ }
4980
+ console.log(`USDT deposit of $${amount.toFixed(2)} initiated.`);
4981
+ if (json.data?.paymentUrl || json.paymentUrl) {
4982
+ console.log(`Payment URL: ${json.data?.paymentUrl ?? json.paymentUrl}`);
4983
+ }
4984
+ });
4481
4985
  return cmd;
4482
4986
  }
4483
4987
 
@@ -4494,7 +4998,7 @@ function usersPrefix() {
4494
4998
  }
4495
4999
  return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4496
5000
  }
4497
- var COLUMNS6 = [
5001
+ var COLUMNS7 = [
4498
5002
  { key: "id", header: "ID", width: 36 },
4499
5003
  { key: "email", header: "EMAIL", width: 30 },
4500
5004
  { key: "name", header: "NAME", width: 20 },
@@ -4525,7 +5029,7 @@ Examples:
4525
5029
  role: u.role,
4526
5030
  status: u.status
4527
5031
  }));
4528
- printData(rows, COLUMNS6, opts.format);
5032
+ printData(rows, COLUMNS7, opts.format);
4529
5033
  });
4530
5034
  addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
4531
5035
  const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
@@ -4605,6 +5109,18 @@ Examples:
4605
5109
  }
4606
5110
 
4607
5111
  // src/commands/admin.ts
5112
+ async function confirmAction(message, yes) {
5113
+ if (yes)
5114
+ return;
5115
+ const rl = await import("readline");
5116
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
5117
+ const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
5118
+ iface.close();
5119
+ if (answer.toLowerCase() !== "y") {
5120
+ console.log("Cancelled.");
5121
+ process.exit(0);
5122
+ }
5123
+ }
4608
5124
  function createAdminCommand() {
4609
5125
  const cmd = new Command("admin").description(`Platform administration (Console)
4610
5126
 
@@ -4645,34 +5161,118 @@ Examples:
4645
5161
  }
4646
5162
  printDetail(json.data ?? json, opts.format);
4647
5163
  });
4648
- const members = tenants.command("members").description("Tenant member management");
4649
- addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
4650
- if (!opts.tenant) {
4651
- console.error("Error: --tenant is required.");
5164
+ 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", `
5165
+ Examples:
5166
+ $ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
5167
+ $ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
5168
+ if (!opts.name) {
5169
+ console.error("Error: --name is required.");
4652
5170
  process.exit(1);
4653
5171
  }
4654
- const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
5172
+ if (!opts.email) {
5173
+ console.error("Error: --email is required.");
5174
+ process.exit(1);
5175
+ }
5176
+ const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
5177
+ const body = {
5178
+ tenant: {
5179
+ companyName: opts.name,
5180
+ capabilities
5181
+ },
5182
+ admin: {
5183
+ email: opts.email,
5184
+ name: opts.adminName ?? undefined
5185
+ }
5186
+ };
5187
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/tenants`, body });
4655
5188
  const json = await resp.json();
4656
5189
  if (!resp.ok) {
4657
- console.error(`Error: ${json.error ?? resp.statusText}`);
5190
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4658
5191
  process.exit(1);
4659
5192
  }
4660
- const rows = (json.data ?? json).map((m) => ({
4661
- id: m.userId ?? m.user_id ?? m.id,
4662
- email: m.email,
4663
- role: m.role,
4664
- status: m.status
4665
- }));
4666
- printData(rows, [
4667
- { key: "id", header: "USER ID", width: 36 },
4668
- { key: "email", header: "EMAIL", width: 30 },
4669
- { key: "role", header: "ROLE", width: 20 }
4670
- ], opts.format);
5193
+ const data = json.data ?? json;
5194
+ const tenant = data.tenant ?? data;
5195
+ console.log(`Tenant created: ${tenant.id}`);
5196
+ if (tenant.companyName)
5197
+ console.log(` Name: ${tenant.companyName}`);
5198
+ if (tenant.status)
5199
+ console.log(` Status: ${tenant.status}`);
5200
+ if (data.admin) {
5201
+ console.log(` Admin: ${data.admin.email} (userId: ${data.admin.userId})`);
5202
+ }
4671
5203
  });
4672
- addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
4673
- const params = new URLSearchParams({ limit: opts.limit });
4674
- if (opts.tenant)
4675
- params.set("tenant_id", opts.tenant);
5204
+ tenants.command("update").description("Update a tenant.").argument("<id>", "Tenant ID").option("--name <name>", "New company name").option("--status <status>", "New status: ACTIVE, SUSPENDED, TESTING").action(async (id, opts) => {
5205
+ const body = {};
5206
+ if (opts.name)
5207
+ body.companyName = opts.name;
5208
+ if (opts.status)
5209
+ body.status = opts.status.toUpperCase();
5210
+ if (Object.keys(body).length === 0) {
5211
+ console.error("Error: at least one of --name or --status is required.");
5212
+ process.exit(1);
5213
+ }
5214
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/tenants/${id}`, body });
5215
+ if (!resp.ok) {
5216
+ const j = await resp.json();
5217
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5218
+ process.exit(1);
5219
+ }
5220
+ console.log(`Tenant ${id} updated.`);
5221
+ });
5222
+ tenants.command("suspend").description("Suspend a tenant (set status to SUSPENDED).").argument("<id>", "Tenant ID").action(async (id) => {
5223
+ const resp = await apiRequest({
5224
+ method: "PATCH",
5225
+ path: `${consolePrefix()}/tenants/${id}`,
5226
+ body: { status: "SUSPENDED" }
5227
+ });
5228
+ if (!resp.ok) {
5229
+ const j = await resp.json();
5230
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5231
+ process.exit(1);
5232
+ }
5233
+ console.log(`Tenant ${id} suspended.`);
5234
+ });
5235
+ tenants.command("activate").description("Activate a tenant (set status to ACTIVE).").argument("<id>", "Tenant ID").action(async (id) => {
5236
+ const resp = await apiRequest({
5237
+ method: "PATCH",
5238
+ path: `${consolePrefix()}/tenants/${id}`,
5239
+ body: { status: "ACTIVE" }
5240
+ });
5241
+ if (!resp.ok) {
5242
+ const j = await resp.json();
5243
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5244
+ process.exit(1);
5245
+ }
5246
+ console.log(`Tenant ${id} activated.`);
5247
+ });
5248
+ const members = tenants.command("members").description("Tenant member management");
5249
+ addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
5250
+ if (!opts.tenant) {
5251
+ console.error("Error: --tenant is required.");
5252
+ process.exit(1);
5253
+ }
5254
+ const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
5255
+ const json = await resp.json();
5256
+ if (!resp.ok) {
5257
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5258
+ process.exit(1);
5259
+ }
5260
+ const rows = (json.data ?? json).map((m) => ({
5261
+ id: m.userId ?? m.user_id ?? m.id,
5262
+ email: m.email,
5263
+ role: m.role,
5264
+ status: m.status
5265
+ }));
5266
+ printData(rows, [
5267
+ { key: "id", header: "USER ID", width: 36 },
5268
+ { key: "email", header: "EMAIL", width: 30 },
5269
+ { key: "role", header: "ROLE", width: 20 }
5270
+ ], opts.format);
5271
+ });
5272
+ addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
5273
+ const params = new URLSearchParams({ limit: opts.limit });
5274
+ if (opts.tenant)
5275
+ params.set("tenant_id", opts.tenant);
4676
5276
  const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
4677
5277
  const json = await resp.json();
4678
5278
  if (!resp.ok) {
@@ -4712,42 +5312,28 @@ Examples:
4712
5312
  }
4713
5313
  console.log("Cache cleared.");
4714
5314
  });
4715
- return cmd;
4716
- }
4717
-
4718
- // src/commands/settings.ts
4719
- function settingsPrefix() {
4720
- const ctx = loadContext();
4721
- const context = getCurrentContext(ctx);
4722
- const app = context?.app ?? "dsp";
4723
- if (app === "console") {
4724
- console.error("Error: Settings commands are not available in Console mode.");
4725
- console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
4726
- process.exit(1);
4727
- }
4728
- return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4729
- }
4730
- function createSettingsCommand() {
4731
- const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
4732
-
4733
- Requires: ADVERTISER or PUBLISHER capability.
4734
- Not available in Console mode.`).addHelpText("after", `
4735
- Examples:
4736
- $ a8techads settings show
4737
- $ a8techads settings update --from-json settings.json
4738
- $ a8techads settings profile
4739
- $ a8techads settings profile-update --company-name "New Name"`);
4740
- addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
4741
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
5315
+ const tracking = cmd.command("tracking-identities").description("User identity graph stats");
5316
+ tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
5317
+ const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
4742
5318
  const json = await resp.json();
4743
5319
  if (!resp.ok) {
4744
5320
  console.error(`Error: ${json.error ?? resp.statusText}`);
4745
5321
  process.exit(1);
4746
5322
  }
4747
- printDetail(json.data ?? json, opts.format);
5323
+ const data = json.data ?? json;
5324
+ console.log(`Tracking Identity Stats:`);
5325
+ console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
5326
+ console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
5327
+ console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
5328
+ console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
5329
+ if (data.earliest_seen)
5330
+ console.log(` Earliest Seen: ${data.earliest_seen}`);
5331
+ if (data.latest_seen)
5332
+ console.log(` Latest Seen: ${data.latest_seen}`);
4748
5333
  });
4749
- addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
4750
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
5334
+ const finance = cmd.command("finance").description("Financial overview and audit");
5335
+ addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
5336
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
4751
5337
  const json = await resp.json();
4752
5338
  if (!resp.ok) {
4753
5339
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4755,85 +5341,232 @@ Examples:
4755
5341
  }
4756
5342
  printDetail(json.data ?? json, opts.format);
4757
5343
  });
4758
- cmd.command("update").description(`Update tenant settings.
4759
-
4760
- Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4761
- if (!opts.fromJson) {
4762
- console.error("Error: --from-json is required for settings update.");
5344
+ finance.command("finalize-daily").description("Run daily billing finalization for a date.").option("--date <yyyy-mm-dd>", "Date to finalize (default: today UTC)").option("--yes", "Skip confirmation", false).action(async (opts) => {
5345
+ const targetDate = opts.date || new Date().toISOString().slice(0, 10);
5346
+ await confirmAction(`Run daily billing finalization for ${targetDate}?`, opts.yes);
5347
+ const body = {};
5348
+ if (opts.date)
5349
+ body.date = opts.date;
5350
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/finalize-daily`, body });
5351
+ const json = await resp.json();
5352
+ if (!resp.ok) {
5353
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4763
5354
  process.exit(1);
4764
5355
  }
4765
- const { readFileSync: readFileSync3 } = await import("fs");
4766
- const body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4767
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
5356
+ const data = json.data ?? json;
5357
+ console.log(`Daily billing finalized for ${data.date ?? targetDate}.`);
5358
+ });
5359
+ addFormatOption(finance.command("events").description("List billing events.").option("--limit <n>", "Max results", "20").option("--aggregate-type <type>", "Filter by aggregate type").option("--event-type <type>", "Filter by event type")).action(async (opts) => {
5360
+ const params = new URLSearchParams({ limit: opts.limit });
5361
+ if (opts.aggregateType)
5362
+ params.set("aggregate_type", opts.aggregateType);
5363
+ if (opts.eventType)
5364
+ params.set("event_type", opts.eventType);
5365
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/events?${params}` });
5366
+ const json = await resp.json();
5367
+ if (!resp.ok) {
5368
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5369
+ process.exit(1);
5370
+ }
5371
+ const payload = json.data ?? json;
5372
+ const rows = (payload.events ?? payload).map((e) => ({
5373
+ id: e.id,
5374
+ eventType: e.eventType ?? e.event_type,
5375
+ aggregateType: e.aggregateType ?? e.aggregate_type,
5376
+ createdAt: e.createdAt ?? e.created_at
5377
+ }));
5378
+ printData(rows, [
5379
+ { key: "id", header: "ID", width: 36 },
5380
+ { key: "eventType", header: "EVENT_TYPE", width: 25 },
5381
+ { key: "aggregateType", header: "AGGREGATE_TYPE", width: 20 },
5382
+ { key: "createdAt", header: "CREATED_AT", width: 22 }
5383
+ ], opts.format);
5384
+ });
5385
+ addFormatOption(finance.command("operations").description("List billing operations.").option("--limit <n>", "Max results", "20").option("--type <type>", "Filter by operation type").option("--status <status>", "Filter by status")).action(async (opts) => {
5386
+ const params = new URLSearchParams({ limit: opts.limit });
5387
+ if (opts.type)
5388
+ params.set("type", opts.type);
5389
+ if (opts.status)
5390
+ params.set("status", opts.status);
5391
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/operations?${params}` });
5392
+ const json = await resp.json();
5393
+ if (!resp.ok) {
5394
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5395
+ process.exit(1);
5396
+ }
5397
+ const payload = json.data ?? json;
5398
+ const rows = (payload.operations ?? payload).map((o) => ({
5399
+ id: o.id,
5400
+ type: o.type,
5401
+ target: o.target ?? o.targetId ?? o.target_id,
5402
+ amount: o.amount,
5403
+ status: o.status,
5404
+ createdAt: o.createdAt ?? o.created_at
5405
+ }));
5406
+ printData(rows, [
5407
+ { key: "id", header: "ID", width: 36 },
5408
+ { key: "type", header: "TYPE", width: 18 },
5409
+ { key: "target", header: "TARGET", width: 20 },
5410
+ { key: "amount", header: "AMOUNT", width: 12 },
5411
+ { key: "status", header: "STATUS", width: 12 },
5412
+ { key: "createdAt", header: "CREATED_AT", width: 22 }
5413
+ ], opts.format);
5414
+ });
5415
+ const invoices = cmd.command("invoices").description("Invoice management");
5416
+ addFormatOption(invoices.command("list").description("List invoices.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status").option("--tenant-type <type>", "Filter by tenant type")).action(async (opts) => {
5417
+ const params = new URLSearchParams({ limit: opts.limit });
5418
+ if (opts.status)
5419
+ params.set("status", opts.status);
5420
+ if (opts.tenantType)
5421
+ params.set("tenantType", opts.tenantType);
5422
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
5423
+ const json = await resp.json();
5424
+ if (!resp.ok) {
5425
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5426
+ process.exit(1);
5427
+ }
5428
+ const rows = (json.data ?? json).map((i) => ({
5429
+ invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
5430
+ tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
5431
+ amount: i.amount,
5432
+ status: i.status,
5433
+ dueDate: i.dueDate ?? i.due_date
5434
+ }));
5435
+ printData(rows, [
5436
+ { key: "invoiceNumber", header: "INVOICE#", width: 20 },
5437
+ { key: "tenant", header: "TENANT", width: 25 },
5438
+ { key: "amount", header: "AMOUNT", width: 12 },
5439
+ { key: "status", header: "STATUS", width: 12 },
5440
+ { key: "dueDate", header: "DUE_DATE", width: 16 }
5441
+ ], opts.format);
5442
+ });
5443
+ addFormatOption(invoices.command("preview").description("Preview unbilled invoices for current period.")).action(async (opts) => {
5444
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=advertiser` });
5445
+ const json = await resp.json();
5446
+ if (!resp.ok) {
5447
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5448
+ process.exit(1);
5449
+ }
5450
+ const data = json.data ?? json;
5451
+ if (data.summary) {
5452
+ console.log(`
5453
+ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Total Due: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
5454
+ `);
5455
+ }
5456
+ const rows = (data.previews ?? []).map((p) => ({
5457
+ tenant: p.tenantName,
5458
+ adSpend: p.adSpend,
5459
+ platformFee: p.platformFee,
5460
+ totalDue: p.totalDue,
5461
+ days: p.daysWithData,
5462
+ impressions: p.impressions
5463
+ }));
5464
+ printData(rows, [
5465
+ { key: "tenant", header: "TENANT", width: 25 },
5466
+ { key: "adSpend", header: "AD_SPEND", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
5467
+ { key: "platformFee", header: "PLATFORM_FEE", width: 14, format: (v) => `$${Number(v).toFixed(2)}` },
5468
+ { key: "totalDue", header: "TOTAL_DUE", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
5469
+ { key: "days", header: "DAYS", width: 6 },
5470
+ { key: "impressions", header: "IMPRESSIONS", width: 12 }
5471
+ ], opts.format);
5472
+ });
5473
+ addFormatOption(invoices.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
5474
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
5475
+ const json = await resp.json();
5476
+ if (!resp.ok) {
5477
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5478
+ process.exit(1);
5479
+ }
5480
+ printDetail(json.data ?? json, opts.format);
5481
+ });
5482
+ invoices.command("issue").description("Issue an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5483
+ await confirmAction(`Issue invoice ${id}?`, opts.yes);
5484
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/issue` });
4768
5485
  if (!resp.ok) {
4769
5486
  const j = await resp.json();
4770
5487
  console.error(`Error: ${j.error ?? resp.statusText}`);
4771
5488
  process.exit(1);
4772
5489
  }
4773
- console.log("Settings updated.");
5490
+ console.log(`Invoice ${id} issued.`);
4774
5491
  });
4775
- cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4776
- let body;
4777
- if (opts.fromJson) {
4778
- const { readFileSync: readFileSync3 } = await import("fs");
4779
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4780
- } else {
4781
- body = {};
4782
- if (opts.companyName)
4783
- body.companyName = opts.companyName;
5492
+ invoices.command("void").description("Void an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5493
+ await confirmAction(`Void invoice ${id}?`, opts.yes);
5494
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/void` });
5495
+ if (!resp.ok) {
5496
+ const j = await resp.json();
5497
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5498
+ process.exit(1);
4784
5499
  }
4785
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
5500
+ console.log(`Invoice ${id} voided.`);
5501
+ });
5502
+ invoices.command("mark-paid").description("Mark an invoice as paid.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5503
+ await confirmAction(`Mark invoice ${id} as paid?`, opts.yes);
5504
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/mark-paid` });
4786
5505
  if (!resp.ok) {
4787
5506
  const j = await resp.json();
4788
5507
  console.error(`Error: ${j.error ?? resp.statusText}`);
4789
5508
  process.exit(1);
4790
5509
  }
4791
- console.log("Profile updated.");
5510
+ console.log(`Invoice ${id} marked as paid.`);
4792
5511
  });
4793
- return cmd;
4794
- }
4795
-
4796
- // src/commands/invoices.ts
4797
- function createInvoicesCommand() {
4798
- const cmd = new Command("invoices").description(`Invoice management (DSP)
4799
-
4800
- Requires: ADVERTISER capability.`).addHelpText("after", `
4801
- Examples:
4802
- $ a8techads invoices list
4803
- $ a8techads invoices get <id>
4804
- $ a8techads invoices spending`);
4805
- addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
4806
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
5512
+ const statements = cmd.command("statements").description("Publisher statement management");
5513
+ addFormatOption(statements.command("list").description("List publisher statements.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
5514
+ const params = new URLSearchParams({ limit: opts.limit, tenantType: "publisher" });
5515
+ if (opts.status)
5516
+ params.set("status", opts.status);
5517
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
4807
5518
  const json = await resp.json();
4808
5519
  if (!resp.ok) {
4809
5520
  console.error(`Error: ${json.error ?? resp.statusText}`);
4810
5521
  process.exit(1);
4811
5522
  }
4812
- const columns = [
4813
- { key: "id", header: "ID", width: 36 },
4814
- { key: "period", header: "PERIOD", width: 20 },
4815
- { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
4816
- { key: "status", header: "STATUS", width: 12 }
4817
- ];
4818
5523
  const rows = (json.data ?? json).map((i) => ({
4819
- id: i.id,
4820
- period: i.period ?? i.billingPeriod,
4821
- amount: i.amount ?? i.totalAmount,
4822
- status: i.status
5524
+ invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
5525
+ tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
5526
+ amount: i.amount,
5527
+ status: i.status,
5528
+ dueDate: i.dueDate ?? i.due_date
4823
5529
  }));
4824
- printData(rows, columns, opts.format);
5530
+ printData(rows, [
5531
+ { key: "invoiceNumber", header: "INVOICE#", width: 20 },
5532
+ { key: "tenant", header: "TENANT", width: 25 },
5533
+ { key: "amount", header: "AMOUNT", width: 12 },
5534
+ { key: "status", header: "STATUS", width: 12 },
5535
+ { key: "dueDate", header: "DUE_DATE", width: 16 }
5536
+ ], opts.format);
4825
5537
  });
4826
- addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
4827
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
5538
+ addFormatOption(statements.command("preview").description("Preview unbilled statements for current period.")).action(async (opts) => {
5539
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=publisher` });
4828
5540
  const json = await resp.json();
4829
5541
  if (!resp.ok) {
4830
5542
  console.error(`Error: ${json.error ?? resp.statusText}`);
4831
5543
  process.exit(1);
4832
5544
  }
4833
- printDetail(json.data ?? json, opts.format);
5545
+ const data = json.data ?? json;
5546
+ if (data.summary) {
5547
+ console.log(`
5548
+ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Net Payable: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
5549
+ `);
5550
+ }
5551
+ const rows = (data.previews ?? []).map((p) => ({
5552
+ tenant: p.tenantName,
5553
+ grossRevenue: p.grossRevenue,
5554
+ commission: p.platformCommission,
5555
+ netPayable: p.netPayable,
5556
+ days: p.daysWithData,
5557
+ impressions: p.impressions
5558
+ }));
5559
+ printData(rows, [
5560
+ { key: "tenant", header: "TENANT", width: 25 },
5561
+ { key: "grossRevenue", header: "GROSS_REV", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
5562
+ { key: "commission", header: "COMMISSION", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
5563
+ { key: "netPayable", header: "NET_PAY", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
5564
+ { key: "days", header: "DAYS", width: 6 },
5565
+ { key: "impressions", header: "IMPRESSIONS", width: 12 }
5566
+ ], opts.format);
4834
5567
  });
4835
- addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
4836
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
5568
+ addFormatOption(statements.command("get").description("Get statement details.").argument("<id>", "Statement ID")).action(async (id, opts) => {
5569
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
4837
5570
  const json = await resp.json();
4838
5571
  if (!resp.ok) {
4839
5572
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4841,12 +5574,1074 @@ Examples:
4841
5574
  }
4842
5575
  printDetail(json.data ?? json, opts.format);
4843
5576
  });
4844
- return cmd;
5577
+ const payoutsAdmin = cmd.command("payouts").description("Payout operations");
5578
+ addFormatOption(payoutsAdmin.command("list").description("List payouts.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
5579
+ const params = new URLSearchParams({ limit: opts.limit });
5580
+ if (opts.status)
5581
+ params.set("status", opts.status);
5582
+ const resp = await apiRequest({ path: `${consolePrefix()}/payout-ops/payouts?${params}` });
5583
+ const json = await resp.json();
5584
+ if (!resp.ok) {
5585
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5586
+ process.exit(1);
5587
+ }
5588
+ const rows = (json.data ?? json).map((p) => ({
5589
+ id: p.id,
5590
+ publisher: p.publisherName ?? p.publisher_name ?? p.tenantId ?? p.tenant_id,
5591
+ amount: p.amount,
5592
+ status: p.status,
5593
+ requestedAt: p.requestedAt ?? p.requested_at ?? p.createdAt ?? p.created_at
5594
+ }));
5595
+ printData(rows, [
5596
+ { key: "id", header: "ID", width: 36 },
5597
+ { key: "publisher", header: "PUBLISHER", width: 25 },
5598
+ { key: "amount", header: "AMOUNT", width: 12 },
5599
+ { key: "status", header: "STATUS", width: 12 },
5600
+ { key: "requestedAt", header: "REQUESTED_AT", width: 22 }
5601
+ ], opts.format);
5602
+ });
5603
+ payoutsAdmin.command("approve").description("Approve a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5604
+ await confirmAction(`Approve payout ${id}?`, opts.yes);
5605
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/approve` });
5606
+ if (!resp.ok) {
5607
+ const j = await resp.json();
5608
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5609
+ process.exit(1);
5610
+ }
5611
+ console.log(`Payout ${id} approved.`);
5612
+ });
5613
+ payoutsAdmin.command("reject").description("Reject a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
5614
+ await confirmAction(`Reject payout ${id}?`, opts.yes);
5615
+ const body = {};
5616
+ if (opts.reason)
5617
+ body.reason = opts.reason;
5618
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/reject`, body });
5619
+ if (!resp.ok) {
5620
+ const j = await resp.json();
5621
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5622
+ process.exit(1);
5623
+ }
5624
+ console.log(`Payout ${id} rejected.`);
5625
+ });
5626
+ payoutsAdmin.command("process").description("Process a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5627
+ await confirmAction(`Process payout ${id}?`, opts.yes);
5628
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/process` });
5629
+ if (!resp.ok) {
5630
+ const j = await resp.json();
5631
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5632
+ process.exit(1);
5633
+ }
5634
+ console.log(`Payout ${id} processed.`);
5635
+ });
5636
+ payoutsAdmin.command("manual-create").description("Manually create a payout.").option("--tenant-id <id>", "Publisher tenant ID (required)").option("--amount <amount>", "Payout amount (required)").option("--reason <reason>", "Reason for manual payout").option("--wallet <wallet>", "Wallet address").option("--network <network>", "Payment network").option("--yes", "Skip confirmation", false).action(async (opts) => {
5637
+ if (!opts.tenantId) {
5638
+ console.error("Error: --tenant-id is required.");
5639
+ process.exit(1);
5640
+ }
5641
+ if (!opts.amount) {
5642
+ console.error("Error: --amount is required.");
5643
+ process.exit(1);
5644
+ }
5645
+ await confirmAction(`Create manual payout of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
5646
+ const body = { tenantId: opts.tenantId, amount: opts.amount };
5647
+ if (opts.reason)
5648
+ body.reason = opts.reason;
5649
+ if (opts.wallet)
5650
+ body.wallet = opts.wallet;
5651
+ if (opts.network)
5652
+ body.network = opts.network;
5653
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-payout`, body });
5654
+ const json = await resp.json();
5655
+ if (!resp.ok) {
5656
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5657
+ process.exit(1);
5658
+ }
5659
+ console.log(`Manual payout created: ${(json.data ?? json).id ?? "OK"}`);
5660
+ });
5661
+ const deposits = cmd.command("deposits").description("Deposit operations");
5662
+ addFormatOption(deposits.command("list").description("List deposits.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
5663
+ const params = new URLSearchParams({ limit: opts.limit });
5664
+ if (opts.status)
5665
+ params.set("status", opts.status);
5666
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/deposits?${params}` });
5667
+ const json = await resp.json();
5668
+ if (!resp.ok) {
5669
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5670
+ process.exit(1);
5671
+ }
5672
+ const transactions = json.data?.transactions ?? json.transactions ?? json.data ?? json;
5673
+ const items = Array.isArray(transactions) ? transactions : [];
5674
+ const rows = items.map((d) => ({
5675
+ id: d.id,
5676
+ advertiser: d.advertiser?.name ?? d.advertiserName ?? d.advertiser_name ?? d.tenantId ?? d.tenant_id,
5677
+ amount: d.amount,
5678
+ status: d.status,
5679
+ date: d.createdAt ?? d.created_at ?? d.date
5680
+ }));
5681
+ printData(rows, [
5682
+ { key: "id", header: "ID", width: 36 },
5683
+ { key: "advertiser", header: "ADVERTISER", width: 25 },
5684
+ { key: "amount", header: "AMOUNT", width: 12 },
5685
+ { key: "status", header: "STATUS", width: 12 },
5686
+ { key: "date", header: "DATE", width: 22 }
5687
+ ], opts.format);
5688
+ });
5689
+ deposits.command("confirm").description("Confirm a deposit.").argument("<id>", "Deposit ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5690
+ await confirmAction(`Confirm deposit ${id}?`, opts.yes);
5691
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/deposits/${id}/confirm` });
5692
+ if (!resp.ok) {
5693
+ const j = await resp.json();
5694
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5695
+ process.exit(1);
5696
+ }
5697
+ console.log(`Deposit ${id} confirmed.`);
5698
+ });
5699
+ deposits.command("manual-create").description("Manually create a deposit.").option("--tenant-id <id>", "Advertiser tenant ID (required)").option("--amount <amount>", "Deposit amount (required)").option("--reason <reason>", "Reason for manual deposit").option("--reference <ref>", "External reference").option("--notes <notes>", "Additional notes").option("--yes", "Skip confirmation", false).action(async (opts) => {
5700
+ if (!opts.tenantId) {
5701
+ console.error("Error: --tenant-id is required.");
5702
+ process.exit(1);
5703
+ }
5704
+ if (!opts.amount) {
5705
+ console.error("Error: --amount is required.");
5706
+ process.exit(1);
5707
+ }
5708
+ await confirmAction(`Create manual deposit of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
5709
+ const body = { tenantId: opts.tenantId, amount: opts.amount };
5710
+ if (opts.reason)
5711
+ body.reason = opts.reason;
5712
+ if (opts.reference)
5713
+ body.reference = opts.reference;
5714
+ if (opts.notes)
5715
+ body.notes = opts.notes;
5716
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-deposit`, body });
5717
+ const json = await resp.json();
5718
+ if (!resp.ok) {
5719
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5720
+ process.exit(1);
5721
+ }
5722
+ console.log(`Manual deposit created: ${(json.data ?? json).id ?? "OK"}`);
5723
+ });
5724
+ const refunds = cmd.command("refunds").description("Refund operations");
5725
+ addFormatOption(refunds.command("list").description("List refunds.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
5726
+ const params = new URLSearchParams({ limit: opts.limit });
5727
+ if (opts.status)
5728
+ params.set("status", opts.status);
5729
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds?${params}` });
5730
+ const json = await resp.json();
5731
+ if (!resp.ok) {
5732
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5733
+ process.exit(1);
5734
+ }
5735
+ const rows = (json.data ?? json).map((r) => ({
5736
+ id: r.id,
5737
+ advertiser: r.advertiserName ?? r.advertiser_name ?? r.accountId ?? r.account_id,
5738
+ amount: r.amount,
5739
+ reason: r.reason,
5740
+ status: r.status
5741
+ }));
5742
+ printData(rows, [
5743
+ { key: "id", header: "ID", width: 36 },
5744
+ { key: "advertiser", header: "ADVERTISER", width: 25 },
5745
+ { key: "amount", header: "AMOUNT", width: 12 },
5746
+ { key: "reason", header: "REASON", width: 20 },
5747
+ { key: "status", header: "STATUS", width: 12 }
5748
+ ], opts.format);
5749
+ });
5750
+ addFormatOption(refunds.command("get").description("Get refund details.").argument("<id>", "Refund ID")).action(async (id, opts) => {
5751
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds/${id}` });
5752
+ const json = await resp.json();
5753
+ if (!resp.ok) {
5754
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5755
+ process.exit(1);
5756
+ }
5757
+ printDetail(json.data ?? json, opts.format);
5758
+ });
5759
+ refunds.command("create").description("Create a refund.").option("--account-id <id>", "Account ID (required)").option("--amount <amount>", "Refund amount (required)").option("--reason <reason>", "Refund reason").option("--yes", "Skip confirmation", false).action(async (opts) => {
5760
+ if (!opts.accountId) {
5761
+ console.error("Error: --account-id is required.");
5762
+ process.exit(1);
5763
+ }
5764
+ if (!opts.amount) {
5765
+ console.error("Error: --amount is required.");
5766
+ process.exit(1);
5767
+ }
5768
+ await confirmAction(`Create refund of ${opts.amount} for account ${opts.accountId}?`, opts.yes);
5769
+ const body = { accountId: opts.accountId, amount: opts.amount };
5770
+ if (opts.reason)
5771
+ body.reason = opts.reason;
5772
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds`, body });
5773
+ const json = await resp.json();
5774
+ if (!resp.ok) {
5775
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5776
+ process.exit(1);
5777
+ }
5778
+ console.log(`Refund created: ${(json.data ?? json).id ?? "OK"}`);
5779
+ });
5780
+ refunds.command("approve").description("Approve a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5781
+ await confirmAction(`Approve refund ${id}?`, opts.yes);
5782
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/approve` });
5783
+ if (!resp.ok) {
5784
+ const j = await resp.json();
5785
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5786
+ process.exit(1);
5787
+ }
5788
+ console.log(`Refund ${id} approved.`);
5789
+ });
5790
+ refunds.command("reject").description("Reject a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
5791
+ await confirmAction(`Reject refund ${id}?`, opts.yes);
5792
+ const body = {};
5793
+ if (opts.reason)
5794
+ body.reason = opts.reason;
5795
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/reject`, body });
5796
+ if (!resp.ok) {
5797
+ const j = await resp.json();
5798
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5799
+ process.exit(1);
5800
+ }
5801
+ console.log(`Refund ${id} rejected.`);
5802
+ });
5803
+ refunds.command("process").description("Process an approved refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5804
+ await confirmAction(`Process refund ${id}?`, opts.yes);
5805
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/process` });
5806
+ if (!resp.ok) {
5807
+ const j = await resp.json();
5808
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5809
+ process.exit(1);
5810
+ }
5811
+ console.log(`Refund ${id} processed.`);
5812
+ });
5813
+ const balances = cmd.command("balances").description("Account balance management");
5814
+ addFormatOption(balances.command("list").description("List account balances.")).action(async (opts) => {
5815
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/balances` });
5816
+ const json = await resp.json();
5817
+ if (!resp.ok) {
5818
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5819
+ process.exit(1);
5820
+ }
5821
+ printDetail(json.data ?? json, opts.format);
5822
+ });
5823
+ balances.command("adjust").description("Adjust an account balance.").option("--tenant-id <id>", "Tenant ID (required)").option("--amount <amount>", "Adjustment amount (required)").option("--reason <reason>", "Reason for adjustment").option("--account-type <type>", "Account type", "advertiser").option("--yes", "Skip confirmation", false).action(async (opts) => {
5824
+ if (!opts.tenantId) {
5825
+ console.error("Error: --tenant-id is required.");
5826
+ process.exit(1);
5827
+ }
5828
+ if (!opts.amount) {
5829
+ console.error("Error: --amount is required.");
5830
+ process.exit(1);
5831
+ }
5832
+ await confirmAction(`Adjust balance by ${opts.amount} for tenant ${opts.tenantId} (${opts.accountType})?`, opts.yes);
5833
+ const body = { tenantId: opts.tenantId, amount: opts.amount, accountType: opts.accountType };
5834
+ if (opts.reason)
5835
+ body.reason = opts.reason;
5836
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/adjust`, body });
5837
+ const json = await resp.json();
5838
+ if (!resp.ok) {
5839
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5840
+ process.exit(1);
5841
+ }
5842
+ console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
5843
+ });
5844
+ return cmd;
5845
+ }
5846
+
5847
+ // src/commands/external-ssp.ts
5848
+ var COLUMNS8 = [
5849
+ { key: "id", header: "ID", width: 36 },
5850
+ { key: "name", header: "NAME", width: 25 },
5851
+ { key: "code", header: "CODE", width: 15 },
5852
+ { key: "status", header: "STATUS", width: 12 },
5853
+ { key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
5854
+ ];
5855
+ function createExternalSspCommand() {
5856
+ const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
5857
+
5858
+ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
5859
+ Examples:
5860
+ $ a8techads external-ssp list
5861
+ $ a8techads external-ssp get <id>
5862
+ $ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
5863
+ $ a8techads external-ssp activate <id>
5864
+ $ a8techads external-ssp pause <id>`);
5865
+ addFormatOption(cmd.command("list").description("List all external SSP partners.").option("--status <status>", "Filter by status (TESTING, ACTIVE, SUSPENDED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
5866
+ const params = new URLSearchParams;
5867
+ if (opts.status)
5868
+ params.set("status", opts.status);
5869
+ params.set("limit", opts.limit);
5870
+ const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners?${params}` });
5871
+ const json = await resp.json();
5872
+ if (!resp.ok) {
5873
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5874
+ process.exit(1);
5875
+ }
5876
+ const rows = (json.data ?? json).map((p) => ({
5877
+ id: p.id,
5878
+ name: p.name,
5879
+ code: p.code,
5880
+ status: p.status,
5881
+ formats: p.supportedFormats ?? p.supported_formats ?? []
5882
+ }));
5883
+ printData(rows, COLUMNS8, opts.format);
5884
+ });
5885
+ addFormatOption(cmd.command("get").description("Get external SSP partner details (includes API key).").argument("<id>", "Partner ID")).action(async (id, opts) => {
5886
+ const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners/${id}` });
5887
+ const json = await resp.json();
5888
+ if (!resp.ok) {
5889
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5890
+ process.exit(1);
5891
+ }
5892
+ printDetail(json.data ?? json, opts.format);
5893
+ });
5894
+ cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").addHelpText("after", `
5895
+ Examples:
5896
+ $ a8techads external-ssp create --name "OpenX"
5897
+ $ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO`).action(async (opts) => {
5898
+ if (!opts.name) {
5899
+ console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
5900
+ process.exit(1);
5901
+ }
5902
+ const body = { name: opts.name };
5903
+ if (opts.code)
5904
+ body.code = opts.code;
5905
+ if (opts.formats)
5906
+ body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
5907
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
5908
+ const json = await resp.json();
5909
+ if (!resp.ok) {
5910
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5911
+ process.exit(1);
5912
+ }
5913
+ const data = json.data ?? json;
5914
+ console.log(`Partner created: ${data.id}`);
5915
+ if (data.code)
5916
+ console.log(` Code: ${data.code}`);
5917
+ if (data.apiKey)
5918
+ console.log(` API Key: ${data.apiKey}`);
5919
+ });
5920
+ cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").action(async (id, opts) => {
5921
+ const body = {};
5922
+ if (opts.name)
5923
+ body.name = opts.name;
5924
+ if (opts.formats)
5925
+ body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
5926
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
5927
+ if (!resp.ok) {
5928
+ const j = await resp.json();
5929
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5930
+ process.exit(1);
5931
+ }
5932
+ console.log(`Partner ${id} updated.`);
5933
+ });
5934
+ cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
5935
+ if (!opts.yes) {
5936
+ console.error("Add --yes to confirm deletion.");
5937
+ process.exit(1);
5938
+ }
5939
+ const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/ssp-partners/${id}` });
5940
+ if (!resp.ok && resp.status !== 204) {
5941
+ console.error(`Error: ${resp.statusText}`);
5942
+ process.exit(1);
5943
+ }
5944
+ console.log(`Partner ${id} deleted.`);
5945
+ });
5946
+ cmd.command("activate").description("Activate an external SSP partner (TESTING/SUSPENDED → ACTIVE).").argument("<id>", "Partner ID").action(async (id) => {
5947
+ const resp = await apiRequest({
5948
+ method: "PATCH",
5949
+ path: `${consolePrefix()}/ssp-partners/${id}/status`,
5950
+ body: { status: "ACTIVE" }
5951
+ });
5952
+ if (!resp.ok) {
5953
+ const j = await resp.json();
5954
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5955
+ process.exit(1);
5956
+ }
5957
+ console.log(`Partner ${id} activated.`);
5958
+ });
5959
+ cmd.command("pause").description("Pause an external SSP partner (ACTIVE → SUSPENDED).").argument("<id>", "Partner ID").action(async (id) => {
5960
+ const resp = await apiRequest({
5961
+ method: "PATCH",
5962
+ path: `${consolePrefix()}/ssp-partners/${id}/status`,
5963
+ body: { status: "SUSPENDED" }
5964
+ });
5965
+ if (!resp.ok) {
5966
+ const j = await resp.json();
5967
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5968
+ process.exit(1);
5969
+ }
5970
+ console.log(`Partner ${id} paused.`);
5971
+ });
5972
+ return cmd;
5973
+ }
5974
+
5975
+ // src/commands/settings.ts
5976
+ function settingsPrefix() {
5977
+ const ctx = loadContext();
5978
+ const context = getCurrentContext(ctx);
5979
+ const app = context?.app ?? "dsp";
5980
+ if (app === "console") {
5981
+ console.error("Error: Settings commands are not available in Console mode.");
5982
+ console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
5983
+ process.exit(1);
5984
+ }
5985
+ return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
5986
+ }
5987
+ function createSettingsCommand() {
5988
+ const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
5989
+
5990
+ Requires: ADVERTISER or PUBLISHER capability.
5991
+ Not available in Console mode.`).addHelpText("after", `
5992
+ Examples:
5993
+ $ a8techads settings show
5994
+ $ a8techads settings update --from-json settings.json
5995
+ $ a8techads settings profile
5996
+ $ a8techads settings profile-update --company-name "New Name"`);
5997
+ addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
5998
+ const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
5999
+ const json = await resp.json();
6000
+ if (!resp.ok) {
6001
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6002
+ process.exit(1);
6003
+ }
6004
+ printDetail(json.data ?? json, opts.format);
6005
+ });
6006
+ addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
6007
+ const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
6008
+ const json = await resp.json();
6009
+ if (!resp.ok) {
6010
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6011
+ process.exit(1);
6012
+ }
6013
+ printDetail(json.data ?? json, opts.format);
6014
+ });
6015
+ cmd.command("update").description(`Update tenant settings.
6016
+
6017
+ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
6018
+ if (!opts.fromJson) {
6019
+ console.error("Error: --from-json is required for settings update.");
6020
+ process.exit(1);
6021
+ }
6022
+ const { readFileSync: readFileSync4 } = await import("fs");
6023
+ const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6024
+ const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
6025
+ if (!resp.ok) {
6026
+ const j = await resp.json();
6027
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6028
+ process.exit(1);
6029
+ }
6030
+ console.log("Settings updated.");
6031
+ });
6032
+ cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
6033
+ let body;
6034
+ if (opts.fromJson) {
6035
+ const { readFileSync: readFileSync4 } = await import("fs");
6036
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6037
+ } else {
6038
+ body = {};
6039
+ if (opts.companyName)
6040
+ body.companyName = opts.companyName;
6041
+ }
6042
+ const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
6043
+ if (!resp.ok) {
6044
+ const j = await resp.json();
6045
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6046
+ process.exit(1);
6047
+ }
6048
+ console.log("Profile updated.");
6049
+ });
6050
+ return cmd;
6051
+ }
6052
+
6053
+ // src/commands/invoices.ts
6054
+ function createInvoicesCommand() {
6055
+ const cmd = new Command("invoices").description(`Invoice management (DSP)
6056
+
6057
+ Requires: ADVERTISER capability.`).addHelpText("after", `
6058
+ Examples:
6059
+ $ a8techads invoices list
6060
+ $ a8techads invoices get <id>
6061
+ $ a8techads invoices spending`);
6062
+ addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
6063
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
6064
+ const json = await resp.json();
6065
+ if (!resp.ok) {
6066
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6067
+ process.exit(1);
6068
+ }
6069
+ const columns = [
6070
+ { key: "id", header: "ID", width: 36 },
6071
+ { key: "period", header: "PERIOD", width: 20 },
6072
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
6073
+ { key: "status", header: "STATUS", width: 12 }
6074
+ ];
6075
+ const rows = (json.data ?? json).map((i) => ({
6076
+ id: i.id,
6077
+ period: i.period ?? i.billingPeriod,
6078
+ amount: i.amount ?? i.totalAmount,
6079
+ status: i.status
6080
+ }));
6081
+ printData(rows, columns, opts.format);
6082
+ });
6083
+ addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
6084
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
6085
+ const json = await resp.json();
6086
+ if (!resp.ok) {
6087
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6088
+ process.exit(1);
6089
+ }
6090
+ printDetail(json.data ?? json, opts.format);
6091
+ });
6092
+ addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
6093
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
6094
+ const json = await resp.json();
6095
+ if (!resp.ok) {
6096
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6097
+ process.exit(1);
6098
+ }
6099
+ printDetail(json.data ?? json, opts.format);
6100
+ });
6101
+ return cmd;
6102
+ }
6103
+
6104
+ // src/commands/conversion-goals.ts
6105
+ var COLUMNS9 = [
6106
+ { key: "id", header: "ID", width: 36 },
6107
+ { key: "name", header: "NAME", width: 24 },
6108
+ { key: "goalOrder", header: "G#", width: 4 },
6109
+ { key: "conversionType", header: "TYPE", width: 20 },
6110
+ { key: "valueType", header: "VALUE", width: 10 },
6111
+ { key: "status", header: "STATUS", width: 10 }
6112
+ ];
6113
+ function createConversionGoalsCommand() {
6114
+ const cmd = new Command("conversion-goals").description(`Conversion goal management (DSP)
6115
+
6116
+ Requires: ADVERTISER capability.`).addHelpText("after", `
6117
+ Examples:
6118
+ $ a8techads conversion-goals list
6119
+ $ a8techads conversion-goals get <id>
6120
+ $ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
6121
+ $ a8techads conversion-goals pause <id>`);
6122
+ 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) => {
6123
+ const params = new URLSearchParams;
6124
+ if (opts.status)
6125
+ params.set("status", opts.status);
6126
+ params.set("limit", opts.limit);
6127
+ const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals?${params}` });
6128
+ const json = await resp.json();
6129
+ if (!resp.ok) {
6130
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6131
+ process.exit(1);
6132
+ }
6133
+ const rows = (json.data ?? json).map((g) => ({
6134
+ id: g.id,
6135
+ name: g.name,
6136
+ goalOrder: `G${g.goalOrder ?? g.goal_order}`,
6137
+ conversionType: g.conversionType ?? g.conversion_type,
6138
+ valueType: g.valueType ?? g.value_type,
6139
+ status: g.status
6140
+ }));
6141
+ printData(rows, COLUMNS9, opts.format);
6142
+ });
6143
+ addFormatOption(cmd.command("get").description("Get conversion goal details.").argument("<id>", "Goal ID")).action(async (id, opts) => {
6144
+ const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
6145
+ const json = await resp.json();
6146
+ if (!resp.ok) {
6147
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6148
+ process.exit(1);
6149
+ }
6150
+ printDetail(json.data ?? json, opts.format);
6151
+ });
6152
+ cmd.command("create").description(`Create a conversion goal.
6153
+
6154
+ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6155
+ 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) => {
6156
+ let body;
6157
+ if (opts.fromJson) {
6158
+ const { readFileSync: readFileSync4 } = await import("fs");
6159
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6160
+ } else {
6161
+ if (!opts.name) {
6162
+ console.error("Error: --name is required");
6163
+ process.exit(1);
6164
+ }
6165
+ if (!opts.conversionType) {
6166
+ console.error("Error: --conversion-type is required");
6167
+ process.exit(1);
6168
+ }
6169
+ if (!opts.goalOrder) {
6170
+ console.error("Error: --goal-order is required (1-10)");
6171
+ process.exit(1);
6172
+ }
6173
+ body = {
6174
+ name: opts.name,
6175
+ conversionType: opts.conversionType,
6176
+ goalOrder: Number(opts.goalOrder),
6177
+ valueType: opts.valueType,
6178
+ countType: opts.countType,
6179
+ conversionWindowHours: Number(opts.window)
6180
+ };
6181
+ if (opts.fixedValue)
6182
+ body.fixedValue = Number(opts.fixedValue);
6183
+ if (opts.description)
6184
+ body.description = opts.description;
6185
+ }
6186
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals`, body });
6187
+ const json = await resp.json();
6188
+ if (!resp.ok) {
6189
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6190
+ process.exit(1);
6191
+ }
6192
+ const goal = json.data ?? json;
6193
+ console.log(`Conversion goal created: ${goal.id}`);
6194
+ console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
6195
+ console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
6196
+ });
6197
+ 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) => {
6198
+ let body;
6199
+ if (opts.fromJson) {
6200
+ const { readFileSync: readFileSync4 } = await import("fs");
6201
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6202
+ } else {
6203
+ body = {};
6204
+ if (opts.name)
6205
+ body.name = opts.name;
6206
+ if (opts.description)
6207
+ body.description = opts.description;
6208
+ if (opts.valueType)
6209
+ body.valueType = opts.valueType;
6210
+ if (opts.fixedValue)
6211
+ body.fixedValue = Number(opts.fixedValue);
6212
+ if (opts.countType)
6213
+ body.countType = opts.countType;
6214
+ if (opts.window)
6215
+ body.conversionWindowHours = Number(opts.window);
6216
+ }
6217
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}`, body });
6218
+ if (!resp.ok) {
6219
+ const j = await resp.json();
6220
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6221
+ process.exit(1);
6222
+ }
6223
+ console.log(`Conversion goal ${id} updated.`);
6224
+ });
6225
+ cmd.command("delete").description("Delete a conversion goal.").argument("<id>", "Goal ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
6226
+ if (!opts.yes) {
6227
+ console.error("Add --yes to confirm deletion.");
6228
+ process.exit(1);
6229
+ }
6230
+ const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/conversion-goals/${id}` });
6231
+ if (!resp.ok && resp.status !== 204) {
6232
+ console.error(`Error: ${resp.statusText}`);
6233
+ process.exit(1);
6234
+ }
6235
+ console.log(`Conversion goal ${id} deleted.`);
6236
+ });
6237
+ for (const action of ["pause", "activate", "archive"]) {
6238
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a conversion goal.`).argument("<id>", "Goal ID").action(async (id) => {
6239
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}/${action}` });
6240
+ if (!resp.ok) {
6241
+ const j = await resp.json();
6242
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6243
+ process.exit(1);
6244
+ }
6245
+ console.log(`Conversion goal ${id} ${action}d.`);
6246
+ });
6247
+ }
6248
+ cmd.command("regenerate-secret").description("Regenerate secret key and postback URL.").argument("<id>", "Goal ID").action(async (id) => {
6249
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals/${id}/regenerate-secret` });
6250
+ const json = await resp.json();
6251
+ if (!resp.ok) {
6252
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6253
+ process.exit(1);
6254
+ }
6255
+ const goal = json.data ?? json;
6256
+ console.log(`Secret regenerated for goal ${id}`);
6257
+ console.log(` New postback URL: ${goal.postbackUrl ?? goal.postback_url}`);
6258
+ });
6259
+ return cmd;
6260
+ }
6261
+
6262
+ // src/commands/algorithms.ts
6263
+ var COLUMNS10 = [
6264
+ { key: "id", header: "ID", width: 36 },
6265
+ { key: "name", header: "NAME", width: 28 },
6266
+ { key: "goal", header: "GOAL", width: 8 },
6267
+ { key: "conversionGoalId", header: "CONV_GOAL", width: 36 },
6268
+ { key: "status", header: "STATUS", width: 10 },
6269
+ { key: "campaignCount", header: "CAMPAIGNS", width: 10 }
6270
+ ];
6271
+ function createAlgorithmsCommand() {
6272
+ const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
6273
+
6274
+ Requires: ADVERTISER capability.`).addHelpText("after", `
6275
+ Examples:
6276
+ $ a8techads algorithms list
6277
+ $ a8techads algorithms get <id>
6278
+ $ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
6279
+ $ a8techads algorithms update <id> --conversion-goal-id <goal-id>
6280
+ $ a8techads algorithms pause <id>`);
6281
+ 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) => {
6282
+ const params = new URLSearchParams;
6283
+ if (opts.status)
6284
+ params.set("status", opts.status);
6285
+ params.set("limit", opts.limit);
6286
+ const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
6287
+ const json = await resp.json();
6288
+ if (!resp.ok) {
6289
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6290
+ process.exit(1);
6291
+ }
6292
+ const rows = (json.data ?? json).map((algo) => ({
6293
+ id: algo.id,
6294
+ name: algo.name,
6295
+ goal: algo.optimizationGoal,
6296
+ conversionGoalId: algo.conversionGoalId,
6297
+ status: algo.status,
6298
+ campaignCount: algo.campaignCount ?? 0
6299
+ }));
6300
+ printData(rows, COLUMNS10, opts.format);
6301
+ });
6302
+ addFormatOption(cmd.command("get").description("Get bidder algorithm details.").argument("<id>", "Algorithm ID")).action(async (id, opts) => {
6303
+ const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
6304
+ const json = await resp.json();
6305
+ if (!resp.ok) {
6306
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6307
+ process.exit(1);
6308
+ }
6309
+ printDetail(json.data ?? json, opts.format);
6310
+ });
6311
+ 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) => {
6312
+ const body = await buildAlgorithmBody(opts, true);
6313
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
6314
+ const json = await resp.json();
6315
+ if (!resp.ok) {
6316
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6317
+ process.exit(1);
6318
+ }
6319
+ console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
6320
+ });
6321
+ 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) => {
6322
+ const body = await buildAlgorithmBody(opts, false);
6323
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
6324
+ const json = await resp.json().catch(() => ({}));
6325
+ if (!resp.ok) {
6326
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6327
+ process.exit(1);
6328
+ }
6329
+ console.log(`Algorithm ${id} updated.`);
6330
+ });
6331
+ cmd.command("archive").description("Archive a bidder algorithm by setting status to ARCHIVED.").argument("<id>", "Algorithm ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
6332
+ if (!opts.yes) {
6333
+ console.error("Add --yes to confirm archiving.");
6334
+ process.exit(1);
6335
+ }
6336
+ const resp = await apiRequest({
6337
+ method: "PATCH",
6338
+ path: `${dspPrefix()}/bidder-algorithms/${id}`,
6339
+ body: { status: "ARCHIVED" }
6340
+ });
6341
+ const json = await resp.json().catch(() => ({}));
6342
+ if (!resp.ok) {
6343
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6344
+ process.exit(1);
6345
+ }
6346
+ console.log(`Algorithm ${id} archived.`);
6347
+ });
6348
+ for (const action of ["pause", "activate"]) {
6349
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a bidder algorithm.`).argument("<id>", "Algorithm ID").action(async (id) => {
6350
+ const resp = await apiRequest({
6351
+ method: "PATCH",
6352
+ path: `${dspPrefix()}/bidder-algorithms/${id}/${action}`
6353
+ });
6354
+ const json = await resp.json().catch(() => ({}));
6355
+ if (!resp.ok) {
6356
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6357
+ process.exit(1);
6358
+ }
6359
+ console.log(`Algorithm ${id} ${action}d.`);
6360
+ });
6361
+ }
6362
+ return cmd;
6363
+ }
6364
+ async function buildAlgorithmBody(opts, creating) {
6365
+ if (opts.fromJson) {
6366
+ const { readFileSync: readFileSync4 } = await import("fs");
6367
+ return JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6368
+ }
6369
+ const body = {};
6370
+ if (opts.name)
6371
+ body.name = opts.name;
6372
+ if (opts.description)
6373
+ body.description = opts.description;
6374
+ if (opts.optimizationGoal)
6375
+ body.optimizationGoal = String(opts.optimizationGoal).toUpperCase();
6376
+ if (opts.targetValue !== undefined)
6377
+ body.targetValue = Number(opts.targetValue);
6378
+ if (opts.conversionGoalId)
6379
+ body.conversionGoalId = opts.conversionGoalId;
6380
+ if (creating) {
6381
+ if (!body.name) {
6382
+ console.error("Error: --name is required.");
6383
+ process.exit(1);
6384
+ }
6385
+ if (!body.optimizationGoal) {
6386
+ console.error("Error: --optimization-goal is required.");
6387
+ process.exit(1);
6388
+ }
6389
+ if (body.targetValue === undefined) {
6390
+ console.error("Error: --target-value is required.");
6391
+ process.exit(1);
6392
+ }
6393
+ }
6394
+ return body;
6395
+ }
6396
+
6397
+ // src/commands/payouts.ts
6398
+ function createPayoutsCommand() {
6399
+ const cmd = new Command("payouts").description(`Payout management (SSP)
6400
+
6401
+ Requires: PUBLISHER capability.`).addHelpText("after", `
6402
+ Examples:
6403
+ $ a8techads payouts balance
6404
+ $ a8techads payouts account
6405
+ $ a8techads payouts list --limit 10
6406
+ $ a8techads payouts request --amount 50 --method usdt --yes`);
6407
+ addFormatOption(cmd.command("balance").description("Show current payout balance.")).action(async (opts) => {
6408
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/balance` });
6409
+ const json = await resp.json();
6410
+ if (!resp.ok) {
6411
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6412
+ process.exit(1);
6413
+ }
6414
+ printDetail(json.data ?? json, opts.format);
6415
+ });
6416
+ addFormatOption(cmd.command("account").description("Show payout account details.")).action(async (opts) => {
6417
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/account` });
6418
+ const json = await resp.json();
6419
+ if (!resp.ok) {
6420
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6421
+ process.exit(1);
6422
+ }
6423
+ printDetail(json.data ?? json, opts.format);
6424
+ });
6425
+ addFormatOption(cmd.command("list").description("List payout history.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
6426
+ const params = new URLSearchParams({ limit: opts.limit });
6427
+ if (opts.status)
6428
+ params.set("status", opts.status);
6429
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/list?${params}` });
6430
+ const json = await resp.json();
6431
+ if (!resp.ok) {
6432
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6433
+ process.exit(1);
6434
+ }
6435
+ const columns = [
6436
+ { key: "id", header: "ID", width: 36 },
6437
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
6438
+ { key: "status", header: "STATUS", width: 14 },
6439
+ { key: "method", header: "METHOD", width: 10 },
6440
+ { key: "requestedAt", header: "REQUESTED_AT", width: 20 }
6441
+ ];
6442
+ const rows = (json.data ?? json).map((t) => ({
6443
+ id: t.id,
6444
+ amount: t.amount,
6445
+ status: t.status,
6446
+ method: t.method,
6447
+ requestedAt: t.requestedAt ?? t.requested_at
6448
+ }));
6449
+ printData(rows, columns, opts.format);
6450
+ });
6451
+ cmd.command("request").description("Request a payout.").option("--amount <dollars>", "Payout amount in dollars (required)").option("--method <method>", "Payout method", "usdt").option("--wallet <address>", "Wallet address").option("--network <network>", "Network for crypto payouts", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
6452
+ if (!opts.amount) {
6453
+ console.error("Error: --amount is required.");
6454
+ process.exit(1);
6455
+ }
6456
+ const amount = Number(opts.amount);
6457
+ if (isNaN(amount) || amount <= 0) {
6458
+ console.error("Error: Amount must be a positive number.");
6459
+ process.exit(1);
6460
+ }
6461
+ if (!opts.yes) {
6462
+ const rl = await import("readline");
6463
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
6464
+ const answer = await new Promise((resolve) => iface.question(`Request payout of $${amount.toFixed(2)}? (y/N) `, resolve));
6465
+ iface.close();
6466
+ if (answer.toLowerCase() !== "y") {
6467
+ console.log("Cancelled.");
6468
+ process.exit(0);
6469
+ }
6470
+ }
6471
+ let resp;
6472
+ if (opts.method === "usdt") {
6473
+ const body = { amount, walletAddress: opts.wallet, network: opts.network };
6474
+ resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payments/usdt/payout`, body });
6475
+ } else {
6476
+ resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payouts/request`, body: { amount } });
6477
+ }
6478
+ const json = await resp.json();
6479
+ if (!resp.ok) {
6480
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6481
+ process.exit(1);
6482
+ }
6483
+ console.log(`Payout of $${amount.toFixed(2)} requested successfully.`);
6484
+ });
6485
+ return cmd;
6486
+ }
6487
+
6488
+ // src/commands/statements.ts
6489
+ function createStatementsCommand() {
6490
+ const cmd = new Command("statements").description(`Statement management (SSP)
6491
+
6492
+ Requires: PUBLISHER capability.`).addHelpText("after", `
6493
+ Examples:
6494
+ $ a8techads statements list
6495
+ $ a8techads statements get <id>`);
6496
+ addFormatOption(cmd.command("list").description("List statements.").option("--limit <n>", "Max results", "20")).action(async (opts) => {
6497
+ const params = new URLSearchParams({ limit: opts.limit });
6498
+ const resp = await apiRequest({ path: `${sspPrefix()}/statements?${params}` });
6499
+ const json = await resp.json();
6500
+ if (!resp.ok) {
6501
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6502
+ process.exit(1);
6503
+ }
6504
+ const columns = [
6505
+ { key: "id", header: "ID", width: 36 },
6506
+ { key: "number", header: "NUMBER", width: 14 },
6507
+ { key: "period", header: "PERIOD", width: 16 },
6508
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
6509
+ { key: "status", header: "STATUS", width: 12 }
6510
+ ];
6511
+ const rows = (json.data ?? json).map((t) => ({
6512
+ id: t.id,
6513
+ number: t.number,
6514
+ period: t.period,
6515
+ amount: t.amount,
6516
+ status: t.status
6517
+ }));
6518
+ printData(rows, columns, opts.format);
6519
+ });
6520
+ addFormatOption(cmd.command("get <id>").description("Show statement details.")).action(async (id, opts) => {
6521
+ const resp = await apiRequest({ path: `${sspPrefix()}/statements/${id}` });
6522
+ const json = await resp.json();
6523
+ if (!resp.ok) {
6524
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6525
+ process.exit(1);
6526
+ }
6527
+ printDetail(json.data ?? json, opts.format);
6528
+ });
6529
+ return cmd;
6530
+ }
6531
+
6532
+ // src/commands/simulator.ts
6533
+ import { readFileSync as readFileSync4 } from "fs";
6534
+ async function confirmAction2(message, yes) {
6535
+ if (yes)
6536
+ return;
6537
+ const rl = await import("readline");
6538
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
6539
+ const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
6540
+ iface.close();
6541
+ if (answer.toLowerCase() !== "y") {
6542
+ console.log("Cancelled.");
6543
+ process.exit(0);
6544
+ }
6545
+ }
6546
+ function createSimulatorCommand() {
6547
+ const cmd = new Command("simulator").description(`Simulator control and inspection
6548
+
6549
+ Used for replay and traffic verification workflows.`).addHelpText("after", `
6550
+ Examples:
6551
+ $ a8techads simulator status
6552
+ $ a8techads simulator start
6553
+ $ a8techads simulator stop --yes
6554
+ $ a8techads simulator reset --yes
6555
+ $ a8techads simulator config show
6556
+ $ a8techads simulator config update --from-json ./simulator-config.json`);
6557
+ addFormatOption(cmd.command("status").description("Show simulator runtime status and counters.")).action(async (opts) => {
6558
+ const resp = await simulatorRequest("GET", "/api/v1/simulator/status");
6559
+ const json = await resp.json();
6560
+ if (!resp.ok) {
6561
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6562
+ process.exit(1);
6563
+ }
6564
+ printDetail(json, opts.format);
6565
+ });
6566
+ cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
6567
+ let body = undefined;
6568
+ if (opts.fromJson) {
6569
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6570
+ }
6571
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
6572
+ const json = await resp.json();
6573
+ if (!resp.ok) {
6574
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6575
+ process.exit(1);
6576
+ }
6577
+ console.log(json.message ?? "Simulator started.");
6578
+ });
6579
+ cmd.command("stop").description("Stop the simulator.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
6580
+ await confirmAction2("Stop simulator?", opts.yes);
6581
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/stop");
6582
+ const json = await resp.json();
6583
+ if (!resp.ok) {
6584
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6585
+ process.exit(1);
6586
+ }
6587
+ console.log(json.message ?? "Simulator stopped.");
6588
+ });
6589
+ cmd.command("reset").description("Reset simulator counters and stats.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
6590
+ await confirmAction2("Reset simulator stats?", opts.yes);
6591
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/reset");
6592
+ const json = await resp.json();
6593
+ if (!resp.ok) {
6594
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6595
+ process.exit(1);
6596
+ }
6597
+ console.log(json.message ?? "Simulator stats reset.");
6598
+ });
6599
+ const config = cmd.command("config").description("Simulator config management");
6600
+ addFormatOption(config.command("show").description("Show current simulator config.")).action(async (opts) => {
6601
+ const resp = await simulatorRequest("GET", "/api/v1/simulator/config");
6602
+ const json = await resp.json();
6603
+ if (!resp.ok) {
6604
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6605
+ process.exit(1);
6606
+ }
6607
+ printDetail(json, opts.format);
6608
+ });
6609
+ config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
6610
+ const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6611
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
6612
+ const json = await resp.json();
6613
+ if (!resp.ok) {
6614
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6615
+ process.exit(1);
6616
+ }
6617
+ console.log("Simulator config updated.");
6618
+ if (json.config) {
6619
+ printDetail(json.config, "table");
6620
+ }
6621
+ });
6622
+ return cmd;
6623
+ }
6624
+ async function simulatorRequest(method, path, body) {
6625
+ const creds = loadCredentials();
6626
+ const profile = getCurrentProfile(creds);
6627
+ if (!profile) {
6628
+ throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
6629
+ }
6630
+ const baseUrl = toConsoleApiBase(profile.api_url);
6631
+ const request = await buildAuthenticatedRequest({ method, path, body, baseUrl });
6632
+ return fetch(request.url, request.init);
6633
+ }
6634
+ function toConsoleApiBase(apiUrl) {
6635
+ const url = new URL(apiUrl);
6636
+ if (url.hostname.startsWith("api.")) {
6637
+ url.hostname = url.hostname.replace(/^api\./, "console.");
6638
+ }
6639
+ return url.origin;
4845
6640
  }
4846
6641
 
4847
6642
  // src/index.ts
4848
6643
  function createProgram() {
4849
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.0").addHelpText("after", `
6644
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
4850
6645
  Command Groups:
4851
6646
  auth Authentication (login, logout, token, status)
4852
6647
  profile Multi-profile management
@@ -4854,13 +6649,21 @@ Command Groups:
4854
6649
  audiences Audience management (DSP)
4855
6650
  campaigns Campaign management (DSP)
4856
6651
  variations Ad variation management (DSP)
6652
+ conversion-goals Conversion goal management (DSP)
6653
+ media-assets Media library management (DSP)
6654
+ algorithms Bidder algorithm management (DSP)
4857
6655
  sites Site management (SSP)
4858
6656
  zones Ad zone management (SSP)
4859
6657
  reports Analytics and reporting
4860
6658
  billing Billing and payments (DSP)
4861
6659
  invoices Invoice management (DSP)
6660
+ payouts Payout management (SSP)
6661
+ statements Statement management (SSP)
6662
+ simulator Simulator control and replay support
4862
6663
  users Team member management
4863
6664
  settings Tenant settings
6665
+ admin Platform administration (Console)
6666
+ external-ssp External SSP partner management (Console)
4864
6667
 
4865
6668
  Getting Started:
4866
6669
  $ a8techads auth login # Authenticate
@@ -4874,14 +6677,21 @@ Getting Started:
4874
6677
  program2.addCommand(createAudiencesCommand());
4875
6678
  program2.addCommand(createCampaignsCommand());
4876
6679
  program2.addCommand(createVariationsCommand());
6680
+ program2.addCommand(createMediaAssetsCommand());
6681
+ program2.addCommand(createConversionGoalsCommand());
6682
+ program2.addCommand(createAlgorithmsCommand());
4877
6683
  program2.addCommand(createSitesCommand());
4878
6684
  program2.addCommand(createZonesCommand());
4879
6685
  program2.addCommand(createReportsCommand());
4880
6686
  program2.addCommand(createBillingCommand());
6687
+ program2.addCommand(createPayoutsCommand());
6688
+ program2.addCommand(createStatementsCommand());
6689
+ program2.addCommand(createSimulatorCommand());
4881
6690
  program2.addCommand(createUsersCommand());
4882
6691
  program2.addCommand(createSettingsCommand());
4883
6692
  program2.addCommand(createInvoicesCommand());
4884
6693
  program2.addCommand(createAdminCommand());
6694
+ program2.addCommand(createExternalSspCommand());
4885
6695
  return program2;
4886
6696
  }
4887
6697