@a8techads/cli 0.3.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 +2239 -201
  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();
@@ -3200,6 +3212,58 @@ function createProfileCommand() {
3200
3212
  return profile;
3201
3213
  }
3202
3214
 
3215
+ // src/utils/http.ts
3216
+ async function apiRequest(opts) {
3217
+ const request = await buildAuthenticatedRequest(opts);
3218
+ return fetch(request.url, request.init);
3219
+ }
3220
+ async function buildAuthenticatedRequest(opts) {
3221
+ await refreshTokenIfNeeded();
3222
+ const creds = loadCredentials();
3223
+ const profile = getCurrentProfile(creds);
3224
+ if (!profile) {
3225
+ throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
3226
+ }
3227
+ const ctx = loadContext();
3228
+ const context = getCurrentContext(ctx);
3229
+ validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
3230
+ const headers = {
3231
+ Authorization: `Bearer ${profile.access_token}`,
3232
+ "Content-Type": "application/json",
3233
+ ...opts.headers
3234
+ };
3235
+ if (context?.impersonation) {
3236
+ headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
3237
+ if (context.impersonation.effective_capability) {
3238
+ headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
3239
+ }
3240
+ } else if (context?.current_capability) {
3241
+ headers["X-Effective-Capability"] = context.current_capability;
3242
+ }
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
+ };
3252
+ }
3253
+ function validateTenantMatch(accessToken, contextTenantId) {
3254
+ const claims = decodeJwt(accessToken);
3255
+ if (!claims)
3256
+ return;
3257
+ const authClaims = getAuthClaims(claims);
3258
+ const tokenTenantId = authClaims?.tenant_id ?? null;
3259
+ if (!contextTenantId || !tokenTenantId)
3260
+ return;
3261
+ if (contextTenantId !== tokenTenantId) {
3262
+ throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
3263
+ ` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
3264
+ }
3265
+ }
3266
+
3203
3267
  // src/commands/context.ts
3204
3268
  function createContextCommand() {
3205
3269
  const context = new Command("context").description("Workspace context — tenant, capability, and app selection").addHelpText("after", `
@@ -3242,6 +3306,16 @@ Examples:
3242
3306
  }
3243
3307
  console.log(`App: ${profileCtx?.app ?? "none"}`);
3244
3308
  console.log(`Capability: ${profileCtx?.current_capability ?? "null (platform)"}`);
3309
+ if (profileCtx?.impersonation) {
3310
+ const imp = profileCtx.impersonation;
3311
+ console.log(`
3312
+ --- Managed Access Session ---`);
3313
+ console.log(`Target: ${imp.target_tenant_name} (${imp.target_tenant_id})`);
3314
+ console.log(`Capability: ${imp.effective_capability}`);
3315
+ console.log(`Role: ${imp.effective_role}`);
3316
+ console.log(`Grant: ${imp.managed_grant_id ?? "none"}`);
3317
+ console.log(`Started: ${imp.started_at}`);
3318
+ }
3245
3319
  });
3246
3320
  context.command("set-tenant").description(`Switch to a different tenant. Triggers re-authentication if the
3247
3321
  tenant differs from the current token tenant.
@@ -3376,6 +3450,74 @@ Shortcuts:
3376
3450
  console.log("Capability: PUBLISHER");
3377
3451
  console.log("App: ssp");
3378
3452
  });
3453
+ context.command("assume-role").description(`Start a managed access session as a platform operator.
3454
+
3455
+ Requires: platform_owner or platform_admin role.`).option("--tenant <id>", "Target tenant ID (required)").option("--capability <cap>", "Effective capability: ADVERTISER or PUBLISHER").addHelpText("after", `
3456
+ Examples:
3457
+ $ a8techads context assume-role --tenant 00000000-... --capability ADVERTISER
3458
+ $ a8techads context assume-role --tenant <id>`).action(async (opts) => {
3459
+ if (!opts.tenant) {
3460
+ console.error("Error: --tenant is required.");
3461
+ console.error('Run "a8techads context assume-role --help" for usage.');
3462
+ process.exit(1);
3463
+ }
3464
+ try {
3465
+ const body = { target_tenant_id: opts.tenant };
3466
+ if (opts.capability)
3467
+ body.target_capability = opts.capability;
3468
+ const resp = await apiRequest({
3469
+ method: "POST",
3470
+ path: "/api/v1/console/impersonation/start",
3471
+ body
3472
+ });
3473
+ if (!resp.ok) {
3474
+ const err = await resp.json().catch(() => ({ error: resp.statusText }));
3475
+ console.error(`Error: ${err.error || resp.statusText}`);
3476
+ process.exit(1);
3477
+ }
3478
+ const data = await resp.json();
3479
+ const targetTenant = data.target_tenant || {};
3480
+ const ctx = loadContext();
3481
+ const impersonation = {
3482
+ target_tenant_id: opts.tenant,
3483
+ target_tenant_name: targetTenant.company_name || "Unknown",
3484
+ effective_capability: data.effective_capability || opts.capability || "",
3485
+ effective_role: data.effective_role || "",
3486
+ managed_grant_id: data.managed_grant_id || null,
3487
+ started_at: new Date().toISOString()
3488
+ };
3489
+ setCurrentContext(ctx, { impersonation });
3490
+ saveContext(ctx);
3491
+ console.log("Managed access session started.");
3492
+ console.log(`Target: ${impersonation.target_tenant_name} (${opts.tenant})`);
3493
+ console.log(`Capability: ${impersonation.effective_capability}`);
3494
+ console.log(`Grant: ${impersonation.managed_grant_id || "auto-created"}`);
3495
+ } catch (err) {
3496
+ console.error(`Failed: ${err.message}`);
3497
+ process.exit(1);
3498
+ }
3499
+ });
3500
+ context.command("end-session").description(`End the current managed access session.
3501
+
3502
+ Requires: an active managed access session.`).addHelpText("after", `
3503
+ Examples:
3504
+ $ a8techads context end-session`).action(async () => {
3505
+ const ctx = loadContext();
3506
+ const profileCtx = getCurrentContext(ctx);
3507
+ if (!profileCtx?.impersonation) {
3508
+ console.log("No active managed access session.");
3509
+ return;
3510
+ }
3511
+ try {
3512
+ await apiRequest({
3513
+ method: "POST",
3514
+ path: "/api/v1/console/impersonation/end"
3515
+ });
3516
+ } catch {}
3517
+ setCurrentContext(ctx, { impersonation: null });
3518
+ saveContext(ctx);
3519
+ console.log("Managed access session ended.");
3520
+ });
3379
3521
  return context;
3380
3522
  }
3381
3523
  function deriveAppFromTenant(tenant) {
@@ -3392,46 +3534,6 @@ function deriveAuthUrl(tokenEndpoint) {
3392
3534
  return url.origin;
3393
3535
  }
3394
3536
 
3395
- // src/utils/http.ts
3396
- async function apiRequest(opts) {
3397
- await refreshTokenIfNeeded();
3398
- const creds = loadCredentials();
3399
- const profile = getCurrentProfile(creds);
3400
- if (!profile) {
3401
- throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
3402
- }
3403
- const ctx = loadContext();
3404
- const context = getCurrentContext(ctx);
3405
- validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
3406
- const headers = {
3407
- Authorization: `Bearer ${profile.access_token}`,
3408
- "Content-Type": "application/json",
3409
- ...opts.headers
3410
- };
3411
- if (context?.current_capability) {
3412
- headers["X-Effective-Capability"] = context.current_capability;
3413
- }
3414
- const url = `${profile.api_url}${opts.path}`;
3415
- return fetch(url, {
3416
- method: opts.method ?? "GET",
3417
- headers,
3418
- body: opts.body ? JSON.stringify(opts.body) : undefined
3419
- });
3420
- }
3421
- function validateTenantMatch(accessToken, contextTenantId) {
3422
- const claims = decodeJwt(accessToken);
3423
- if (!claims)
3424
- return;
3425
- const authClaims = getAuthClaims(claims);
3426
- const tokenTenantId = authClaims?.tenant_id ?? null;
3427
- if (!contextTenantId || !tokenTenantId)
3428
- return;
3429
- if (contextTenantId !== tokenTenantId) {
3430
- throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
3431
- ` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
3432
- }
3433
- }
3434
-
3435
3537
  // src/utils/api-prefix.ts
3436
3538
  function getApiPrefix() {
3437
3539
  const ctx = loadContext();
@@ -3454,6 +3556,9 @@ function dspPrefix() {
3454
3556
  function sspPrefix() {
3455
3557
  return "/api/v1/ssp";
3456
3558
  }
3559
+ function consolePrefix() {
3560
+ return "/api/v1/console";
3561
+ }
3457
3562
 
3458
3563
  // src/utils/output.ts
3459
3564
  function printData(data, columns, format = "table") {
@@ -3562,21 +3667,24 @@ Examples:
3562
3667
  printData(rows, COLUMNS, opts.format);
3563
3668
  });
3564
3669
  addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
3565
- const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
3566
- const json = await resp.json();
3567
- if (!resp.ok) {
3568
- console.error(`Error: ${json.error ?? resp.statusText}`);
3569
- process.exit(1);
3570
- }
3571
- printDetail(json.data ?? json, opts.format);
3670
+ const audience = await fetchAudience(id);
3671
+ printDetail(audience, opts.format);
3572
3672
  });
3573
- cmd.command("create").description(`Create a new audience.
3574
-
3575
- Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type (default: UPLOADED_LIST)", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").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", `
3576
3684
  Examples:
3577
3685
  $ a8techads audiences create --name "High-Value Customers"
3578
- $ a8techads audiences create --name "Retarget Pool" --ttl 30
3579
- $ 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) => {
3580
3688
  let body;
3581
3689
  if (opts.fromJson) {
3582
3690
  const { readFileSync: readFileSync3 } = await import("fs");
@@ -3586,6 +3694,14 @@ Examples:
3586
3694
  console.error('Error: --name is required. Run "a8techads audiences create --help".');
3587
3695
  process.exit(1);
3588
3696
  }
3697
+ if (opts.type === "RETARGETING" && !opts.goalId) {
3698
+ console.error("Error: --goal-id is required for RETARGETING type.");
3699
+ process.exit(1);
3700
+ }
3701
+ if (opts.type === "LOOKALIKE" && !opts.seed) {
3702
+ console.error("Error: --seed is required for LOOKALIKE type.");
3703
+ process.exit(1);
3704
+ }
3589
3705
  body = {
3590
3706
  name: opts.name,
3591
3707
  type: opts.type,
@@ -3593,6 +3709,12 @@ Examples:
3593
3709
  };
3594
3710
  if (opts.description)
3595
3711
  body.description = opts.description;
3712
+ if (opts.goalId) {
3713
+ body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
3714
+ }
3715
+ if (opts.seed) {
3716
+ body.rules = { seed_audience_id: opts.seed, expansion_ratio: Number(opts.ratio) };
3717
+ }
3596
3718
  }
3597
3719
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
3598
3720
  const json = await resp.json();
@@ -3657,31 +3779,89 @@ Examples:
3657
3779
  }
3658
3780
  console.log(`Audience ${id} archived.`);
3659
3781
  });
3660
- cmd.command("upload").description(`Upload a user list to populate audience membership.
3782
+ cmd.command("upload").description(`Upload user identifiers to populate audience membership.
3661
3783
 
3662
- Accepts CSV or JSON files with user identifiers.`).argument("<id>", "Audience ID").option("--file <path>", "File path (CSV or JSON)").option("--identifier-type <type>", "Identifier type: EMAIL_HASH or DEVICE_ID", "EMAIL_HASH").option("--estimated-size <n>", "Estimated number of users in file").addHelpText("after", `
3784
+ Reads a file with one identifier per line (email hash, device ID, etc.)
3785
+ and uploads to the audience membership store.`).argument("<id>", "Audience ID").option("--file <path>", "File path (one identifier per line, or JSON array)").option("--identifier-type <type>", "Identifier type: EMAIL_HASH or DEVICE_ID", "EMAIL_HASH").addHelpText("after", `
3663
3786
  Examples:
3664
- $ a8techads audiences upload <id> --file users.csv --identifier-type EMAIL_HASH
3665
- $ a8techads audiences upload <id> --file devices.json --identifier-type DEVICE_ID`).action(async (id, opts) => {
3787
+ $ a8techads audiences upload <id> --file users.txt --identifier-type EMAIL_HASH
3788
+ $ a8techads audiences upload <id> --file devices.txt --identifier-type DEVICE_ID
3789
+
3790
+ File format (one per line):
3791
+ a1b2c3d4e5f6...
3792
+ f6e5d4c3b2a1...`).action(async (id, opts) => {
3666
3793
  if (!opts.file) {
3667
3794
  console.error("Error: --file is required.");
3668
3795
  process.exit(1);
3669
3796
  }
3797
+ const { readFileSync: readFileSync3 } = await import("fs");
3798
+ let identifiers;
3799
+ try {
3800
+ const content = readFileSync3(opts.file, "utf-8").trim();
3801
+ try {
3802
+ identifiers = JSON.parse(content);
3803
+ if (!Array.isArray(identifiers))
3804
+ throw new Error("not array");
3805
+ } catch {
3806
+ identifiers = content.split(`
3807
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
3808
+ }
3809
+ } catch (err) {
3810
+ console.error(`Error reading file: ${err.message}`);
3811
+ process.exit(1);
3812
+ }
3813
+ if (identifiers.length === 0) {
3814
+ console.error("Error: file contains no identifiers.");
3815
+ process.exit(1);
3816
+ }
3817
+ console.log(`Uploading ${identifiers.length} identifiers...`);
3670
3818
  const body = {
3819
+ identifiers,
3671
3820
  identifierType: opts.identifierType
3672
3821
  };
3673
- if (opts.estimatedSize)
3674
- body.estimatedSize = Number(opts.estimatedSize);
3675
3822
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences/${id}/upload`, body });
3676
3823
  const json = await resp.json();
3677
3824
  if (!resp.ok) {
3678
3825
  console.error(`Error: ${json.error ?? resp.statusText}`);
3679
3826
  process.exit(1);
3680
3827
  }
3681
- console.log(`Upload processed. Audience status: ${json.data?.status ?? json.status}`);
3828
+ console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
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
+ }
3682
3853
  });
3683
3854
  return cmd;
3684
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
+ }
3685
3865
 
3686
3866
  // src/commands/campaigns.ts
3687
3867
  var COLUMNS2 = [
@@ -3826,8 +4006,178 @@ Examples:
3826
4006
  }
3827
4007
  console.log(`Campaign duplicated. New ID: ${json.data?.id ?? json.id}`);
3828
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);
3829
4133
  return cmd;
3830
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
+ }
3831
4181
 
3832
4182
  // src/commands/variations.ts
3833
4183
  var COLUMNS3 = [
@@ -3925,85 +4275,295 @@ Examples:
3925
4275
  return cmd;
3926
4276
  }
3927
4277
 
3928
- // 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";
3929
4281
  var COLUMNS4 = [
3930
4282
  { key: "id", header: "ID", width: 36 },
3931
- { key: "name", header: "NAME", width: 25 },
3932
- { key: "domain", header: "DOMAIN", width: 25 },
3933
- { key: "status", header: "STATUS", width: 12 },
3934
- { key: "zoneCount", header: "ZONES", width: 6 }
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 }
3935
4288
  ];
3936
- function createSitesCommand() {
3937
- const cmd = new Command("sites").description(`Site management (SSP)
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)
3938
4326
 
3939
- Requires: PUBLISHER capability.`).addHelpText("after", `
4327
+ Requires: ADVERTISER capability.`).addHelpText("after", `
3940
4328
  Examples:
3941
- $ a8techads sites list
3942
- $ a8techads sites get <id>
3943
- $ a8techads sites create --name "My Blog" --domain blog.example.com`);
3944
- addFormatOption(cmd.command("list").description("List publisher sites.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20")).action(async (opts) => {
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) => {
3945
4334
  const params = new URLSearchParams;
4335
+ params.set("limit", opts.limit);
4336
+ params.set("offset", opts.offset);
3946
4337
  if (opts.status)
3947
4338
  params.set("status", opts.status);
3948
- params.set("limit", opts.limit);
3949
- const resp = await apiRequest({ path: `${sspPrefix()}/sites?${params}` });
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}` });
3950
4346
  const json = await resp.json();
3951
4347
  if (!resp.ok) {
3952
4348
  console.error(`Error: ${json.error ?? resp.statusText}`);
3953
4349
  process.exit(1);
3954
4350
  }
3955
- const rows = (json.data ?? json).map((s) => ({
3956
- id: s.id,
3957
- name: s.name,
3958
- domain: s.domain,
3959
- status: s.status,
3960
- zoneCount: s.zoneCount ?? s.zone_count ?? "-"
3961
- }));
4351
+ const rows = (json.data ?? json).map(normalizeAsset);
3962
4352
  printData(rows, COLUMNS4, opts.format);
3963
4353
  });
3964
- addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
3965
- const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
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
+ });
3966
4367
  const json = await resp.json();
3967
4368
  if (!resp.ok) {
3968
4369
  console.error(`Error: ${json.error ?? resp.statusText}`);
3969
4370
  process.exit(1);
3970
4371
  }
3971
- printDetail(json.data ?? json, opts.format);
4372
+ const rows = (json.data ?? json).map(normalizeAsset);
4373
+ printData(rows, COLUMNS4, opts.format);
3972
4374
  });
3973
- 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) => {
3974
- let body;
3975
- if (opts.fromJson) {
3976
- const { readFileSync: readFileSync3 } = await import("fs");
3977
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
3978
- } else {
3979
- if (!opts.name || !opts.domain) {
3980
- console.error("Error: --name and --domain are required.");
3981
- process.exit(1);
3982
- }
3983
- body = { name: opts.name, domain: opts.domain, type: opts.type };
3984
- }
3985
- const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
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}` });
3986
4377
  const json = await resp.json();
3987
4378
  if (!resp.ok) {
3988
4379
  console.error(`Error: ${json.error ?? resp.statusText}`);
3989
4380
  process.exit(1);
3990
4381
  }
3991
- console.log(`Site created: ${json.data?.id ?? json.id}`);
4382
+ printDetail(json.data ?? json, opts.format);
3992
4383
  });
3993
- 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) => {
3994
- let body;
3995
- if (opts.fromJson) {
3996
- const { readFileSync: readFileSync3 } = await import("fs");
3997
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
3998
- } else {
3999
- body = {};
4000
- if (opts.name)
4001
- body.name = opts.name;
4002
- }
4003
- const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
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();
4004
4387
  if (!resp.ok) {
4005
- const j = await resp.json();
4006
- console.error(`Error: ${j.error ?? resp.statusText}`);
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 = [
4490
+ { key: "id", header: "ID", width: 36 },
4491
+ { key: "name", header: "NAME", width: 25 },
4492
+ { key: "domain", header: "DOMAIN", width: 25 },
4493
+ { key: "status", header: "STATUS", width: 12 },
4494
+ { key: "zoneCount", header: "ZONES", width: 6 }
4495
+ ];
4496
+ function createSitesCommand() {
4497
+ const cmd = new Command("sites").description(`Site management (SSP)
4498
+
4499
+ Requires: PUBLISHER capability.`).addHelpText("after", `
4500
+ Examples:
4501
+ $ a8techads sites list
4502
+ $ a8techads sites get <id>
4503
+ $ a8techads sites create --name "My Blog" --domain blog.example.com`);
4504
+ addFormatOption(cmd.command("list").description("List publisher sites.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20")).action(async (opts) => {
4505
+ const params = new URLSearchParams;
4506
+ if (opts.status)
4507
+ params.set("status", opts.status);
4508
+ params.set("limit", opts.limit);
4509
+ const resp = await apiRequest({ path: `${sspPrefix()}/sites?${params}` });
4510
+ const json = await resp.json();
4511
+ if (!resp.ok) {
4512
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4513
+ process.exit(1);
4514
+ }
4515
+ const rows = (json.data ?? json).map((s) => ({
4516
+ id: s.id,
4517
+ name: s.name,
4518
+ domain: s.domain,
4519
+ status: s.status,
4520
+ zoneCount: s.zoneCount ?? s.zone_count ?? "-"
4521
+ }));
4522
+ printData(rows, COLUMNS5, opts.format);
4523
+ });
4524
+ addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
4525
+ const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
4526
+ const json = await resp.json();
4527
+ if (!resp.ok) {
4528
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4529
+ process.exit(1);
4530
+ }
4531
+ printDetail(json.data ?? json, opts.format);
4532
+ });
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) => {
4534
+ let body;
4535
+ if (opts.fromJson) {
4536
+ const { readFileSync: readFileSync4 } = await import("fs");
4537
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4538
+ } else {
4539
+ if (!opts.name || !opts.domain) {
4540
+ console.error("Error: --name and --domain are required.");
4541
+ process.exit(1);
4542
+ }
4543
+ body = { name: opts.name, domain: opts.domain, type: opts.type };
4544
+ }
4545
+ const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
4546
+ const json = await resp.json();
4547
+ if (!resp.ok) {
4548
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4549
+ process.exit(1);
4550
+ }
4551
+ console.log(`Site created: ${json.data?.id ?? json.id}`);
4552
+ });
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) => {
4554
+ let body;
4555
+ if (opts.fromJson) {
4556
+ const { readFileSync: readFileSync4 } = await import("fs");
4557
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4558
+ } else {
4559
+ body = {};
4560
+ if (opts.name)
4561
+ body.name = opts.name;
4562
+ }
4563
+ const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
4564
+ if (!resp.ok) {
4565
+ const j = await resp.json();
4566
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4007
4567
  process.exit(1);
4008
4568
  }
4009
4569
  console.log(`Site ${id} updated.`);
@@ -4045,7 +4605,7 @@ Examples:
4045
4605
  }
4046
4606
 
4047
4607
  // src/commands/zones.ts
4048
- var COLUMNS5 = [
4608
+ var COLUMNS6 = [
4049
4609
  { key: "id", header: "ID", width: 36 },
4050
4610
  { key: "name", header: "NAME", width: 25 },
4051
4611
  { key: "format", header: "FORMAT", width: 18 },
@@ -4077,7 +4637,7 @@ Examples:
4077
4637
  format: z.adFormat ?? z.ad_format ?? "-",
4078
4638
  status: z.status
4079
4639
  }));
4080
- printData(rows, COLUMNS5, opts.format);
4640
+ printData(rows, COLUMNS6, opts.format);
4081
4641
  });
4082
4642
  addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
4083
4643
  const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
@@ -4091,8 +4651,8 @@ Examples:
4091
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) => {
4092
4652
  let body;
4093
4653
  if (opts.fromJson) {
4094
- const { readFileSync: readFileSync3 } = await import("fs");
4095
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4654
+ const { readFileSync: readFileSync4 } = await import("fs");
4655
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4096
4656
  } else {
4097
4657
  if (!opts.site || !opts.name) {
4098
4658
  console.error("Error: --site and --name are required.");
@@ -4113,8 +4673,8 @@ Examples:
4113
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) => {
4114
4674
  let body;
4115
4675
  if (opts.fromJson) {
4116
- const { readFileSync: readFileSync3 } = await import("fs");
4117
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4676
+ const { readFileSync: readFileSync4 } = await import("fs");
4677
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4118
4678
  } else {
4119
4679
  body = {};
4120
4680
  if (opts.name)
@@ -4163,11 +4723,12 @@ Examples:
4163
4723
  $ a8techads reports templates`);
4164
4724
  addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
4165
4725
 
4166
- 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", `
4167
4727
  Examples:
4168
4728
  $ a8techads reports query --metrics spend,impressions --dimensions date --from 2026-03-01 --to 2026-03-20
4169
4729
  $ a8techads reports query --metrics revenue --dimensions publisher --format json
4170
- $ 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) => {
4171
4732
  const body = {
4172
4733
  metrics: opts.metrics.split(","),
4173
4734
  dimensions: opts.dimensions.split(","),
@@ -4183,19 +4744,20 @@ Examples:
4183
4744
  console.error(`Error: ${json.error ?? resp.statusText}`);
4184
4745
  process.exit(1);
4185
4746
  }
4186
- const rows = json.data?.rows ?? json.rows ?? [];
4747
+ const data = json.data ?? json;
4748
+ const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
4187
4749
  if (rows.length === 0) {
4188
4750
  console.log("No results.");
4189
4751
  return;
4190
4752
  }
4191
4753
  if (opts.format === "json") {
4192
- console.log(JSON.stringify(json.data ?? json, null, 2));
4754
+ console.log(JSON.stringify(data, null, 2));
4193
4755
  return;
4194
4756
  }
4195
4757
  const keys = Object.keys(rows[0]);
4196
4758
  const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
4197
4759
  printData(rows, columns, opts.format);
4198
- const summary = json.data?.summary ?? json.summary;
4760
+ const summary = data.summary ?? data.totals ?? json.summary ?? json.totals;
4199
4761
  if (summary && opts.format === "table") {
4200
4762
  console.log(`
4201
4763
  Summary:`);
@@ -4211,7 +4773,9 @@ Summary:`);
4211
4773
  console.error(`Error: ${json.error ?? resp.statusText}`);
4212
4774
  process.exit(1);
4213
4775
  }
4214
- 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 }));
4215
4779
  printData(rows, [
4216
4780
  { key: "id", header: "ID", width: 36 },
4217
4781
  { key: "name", header: "NAME", width: 30 },
@@ -4234,7 +4798,9 @@ Summary:`);
4234
4798
  console.error(`Error: ${json.error ?? resp.statusText}`);
4235
4799
  process.exit(1);
4236
4800
  }
4237
- 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 }));
4238
4804
  printData(rows, [
4239
4805
  { key: "id", header: "ID", width: 20 },
4240
4806
  { key: "name", header: "NAME", width: 25 },
@@ -4244,8 +4810,8 @@ Summary:`);
4244
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) => {
4245
4811
  let body;
4246
4812
  if (opts.fromJson) {
4247
- const { readFileSync: readFileSync3 } = await import("fs");
4248
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4813
+ const { readFileSync: readFileSync4 } = await import("fs");
4814
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4249
4815
  } else {
4250
4816
  if (!opts.name) {
4251
4817
  console.error("Error: --name is required.");
@@ -4347,8 +4913,8 @@ Examples:
4347
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) => {
4348
4914
  let body;
4349
4915
  if (opts.fromJson) {
4350
- const { readFileSync: readFileSync3 } = await import("fs");
4351
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4916
+ const { readFileSync: readFileSync4 } = await import("fs");
4917
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4352
4918
  } else {
4353
4919
  body = {};
4354
4920
  if (opts.autoRecharge !== undefined)
@@ -4362,6 +4928,60 @@ Examples:
4362
4928
  }
4363
4929
  console.log("Billing settings updated.");
4364
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
+ });
4365
4985
  return cmd;
4366
4986
  }
4367
4987
 
@@ -4378,7 +4998,7 @@ function usersPrefix() {
4378
4998
  }
4379
4999
  return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4380
5000
  }
4381
- var COLUMNS6 = [
5001
+ var COLUMNS7 = [
4382
5002
  { key: "id", header: "ID", width: 36 },
4383
5003
  { key: "email", header: "EMAIL", width: 30 },
4384
5004
  { key: "name", header: "NAME", width: 20 },
@@ -4409,7 +5029,7 @@ Examples:
4409
5029
  role: u.role,
4410
5030
  status: u.status
4411
5031
  }));
4412
- printData(rows, COLUMNS6, opts.format);
5032
+ printData(rows, COLUMNS7, opts.format);
4413
5033
  });
4414
5034
  addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
4415
5035
  const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
@@ -4488,39 +5108,52 @@ Examples:
4488
5108
  return cmd;
4489
5109
  }
4490
5110
 
4491
- // src/commands/settings.ts
4492
- function settingsPrefix() {
4493
- const ctx = loadContext();
4494
- const context = getCurrentContext(ctx);
4495
- const app = context?.app ?? "dsp";
4496
- if (app === "console") {
4497
- console.error("Error: Settings commands are not available in Console mode.");
4498
- console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
4499
- process.exit(1);
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);
4500
5122
  }
4501
- return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4502
5123
  }
4503
- function createSettingsCommand() {
4504
- const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
5124
+ function createAdminCommand() {
5125
+ const cmd = new Command("admin").description(`Platform administration (Console)
4505
5126
 
4506
- Requires: ADVERTISER or PUBLISHER capability.
4507
- Not available in Console mode.`).addHelpText("after", `
5127
+ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
4508
5128
  Examples:
4509
- $ a8techads settings show
4510
- $ a8techads settings update --from-json settings.json
4511
- $ a8techads settings profile
4512
- $ a8techads settings profile-update --company-name "New Name"`);
4513
- addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
4514
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
5129
+ $ a8techads admin tenants list
5130
+ $ a8techads admin tenants get <id>
5131
+ $ a8techads admin audit-logs
5132
+ $ a8techads admin system health`);
5133
+ const tenants = cmd.command("tenants").description("Tenant management");
5134
+ addFormatOption(tenants.command("list").description("List all tenants.")).action(async (opts) => {
5135
+ const resp = await apiRequest({ path: "/api/v1/console/tenants" });
4515
5136
  const json = await resp.json();
4516
5137
  if (!resp.ok) {
4517
5138
  console.error(`Error: ${json.error ?? resp.statusText}`);
4518
5139
  process.exit(1);
4519
5140
  }
4520
- printDetail(json.data ?? json, opts.format);
5141
+ const rows = (json.data ?? json).map((t) => ({
5142
+ id: t.id,
5143
+ name: t.companyName ?? t.company_name,
5144
+ type: t.tenantType ?? t.tenant_type,
5145
+ status: t.status
5146
+ }));
5147
+ const columns = [
5148
+ { key: "id", header: "ID", width: 36 },
5149
+ { key: "name", header: "NAME", width: 25 },
5150
+ { key: "type", header: "TYPE", width: 12 },
5151
+ { key: "status", header: "STATUS", width: 10 }
5152
+ ];
5153
+ printData(rows, columns, opts.format);
4521
5154
  });
4522
- addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
4523
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
5155
+ addFormatOption(tenants.command("get").description("Get tenant details.").argument("<id>", "Tenant ID")).action(async (id, opts) => {
5156
+ const resp = await apiRequest({ path: `/api/v1/console/tenants/${id}` });
4524
5157
  const json = await resp.json();
4525
5158
  if (!resp.ok) {
4526
5159
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4528,76 +5161,1364 @@ Examples:
4528
5161
  }
4529
5162
  printDetail(json.data ?? json, opts.format);
4530
5163
  });
4531
- cmd.command("update").description(`Update tenant settings.
4532
-
4533
- Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4534
- if (!opts.fromJson) {
4535
- console.error("Error: --from-json is required for settings update.");
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.");
4536
5170
  process.exit(1);
4537
5171
  }
4538
- const { readFileSync: readFileSync3 } = await import("fs");
4539
- const body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4540
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
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 });
5188
+ const json = await resp.json();
5189
+ if (!resp.ok) {
5190
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5191
+ process.exit(1);
5192
+ }
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
+ }
5203
+ });
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 });
4541
5215
  if (!resp.ok) {
4542
5216
  const j = await resp.json();
4543
5217
  console.error(`Error: ${j.error ?? resp.statusText}`);
4544
5218
  process.exit(1);
4545
5219
  }
4546
- console.log("Settings updated.");
5220
+ console.log(`Tenant ${id} updated.`);
4547
5221
  });
4548
- 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) => {
4549
- let body;
4550
- if (opts.fromJson) {
4551
- const { readFileSync: readFileSync3 } = await import("fs");
4552
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4553
- } else {
4554
- body = {};
4555
- if (opts.companyName)
4556
- body.companyName = opts.companyName;
4557
- }
4558
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
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
+ });
4559
5228
  if (!resp.ok) {
4560
5229
  const j = await resp.json();
4561
5230
  console.error(`Error: ${j.error ?? resp.statusText}`);
4562
5231
  process.exit(1);
4563
5232
  }
4564
- console.log("Profile updated.");
5233
+ console.log(`Tenant ${id} suspended.`);
4565
5234
  });
4566
- return cmd;
4567
- }
4568
-
4569
- // src/commands/invoices.ts
4570
- function createInvoicesCommand() {
4571
- const cmd = new Command("invoices").description(`Invoice management (DSP)
4572
-
4573
- Requires: ADVERTISER capability.`).addHelpText("after", `
4574
- Examples:
4575
- $ a8techads invoices list
4576
- $ a8techads invoices get <id>
4577
- $ a8techads invoices spending`);
4578
- addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
4579
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
4580
- const json = await resp.json();
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
+ });
4581
5241
  if (!resp.ok) {
4582
- console.error(`Error: ${json.error ?? resp.statusText}`);
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);
5276
+ const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
5277
+ const json = await resp.json();
5278
+ if (!resp.ok) {
5279
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5280
+ process.exit(1);
5281
+ }
5282
+ const rows = (json.data ?? json).map((l) => ({
5283
+ action: l.action,
5284
+ target: l.targetType ?? l.target_type,
5285
+ targetId: l.targetId ?? l.target_id,
5286
+ user: l.adminUserId ?? l.admin_user_id,
5287
+ time: l.createdAt ?? l.created_at
5288
+ }));
5289
+ printData(rows, [
5290
+ { key: "action", header: "ACTION", width: 25 },
5291
+ { key: "target", header: "TARGET", width: 12 },
5292
+ { key: "targetId", header: "TARGET ID", width: 36 },
5293
+ { key: "time", header: "TIME", width: 22 }
5294
+ ], opts.format);
5295
+ });
5296
+ const system = cmd.command("system").description("System operations");
5297
+ addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
5298
+ const resp = await apiRequest({ path: "/api/v1/console/system/health" });
5299
+ const json = await resp.json();
5300
+ if (!resp.ok) {
5301
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5302
+ process.exit(1);
5303
+ }
5304
+ printDetail(json.data ?? json, opts.format);
5305
+ });
5306
+ system.command("cache-clear").description("Clear system cache.").action(async () => {
5307
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
5308
+ if (!resp.ok) {
5309
+ const j = await resp.json();
5310
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5311
+ process.exit(1);
5312
+ }
5313
+ console.log("Cache cleared.");
5314
+ });
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" });
5318
+ const json = await resp.json();
5319
+ if (!resp.ok) {
5320
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5321
+ process.exit(1);
5322
+ }
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}`);
5333
+ });
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` });
5337
+ const json = await resp.json();
5338
+ if (!resp.ok) {
5339
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5340
+ process.exit(1);
5341
+ }
5342
+ printDetail(json.data ?? json, opts.format);
5343
+ });
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}`);
5354
+ process.exit(1);
5355
+ }
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` });
5485
+ if (!resp.ok) {
5486
+ const j = await resp.json();
5487
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5488
+ process.exit(1);
5489
+ }
5490
+ console.log(`Invoice ${id} issued.`);
5491
+ });
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);
5499
+ }
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` });
5505
+ if (!resp.ok) {
5506
+ const j = await resp.json();
5507
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5508
+ process.exit(1);
5509
+ }
5510
+ console.log(`Invoice ${id} marked as paid.`);
5511
+ });
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}` });
5518
+ const json = await resp.json();
5519
+ if (!resp.ok) {
5520
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5521
+ process.exit(1);
5522
+ }
5523
+ const rows = (json.data ?? json).map((i) => ({
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
5529
+ }));
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);
5537
+ });
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` });
5540
+ const json = await resp.json();
5541
+ if (!resp.ok) {
5542
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5543
+ process.exit(1);
5544
+ }
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);
5567
+ });
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}` });
5570
+ const json = await resp.json();
5571
+ if (!resp.ok) {
5572
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5573
+ process.exit(1);
5574
+ }
5575
+ printDetail(json.data ?? json, opts.format);
5576
+ });
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}`);
4583
6433
  process.exit(1);
4584
6434
  }
4585
6435
  const columns = [
4586
6436
  { key: "id", header: "ID", width: 36 },
4587
- { key: "period", header: "PERIOD", width: 20 },
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 },
4588
6508
  { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
4589
6509
  { key: "status", header: "STATUS", width: 12 }
4590
6510
  ];
4591
- const rows = (json.data ?? json).map((i) => ({
4592
- id: i.id,
4593
- period: i.period ?? i.billingPeriod,
4594
- amount: i.amount ?? i.totalAmount,
4595
- status: i.status
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
4596
6517
  }));
4597
6518
  printData(rows, columns, opts.format);
4598
6519
  });
4599
- addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
4600
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
6520
+ addFormatOption(cmd.command("get <id>").description("Show statement details.")).action(async (id, opts) => {
6521
+ const resp = await apiRequest({ path: `${sspPrefix()}/statements/${id}` });
4601
6522
  const json = await resp.json();
4602
6523
  if (!resp.ok) {
4603
6524
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4605,21 +6526,122 @@ Examples:
4605
6526
  }
4606
6527
  printDetail(json.data ?? json, opts.format);
4607
6528
  });
4608
- addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
4609
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
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");
4610
6559
  const json = await resp.json();
4611
6560
  if (!resp.ok) {
4612
6561
  console.error(`Error: ${json.error ?? resp.statusText}`);
4613
6562
  process.exit(1);
4614
6563
  }
4615
- printDetail(json.data ?? json, opts.format);
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
+ }
4616
6621
  });
4617
6622
  return cmd;
4618
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;
6640
+ }
4619
6641
 
4620
6642
  // src/index.ts
4621
6643
  function createProgram() {
4622
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.2.0").addHelpText("after", `
6644
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
4623
6645
  Command Groups:
4624
6646
  auth Authentication (login, logout, token, status)
4625
6647
  profile Multi-profile management
@@ -4627,13 +6649,21 @@ Command Groups:
4627
6649
  audiences Audience management (DSP)
4628
6650
  campaigns Campaign management (DSP)
4629
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)
4630
6655
  sites Site management (SSP)
4631
6656
  zones Ad zone management (SSP)
4632
6657
  reports Analytics and reporting
4633
6658
  billing Billing and payments (DSP)
4634
6659
  invoices Invoice management (DSP)
6660
+ payouts Payout management (SSP)
6661
+ statements Statement management (SSP)
6662
+ simulator Simulator control and replay support
4635
6663
  users Team member management
4636
6664
  settings Tenant settings
6665
+ admin Platform administration (Console)
6666
+ external-ssp External SSP partner management (Console)
4637
6667
 
4638
6668
  Getting Started:
4639
6669
  $ a8techads auth login # Authenticate
@@ -4647,13 +6677,21 @@ Getting Started:
4647
6677
  program2.addCommand(createAudiencesCommand());
4648
6678
  program2.addCommand(createCampaignsCommand());
4649
6679
  program2.addCommand(createVariationsCommand());
6680
+ program2.addCommand(createMediaAssetsCommand());
6681
+ program2.addCommand(createConversionGoalsCommand());
6682
+ program2.addCommand(createAlgorithmsCommand());
4650
6683
  program2.addCommand(createSitesCommand());
4651
6684
  program2.addCommand(createZonesCommand());
4652
6685
  program2.addCommand(createReportsCommand());
4653
6686
  program2.addCommand(createBillingCommand());
6687
+ program2.addCommand(createPayoutsCommand());
6688
+ program2.addCommand(createStatementsCommand());
6689
+ program2.addCommand(createSimulatorCommand());
4654
6690
  program2.addCommand(createUsersCommand());
4655
6691
  program2.addCommand(createSettingsCommand());
4656
6692
  program2.addCommand(createInvoicesCommand());
6693
+ program2.addCommand(createAdminCommand());
6694
+ program2.addCommand(createExternalSspCommand());
4657
6695
  return program2;
4658
6696
  }
4659
6697