@a8techads/cli 0.4.0 → 0.4.2

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 +3899 -249
  2. package/package.json +1 -1
package/dist/a8techads.js CHANGED
@@ -5,35 +5,21 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
8
  var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
9
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
10
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
11
  for (let key of __getOwnPropNames(mod))
24
12
  if (!__hasOwnProp.call(to, key))
25
13
  __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
14
+ get: () => mod[key],
27
15
  enumerable: true
28
16
  });
29
- if (canCache)
30
- cache.set(mod, to);
31
17
  return to;
32
18
  };
33
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
20
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
35
21
 
36
- // node_modules/commander/lib/error.js
22
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
37
23
  var require_error = __commonJS((exports) => {
38
24
  class CommanderError extends Error {
39
25
  constructor(exitCode, code, message) {
@@ -57,7 +43,7 @@ var require_error = __commonJS((exports) => {
57
43
  exports.InvalidArgumentError = InvalidArgumentError;
58
44
  });
59
45
 
60
- // node_modules/commander/lib/argument.js
46
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/argument.js
61
47
  var require_argument = __commonJS((exports) => {
62
48
  var { InvalidArgumentError } = require_error();
63
49
 
@@ -136,7 +122,7 @@ var require_argument = __commonJS((exports) => {
136
122
  exports.humanReadableArgName = humanReadableArgName;
137
123
  });
138
124
 
139
- // node_modules/commander/lib/help.js
125
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/help.js
140
126
  var require_help = __commonJS((exports) => {
141
127
  var { humanReadableArgName } = require_argument();
142
128
 
@@ -486,7 +472,7 @@ ${itemIndentStr}`);
486
472
  exports.stripColor = stripColor;
487
473
  });
488
474
 
489
- // node_modules/commander/lib/option.js
475
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/option.js
490
476
  var require_option = __commonJS((exports) => {
491
477
  var { InvalidArgumentError } = require_error();
492
478
 
@@ -664,7 +650,7 @@ var require_option = __commonJS((exports) => {
664
650
  exports.DualOptions = DualOptions;
665
651
  });
666
652
 
667
- // node_modules/commander/lib/suggestSimilar.js
653
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
668
654
  var require_suggestSimilar = __commonJS((exports) => {
669
655
  var maxDistance = 3;
670
656
  function editDistance(a, b) {
@@ -737,7 +723,7 @@ var require_suggestSimilar = __commonJS((exports) => {
737
723
  exports.suggestSimilar = suggestSimilar;
738
724
  });
739
725
 
740
- // node_modules/commander/lib/command.js
726
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/command.js
741
727
  var require_command = __commonJS((exports) => {
742
728
  var EventEmitter = __require("node:events").EventEmitter;
743
729
  var childProcess = __require("node:child_process");
@@ -2047,7 +2033,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2047
2033
  exports.useColor = useColor;
2048
2034
  });
2049
2035
 
2050
- // node_modules/commander/index.js
2036
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/index.js
2051
2037
  var require_commander = __commonJS((exports) => {
2052
2038
  var { Argument } = require_argument();
2053
2039
  var { Command } = require_command();
@@ -2067,7 +2053,7 @@ var require_commander = __commonJS((exports) => {
2067
2053
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2068
2054
  });
2069
2055
 
2070
- // node_modules/commander/esm.mjs
2056
+ // ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
2071
2057
  var import__ = __toESM(require_commander(), 1);
2072
2058
  var {
2073
2059
  program,
@@ -2087,7 +2073,7 @@ var {
2087
2073
  import { createServer } from "http";
2088
2074
  import { exec } from "child_process";
2089
2075
 
2090
- // node_modules/oauth4webapi/build/index.js
2076
+ // ../../node_modules/.pnpm/oauth4webapi@3.8.5/node_modules/oauth4webapi/build/index.js
2091
2077
  var USER_AGENT;
2092
2078
  if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) {
2093
2079
  const NAME = "oauth4webapi";
@@ -2628,6 +2614,18 @@ async function login(opts = {}) {
2628
2614
  const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL;
2629
2615
  const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
2630
2616
  const authorizationEndpoint = `${authUrl}/.ory/hydra/oauth2/auth`;
2617
+ if (opts.forceLogin) {
2618
+ const existingCreds = loadCredentials();
2619
+ if (existingCreds.profiles[profileName]) {
2620
+ delete existingCreds.profiles[profileName];
2621
+ saveCredentials(existingCreds);
2622
+ }
2623
+ const existingCtx = loadContext();
2624
+ if (existingCtx.profiles[profileName]) {
2625
+ delete existingCtx.profiles[profileName];
2626
+ saveContext(existingCtx);
2627
+ }
2628
+ }
2631
2629
  const codeVerifier = generateRandomCodeVerifier();
2632
2630
  const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
2633
2631
  const state = generateRandomState();
@@ -2831,7 +2829,9 @@ async function loginClientCredentials(opts) {
2831
2829
  const apiUrl = opts.apiUrl ?? DEFAULT_API_URL2;
2832
2830
  const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL2;
2833
2831
  const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
2834
- console.log("Authenticating with client credentials...");
2832
+ if (!opts.silent) {
2833
+ console.log("Authenticating with client credentials...");
2834
+ }
2835
2835
  const resp = await fetch(tokenEndpoint, {
2836
2836
  method: "POST",
2837
2837
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -2849,7 +2849,7 @@ async function loginClientCredentials(opts) {
2849
2849
  const tokens = await resp.json();
2850
2850
  const claims = decodeJwt(tokens.access_token);
2851
2851
  const authClaims = claims ? getAuthClaims(claims) : null;
2852
- const profileName = `client:${opts.clientId}`;
2852
+ const profileName = opts.profile ?? `client:${opts.clientId}`;
2853
2853
  const tenants = await fetchTenantsForClient(apiUrl, tokens.access_token);
2854
2854
  const expiresAt = new Date(Date.now() + tokens.expires_in * 1000).toISOString();
2855
2855
  const profileCreds = {
@@ -2881,19 +2881,21 @@ async function loginClientCredentials(opts) {
2881
2881
  ctx.current_profile = profileName;
2882
2882
  ctx.profiles[profileName] = profileCtx;
2883
2883
  saveContext(ctx);
2884
- console.log(`
2884
+ if (!opts.silent) {
2885
+ console.log(`
2885
2886
  Authenticated as ${profileName}`);
2886
- console.log(`Profile: ${profileName}`);
2887
- if (tenant) {
2888
- console.log(`Tenant: ${tenant.tenant_name} (${tenant.tenant_id})`);
2889
- }
2890
- console.log(`App: ${app}`);
2891
- if (capability) {
2892
- console.log(`Capability: ${capability}`);
2893
- }
2894
- console.log(`Token expires: ${expiresAt}`);
2895
- console.log(`
2887
+ console.log(`Profile: ${profileName}`);
2888
+ if (tenant) {
2889
+ console.log(`Tenant: ${tenant.tenant_name} (${tenant.tenant_id})`);
2890
+ }
2891
+ console.log(`App: ${app}`);
2892
+ if (capability) {
2893
+ console.log(`Capability: ${capability}`);
2894
+ }
2895
+ console.log(`Token expires: ${expiresAt}`);
2896
+ console.log(`
2896
2897
  Note: Client credentials tokens cannot be refreshed. Re-authenticate on expiry.`);
2898
+ }
2897
2899
  }
2898
2900
  function deriveApp2(tenant) {
2899
2901
  if (!tenant)
@@ -2958,16 +2960,124 @@ function logout(profileName) {
2958
2960
  console.log(`Logged out from profile "${name}".`);
2959
2961
  }
2960
2962
 
2963
+ // src/store/oauth-client.ts
2964
+ import { join as join3 } from "path";
2965
+ import { homedir as homedir3 } from "os";
2966
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "fs";
2967
+ var CONFIG_DIR3 = join3(homedir3(), ".alpineads");
2968
+ var OAUTH_CLIENT_PATH = join3(CONFIG_DIR3, "oauth-client.json");
2969
+ var DEFAULT_API_URL3 = "https://api.a8.tech";
2970
+ var DEFAULT_AUTH_URL3 = "https://auth.a8.tech";
2971
+ function ensureDir3() {
2972
+ if (!existsSync3(CONFIG_DIR3)) {
2973
+ mkdirSync3(CONFIG_DIR3, { recursive: true, mode: 448 });
2974
+ }
2975
+ }
2976
+ function defaultClientProfile(clientId) {
2977
+ return `client:${clientId}`;
2978
+ }
2979
+ function loadOAuthClientConfig(profileName) {
2980
+ const fromEnv = loadOAuthClientConfigFromEnv(profileName);
2981
+ if (fromEnv)
2982
+ return fromEnv;
2983
+ const file = loadOAuthClientConfigFile();
2984
+ if (!file)
2985
+ return null;
2986
+ const selectedProfile = profileName ?? file.current_profile;
2987
+ const selected = file.profiles[selectedProfile];
2988
+ if (!selected)
2989
+ return null;
2990
+ return {
2991
+ profile: selectedProfile,
2992
+ ...selected
2993
+ };
2994
+ }
2995
+ function loadOAuthClientConfigFile() {
2996
+ if (!existsSync3(OAUTH_CLIENT_PATH))
2997
+ return null;
2998
+ const raw = JSON.parse(readFileSync3(OAUTH_CLIENT_PATH, "utf-8"));
2999
+ if ("profiles" in raw)
3000
+ return raw;
3001
+ return {
3002
+ version: 1,
3003
+ current_profile: raw.profile,
3004
+ profiles: {
3005
+ [raw.profile]: {
3006
+ client_id: raw.client_id,
3007
+ client_secret: raw.client_secret,
3008
+ api_url: raw.api_url,
3009
+ auth_url: raw.auth_url
3010
+ }
3011
+ }
3012
+ };
3013
+ }
3014
+ function saveOAuthClientConfig(input) {
3015
+ const profile = input.profile ?? defaultClientProfile(input.clientId);
3016
+ const file = loadOAuthClientConfigFile() ?? {
3017
+ version: 1,
3018
+ current_profile: profile,
3019
+ profiles: {}
3020
+ };
3021
+ file.current_profile = profile;
3022
+ file.profiles[profile] = {
3023
+ client_id: input.clientId,
3024
+ client_secret: input.clientSecret,
3025
+ api_url: input.apiUrl ?? DEFAULT_API_URL3,
3026
+ auth_url: input.authUrl ?? DEFAULT_AUTH_URL3
3027
+ };
3028
+ ensureDir3();
3029
+ writeFileSync3(OAUTH_CLIENT_PATH, JSON.stringify(file, null, 2), {
3030
+ mode: 384
3031
+ });
3032
+ chmodSync3(OAUTH_CLIENT_PATH, 384);
3033
+ return {
3034
+ profile,
3035
+ ...file.profiles[profile]
3036
+ };
3037
+ }
3038
+ function getConfiguredOAuthClientProfiles() {
3039
+ const envConfig = loadOAuthClientConfigFromEnv();
3040
+ const file = loadOAuthClientConfigFile();
3041
+ return Array.from(new Set([
3042
+ ...file ? Object.keys(file.profiles) : [],
3043
+ ...envConfig ? [envConfig.profile] : []
3044
+ ]));
3045
+ }
3046
+ function hasConfiguredOAuthClientProfile(profileName) {
3047
+ return loadOAuthClientConfig(profileName) !== null;
3048
+ }
3049
+ function loadOAuthClientConfigFromEnv(profileName) {
3050
+ const clientId = process.env.A8TECHADS_CLIENT_ID ?? process.env.A8TECHADS_OAUTH_CLIENT_ID;
3051
+ const clientSecret = process.env.A8TECHADS_CLIENT_SECRET ?? process.env.A8TECHADS_OAUTH_CLIENT_SECRET;
3052
+ if (!clientId || !clientSecret)
3053
+ return null;
3054
+ const profile = process.env.A8TECHADS_PROFILE ?? process.env.A8TECHADS_OAUTH_PROFILE ?? defaultClientProfile(clientId);
3055
+ if (profileName && profileName !== profile)
3056
+ return null;
3057
+ return {
3058
+ profile,
3059
+ client_id: clientId,
3060
+ client_secret: clientSecret,
3061
+ api_url: process.env.A8TECHADS_API_URL ?? DEFAULT_API_URL3,
3062
+ auth_url: process.env.A8TECHADS_AUTH_URL ?? DEFAULT_AUTH_URL3
3063
+ };
3064
+ }
3065
+
2961
3066
  // src/auth/refresh.ts
2962
3067
  async function refreshTokenIfNeeded(profileName) {
2963
3068
  const creds = loadCredentials();
3069
+ const effectiveProfileName = profileName ?? creds.current_profile;
2964
3070
  const profile = profileName ? getProfile(creds, profileName) : getCurrentProfile(creds);
2965
- if (!profile)
2966
- return false;
3071
+ if (!profile) {
3072
+ return await loginFromConfiguredOAuthClient(effectiveProfileName);
3073
+ }
2967
3074
  const claims = decodeJwt(profile.access_token);
2968
3075
  if (!claims || !isTokenExpired(claims))
2969
3076
  return false;
2970
3077
  if (!profile.refresh_token) {
3078
+ const refreshed = await loginFromConfiguredOAuthClient(effectiveProfileName, profile.client_id);
3079
+ if (refreshed)
3080
+ return true;
2971
3081
  throw new Error('Token expired and no refresh token available. Run "a8techads auth login" to re-authenticate.');
2972
3082
  }
2973
3083
  const response = await fetch(profile.token_endpoint, {
@@ -2981,6 +3091,12 @@ async function refreshTokenIfNeeded(profileName) {
2981
3091
  });
2982
3092
  if (!response.ok) {
2983
3093
  const text = await response.text();
3094
+ const refreshedClaims = decodeJwt(profile.access_token);
3095
+ const expiresAtMs = profile.expires_at ? Date.parse(profile.expires_at) : NaN;
3096
+ const tokenStillUsable = refreshedClaims && !isTokenExpired(refreshedClaims) || !Number.isNaN(expiresAtMs) && expiresAtMs - Date.now() > 30000;
3097
+ if (tokenStillUsable) {
3098
+ return false;
3099
+ }
2984
3100
  throw new Error(`Token refresh failed (${response.status}): ${text}
2985
3101
  Run "a8techads auth login" to re-authenticate.`);
2986
3102
  }
@@ -2994,16 +3110,31 @@ Run "a8techads auth login" to re-authenticate.`);
2994
3110
  saveCredentials(creds);
2995
3111
  return true;
2996
3112
  }
3113
+ async function loginFromConfiguredOAuthClient(profileName, expectedClientId) {
3114
+ const config = loadOAuthClientConfig(profileName);
3115
+ if (!config)
3116
+ return false;
3117
+ if (expectedClientId && expectedClientId !== config.client_id)
3118
+ return false;
3119
+ await loginClientCredentials({
3120
+ clientId: config.client_id,
3121
+ clientSecret: config.client_secret,
3122
+ apiUrl: config.api_url,
3123
+ authUrl: config.auth_url,
3124
+ profile: config.profile,
3125
+ silent: true
3126
+ });
3127
+ return true;
3128
+ }
2997
3129
 
2998
3130
  // src/auth/token.ts
2999
3131
  async function outputToken(profileName) {
3000
- const creds = loadCredentials();
3001
- const name = profileName ?? creds.current_profile;
3002
- await refreshTokenIfNeeded(name);
3132
+ await refreshTokenIfNeeded(profileName);
3003
3133
  const freshCreds = loadCredentials();
3004
- const profile = getProfile(freshCreds, name);
3134
+ const freshName = profileName ?? freshCreds.current_profile;
3135
+ const profile = getProfile(freshCreds, freshName);
3005
3136
  if (!profile) {
3006
- console.error(`No credentials for profile "${name}". Run "a8techads auth login --profile ${name}" first.`);
3137
+ console.error(`No credentials for profile "${freshName}". Run "a8techads auth login --profile ${freshName}" first.`);
3007
3138
  process.exit(1);
3008
3139
  }
3009
3140
  process.stdout.write(profile.access_token);
@@ -3018,8 +3149,16 @@ function showAuthStatus(profileNameOverride) {
3018
3149
  const profileCtx = ctx.profiles[profileName] ?? null;
3019
3150
  console.log(`Profile: ${profileName}`);
3020
3151
  if (!profile) {
3021
- console.log("Auth mode: none");
3022
- console.log('Token: not authenticated (run "a8techads auth login")');
3152
+ const clientConfig = loadOAuthClientConfig(profileName);
3153
+ if (clientConfig) {
3154
+ console.log("Auth mode: client_credentials (configured)");
3155
+ console.log(`Client ID: ${clientConfig.client_id}`);
3156
+ console.log("Token: not cached (will be obtained on next command)");
3157
+ console.log(`API URL: ${clientConfig.api_url}`);
3158
+ } else {
3159
+ console.log("Auth mode: none");
3160
+ console.log('Token: not authenticated (run "a8techads auth login")');
3161
+ }
3023
3162
  return;
3024
3163
  }
3025
3164
  const isClientCreds = profileName.startsWith("client:");
@@ -3078,6 +3217,7 @@ function createAuthCommand() {
3078
3217
  Examples:
3079
3218
  $ a8techads auth login # Browser OAuth + PKCE
3080
3219
  $ a8techads auth login --client-id X --client-secret Y # Client credentials
3220
+ $ a8techads auth configure-client --client-id X --client-secret Y # Save non-interactive client
3081
3221
  $ a8techads auth status # Show current auth state
3082
3222
  $ a8techads auth token # Print access token for scripting
3083
3223
  $ a8techads auth logout # Clear stored tokens`);
@@ -3086,10 +3226,10 @@ Examples:
3086
3226
  Browser mode (default): Opens browser for OAuth 2.1 Authorization Code + PKCE.
3087
3227
  Client credentials mode: Non-interactive auth using --client-id and --client-secret.
3088
3228
 
3089
- Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser mode only)", "default").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").option("--client-id <id>", "OAuth client ID (enables client_credentials flow)").option("--client-secret <secret>", "OAuth client secret (requires --client-id)").option("--force-login", "Force login prompt even if browser session exists (use to switch users)").addHelpText("after", `
3229
+ Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser default: default; client default: client:<client-id>)").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").option("--client-id <id>", "OAuth client ID (enables client_credentials flow)").option("--client-secret <secret>", "OAuth client secret (requires --client-id)").option("--force-login", "Force login prompt even if browser session exists (use to switch users)").addHelpText("after", `
3090
3230
  Examples:
3091
3231
  $ a8techads auth login # Interactive browser login
3092
- $ a8techads auth login -p staging --api-url https://api.staging.a8.tech
3232
+ $ a8techads auth login -p pilot --api-url https://api.a8.tech
3093
3233
  $ a8techads auth login --client-id svc-001 --client-secret s3cret
3094
3234
  $ a8techads auth login -p owner --force-login # Switch to different user
3095
3235
 
@@ -3101,7 +3241,8 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3101
3241
  clientId: opts.clientId,
3102
3242
  clientSecret: opts.clientSecret,
3103
3243
  apiUrl: opts.apiUrl,
3104
- authUrl: opts.authUrl
3244
+ authUrl: opts.authUrl,
3245
+ profile: opts.profile
3105
3246
  });
3106
3247
  } else if (opts.clientId || opts.clientSecret) {
3107
3248
  console.error("Error: Both --client-id and --client-secret are required for client credentials flow.");
@@ -3109,7 +3250,7 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3109
3250
  process.exit(1);
3110
3251
  } else {
3111
3252
  await login({
3112
- profile: opts.profile,
3253
+ profile: opts.profile ?? "default",
3113
3254
  apiUrl: opts.apiUrl,
3114
3255
  authUrl: opts.authUrl,
3115
3256
  forceLogin: opts.forceLogin
@@ -3121,12 +3262,41 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3121
3262
  process.exit(1);
3122
3263
  }
3123
3264
  });
3265
+ auth.command("configure-client").description(`Save OAuth client credentials for non-interactive CLI use.
3266
+
3267
+ After configuration, normal CLI commands can obtain and renew access tokens via client_credentials without running auth login.`).requiredOption("--client-id <id>", "OAuth client ID").requiredOption("--client-secret <secret>", "OAuth client secret").option("-p, --profile <name>", "Profile name (default: client:<client-id>)").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").addHelpText("after", `
3268
+ Examples:
3269
+ $ a8techads auth configure-client --client-id svc-001 --client-secret s3cret
3270
+ $ a8techads campaigns list
3271
+
3272
+ Environment-only alternative:
3273
+ $ export A8TECHADS_CLIENT_ID=svc-001
3274
+ $ export A8TECHADS_CLIENT_SECRET=s3cret
3275
+ $ a8techads campaigns list`).action((opts) => {
3276
+ const config = saveOAuthClientConfig({
3277
+ clientId: opts.clientId,
3278
+ clientSecret: opts.clientSecret,
3279
+ apiUrl: opts.apiUrl,
3280
+ authUrl: opts.authUrl,
3281
+ profile: opts.profile
3282
+ });
3283
+ const creds = loadCredentials();
3284
+ creds.current_profile = config.profile;
3285
+ saveCredentials(creds);
3286
+ const ctx = loadContext();
3287
+ ctx.current_profile = config.profile;
3288
+ saveContext(ctx);
3289
+ console.log("OAuth client configured for non-interactive CLI use.");
3290
+ console.log(`Profile: ${config.profile}`);
3291
+ console.log(`API URL: ${config.api_url}`);
3292
+ console.log("The client secret is stored in ~/.alpineads/oauth-client.json with mode 600.");
3293
+ });
3124
3294
  auth.command("logout").description(`Clear stored tokens for a profile.
3125
3295
 
3126
3296
  Requires: an existing profile.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
3127
3297
  Examples:
3128
3298
  $ a8techads auth logout # Logout current profile
3129
- $ a8techads auth logout -p staging # Logout specific profile`).action((opts) => {
3299
+ $ a8techads auth logout -p pilot # Logout specific profile`).action((opts) => {
3130
3300
  logout(opts.profile);
3131
3301
  });
3132
3302
  auth.command("token").description(`Output current access token to stdout.
@@ -3137,7 +3307,7 @@ cannot be refreshed — re-authenticate if expired.
3137
3307
  Requires: an active profile with valid credentials.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
3138
3308
  Examples:
3139
3309
  $ a8techads auth token # Print token for current profile
3140
- $ a8techads auth token -p staging # Print token for specific profile
3310
+ $ a8techads auth token -p pilot # Print token for specific profile
3141
3311
  $ curl -H "Authorization: Bearer $(a8techads auth token)" https://api.a8.tech/api/v1/dsp/me`).action(async (opts) => {
3142
3312
  try {
3143
3313
  await outputToken(opts.profile);
@@ -3164,20 +3334,31 @@ function createProfileCommand() {
3164
3334
  const profile = new Command("profile").description("Profile management");
3165
3335
  profile.command("list").description("List all profiles").action(() => {
3166
3336
  const creds = loadCredentials();
3167
- const profiles = Object.keys(creds.profiles);
3337
+ const clientProfiles = getConfiguredOAuthClientProfiles();
3338
+ const profiles = Array.from(new Set([
3339
+ ...Object.keys(creds.profiles),
3340
+ ...clientProfiles
3341
+ ]));
3168
3342
  if (profiles.length === 0) {
3169
- console.log('No profiles. Run "a8techads auth login" to create one.');
3343
+ console.log('No profiles. Run "a8techads auth login" or "a8techads auth configure-client" to create one.');
3170
3344
  return;
3171
3345
  }
3172
3346
  console.log(`Profiles:
3173
3347
  `);
3174
3348
  for (const name of profiles) {
3175
3349
  const p = creds.profiles[name];
3350
+ const clientConfig = loadOAuthClientConfig(name);
3176
3351
  const marker = name === creds.current_profile ? " (active)" : "";
3177
3352
  console.log(` ${name}${marker}`);
3178
- console.log(` Email: ${p.email}`);
3179
- console.log(` API: ${p.api_url}`);
3180
- if (p.tenants.length > 0) {
3353
+ if (p) {
3354
+ console.log(` Email: ${p.email}`);
3355
+ console.log(` API: ${p.api_url}`);
3356
+ } else if (clientConfig) {
3357
+ console.log(` Auth: client_credentials (configured)`);
3358
+ console.log(` Client ID: ${clientConfig.client_id}`);
3359
+ console.log(` API: ${clientConfig.api_url}`);
3360
+ }
3361
+ if (p?.tenants.length > 0) {
3181
3362
  console.log(` Tenants: ${p.tenants.map((t) => `${t.tenant_name} (${t.tenant_id})`).join(", ")}`);
3182
3363
  }
3183
3364
  console.log();
@@ -3185,7 +3366,8 @@ function createProfileCommand() {
3185
3366
  });
3186
3367
  profile.command("use").description("Switch active profile").argument("<name>", "Profile name to switch to").action((name) => {
3187
3368
  const creds = loadCredentials();
3188
- if (!creds.profiles[name]) {
3369
+ const clientConfigExists = hasConfiguredOAuthClientProfile(name);
3370
+ if (!creds.profiles[name] && !clientConfigExists) {
3189
3371
  console.error(`Profile "${name}" not found. Run "a8techads profile list" to see available profiles.`);
3190
3372
  process.exit(1);
3191
3373
  }
@@ -3195,13 +3377,21 @@ function createProfileCommand() {
3195
3377
  ctx.current_profile = name;
3196
3378
  saveContext(ctx);
3197
3379
  const p = creds.profiles[name];
3198
- console.log(`Switched to profile "${name}" (${p.email})`);
3380
+ if (p) {
3381
+ console.log(`Switched to profile "${name}" (${p.email})`);
3382
+ } else {
3383
+ console.log(`Switched to profile "${name}" (client_credentials configured)`);
3384
+ }
3199
3385
  });
3200
3386
  return profile;
3201
3387
  }
3202
3388
 
3203
3389
  // src/utils/http.ts
3204
3390
  async function apiRequest(opts) {
3391
+ const request = await buildAuthenticatedRequest(opts);
3392
+ return fetch(request.url, request.init);
3393
+ }
3394
+ async function buildAuthenticatedRequest(opts) {
3205
3395
  await refreshTokenIfNeeded();
3206
3396
  const creds = loadCredentials();
3207
3397
  const profile = getCurrentProfile(creds);
@@ -3216,21 +3406,23 @@ async function apiRequest(opts) {
3216
3406
  "Content-Type": "application/json",
3217
3407
  ...opts.headers
3218
3408
  };
3219
- if (context?.current_capability) {
3220
- headers["X-Effective-Capability"] = context.current_capability;
3221
- }
3222
3409
  if (context?.impersonation) {
3223
3410
  headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
3224
3411
  if (context.impersonation.effective_capability) {
3225
3412
  headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
3226
3413
  }
3414
+ } else if (context?.current_capability) {
3415
+ headers["X-Effective-Capability"] = context.current_capability;
3227
3416
  }
3228
- const url = `${profile.api_url}${opts.path}`;
3229
- return fetch(url, {
3230
- method: opts.method ?? "GET",
3231
- headers,
3232
- body: opts.body ? JSON.stringify(opts.body) : undefined
3233
- });
3417
+ const url = `${opts.baseUrl ?? profile.api_url}${opts.path}`;
3418
+ return {
3419
+ url,
3420
+ init: {
3421
+ method: opts.method ?? "GET",
3422
+ headers,
3423
+ body: opts.body ? JSON.stringify(opts.body) : undefined
3424
+ }
3425
+ };
3234
3426
  }
3235
3427
  function validateTenantMatch(accessToken, contextTenantId) {
3236
3428
  const claims = decodeJwt(accessToken);
@@ -3538,6 +3730,9 @@ function dspPrefix() {
3538
3730
  function sspPrefix() {
3539
3731
  return "/api/v1/ssp";
3540
3732
  }
3733
+ function consolePrefix() {
3734
+ return "/api/v1/console";
3735
+ }
3541
3736
 
3542
3737
  // src/utils/output.ts
3543
3738
  function printData(data, columns, format = "table") {
@@ -3646,25 +3841,28 @@ Examples:
3646
3841
  printData(rows, COLUMNS, opts.format);
3647
3842
  });
3648
3843
  addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
3649
- const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
3650
- const json = await resp.json();
3651
- if (!resp.ok) {
3652
- console.error(`Error: ${json.error ?? resp.statusText}`);
3653
- process.exit(1);
3654
- }
3655
- printDetail(json.data ?? json, opts.format);
3844
+ const audience = await fetchAudience(id);
3845
+ printDetail(audience, opts.format);
3656
3846
  });
3657
- cmd.command("create").description(`Create a new audience.
3658
-
3659
- Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST or RETARGETING", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3847
+ addFormatOption(cmd.command("size").description("Show the current estimated audience size.").argument("<id>", "Audience ID")).action(async (id, opts) => {
3848
+ const audience = await fetchAudience(id);
3849
+ const detail = {
3850
+ id: audience.id,
3851
+ name: audience.name,
3852
+ status: audience.status,
3853
+ estimatedSize: audience.estimatedSize ?? audience.estimated_size ?? null
3854
+ };
3855
+ printDetail(detail, opts.format);
3856
+ });
3857
+ cmd.command("create").description("Create a new audience.").option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST, RETARGETING, or LOOKALIKE", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--seed <id>", "Seed audience ID (required for LOOKALIKE type)").option("--ratio <n>", "Expansion ratio for LOOKALIKE (default: 0.05)", "0.05").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3660
3858
  Examples:
3661
3859
  $ a8techads audiences create --name "High-Value Customers"
3662
- $ a8techads audiences create --name "Retarget Pool" --ttl 30
3663
- $ a8techads audiences create --from-json audience.json`).action(async (opts) => {
3860
+ $ a8techads audiences create --name "Retarget Pool" --type RETARGETING --goal-id <id>
3861
+ $ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
3664
3862
  let body;
3665
3863
  if (opts.fromJson) {
3666
- const { readFileSync: readFileSync3 } = await import("fs");
3667
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
3864
+ const { readFileSync: readFileSync4 } = await import("fs");
3865
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3668
3866
  } else {
3669
3867
  if (!opts.name) {
3670
3868
  console.error('Error: --name is required. Run "a8techads audiences create --help".');
@@ -3674,6 +3872,10 @@ Examples:
3674
3872
  console.error("Error: --goal-id is required for RETARGETING type.");
3675
3873
  process.exit(1);
3676
3874
  }
3875
+ if (opts.type === "LOOKALIKE" && !opts.seed) {
3876
+ console.error("Error: --seed is required for LOOKALIKE type.");
3877
+ process.exit(1);
3878
+ }
3677
3879
  body = {
3678
3880
  name: opts.name,
3679
3881
  type: opts.type,
@@ -3684,6 +3886,9 @@ Examples:
3684
3886
  if (opts.goalId) {
3685
3887
  body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
3686
3888
  }
3889
+ if (opts.seed) {
3890
+ body.rules = { seed_audience_id: opts.seed, expansion_ratio: Number(opts.ratio) };
3891
+ }
3687
3892
  }
3688
3893
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
3689
3894
  const json = await resp.json();
@@ -3763,10 +3968,10 @@ File format (one per line):
3763
3968
  console.error("Error: --file is required.");
3764
3969
  process.exit(1);
3765
3970
  }
3766
- const { readFileSync: readFileSync3 } = await import("fs");
3971
+ const { readFileSync: readFileSync4 } = await import("fs");
3767
3972
  let identifiers;
3768
3973
  try {
3769
- const content = readFileSync3(opts.file, "utf-8").trim();
3974
+ const content = readFileSync4(opts.file, "utf-8").trim();
3770
3975
  try {
3771
3976
  identifiers = JSON.parse(content);
3772
3977
  if (!Array.isArray(identifiers))
@@ -3796,17 +4001,130 @@ File format (one per line):
3796
4001
  }
3797
4002
  console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
3798
4003
  });
4004
+ 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) => {
4005
+ const timeoutMs = Number(opts.timeout) * 1000;
4006
+ const intervalMs = Number(opts.interval) * 1000;
4007
+ const started = Date.now();
4008
+ while (true) {
4009
+ const audience = await fetchAudience(id);
4010
+ const status = audience.status;
4011
+ const size = audience.estimatedSize ?? audience.estimated_size ?? null;
4012
+ console.log(`Status: ${status}${size != null ? ` | Size: ${Number(size).toLocaleString()}` : ""}`);
4013
+ if (status === "READY" || status === "ACTIVE") {
4014
+ console.log(`Audience ${id} is ready.`);
4015
+ return;
4016
+ }
4017
+ if (status === "ERROR" || status === "ARCHIVED") {
4018
+ console.error(`Audience ${id} reached terminal status: ${status}`);
4019
+ process.exit(1);
4020
+ }
4021
+ if (Date.now() - started >= timeoutMs) {
4022
+ console.error(`Timed out waiting for audience ${id}.`);
4023
+ process.exit(1);
4024
+ }
4025
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
4026
+ }
4027
+ });
3799
4028
  return cmd;
3800
4029
  }
4030
+ async function fetchAudience(id) {
4031
+ const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
4032
+ const json = await resp.json();
4033
+ if (!resp.ok) {
4034
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4035
+ process.exit(1);
4036
+ }
4037
+ return json.data ?? json;
4038
+ }
3801
4039
 
3802
4040
  // src/commands/campaigns.ts
3803
4041
  var COLUMNS2 = [
3804
4042
  { key: "id", header: "ID", width: 36 },
3805
4043
  { key: "name", header: "NAME", width: 30 },
3806
4044
  { key: "status", header: "STATUS", width: 12 },
4045
+ { key: "supplyProductCount", header: "SUPPLY", width: 8 },
3807
4046
  { key: "budget", header: "BUDGET", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
3808
4047
  { key: "spent", header: "SPENT", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" }
3809
4048
  ];
4049
+ function formatScalar(value) {
4050
+ if (value === undefined || value === null)
4051
+ return null;
4052
+ if (Array.isArray(value))
4053
+ return value.length > 0 ? value.join(",") : null;
4054
+ if (typeof value === "object")
4055
+ return JSON.stringify(value);
4056
+ return value;
4057
+ }
4058
+ function formatSupplyProducts(value) {
4059
+ if (!Array.isArray(value))
4060
+ return null;
4061
+ const names = value.map((item) => {
4062
+ if (item && typeof item === "object" && "name" in item)
4063
+ return String(item.name);
4064
+ return null;
4065
+ }).filter(Boolean);
4066
+ return names.length > 0 ? names.join(", ") : null;
4067
+ }
4068
+ function printSectionedCampaignDetail(campaign) {
4069
+ const sections = [
4070
+ {
4071
+ title: "Scope",
4072
+ entries: {
4073
+ id: campaign.id,
4074
+ name: campaign.name,
4075
+ status: campaign.status,
4076
+ campaignMode: campaign.campaignMode,
4077
+ categories: formatScalar(campaign.categories),
4078
+ adFormat: campaign.adFormat,
4079
+ adSize: formatScalar(campaign.adSize),
4080
+ zoneTypes: formatScalar(campaign.zoneTypes)
4081
+ }
4082
+ },
4083
+ {
4084
+ title: "Budget And Pricing",
4085
+ entries: {
4086
+ dailyBudget: campaign.dailyBudget,
4087
+ totalBudget: campaign.totalBudget,
4088
+ pricingModel: campaign.pricingModel,
4089
+ priceSettings: formatScalar(campaign.priceSettings)
4090
+ }
4091
+ },
4092
+ {
4093
+ title: "Supply Binding",
4094
+ entries: {
4095
+ supplyProductIds: formatScalar(campaign.supplyProductIds),
4096
+ supplyProductCount: campaign.supplyProductCount ?? (Array.isArray(campaign.supplyProductIds) ? campaign.supplyProductIds.length : 0),
4097
+ supplyProducts: formatSupplyProducts(campaign.supplyProducts),
4098
+ sspPartnerIds: formatScalar(campaign.sspPartnerIds),
4099
+ sourceMasking: formatScalar(campaign.sourceMasking)
4100
+ }
4101
+ },
4102
+ {
4103
+ title: "Delivery",
4104
+ entries: {
4105
+ startDate: campaign.startDate,
4106
+ endDate: campaign.endDate,
4107
+ timezone: campaign.timezone,
4108
+ frequencyCap: formatScalar(campaign.frequencyCap),
4109
+ targeting: formatScalar(campaign.targeting)
4110
+ }
4111
+ },
4112
+ {
4113
+ title: "Meta",
4114
+ entries: {
4115
+ createdAt: campaign.createdAt,
4116
+ updatedAt: campaign.updatedAt,
4117
+ version: campaign.version
4118
+ }
4119
+ }
4120
+ ];
4121
+ for (const [index, section] of sections.entries()) {
4122
+ console.log(section.title);
4123
+ printDetail(section.entries, "table");
4124
+ if (index < sections.length - 1)
4125
+ console.log("");
4126
+ }
4127
+ }
3810
4128
  function createCampaignsCommand() {
3811
4129
  const cmd = new Command("campaigns").description(`Campaign management (DSP)
3812
4130
 
@@ -3815,6 +4133,7 @@ Examples:
3815
4133
  $ a8techads campaigns list
3816
4134
  $ a8techads campaigns get <id>
3817
4135
  $ a8techads campaigns create --name "Summer Sale" --budget 500
4136
+ $ a8techads campaigns update <id> --max-bid 6
3818
4137
  $ a8techads campaigns pause <id>`);
3819
4138
  addFormatOption(cmd.command("list").description("List campaigns with optional filters.").option("--status <status>", "Filter by status (draft, active, paused, etc.)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
3820
4139
  const params = new URLSearchParams;
@@ -3831,6 +4150,7 @@ Examples:
3831
4150
  id: c.id,
3832
4151
  name: c.name,
3833
4152
  status: c.status,
4153
+ supplyProductCount: c.supplyProductCount ?? (Array.isArray(c.supplyProductIds) ? c.supplyProductIds.length : 0),
3834
4154
  budget: c.budget ?? c.dailyBudget,
3835
4155
  spent: c.stats?.spend ?? c.spent
3836
4156
  }));
@@ -3843,7 +4163,12 @@ Examples:
3843
4163
  console.error(`Error: ${json.error ?? resp.statusText}`);
3844
4164
  process.exit(1);
3845
4165
  }
3846
- printDetail(json.data ?? json, opts.format);
4166
+ const data = json.data ?? json;
4167
+ if (opts.format !== "json") {
4168
+ printSectionedCampaignDetail(data);
4169
+ return;
4170
+ }
4171
+ printDetail(data, opts.format);
3847
4172
  });
3848
4173
  addFormatOption(cmd.command("stats").description("Get campaign performance statistics.").argument("<id>", "Campaign ID").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")).action(async (id, opts) => {
3849
4174
  const params = new URLSearchParams;
@@ -3861,14 +4186,15 @@ Examples:
3861
4186
  });
3862
4187
  cmd.command("create").description(`Create a new campaign.
3863
4188
 
3864
- Requires: ADVERTISER capability, advertiser_admin or advertiser_member role.`).option("--name <name>", "Campaign name (required)").option("--budget <amount>", "Daily budget in dollars").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
4189
+ Requires: ADVERTISER capability, advertiser_admin or advertiser_member role.`).option("--name <name>", "Campaign name (required)").option("--budget <amount>", "Daily budget in dollars").option("--max-bid <amount>", "Maximum bid ceiling to store in priceSettings.maxBid").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3865
4190
  Examples:
3866
4191
  $ a8techads campaigns create --name "Summer Sale" --budget 500
4192
+ $ a8techads campaigns create --name "Pilot Supply Binding" --budget 500 --supply-products <supply-product-id>
3867
4193
  $ a8techads campaigns create --from-json campaign.json`).action(async (opts) => {
3868
4194
  let body;
3869
4195
  if (opts.fromJson) {
3870
- const { readFileSync: readFileSync3 } = await import("fs");
3871
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4196
+ const { readFileSync: readFileSync4 } = await import("fs");
4197
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3872
4198
  } else {
3873
4199
  if (!opts.name) {
3874
4200
  console.error('Error: --name is required. Run "a8techads campaigns create --help".');
@@ -3877,6 +4203,11 @@ Examples:
3877
4203
  body = { name: opts.name };
3878
4204
  if (opts.budget)
3879
4205
  body.dailyBudget = Number(opts.budget);
4206
+ if (opts.maxBid)
4207
+ body.priceSettings = { maxBid: Number(opts.maxBid) };
4208
+ const supplyProducts = opts.supplyProducts ?? opts.preferredSupply;
4209
+ if (supplyProducts)
4210
+ body.supplyProductIds = parseCsvList(supplyProducts);
3880
4211
  }
3881
4212
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/campaigns`, body });
3882
4213
  const json = await resp.json();
@@ -3886,17 +4217,52 @@ Examples:
3886
4217
  }
3887
4218
  console.log(`Campaign created: ${json.data?.id ?? json.id}`);
3888
4219
  });
3889
- cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4220
+ cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
3890
4221
  let body;
3891
4222
  if (opts.fromJson) {
3892
- const { readFileSync: readFileSync3 } = await import("fs");
3893
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4223
+ const { readFileSync: readFileSync4 } = await import("fs");
4224
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3894
4225
  } else {
3895
4226
  body = {};
3896
4227
  if (opts.name)
3897
4228
  body.name = opts.name;
3898
4229
  if (opts.budget)
3899
4230
  body.dailyBudget = Number(opts.budget);
4231
+ if (opts.maxBid && opts.clearMaxBid) {
4232
+ console.error("Error: use either --max-bid or --clear-max-bid, not both.");
4233
+ process.exit(1);
4234
+ }
4235
+ const supplyProducts = opts.supplyProducts ?? opts.preferredSupply;
4236
+ const clearSupplyProducts = opts.clearSupplyProducts || opts.clearPreferredSupply;
4237
+ if (supplyProducts && clearSupplyProducts) {
4238
+ console.error("Error: use either --supply-products or --clear-supply-products, not both.");
4239
+ process.exit(1);
4240
+ }
4241
+ const updatesSupplyBindings = Boolean(supplyProducts || clearSupplyProducts);
4242
+ if (updatesSupplyBindings) {
4243
+ const campaign = await fetchCampaign(id);
4244
+ if (String(campaign.status || "").toUpperCase() === "ACTIVE") {
4245
+ console.error("Error: active campaigns cannot change supply product bindings via update.");
4246
+ console.error(`Next steps:`);
4247
+ console.error(` a8techads campaigns pause ${id}`);
4248
+ console.error(` a8techads campaigns update ${id} ${supplyProducts ? `--supply-products ${supplyProducts}` : "--clear-supply-products"}`);
4249
+ console.error(` a8techads campaigns resume ${id}`);
4250
+ process.exit(1);
4251
+ }
4252
+ }
4253
+ if (opts.maxBid || opts.clearMaxBid) {
4254
+ const campaign = await fetchCampaign(id);
4255
+ const priceSettings = normalizePriceSettings(campaign.priceSettings);
4256
+ if (opts.maxBid)
4257
+ priceSettings.maxBid = Number(opts.maxBid);
4258
+ if (opts.clearMaxBid)
4259
+ delete priceSettings.maxBid;
4260
+ body.priceSettings = priceSettings;
4261
+ }
4262
+ if (supplyProducts)
4263
+ body.supplyProductIds = parseCsvList(supplyProducts);
4264
+ if (clearSupplyProducts)
4265
+ body.supplyProductIds = [];
3900
4266
  }
3901
4267
  const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
3902
4268
  if (!resp.ok) {
@@ -3942,8 +4308,181 @@ Examples:
3942
4308
  }
3943
4309
  console.log(`Campaign duplicated. New ID: ${json.data?.id ?? json.id}`);
3944
4310
  });
4311
+ const targetingCmd = new Command("targeting").description("Show or update campaign targeting without editing full JSON.").addHelpText("after", `
4312
+ Examples:
4313
+ $ a8techads campaigns targeting show <campaign-id>
4314
+ $ a8techads campaigns targeting set-geo <campaign-id> --countries US,CA --mode target
4315
+ $ a8techads campaigns targeting add-audience <campaign-id> --audience <id> --mode include
4316
+ $ a8techads campaigns targeting set-day-parting <campaign-id> --timezone UTC --from-json schedule.json`);
4317
+ addFormatOption(targetingCmd.command("show").description("Show campaign targeting, audience include/exclude, and day parting.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
4318
+ const campaign = await fetchCampaign(id);
4319
+ const detail = {
4320
+ campaignId: campaign.id,
4321
+ campaignName: campaign.name,
4322
+ targeting: campaign.targeting ?? {},
4323
+ timezone: campaign.timezone ?? "UTC",
4324
+ dayParting: campaign.dayParting ?? null
4325
+ };
4326
+ printDetail(detail, opts.format);
4327
+ });
4328
+ 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) => {
4329
+ const campaign = await fetchCampaign(id);
4330
+ const targeting = normalizeTargeting(campaign.targeting);
4331
+ const countries = parseCsvList(opts.countries);
4332
+ const mode = opts.mode === "block" ? "block" : "target";
4333
+ targeting.geo = {
4334
+ ...targeting.geo ?? {},
4335
+ countries,
4336
+ mode,
4337
+ countriesMode: mode
4338
+ };
4339
+ await patchCampaign(id, { targeting });
4340
+ console.log(`Campaign ${id} geo targeting updated.`);
4341
+ });
4342
+ 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) => {
4343
+ const mode = normalizeAudienceMode(opts.mode);
4344
+ const campaign = await fetchCampaign(id);
4345
+ const targeting = normalizeTargeting(campaign.targeting);
4346
+ const audiences = normalizeAudiences(targeting.audiences);
4347
+ const list = new Set(mode === "include" ? audiences.include : audiences.exclude);
4348
+ list.add(opts.audience);
4349
+ targeting.audiences = {
4350
+ ...audiences,
4351
+ [mode]: Array.from(list)
4352
+ };
4353
+ await patchCampaign(id, { targeting });
4354
+ console.log(`Campaign ${id} audience ${opts.audience} added to ${mode}.`);
4355
+ });
4356
+ 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) => {
4357
+ const mode = normalizeAudienceMode(opts.mode);
4358
+ const campaign = await fetchCampaign(id);
4359
+ const targeting = normalizeTargeting(campaign.targeting);
4360
+ const audiences = normalizeAudiences(targeting.audiences);
4361
+ targeting.audiences = {
4362
+ ...audiences,
4363
+ [mode]: (mode === "include" ? audiences.include : audiences.exclude).filter((audId) => audId !== opts.audience)
4364
+ };
4365
+ await patchCampaign(id, { targeting });
4366
+ console.log(`Campaign ${id} audience ${opts.audience} removed from ${mode}.`);
4367
+ });
4368
+ 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) => {
4369
+ if (!opts.disable && !opts.fromJson) {
4370
+ console.error("Error: provide --from-json <file> or --disable.");
4371
+ process.exit(1);
4372
+ }
4373
+ const campaign = await fetchCampaign(id);
4374
+ let dayParting;
4375
+ if (opts.disable) {
4376
+ dayParting = null;
4377
+ } else {
4378
+ const { readFileSync: readFileSync4 } = await import("fs");
4379
+ dayParting = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4380
+ }
4381
+ await patchCampaign(id, {
4382
+ dayParting,
4383
+ timezone: opts.timezone ?? campaign.timezone ?? "UTC"
4384
+ });
4385
+ console.log(`Campaign ${id} day parting updated.`);
4386
+ });
4387
+ for (const [resourceName, field] of [
4388
+ ["domain", "domains"],
4389
+ ["keyword", "keywords"],
4390
+ ["ip-range", "ipRanges"]
4391
+ ]) {
4392
+ 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) => {
4393
+ const campaign = await fetchCampaign(id);
4394
+ const targeting = normalizeTargeting(campaign.targeting);
4395
+ const existing = normalizeListTargeting(targeting[field]);
4396
+ const value = String(opts[camelizeOption(resourceName)]).trim();
4397
+ if (!value) {
4398
+ console.error(`Error: --${resourceName} is required.`);
4399
+ process.exit(1);
4400
+ }
4401
+ const list = new Set(existing.list);
4402
+ list.add(value);
4403
+ targeting[field] = {
4404
+ mode: opts.mode === "block" ? "block" : existing.mode,
4405
+ list: Array.from(list)
4406
+ };
4407
+ await patchCampaign(id, { targeting });
4408
+ console.log(`Campaign ${id} ${resourceName} added.`);
4409
+ });
4410
+ 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) => {
4411
+ const campaign = await fetchCampaign(id);
4412
+ const targeting = normalizeTargeting(campaign.targeting);
4413
+ const existing = normalizeListTargeting(targeting[field]);
4414
+ const value = String(opts[camelizeOption(resourceName)]).trim();
4415
+ targeting[field] = {
4416
+ ...existing,
4417
+ list: existing.list.filter((item) => item !== value)
4418
+ };
4419
+ await patchCampaign(id, { targeting });
4420
+ console.log(`Campaign ${id} ${resourceName} removed.`);
4421
+ });
4422
+ targetingCmd.command(`set-${resourceName}-mode`).description(`Set ${resourceName} targeting mode.`).argument("<id>", "Campaign ID").requiredOption("--mode <mode>", "target or block").action(async (id, opts) => {
4423
+ const campaign = await fetchCampaign(id);
4424
+ const targeting = normalizeTargeting(campaign.targeting);
4425
+ const existing = normalizeListTargeting(targeting[field]);
4426
+ targeting[field] = {
4427
+ ...existing,
4428
+ mode: opts.mode === "block" ? "block" : "target"
4429
+ };
4430
+ await patchCampaign(id, { targeting });
4431
+ console.log(`Campaign ${id} ${resourceName} mode updated.`);
4432
+ });
4433
+ }
4434
+ cmd.addCommand(targetingCmd);
3945
4435
  return cmd;
3946
4436
  }
4437
+ async function fetchCampaign(id) {
4438
+ const resp = await apiRequest({ path: `${dspPrefix()}/campaigns/${id}` });
4439
+ const json = await resp.json();
4440
+ if (!resp.ok) {
4441
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4442
+ process.exit(1);
4443
+ }
4444
+ return json.data ?? json;
4445
+ }
4446
+ async function patchCampaign(id, body) {
4447
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
4448
+ const json = await resp.json().catch(() => ({}));
4449
+ if (!resp.ok) {
4450
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4451
+ process.exit(1);
4452
+ }
4453
+ }
4454
+ function parseCsvList(value) {
4455
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
4456
+ }
4457
+ function normalizeAudienceMode(value) {
4458
+ const normalized = String(value).toLowerCase();
4459
+ if (normalized !== "include" && normalized !== "exclude") {
4460
+ console.error("Error: --mode must be include or exclude.");
4461
+ process.exit(1);
4462
+ }
4463
+ return normalized;
4464
+ }
4465
+ function normalizeTargeting(targeting) {
4466
+ return targeting && typeof targeting === "object" ? { ...targeting } : {};
4467
+ }
4468
+ function normalizeAudiences(audiences) {
4469
+ return {
4470
+ include: Array.isArray(audiences?.include) ? audiences.include : [],
4471
+ exclude: Array.isArray(audiences?.exclude) ? audiences.exclude : []
4472
+ };
4473
+ }
4474
+ function normalizeListTargeting(value) {
4475
+ return {
4476
+ mode: value?.mode === "block" ? "block" : "target",
4477
+ list: Array.isArray(value?.list) ? value.list : []
4478
+ };
4479
+ }
4480
+ function normalizePriceSettings(value) {
4481
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
4482
+ }
4483
+ function camelizeOption(value) {
4484
+ return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
4485
+ }
3947
4486
 
3948
4487
  // src/commands/variations.ts
3949
4488
  var COLUMNS3 = [
@@ -3991,8 +4530,8 @@ Examples:
3991
4530
  cmd.command("create").description("Create a new ad variation.").option("--campaign <id>", "Campaign ID (required)").option("--name <name>", "Variation name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
3992
4531
  let body;
3993
4532
  if (opts.fromJson) {
3994
- const { readFileSync: readFileSync3 } = await import("fs");
3995
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4533
+ const { readFileSync: readFileSync4 } = await import("fs");
4534
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3996
4535
  } else {
3997
4536
  if (!opts.campaign || !opts.name) {
3998
4537
  console.error("Error: --campaign and --name are required.");
@@ -4011,8 +4550,8 @@ Examples:
4011
4550
  cmd.command("update").description("Update a variation.").argument("<id>", "Variation ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4012
4551
  let body;
4013
4552
  if (opts.fromJson) {
4014
- const { readFileSync: readFileSync3 } = await import("fs");
4015
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4553
+ const { readFileSync: readFileSync4 } = await import("fs");
4554
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4016
4555
  } else {
4017
4556
  body = {};
4018
4557
  if (opts.name)
@@ -4038,47 +4577,144 @@ Examples:
4038
4577
  }
4039
4578
  console.log(`Variation ${id} deleted.`);
4040
4579
  });
4580
+ cmd.command("submit").description("Submit a variation for approval.").argument("<id>", "Variation ID").action(async (id) => {
4581
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/submit` });
4582
+ if (!resp.ok) {
4583
+ const j = await resp.json();
4584
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4585
+ process.exit(1);
4586
+ }
4587
+ console.log(`Variation ${id} submitted.`);
4588
+ });
4589
+ cmd.command("pause").description("Pause an active variation.").argument("<id>", "Variation ID").action(async (id) => {
4590
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/pause` });
4591
+ if (!resp.ok) {
4592
+ const j = await resp.json();
4593
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4594
+ process.exit(1);
4595
+ }
4596
+ console.log(`Variation ${id} paused.`);
4597
+ });
4598
+ cmd.command("resume").description("Resume a paused variation.").argument("<id>", "Variation ID").action(async (id) => {
4599
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/resume` });
4600
+ if (!resp.ok) {
4601
+ const j = await resp.json();
4602
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4603
+ process.exit(1);
4604
+ }
4605
+ console.log(`Variation ${id} resumed.`);
4606
+ });
4607
+ cmd.command("archive").description("Archive a variation.").argument("<id>", "Variation ID").action(async (id) => {
4608
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/archive` });
4609
+ if (!resp.ok) {
4610
+ const j = await resp.json();
4611
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4612
+ process.exit(1);
4613
+ }
4614
+ console.log(`Variation ${id} archived.`);
4615
+ });
4041
4616
  return cmd;
4042
4617
  }
4043
4618
 
4044
- // src/commands/sites.ts
4619
+ // src/commands/media-assets.ts
4620
+ import { basename, extname } from "node:path";
4621
+ import { readFileSync as readFileSync4 } from "node:fs";
4045
4622
  var COLUMNS4 = [
4046
4623
  { key: "id", header: "ID", width: 36 },
4047
- { key: "name", header: "NAME", width: 25 },
4048
- { key: "domain", header: "DOMAIN", width: 25 },
4049
- { key: "status", header: "STATUS", width: 12 },
4050
- { key: "zoneCount", header: "ZONES", width: 6 }
4624
+ { key: "name", header: "NAME", width: 24 },
4625
+ { key: "type", header: "TYPE", width: 10 },
4626
+ { key: "status", header: "STATUS", width: 10 },
4627
+ { key: "adFormat", header: "FORMAT", width: 12 },
4628
+ { key: "mimeType", header: "MIME", width: 20 }
4051
4629
  ];
4052
- function createSitesCommand() {
4053
- const cmd = new Command("sites").description(`Site management (SSP)
4630
+ function inferMimeType(filePath) {
4631
+ switch (extname(filePath).toLowerCase()) {
4632
+ case ".png":
4633
+ return "image/png";
4634
+ case ".jpg":
4635
+ case ".jpeg":
4636
+ return "image/jpeg";
4637
+ case ".gif":
4638
+ return "image/gif";
4639
+ case ".webp":
4640
+ return "image/webp";
4641
+ case ".svg":
4642
+ return "image/svg+xml";
4643
+ case ".mp4":
4644
+ return "video/mp4";
4645
+ case ".mov":
4646
+ return "video/quicktime";
4647
+ case ".html":
4648
+ return "text/html";
4649
+ case ".txt":
4650
+ return "text/plain";
4651
+ default:
4652
+ return "application/octet-stream";
4653
+ }
4654
+ }
4655
+ function normalizeAsset(asset) {
4656
+ return {
4657
+ id: asset.id,
4658
+ name: asset.name,
4659
+ type: asset.type,
4660
+ status: asset.status,
4661
+ adFormat: asset.adFormat ?? asset.ad_format,
4662
+ mimeType: asset.mimeType ?? asset.mime_type
4663
+ };
4664
+ }
4665
+ function createMediaAssetsCommand() {
4666
+ const cmd = new Command("media-assets").description(`Media library management (DSP)
4054
4667
 
4055
- Requires: PUBLISHER capability.`).addHelpText("after", `
4668
+ Requires: ADVERTISER capability.`).addHelpText("after", `
4056
4669
  Examples:
4057
- $ a8techads sites list
4058
- $ a8techads sites get <id>
4059
- $ a8techads sites create --name "My Blog" --domain blog.example.com`);
4060
- addFormatOption(cmd.command("list").description("List publisher sites.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20")).action(async (opts) => {
4670
+ $ a8techads media-assets list
4671
+ $ a8techads media-assets get <id>
4672
+ $ a8techads media-assets upload --file ./banner.png --ad-format BANNER
4673
+ $ a8techads media-assets pause <id>`);
4674
+ 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) => {
4061
4675
  const params = new URLSearchParams;
4676
+ params.set("limit", opts.limit);
4677
+ params.set("offset", opts.offset);
4062
4678
  if (opts.status)
4063
4679
  params.set("status", opts.status);
4680
+ if (opts.type)
4681
+ params.set("type", opts.type);
4682
+ if (opts.adFormat)
4683
+ params.set("adFormat", opts.adFormat);
4684
+ if (opts.search)
4685
+ params.set("search", opts.search);
4686
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets?${params}` });
4687
+ const json = await resp.json();
4688
+ if (!resp.ok) {
4689
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4690
+ process.exit(1);
4691
+ }
4692
+ const rows = (json.data ?? json).map(normalizeAsset);
4693
+ printData(rows, COLUMNS4, opts.format);
4694
+ });
4695
+ 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) => {
4696
+ const params = new URLSearchParams;
4064
4697
  params.set("limit", opts.limit);
4065
- const resp = await apiRequest({ path: `${sspPrefix()}/sites?${params}` });
4698
+ params.set("offset", opts.offset);
4699
+ if (opts.adFormat)
4700
+ params.set("adFormat", opts.adFormat);
4701
+ if (opts.width)
4702
+ params.set("width", opts.width);
4703
+ if (opts.height)
4704
+ params.set("height", opts.height);
4705
+ const resp = await apiRequest({
4706
+ path: `${dspPrefix()}/media-assets/active?${params}`
4707
+ });
4066
4708
  const json = await resp.json();
4067
4709
  if (!resp.ok) {
4068
4710
  console.error(`Error: ${json.error ?? resp.statusText}`);
4069
4711
  process.exit(1);
4070
4712
  }
4071
- const rows = (json.data ?? json).map((s) => ({
4072
- id: s.id,
4073
- name: s.name,
4074
- domain: s.domain,
4075
- status: s.status,
4076
- zoneCount: s.zoneCount ?? s.zone_count ?? "-"
4077
- }));
4713
+ const rows = (json.data ?? json).map(normalizeAsset);
4078
4714
  printData(rows, COLUMNS4, opts.format);
4079
4715
  });
4080
- addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
4081
- const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
4716
+ addFormatOption(cmd.command("get").description("Get media asset details.").argument("<id>", "Media asset ID")).action(async (id, opts) => {
4717
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/${id}` });
4082
4718
  const json = await resp.json();
4083
4719
  if (!resp.ok) {
4084
4720
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4086,37 +4722,274 @@ Examples:
4086
4722
  }
4087
4723
  printDetail(json.data ?? json, opts.format);
4088
4724
  });
4089
- cmd.command("create").description("Create a new site.").option("--name <name>", "Site name (required)").option("--domain <domain>", "Site domain (required)").option("--type <type>", "Site type: WEB, APP, CTV", "WEB").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4090
- let body;
4091
- if (opts.fromJson) {
4092
- const { readFileSync: readFileSync3 } = await import("fs");
4093
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4094
- } else {
4095
- if (!opts.name || !opts.domain) {
4096
- console.error("Error: --name and --domain are required.");
4097
- process.exit(1);
4098
- }
4099
- body = { name: opts.name, domain: opts.domain, type: opts.type };
4100
- }
4101
- const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
4725
+ addFormatOption(cmd.command("size-limits").description("Get media asset size limits.")).action(async (opts) => {
4726
+ const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/size-limits` });
4102
4727
  const json = await resp.json();
4103
4728
  if (!resp.ok) {
4104
4729
  console.error(`Error: ${json.error ?? resp.statusText}`);
4105
4730
  process.exit(1);
4106
4731
  }
4107
- console.log(`Site created: ${json.data?.id ?? json.id}`);
4732
+ printDetail(json.data ?? json, opts.format);
4108
4733
  });
4109
- cmd.command("update").description("Update a site.").argument("<id>", "Site ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4110
- let body;
4111
- if (opts.fromJson) {
4112
- const { readFileSync: readFileSync3 } = await import("fs");
4113
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4114
- } else {
4115
- body = {};
4116
- if (opts.name)
4117
- body.name = opts.name;
4118
- }
4119
- const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
4734
+ 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) => {
4735
+ const filePath = opts.file;
4736
+ const fileName = basename(filePath);
4737
+ const request = await buildAuthenticatedRequest({
4738
+ method: "POST",
4739
+ path: `${dspPrefix()}/media-assets/upload`,
4740
+ headers: {}
4741
+ });
4742
+ const headers = new Headers(request.init.headers);
4743
+ headers.delete("Content-Type");
4744
+ const body = new FormData;
4745
+ const fileBytes = readFileSync4(filePath);
4746
+ body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
4747
+ if (opts.name)
4748
+ body.append("name", opts.name);
4749
+ if (opts.adFormat)
4750
+ body.append("adFormat", opts.adFormat);
4751
+ const resp = await fetch(request.url, {
4752
+ ...request.init,
4753
+ headers,
4754
+ body
4755
+ });
4756
+ const json = await resp.json();
4757
+ if (!resp.ok) {
4758
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4759
+ process.exit(1);
4760
+ }
4761
+ const asset = json.data ?? json;
4762
+ console.log(`Media asset uploaded: ${asset.id}`);
4763
+ if (asset.name)
4764
+ console.log(` Name: ${asset.name}`);
4765
+ if (asset.cdnUrl ?? asset.cdn_url) {
4766
+ console.log(` CDN URL: ${asset.cdnUrl ?? asset.cdn_url}`);
4767
+ }
4768
+ });
4769
+ 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) => {
4770
+ let body = {};
4771
+ if (opts.fromJson) {
4772
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4773
+ } else {
4774
+ if (opts.name)
4775
+ body.name = opts.name;
4776
+ if (opts.description)
4777
+ body.description = opts.description;
4778
+ if (opts.productCategory)
4779
+ body.productCategory = opts.productCategory;
4780
+ if (opts.targetAudience)
4781
+ body.targetAudience = opts.targetAudience;
4782
+ }
4783
+ const resp = await apiRequest({
4784
+ method: "PATCH",
4785
+ path: `${dspPrefix()}/media-assets/${id}`,
4786
+ body
4787
+ });
4788
+ const json = await resp.json();
4789
+ if (!resp.ok) {
4790
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4791
+ process.exit(1);
4792
+ }
4793
+ console.log(`Media asset ${id} updated.`);
4794
+ });
4795
+ cmd.command("delete").description("Delete a media asset.").argument("<id>", "Media asset ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
4796
+ if (!opts.yes) {
4797
+ console.error("Add --yes to confirm deletion.");
4798
+ process.exit(1);
4799
+ }
4800
+ const resp = await apiRequest({
4801
+ method: "DELETE",
4802
+ path: `${dspPrefix()}/media-assets/${id}`
4803
+ });
4804
+ if (!resp.ok && resp.status !== 204) {
4805
+ const json = await resp.json();
4806
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4807
+ process.exit(1);
4808
+ }
4809
+ console.log(`Media asset ${id} deleted.`);
4810
+ });
4811
+ for (const action of ["pause", "resume", "archive", "unarchive"]) {
4812
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a media asset.`).argument("<id>", "Media asset ID").action(async (id) => {
4813
+ const resp = await apiRequest({
4814
+ method: "PATCH",
4815
+ path: `${dspPrefix()}/media-assets/${id}/${action}`,
4816
+ body: {}
4817
+ });
4818
+ const json = await resp.json();
4819
+ if (!resp.ok) {
4820
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4821
+ process.exit(1);
4822
+ }
4823
+ console.log(`Media asset ${id} ${action}d.`);
4824
+ });
4825
+ }
4826
+ return cmd;
4827
+ }
4828
+
4829
+ // src/commands/landing-pages.ts
4830
+ var GROUP_COLUMNS = [
4831
+ { key: "id", header: "ID", width: 36 },
4832
+ { key: "name", header: "NAME", width: 28 },
4833
+ { key: "status", header: "STATUS", width: 10 },
4834
+ { key: "distributionAlgorithm", header: "DISTRIBUTION", width: 14 },
4835
+ { key: "pagesCount", header: "PAGES", width: 8 }
4836
+ ];
4837
+ var PAGE_COLUMNS = [
4838
+ { key: "id", header: "ID", width: 36 },
4839
+ { key: "name", header: "NAME", width: 24 },
4840
+ { key: "status", header: "STATUS", width: 10 },
4841
+ { key: "weight", header: "WEIGHT", width: 8 },
4842
+ { key: "url", header: "URL", width: 60 }
4843
+ ];
4844
+ function createLandingPagesCommand() {
4845
+ const cmd = new Command("landing-pages").description(`Landing page groups and pages (DSP)
4846
+
4847
+ Requires: ADVERTISER capability.`).addHelpText("after", `
4848
+ Examples:
4849
+ $ a8techads landing-pages list
4850
+ $ a8techads landing-pages active
4851
+ $ a8techads landing-pages get <group-id>
4852
+ $ a8techads landing-pages pages <group-id>`);
4853
+ addFormatOption(cmd.command("list").description("List landing page groups.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "50")).action(async (opts) => {
4854
+ const params = new URLSearchParams;
4855
+ if (opts.status)
4856
+ params.set("status", opts.status);
4857
+ params.set("limit", opts.limit);
4858
+ const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups?${params}` });
4859
+ const json = await resp.json();
4860
+ if (!resp.ok) {
4861
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4862
+ process.exit(1);
4863
+ }
4864
+ const rows = (json.data ?? json).map((g) => ({
4865
+ id: g.id,
4866
+ name: g.name,
4867
+ status: g.status,
4868
+ distributionAlgorithm: g.distributionAlgorithm ?? g.distribution_algorithm,
4869
+ pagesCount: g.pagesCount ?? g.pages_count ?? 0
4870
+ }));
4871
+ printData(rows, GROUP_COLUMNS, opts.format);
4872
+ });
4873
+ addFormatOption(cmd.command("active").description("List active landing page groups.")).action(async (_arg, opts) => {
4874
+ const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/active` });
4875
+ const json = await resp.json();
4876
+ if (!resp.ok) {
4877
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4878
+ process.exit(1);
4879
+ }
4880
+ const rows = (json.data ?? json).map((g) => ({
4881
+ id: g.id,
4882
+ name: g.name,
4883
+ status: g.status,
4884
+ distributionAlgorithm: g.distributionAlgorithm ?? g.distribution_algorithm,
4885
+ pagesCount: g.pagesCount ?? g.pages_count ?? 0
4886
+ }));
4887
+ printData(rows, GROUP_COLUMNS, opts.format);
4888
+ });
4889
+ addFormatOption(cmd.command("get").description("Get landing page group details.").argument("<id>", "Landing page group ID")).action(async (id, opts) => {
4890
+ const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/${id}` });
4891
+ const json = await resp.json();
4892
+ if (!resp.ok) {
4893
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4894
+ process.exit(1);
4895
+ }
4896
+ printDetail(json.data ?? json, opts.format);
4897
+ });
4898
+ addFormatOption(cmd.command("pages").description("List landing pages in a landing page group.").argument("<group-id>", "Landing page group ID")).action(async (groupId, opts) => {
4899
+ const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/${groupId}/pages` });
4900
+ const json = await resp.json();
4901
+ if (!resp.ok) {
4902
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4903
+ process.exit(1);
4904
+ }
4905
+ const rows = (json.data ?? json).map((page) => ({
4906
+ id: page.id,
4907
+ name: page.name,
4908
+ status: page.status,
4909
+ weight: page.weight,
4910
+ url: page.url
4911
+ }));
4912
+ printData(rows, PAGE_COLUMNS, opts.format);
4913
+ });
4914
+ return cmd;
4915
+ }
4916
+
4917
+ // src/commands/sites.ts
4918
+ var COLUMNS5 = [
4919
+ { key: "id", header: "ID", width: 36 },
4920
+ { key: "name", header: "NAME", width: 25 },
4921
+ { key: "domain", header: "DOMAIN", width: 25 },
4922
+ { key: "status", header: "STATUS", width: 12 },
4923
+ { key: "zoneCount", header: "ZONES", width: 6 }
4924
+ ];
4925
+ function createSitesCommand() {
4926
+ const cmd = new Command("sites").description(`Site management (SSP)
4927
+
4928
+ Requires: PUBLISHER capability.`).addHelpText("after", `
4929
+ Examples:
4930
+ $ a8techads sites list
4931
+ $ a8techads sites get <id>
4932
+ $ a8techads sites create --name "My Blog" --domain blog.example.com`);
4933
+ addFormatOption(cmd.command("list").description("List publisher sites.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20")).action(async (opts) => {
4934
+ const params = new URLSearchParams;
4935
+ if (opts.status)
4936
+ params.set("status", opts.status);
4937
+ params.set("limit", opts.limit);
4938
+ const resp = await apiRequest({ path: `${sspPrefix()}/sites?${params}` });
4939
+ const json = await resp.json();
4940
+ if (!resp.ok) {
4941
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4942
+ process.exit(1);
4943
+ }
4944
+ const rows = (json.data ?? json).map((s) => ({
4945
+ id: s.id,
4946
+ name: s.name,
4947
+ domain: s.domain,
4948
+ status: s.status,
4949
+ zoneCount: s.zoneCount ?? s.zone_count ?? "-"
4950
+ }));
4951
+ printData(rows, COLUMNS5, opts.format);
4952
+ });
4953
+ addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
4954
+ const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
4955
+ const json = await resp.json();
4956
+ if (!resp.ok) {
4957
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4958
+ process.exit(1);
4959
+ }
4960
+ printDetail(json.data ?? json, opts.format);
4961
+ });
4962
+ 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) => {
4963
+ let body;
4964
+ if (opts.fromJson) {
4965
+ const { readFileSync: readFileSync5 } = await import("fs");
4966
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4967
+ } else {
4968
+ if (!opts.name || !opts.domain) {
4969
+ console.error("Error: --name and --domain are required.");
4970
+ process.exit(1);
4971
+ }
4972
+ body = { name: opts.name, domain: opts.domain, type: opts.type };
4973
+ }
4974
+ const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
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(`Site created: ${json.data?.id ?? json.id}`);
4981
+ });
4982
+ 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) => {
4983
+ let body;
4984
+ if (opts.fromJson) {
4985
+ const { readFileSync: readFileSync5 } = await import("fs");
4986
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4987
+ } else {
4988
+ body = {};
4989
+ if (opts.name)
4990
+ body.name = opts.name;
4991
+ }
4992
+ const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
4120
4993
  if (!resp.ok) {
4121
4994
  const j = await resp.json();
4122
4995
  console.error(`Error: ${j.error ?? resp.statusText}`);
@@ -4161,7 +5034,7 @@ Examples:
4161
5034
  }
4162
5035
 
4163
5036
  // src/commands/zones.ts
4164
- var COLUMNS5 = [
5037
+ var COLUMNS6 = [
4165
5038
  { key: "id", header: "ID", width: 36 },
4166
5039
  { key: "name", header: "NAME", width: 25 },
4167
5040
  { key: "format", header: "FORMAT", width: 18 },
@@ -4193,7 +5066,7 @@ Examples:
4193
5066
  format: z.adFormat ?? z.ad_format ?? "-",
4194
5067
  status: z.status
4195
5068
  }));
4196
- printData(rows, COLUMNS5, opts.format);
5069
+ printData(rows, COLUMNS6, opts.format);
4197
5070
  });
4198
5071
  addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
4199
5072
  const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
@@ -4207,8 +5080,8 @@ Examples:
4207
5080
  cmd.command("create").description("Create a new ad zone.").option("--site <id>", "Site ID (required)").option("--name <name>", "Zone name (required)").option("--format <format>", "Ad format (e.g., banner_300x250)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4208
5081
  let body;
4209
5082
  if (opts.fromJson) {
4210
- const { readFileSync: readFileSync3 } = await import("fs");
4211
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
5083
+ const { readFileSync: readFileSync5 } = await import("fs");
5084
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4212
5085
  } else {
4213
5086
  if (!opts.site || !opts.name) {
4214
5087
  console.error("Error: --site and --name are required.");
@@ -4229,8 +5102,8 @@ Examples:
4229
5102
  cmd.command("update").description("Update a zone.").argument("<id>", "Zone ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4230
5103
  let body;
4231
5104
  if (opts.fromJson) {
4232
- const { readFileSync: readFileSync3 } = await import("fs");
4233
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
5105
+ const { readFileSync: readFileSync5 } = await import("fs");
5106
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4234
5107
  } else {
4235
5108
  body = {};
4236
5109
  if (opts.name)
@@ -4279,11 +5152,12 @@ Examples:
4279
5152
  $ a8techads reports templates`);
4280
5153
  addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
4281
5154
 
4282
- Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
5155
+ Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo,goal,goal_id,goal_order)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
4283
5156
  Examples:
4284
5157
  $ a8techads reports query --metrics spend,impressions --dimensions date --from 2026-03-01 --to 2026-03-20
4285
5158
  $ a8techads reports query --metrics revenue --dimensions publisher --format json
4286
- $ a8techads reports query --dimensions geo --limit 10`)).action(async (opts) => {
5159
+ $ a8techads reports query --dimensions geo --limit 10
5160
+ $ a8techads reports query --dimensions goal,date --metrics conversions,spend,cpa --from 2026-03-01`)).action(async (opts) => {
4287
5161
  const body = {
4288
5162
  metrics: opts.metrics.split(","),
4289
5163
  dimensions: opts.dimensions.split(","),
@@ -4299,19 +5173,20 @@ Examples:
4299
5173
  console.error(`Error: ${json.error ?? resp.statusText}`);
4300
5174
  process.exit(1);
4301
5175
  }
4302
- const rows = json.data?.rows ?? json.rows ?? [];
5176
+ const data = json.data ?? json;
5177
+ const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
4303
5178
  if (rows.length === 0) {
4304
5179
  console.log("No results.");
4305
5180
  return;
4306
5181
  }
4307
5182
  if (opts.format === "json") {
4308
- console.log(JSON.stringify(json.data ?? json, null, 2));
5183
+ console.log(JSON.stringify(data, null, 2));
4309
5184
  return;
4310
5185
  }
4311
5186
  const keys = Object.keys(rows[0]);
4312
5187
  const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
4313
5188
  printData(rows, columns, opts.format);
4314
- const summary = json.data?.summary ?? json.summary;
5189
+ const summary = data.summary ?? data.totals ?? json.summary ?? json.totals;
4315
5190
  if (summary && opts.format === "table") {
4316
5191
  console.log(`
4317
5192
  Summary:`);
@@ -4327,7 +5202,9 @@ Summary:`);
4327
5202
  console.error(`Error: ${json.error ?? resp.statusText}`);
4328
5203
  process.exit(1);
4329
5204
  }
4330
- const rows = (json.data ?? json).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
5205
+ const data = json.data ?? json;
5206
+ const reports = data.reports ?? json.reports ?? data;
5207
+ const rows = (Array.isArray(reports) ? reports : []).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
4331
5208
  printData(rows, [
4332
5209
  { key: "id", header: "ID", width: 36 },
4333
5210
  { key: "name", header: "NAME", width: 30 },
@@ -4350,7 +5227,9 @@ Summary:`);
4350
5227
  console.error(`Error: ${json.error ?? resp.statusText}`);
4351
5228
  process.exit(1);
4352
5229
  }
4353
- const rows = (json.data ?? json).map((t) => ({ id: t.id, name: t.name, description: t.description }));
5230
+ const data = json.data ?? json;
5231
+ const templates = data.templates ?? json.templates ?? data;
5232
+ const rows = (Array.isArray(templates) ? templates : []).map((t) => ({ id: t.id, name: t.name, description: t.description }));
4354
5233
  printData(rows, [
4355
5234
  { key: "id", header: "ID", width: 20 },
4356
5235
  { key: "name", header: "NAME", width: 25 },
@@ -4360,8 +5239,8 @@ Summary:`);
4360
5239
  cmd.command("create").description("Create a saved report.").option("--name <name>", "Report name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
4361
5240
  let body;
4362
5241
  if (opts.fromJson) {
4363
- const { readFileSync: readFileSync3 } = await import("fs");
4364
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
5242
+ const { readFileSync: readFileSync5 } = await import("fs");
5243
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4365
5244
  } else {
4366
5245
  if (!opts.name) {
4367
5246
  console.error("Error: --name is required.");
@@ -4463,8 +5342,8 @@ Examples:
4463
5342
  cmd.command("settings-update").description("Update billing settings.").option("--auto-recharge <bool>", "Enable/disable auto-recharge (true/false)").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4464
5343
  let body;
4465
5344
  if (opts.fromJson) {
4466
- const { readFileSync: readFileSync3 } = await import("fs");
4467
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
5345
+ const { readFileSync: readFileSync5 } = await import("fs");
5346
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4468
5347
  } else {
4469
5348
  body = {};
4470
5349
  if (opts.autoRecharge !== undefined)
@@ -4478,6 +5357,60 @@ Examples:
4478
5357
  }
4479
5358
  console.log("Billing settings updated.");
4480
5359
  });
5360
+ addFormatOption(cmd.command("payment-methods").description("List saved payment methods.")).action(async (opts) => {
5361
+ const resp = await apiRequest({ path: `${dspPrefix()}/payments/stripe/payment-methods` });
5362
+ const json = await resp.json();
5363
+ if (!resp.ok) {
5364
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5365
+ process.exit(1);
5366
+ }
5367
+ const columns = [
5368
+ { key: "id", header: "ID", width: 30 },
5369
+ { key: "type", header: "TYPE", width: 10 },
5370
+ { key: "last4", header: "LAST4", width: 6 },
5371
+ { key: "brand", header: "BRAND", width: 12 },
5372
+ { key: "expires", header: "EXPIRES", width: 10 }
5373
+ ];
5374
+ const rows = (json.data ?? json).map((pm) => ({
5375
+ id: pm.id,
5376
+ type: pm.type,
5377
+ last4: pm.last4 ?? pm.card?.last4,
5378
+ brand: pm.brand ?? pm.card?.brand,
5379
+ expires: pm.expires ?? (pm.card ? `${pm.card.exp_month}/${pm.card.exp_year}` : "-")
5380
+ }));
5381
+ printData(rows, columns, opts.format);
5382
+ });
5383
+ 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) => {
5384
+ if (!opts.amount) {
5385
+ console.error("Error: --amount is required.");
5386
+ process.exit(1);
5387
+ }
5388
+ const amount = Number(opts.amount);
5389
+ if (isNaN(amount) || amount <= 0) {
5390
+ console.error("Error: Amount must be a positive number.");
5391
+ process.exit(1);
5392
+ }
5393
+ if (!opts.yes) {
5394
+ const rl = await import("readline");
5395
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
5396
+ const answer = await new Promise((resolve) => iface.question(`Initiate USDT deposit of $${amount.toFixed(2)}? (y/N) `, resolve));
5397
+ iface.close();
5398
+ if (answer.toLowerCase() !== "y") {
5399
+ console.log("Cancelled.");
5400
+ process.exit(0);
5401
+ }
5402
+ }
5403
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/payments/usdt/checkout`, body: { amount, network: opts.network } });
5404
+ const json = await resp.json();
5405
+ if (!resp.ok) {
5406
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5407
+ process.exit(1);
5408
+ }
5409
+ console.log(`USDT deposit of $${amount.toFixed(2)} initiated.`);
5410
+ if (json.data?.paymentUrl || json.paymentUrl) {
5411
+ console.log(`Payment URL: ${json.data?.paymentUrl ?? json.paymentUrl}`);
5412
+ }
5413
+ });
4481
5414
  return cmd;
4482
5415
  }
4483
5416
 
@@ -4494,7 +5427,7 @@ function usersPrefix() {
4494
5427
  }
4495
5428
  return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4496
5429
  }
4497
- var COLUMNS6 = [
5430
+ var COLUMNS7 = [
4498
5431
  { key: "id", header: "ID", width: 36 },
4499
5432
  { key: "email", header: "EMAIL", width: 30 },
4500
5433
  { key: "name", header: "NAME", width: 20 },
@@ -4525,7 +5458,7 @@ Examples:
4525
5458
  role: u.role,
4526
5459
  status: u.status
4527
5460
  }));
4528
- printData(rows, COLUMNS6, opts.format);
5461
+ printData(rows, COLUMNS7, opts.format);
4529
5462
  });
4530
5463
  addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
4531
5464
  const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
@@ -4605,6 +5538,40 @@ Examples:
4605
5538
  }
4606
5539
 
4607
5540
  // src/commands/admin.ts
5541
+ function parseDurationMs(input) {
5542
+ const trimmed = input.trim();
5543
+ const match = /^(\d+)\s*(ms|s|m|h|d)?$/i.exec(trimmed);
5544
+ if (!match)
5545
+ return 60 * 60 * 1000;
5546
+ const value = parseInt(match[1], 10);
5547
+ const unit = (match[2] || "s").toLowerCase();
5548
+ switch (unit) {
5549
+ case "ms":
5550
+ return value;
5551
+ case "s":
5552
+ return value * 1000;
5553
+ case "m":
5554
+ return value * 60 * 1000;
5555
+ case "h":
5556
+ return value * 60 * 60 * 1000;
5557
+ case "d":
5558
+ return value * 24 * 60 * 60 * 1000;
5559
+ default:
5560
+ return value * 1000;
5561
+ }
5562
+ }
5563
+ async function confirmAction(message, yes) {
5564
+ if (yes)
5565
+ return;
5566
+ const rl = await import("readline");
5567
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
5568
+ const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
5569
+ iface.close();
5570
+ if (answer.toLowerCase() !== "y") {
5571
+ console.log("Cancelled.");
5572
+ process.exit(0);
5573
+ }
5574
+ }
4608
5575
  function createAdminCommand() {
4609
5576
  const cmd = new Command("admin").description(`Platform administration (Console)
4610
5577
 
@@ -4612,6 +5579,8 @@ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
4612
5579
  Examples:
4613
5580
  $ a8techads admin tenants list
4614
5581
  $ a8techads admin tenants get <id>
5582
+ $ a8techads admin campaign-reviews pending
5583
+ $ a8techads admin campaign-reviews approve-all <campaign-id> --yes
4615
5584
  $ a8techads admin audit-logs
4616
5585
  $ a8techads admin system health`);
4617
5586
  const tenants = cmd.command("tenants").description("Tenant management");
@@ -4645,6 +5614,108 @@ Examples:
4645
5614
  }
4646
5615
  printDetail(json.data ?? json, opts.format);
4647
5616
  });
5617
+ tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required)").option("--admin-name <name>", "Admin user name").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
5618
+ Examples:
5619
+ $ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
5620
+ $ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
5621
+ if (!opts.name) {
5622
+ console.error("Error: --name is required.");
5623
+ process.exit(1);
5624
+ }
5625
+ if (!opts.email) {
5626
+ console.error("Error: --email is required.");
5627
+ process.exit(1);
5628
+ }
5629
+ const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
5630
+ const body = {
5631
+ tenant: {
5632
+ companyName: opts.name,
5633
+ capabilities
5634
+ },
5635
+ admin: {
5636
+ email: opts.email,
5637
+ name: opts.adminName ?? undefined
5638
+ }
5639
+ };
5640
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/tenants`, body });
5641
+ const json = await resp.json();
5642
+ if (!resp.ok) {
5643
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5644
+ process.exit(1);
5645
+ }
5646
+ const data = json.data ?? json;
5647
+ const tenant = data.tenant ?? data;
5648
+ console.log(`Tenant created: ${tenant.id}`);
5649
+ if (tenant.companyName)
5650
+ console.log(` Name: ${tenant.companyName}`);
5651
+ if (tenant.status)
5652
+ console.log(` Status: ${tenant.status}`);
5653
+ if (data.admin) {
5654
+ console.log(` Admin: ${data.admin.email} (userId: ${data.admin.userId})`);
5655
+ }
5656
+ });
5657
+ 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) => {
5658
+ const body = {};
5659
+ if (opts.name)
5660
+ body.companyName = opts.name;
5661
+ if (opts.status)
5662
+ body.status = opts.status.toUpperCase();
5663
+ if (Object.keys(body).length === 0) {
5664
+ console.error("Error: at least one of --name or --status is required.");
5665
+ process.exit(1);
5666
+ }
5667
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/tenants/${id}`, body });
5668
+ if (!resp.ok) {
5669
+ const j = await resp.json();
5670
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5671
+ process.exit(1);
5672
+ }
5673
+ console.log(`Tenant ${id} updated.`);
5674
+ });
5675
+ tenants.command("suspend").description("Suspend a tenant (set status to SUSPENDED).").argument("<id>", "Tenant ID").action(async (id) => {
5676
+ const resp = await apiRequest({
5677
+ method: "PATCH",
5678
+ path: `${consolePrefix()}/tenants/${id}`,
5679
+ body: { status: "SUSPENDED" }
5680
+ });
5681
+ if (!resp.ok) {
5682
+ const j = await resp.json();
5683
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5684
+ process.exit(1);
5685
+ }
5686
+ console.log(`Tenant ${id} suspended.`);
5687
+ });
5688
+ tenants.command("activate").description("Activate a tenant (set status to ACTIVE).").argument("<id>", "Tenant ID").action(async (id) => {
5689
+ const resp = await apiRequest({
5690
+ method: "PATCH",
5691
+ path: `${consolePrefix()}/tenants/${id}`,
5692
+ body: { status: "ACTIVE" }
5693
+ });
5694
+ if (!resp.ok) {
5695
+ const j = await resp.json();
5696
+ console.error(`Error: ${j.error ?? resp.statusText}`);
5697
+ process.exit(1);
5698
+ }
5699
+ console.log(`Tenant ${id} activated.`);
5700
+ });
5701
+ tenants.command("delete").description("Delete a tenant, its members, and associated data.").argument("<id>", "Tenant ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
5702
+ await confirmAction(`Delete tenant ${id}? This removes the organization, its members, and associated data.`, opts.yes);
5703
+ const resp = await apiRequest({
5704
+ method: "DELETE",
5705
+ path: `${consolePrefix()}/tenants/${id}`
5706
+ });
5707
+ const json = await resp.json().catch(() => null);
5708
+ if (!resp.ok) {
5709
+ console.error(`Error: ${json?.error ?? json?.errors ?? resp.statusText}`);
5710
+ process.exit(1);
5711
+ }
5712
+ const data = json?.data ?? json ?? {};
5713
+ console.log(`Tenant ${id} deleted.`);
5714
+ if (data.deletedUserCount !== undefined)
5715
+ console.log(` Deleted users: ${data.deletedUserCount}`);
5716
+ if (data.removedMembershipCount !== undefined)
5717
+ console.log(` Removed memberships: ${data.removedMembershipCount}`);
5718
+ });
4648
5719
  const members = tenants.command("members").description("Tenant member management");
4649
5720
  addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
4650
5721
  if (!opts.tenant) {
@@ -4693,18 +5764,114 @@ Examples:
4693
5764
  { key: "time", header: "TIME", width: 22 }
4694
5765
  ], opts.format);
4695
5766
  });
4696
- const system = cmd.command("system").description("System operations");
4697
- addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
4698
- const resp = await apiRequest({ path: "/api/v1/console/system/health" });
5767
+ const campaignReviews = cmd.command("campaign-reviews").description("Campaign review operations (Console).").addHelpText("after", `
5768
+ Examples:
5769
+ $ a8techads admin campaign-reviews pending
5770
+ $ a8techads admin campaign-reviews show <campaign-id>
5771
+ $ a8techads admin campaign-reviews approve-all <campaign-id> --yes
5772
+ $ a8techads admin campaign-reviews decline <campaign-id> --reason-code policy --feedback "Needs changes" --yes`);
5773
+ const reviewColumns = [
5774
+ { key: "id", header: "ID", width: 36 },
5775
+ { key: "name", header: "NAME", width: 28 },
5776
+ { key: "status", header: "STATUS", width: 12 },
5777
+ { key: "tenantName", header: "TENANT", width: 24 },
5778
+ { key: "adFormat", header: "FORMAT", width: 12 },
5779
+ { key: "pendingVariations", header: "PENDING", width: 8 },
5780
+ { key: "totalVariations", header: "TOTAL", width: 8 }
5781
+ ];
5782
+ function normalizeReviewCampaign(campaign) {
5783
+ return {
5784
+ id: campaign.id ?? campaign.campaignId,
5785
+ name: campaign.name,
5786
+ status: campaign.status ?? campaign.reviewStatus,
5787
+ tenantName: campaign.tenantName ?? campaign.tenant?.companyName ?? campaign.tenant?.company_name,
5788
+ adFormat: campaign.adFormat ?? campaign.ad_format,
5789
+ pendingVariations: campaign.pendingVariations ?? campaign.pending_variations,
5790
+ totalVariations: campaign.totalVariations ?? campaign.total_variations,
5791
+ submittedAt: campaign.submittedAt ?? campaign.submitted_at
5792
+ };
5793
+ }
5794
+ addFormatOption(campaignReviews.command("pending").description("List campaigns pending review.").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
5795
+ const params = new URLSearchParams({ limit: opts.limit, offset: opts.offset });
5796
+ const resp = await apiRequest({ path: `${consolePrefix()}/campaigns/pending?${params}` });
4699
5797
  const json = await resp.json();
4700
5798
  if (!resp.ok) {
4701
- console.error(`Error: ${json.error ?? resp.statusText}`);
5799
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
4702
5800
  process.exit(1);
4703
5801
  }
4704
- printDetail(json.data ?? json, opts.format);
5802
+ const rows = (json.data ?? json).map(normalizeReviewCampaign);
5803
+ printData(rows, reviewColumns, opts.format);
4705
5804
  });
4706
- system.command("cache-clear").description("Clear system cache.").action(async () => {
4707
- const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
5805
+ addFormatOption(campaignReviews.command("show").description("Show campaign review details, including variations.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
5806
+ const resp = await apiRequest({ path: `${consolePrefix()}/campaigns/${id}/review` });
5807
+ const json = await resp.json();
5808
+ if (!resp.ok) {
5809
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5810
+ process.exit(1);
5811
+ }
5812
+ const data = json.data ?? json;
5813
+ if (opts.format === "json") {
5814
+ printDetail(data, "json");
5815
+ return;
5816
+ }
5817
+ const summary = normalizeReviewCampaign(data);
5818
+ printDetail(summary, "table");
5819
+ const variations = Array.isArray(data.variations) ? data.variations : [];
5820
+ if (variations.length > 0) {
5821
+ console.log(`
5822
+ Variations`);
5823
+ printData(variations.map((variation) => ({
5824
+ id: variation.id,
5825
+ name: variation.name,
5826
+ type: variation.type,
5827
+ status: variation.reviewStatus ?? variation.status,
5828
+ mediaAsset: variation.mediaAsset?.name ?? variation.media_asset?.name
5829
+ })), [
5830
+ { key: "id", header: "ID", width: 36 },
5831
+ { key: "name", header: "NAME", width: 28 },
5832
+ { key: "type", header: "TYPE", width: 12 },
5833
+ { key: "status", header: "STATUS", width: 12 },
5834
+ { key: "mediaAsset", header: "MEDIA", width: 24 }
5835
+ ], "table");
5836
+ }
5837
+ });
5838
+ addFormatOption(campaignReviews.command("approve-all").description("Approve all pending variations and activate the campaign atomically.").argument("<id>", "Campaign ID").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
5839
+ await confirmAction(`Approve all pending variations and activate campaign ${id}?`, opts.yes);
5840
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/campaigns/${id}/approve-all` });
5841
+ const json = await resp.json();
5842
+ if (!resp.ok) {
5843
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5844
+ process.exit(1);
5845
+ }
5846
+ printDetail(json.data ?? json, opts.format);
5847
+ });
5848
+ addFormatOption(campaignReviews.command("decline").description("Decline all pending variations for a campaign atomically.").argument("<id>", "Campaign ID").option("--feedback <text>", "Review feedback").option("--reason-code <code>", "Review reason code").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
5849
+ await confirmAction(`Decline campaign ${id}?`, opts.yes);
5850
+ const body = {};
5851
+ if (opts.feedback)
5852
+ body.feedback = opts.feedback;
5853
+ if (opts.reasonCode)
5854
+ body.reasonCode = opts.reasonCode;
5855
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/campaigns/${id}/decline`, body });
5856
+ const json = await resp.json();
5857
+ if (!resp.ok) {
5858
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5859
+ process.exit(1);
5860
+ }
5861
+ printDetail(json.data ?? json, opts.format);
5862
+ });
5863
+ const system = cmd.command("system").description("System operations");
5864
+ addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
5865
+ const resp = await apiRequest({ path: "/api/v1/console/system/health" });
5866
+ const json = await resp.json();
5867
+ if (!resp.ok) {
5868
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5869
+ process.exit(1);
5870
+ }
5871
+ printDetail(json.data ?? json, opts.format);
5872
+ });
5873
+ system.command("cache-clear").description("Clear system cache.").action(async () => {
5874
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
4708
5875
  if (!resp.ok) {
4709
5876
  const j = await resp.json();
4710
5877
  console.error(`Error: ${j.error ?? resp.statusText}`);
@@ -4712,33 +5879,10 @@ Examples:
4712
5879
  }
4713
5880
  console.log("Cache cleared.");
4714
5881
  });
4715
- return cmd;
4716
- }
4717
-
4718
- // src/commands/settings.ts
4719
- function settingsPrefix() {
4720
- const ctx = loadContext();
4721
- const context = getCurrentContext(ctx);
4722
- const app = context?.app ?? "dsp";
4723
- if (app === "console") {
4724
- console.error("Error: Settings commands are not available in Console mode.");
4725
- console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
4726
- process.exit(1);
4727
- }
4728
- return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
4729
- }
4730
- function createSettingsCommand() {
4731
- const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
4732
-
4733
- Requires: ADVERTISER or PUBLISHER capability.
4734
- Not available in Console mode.`).addHelpText("after", `
4735
- Examples:
4736
- $ a8techads settings show
4737
- $ a8techads settings update --from-json settings.json
4738
- $ a8techads settings profile
4739
- $ a8techads settings profile-update --company-name "New Name"`);
4740
- addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
4741
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
5882
+ const biddingGate = system.command("bidding-gate").description("Manage bidder runtime gate without redeploy.");
5883
+ addFormatOption(biddingGate.command("show").description("Show bidder runtime gate state.").option("--partner-code <code>", "Show gate state for one supply partner code")).action(async (opts) => {
5884
+ const path = opts.partnerCode ? `/api/v1/console/system/bidding-gate/${encodeURIComponent(String(opts.partnerCode))}` : "/api/v1/console/system/bidding-gate";
5885
+ const resp = await apiRequest({ path });
4742
5886
  const json = await resp.json();
4743
5887
  if (!resp.ok) {
4744
5888
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4746,8 +5890,139 @@ Examples:
4746
5890
  }
4747
5891
  printDetail(json.data ?? json, opts.format);
4748
5892
  });
4749
- addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
4750
- const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
5893
+ biddingGate.command("set").description("Set bidder runtime gate mode.").argument("<mode>", "active | observe_only").option("--partner-code <code>", "Set gate mode for one supply partner code").option("--yes", "Skip confirmation", false).addHelpText("after", `
5894
+ Examples:
5895
+ $ a8techads admin system bidding-gate show
5896
+ $ a8techads admin system bidding-gate set observe_only --yes
5897
+ $ a8techads admin system bidding-gate set active --yes
5898
+ $ a8techads admin system bidding-gate set observe_only --partner-code PROPELLERADS --yes`).action(async (mode, opts) => {
5899
+ const normalizedMode = String(mode).trim();
5900
+ if (!["active", "observe_only"].includes(normalizedMode)) {
5901
+ console.error("Error: mode must be one of: active, observe_only");
5902
+ process.exit(1);
5903
+ }
5904
+ const partnerCode = opts.partnerCode ? String(opts.partnerCode).trim() : "";
5905
+ await confirmAction(partnerCode ? `Set bidder runtime gate for ${partnerCode} to ${normalizedMode}?` : `Set bidder runtime gate to ${normalizedMode}?`, opts.yes);
5906
+ const resp = await apiRequest({
5907
+ method: "PUT",
5908
+ path: partnerCode ? `/api/v1/console/system/bidding-gate/${encodeURIComponent(partnerCode)}` : "/api/v1/console/system/bidding-gate",
5909
+ body: { mode: normalizedMode }
5910
+ });
5911
+ const json = await resp.json();
5912
+ if (!resp.ok) {
5913
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5914
+ process.exit(1);
5915
+ }
5916
+ printDetail(json.data ?? json);
5917
+ });
5918
+ const debug = biddingGate.command("debug").description("Phase 4 per-request pacer debug funnel.");
5919
+ debug.command("arm").description("Arm N debug slot(s) for a partner. Each consumed slot routes one request via the pacer path.").requiredOption("--partner <code>", "Partner code (e.g. HILLTOPADS)").option("--count <n>", "Number of slots to arm", "1").option("--region <region>", "sin | dfw | all", "dfw").addHelpText("after", `
5920
+ Examples:
5921
+ $ a8techads admin system bidding-gate debug arm --partner HILLTOPADS
5922
+ $ a8techads admin system bidding-gate debug arm --partner HILLTOPADS --count 10
5923
+ $ a8techads admin system bidding-gate debug arm --partner HILLTOPADS --region sin --count 1`).action(async (opts) => {
5924
+ const body = {
5925
+ partner_code: String(opts.partner).trim(),
5926
+ count: parseInt(String(opts.count), 10),
5927
+ region: String(opts.region).trim()
5928
+ };
5929
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/arm", body });
5930
+ const json = await resp.json();
5931
+ if (!resp.ok) {
5932
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5933
+ process.exit(1);
5934
+ }
5935
+ printDetail(json.data ?? json);
5936
+ });
5937
+ debug.command("disarm").description("Disarm debug slots. --all wipes every armed partner across regions.").option("--partner <code>", "Partner code to disarm").option("--region <region>", "sin | dfw | all", "all").option("--all", "Disarm every armed partner across all regions", false).action(async (opts) => {
5938
+ if (!opts.all && !opts.partner) {
5939
+ console.error("Error: provide --partner <code> or --all");
5940
+ process.exit(1);
5941
+ }
5942
+ const body = opts.all ? { all: true } : { partner_code: String(opts.partner).trim(), region: String(opts.region).trim() };
5943
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/disarm", body });
5944
+ const json = await resp.json();
5945
+ if (!resp.ok) {
5946
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5947
+ process.exit(1);
5948
+ }
5949
+ printDetail(json.data ?? json);
5950
+ });
5951
+ debug.command("show").description("Show armed counters and cumulative tallies for a partner across regions.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw | all", "all").action(async (opts) => {
5952
+ const params = new URLSearchParams({
5953
+ partner_code: String(opts.partner).trim(),
5954
+ region: String(opts.region).trim()
5955
+ });
5956
+ const resp = await apiRequest({ path: `/api/v1/console/system/bidding-gate/debug/show?${params}` });
5957
+ const json = await resp.json();
5958
+ if (!resp.ok) {
5959
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5960
+ process.exit(1);
5961
+ }
5962
+ printDetail(json.data ?? json);
5963
+ });
5964
+ debug.command("tail").description("Tail captured fallback entries for a partner. Each entry is one request that came back as non-2xx-non-204 from the bidder.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw", "dfw").option("--since <duration>", "How far back to read (e.g. 5m, 1h)", "1h").option("--max <n>", "Cap on entries returned", "100").action(async (opts) => {
5965
+ const sinceMs = Date.now() - parseDurationMs(String(opts.since));
5966
+ const params = new URLSearchParams({
5967
+ partner_code: String(opts.partner).trim(),
5968
+ region: String(opts.region).trim(),
5969
+ since_ms: String(sinceMs),
5970
+ max_count: String(opts.max)
5971
+ });
5972
+ const resp = await apiRequest({ path: `/api/v1/console/system/bidding-gate/debug/tail?${params}` });
5973
+ const json = await resp.json();
5974
+ if (!resp.ok) {
5975
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5976
+ process.exit(1);
5977
+ }
5978
+ printDetail(json.data ?? json);
5979
+ });
5980
+ debug.command("prune").description("Trim captured fallback stream entries older than --max-age.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw | all", "all").option("--max-age <duration>", "Drop entries older than this", "24h").action(async (opts) => {
5981
+ const cutoffMs = Date.now() - parseDurationMs(String(opts.maxAge));
5982
+ const body = {
5983
+ partner_code: String(opts.partner).trim(),
5984
+ region: String(opts.region).trim(),
5985
+ cutoff_ms: cutoffMs
5986
+ };
5987
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/prune", body });
5988
+ const json = await resp.json();
5989
+ if (!resp.ok) {
5990
+ console.error(`Error: ${json.error ?? resp.statusText}`);
5991
+ process.exit(1);
5992
+ }
5993
+ printDetail(json.data ?? json);
5994
+ });
5995
+ debug.command("preflight").description("Dependency preflight: control-plane reachable, runtime Redis healthy, BILLING_CONFIRM stream alive. Touches no real billing.").action(async () => {
5996
+ const resp = await apiRequest({ path: "/api/v1/console/system/bidding-gate/debug/preflight" });
5997
+ const json = await resp.json();
5998
+ if (!resp.ok) {
5999
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6000
+ process.exit(1);
6001
+ }
6002
+ printDetail(json.data ?? json);
6003
+ });
6004
+ const tracking = cmd.command("tracking-identities").description("User identity graph stats");
6005
+ tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
6006
+ const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
6007
+ const json = await resp.json();
6008
+ if (!resp.ok) {
6009
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6010
+ process.exit(1);
6011
+ }
6012
+ const data = json.data ?? json;
6013
+ console.log(`Tracking Identity Stats:`);
6014
+ console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
6015
+ console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
6016
+ console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
6017
+ console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
6018
+ if (data.earliest_seen)
6019
+ console.log(` Earliest Seen: ${data.earliest_seen}`);
6020
+ if (data.latest_seen)
6021
+ console.log(` Latest Seen: ${data.latest_seen}`);
6022
+ });
6023
+ const finance = cmd.command("finance").description("Financial overview and audit");
6024
+ addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
6025
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
4751
6026
  const json = await resp.json();
4752
6027
  if (!resp.ok) {
4753
6028
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4755,85 +6030,236 @@ Examples:
4755
6030
  }
4756
6031
  printDetail(json.data ?? json, opts.format);
4757
6032
  });
4758
- cmd.command("update").description(`Update tenant settings.
4759
-
4760
- Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4761
- if (!opts.fromJson) {
4762
- console.error("Error: --from-json is required for settings update.");
6033
+ 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) => {
6034
+ const targetDate = opts.date || new Date().toISOString().slice(0, 10);
6035
+ await confirmAction(`Run daily billing finalization for ${targetDate}?`, opts.yes);
6036
+ const body = {};
6037
+ if (opts.date)
6038
+ body.date = opts.date;
6039
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/finalize-daily`, body });
6040
+ const json = await resp.json();
6041
+ if (!resp.ok) {
6042
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4763
6043
  process.exit(1);
4764
6044
  }
4765
- const { readFileSync: readFileSync3 } = await import("fs");
4766
- const body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4767
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
6045
+ const data = json.data ?? json;
6046
+ console.log(`Daily billing finalized for ${data.date ?? targetDate}.`);
6047
+ });
6048
+ 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) => {
6049
+ const params = new URLSearchParams({ limit: opts.limit });
6050
+ if (opts.aggregateType)
6051
+ params.set("aggregate_type", opts.aggregateType);
6052
+ if (opts.eventType)
6053
+ params.set("event_type", opts.eventType);
6054
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/events?${params}` });
6055
+ const json = await resp.json();
6056
+ if (!resp.ok) {
6057
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6058
+ process.exit(1);
6059
+ }
6060
+ const payload = json.data ?? json;
6061
+ const rows = (payload.events ?? payload).map((e) => ({
6062
+ id: e.id,
6063
+ eventType: e.eventType ?? e.event_type,
6064
+ aggregateType: e.aggregateType ?? e.aggregate_type,
6065
+ createdAt: e.createdAt ?? e.created_at
6066
+ }));
6067
+ printData(rows, [
6068
+ { key: "id", header: "ID", width: 36 },
6069
+ { key: "eventType", header: "EVENT_TYPE", width: 25 },
6070
+ { key: "aggregateType", header: "AGGREGATE_TYPE", width: 20 },
6071
+ { key: "createdAt", header: "CREATED_AT", width: 22 }
6072
+ ], opts.format);
6073
+ });
6074
+ 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) => {
6075
+ const params = new URLSearchParams({ limit: opts.limit });
6076
+ if (opts.type)
6077
+ params.set("type", opts.type);
6078
+ if (opts.status)
6079
+ params.set("status", opts.status);
6080
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/operations?${params}` });
6081
+ const json = await resp.json();
6082
+ if (!resp.ok) {
6083
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6084
+ process.exit(1);
6085
+ }
6086
+ const payload = json.data ?? json;
6087
+ const rows = (payload.operations ?? payload).map((o) => ({
6088
+ id: o.id,
6089
+ type: o.type,
6090
+ target: o.target ?? o.targetId ?? o.target_id,
6091
+ amount: o.amount,
6092
+ status: o.status,
6093
+ createdAt: o.createdAt ?? o.created_at
6094
+ }));
6095
+ printData(rows, [
6096
+ { key: "id", header: "ID", width: 36 },
6097
+ { key: "type", header: "TYPE", width: 18 },
6098
+ { key: "target", header: "TARGET", width: 20 },
6099
+ { key: "amount", header: "AMOUNT", width: 12 },
6100
+ { key: "status", header: "STATUS", width: 12 },
6101
+ { key: "createdAt", header: "CREATED_AT", width: 22 }
6102
+ ], opts.format);
6103
+ });
6104
+ const invoices = cmd.command("invoices").description("Invoice management");
6105
+ 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) => {
6106
+ const params = new URLSearchParams({ limit: opts.limit });
6107
+ if (opts.status)
6108
+ params.set("status", opts.status);
6109
+ if (opts.tenantType)
6110
+ params.set("tenantType", opts.tenantType);
6111
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
6112
+ const json = await resp.json();
6113
+ if (!resp.ok) {
6114
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6115
+ process.exit(1);
6116
+ }
6117
+ const payload = json.data ?? json;
6118
+ const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
6119
+ const rows = invoiceRows.map((i) => ({
6120
+ invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
6121
+ tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
6122
+ amount: i.amount ?? i.totalAmount ?? i.total_amount,
6123
+ status: i.status,
6124
+ dueDate: i.dueDate ?? i.due_date
6125
+ }));
6126
+ printData(rows, [
6127
+ { key: "invoiceNumber", header: "INVOICE#", width: 20 },
6128
+ { key: "tenant", header: "TENANT", width: 25 },
6129
+ { key: "amount", header: "AMOUNT", width: 12 },
6130
+ { key: "status", header: "STATUS", width: 12 },
6131
+ { key: "dueDate", header: "DUE_DATE", width: 16 }
6132
+ ], opts.format);
6133
+ });
6134
+ addFormatOption(invoices.command("preview").description("Preview unbilled invoices for current period.")).action(async (opts) => {
6135
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=advertiser` });
6136
+ const json = await resp.json();
6137
+ if (!resp.ok) {
6138
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6139
+ process.exit(1);
6140
+ }
6141
+ const data = json.data ?? json;
6142
+ if (data.summary) {
6143
+ console.log(`
6144
+ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Total Due: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
6145
+ `);
6146
+ }
6147
+ const rows = (data.previews ?? []).map((p) => ({
6148
+ tenant: p.tenantName,
6149
+ adSpend: p.adSpend,
6150
+ platformFee: p.platformFee,
6151
+ totalDue: p.totalDue,
6152
+ days: p.daysWithData,
6153
+ impressions: p.impressions
6154
+ }));
6155
+ printData(rows, [
6156
+ { key: "tenant", header: "TENANT", width: 25 },
6157
+ { key: "adSpend", header: "AD_SPEND", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
6158
+ { key: "platformFee", header: "PLATFORM_FEE", width: 14, format: (v) => `$${Number(v).toFixed(2)}` },
6159
+ { key: "totalDue", header: "TOTAL_DUE", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
6160
+ { key: "days", header: "DAYS", width: 6 },
6161
+ { key: "impressions", header: "IMPRESSIONS", width: 12 }
6162
+ ], opts.format);
6163
+ });
6164
+ addFormatOption(invoices.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
6165
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
6166
+ const json = await resp.json();
6167
+ if (!resp.ok) {
6168
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6169
+ process.exit(1);
6170
+ }
6171
+ printDetail(json.data ?? json, opts.format);
6172
+ });
6173
+ invoices.command("issue").description("Issue an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6174
+ await confirmAction(`Issue invoice ${id}?`, opts.yes);
6175
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/issue` });
4768
6176
  if (!resp.ok) {
4769
6177
  const j = await resp.json();
4770
6178
  console.error(`Error: ${j.error ?? resp.statusText}`);
4771
6179
  process.exit(1);
4772
6180
  }
4773
- console.log("Settings updated.");
6181
+ console.log(`Invoice ${id} issued.`);
4774
6182
  });
4775
- cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
4776
- let body;
4777
- if (opts.fromJson) {
4778
- const { readFileSync: readFileSync3 } = await import("fs");
4779
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4780
- } else {
4781
- body = {};
4782
- if (opts.companyName)
4783
- body.companyName = opts.companyName;
6183
+ invoices.command("void").description("Void an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6184
+ await confirmAction(`Void invoice ${id}?`, opts.yes);
6185
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/void` });
6186
+ if (!resp.ok) {
6187
+ const j = await resp.json();
6188
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6189
+ process.exit(1);
4784
6190
  }
4785
- const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
6191
+ console.log(`Invoice ${id} voided.`);
6192
+ });
6193
+ invoices.command("mark-paid").description("Mark an invoice as paid.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6194
+ await confirmAction(`Mark invoice ${id} as paid?`, opts.yes);
6195
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/mark-paid` });
4786
6196
  if (!resp.ok) {
4787
6197
  const j = await resp.json();
4788
6198
  console.error(`Error: ${j.error ?? resp.statusText}`);
4789
6199
  process.exit(1);
4790
6200
  }
4791
- console.log("Profile updated.");
6201
+ console.log(`Invoice ${id} marked as paid.`);
4792
6202
  });
4793
- return cmd;
4794
- }
4795
-
4796
- // src/commands/invoices.ts
4797
- function createInvoicesCommand() {
4798
- const cmd = new Command("invoices").description(`Invoice management (DSP)
4799
-
4800
- Requires: ADVERTISER capability.`).addHelpText("after", `
4801
- Examples:
4802
- $ a8techads invoices list
4803
- $ a8techads invoices get <id>
4804
- $ a8techads invoices spending`);
4805
- addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
4806
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
6203
+ const statements = cmd.command("statements").description("Publisher statement management");
6204
+ addFormatOption(statements.command("list").description("List publisher statements.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
6205
+ const params = new URLSearchParams({ limit: opts.limit, tenantType: "publisher" });
6206
+ if (opts.status)
6207
+ params.set("status", opts.status);
6208
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
4807
6209
  const json = await resp.json();
4808
6210
  if (!resp.ok) {
4809
6211
  console.error(`Error: ${json.error ?? resp.statusText}`);
4810
6212
  process.exit(1);
4811
6213
  }
4812
- const columns = [
4813
- { key: "id", header: "ID", width: 36 },
4814
- { key: "period", header: "PERIOD", width: 20 },
4815
- { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
4816
- { key: "status", header: "STATUS", width: 12 }
4817
- ];
4818
- const rows = (json.data ?? json).map((i) => ({
4819
- id: i.id,
4820
- period: i.period ?? i.billingPeriod,
4821
- amount: i.amount ?? i.totalAmount,
4822
- status: i.status
6214
+ const payload = json.data ?? json;
6215
+ const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
6216
+ const rows = invoiceRows.map((i) => ({
6217
+ invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
6218
+ tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
6219
+ amount: i.amount ?? i.totalAmount ?? i.total_amount,
6220
+ status: i.status,
6221
+ dueDate: i.dueDate ?? i.due_date
4823
6222
  }));
4824
- printData(rows, columns, opts.format);
6223
+ printData(rows, [
6224
+ { key: "invoiceNumber", header: "INVOICE#", width: 20 },
6225
+ { key: "tenant", header: "TENANT", width: 25 },
6226
+ { key: "amount", header: "AMOUNT", width: 12 },
6227
+ { key: "status", header: "STATUS", width: 12 },
6228
+ { key: "dueDate", header: "DUE_DATE", width: 16 }
6229
+ ], opts.format);
4825
6230
  });
4826
- addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
4827
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
6231
+ addFormatOption(statements.command("preview").description("Preview unbilled statements for current period.")).action(async (opts) => {
6232
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=publisher` });
4828
6233
  const json = await resp.json();
4829
6234
  if (!resp.ok) {
4830
6235
  console.error(`Error: ${json.error ?? resp.statusText}`);
4831
6236
  process.exit(1);
4832
6237
  }
4833
- printDetail(json.data ?? json, opts.format);
6238
+ const data = json.data ?? json;
6239
+ if (data.summary) {
6240
+ console.log(`
6241
+ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Net Payable: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
6242
+ `);
6243
+ }
6244
+ const rows = (data.previews ?? []).map((p) => ({
6245
+ tenant: p.tenantName,
6246
+ grossRevenue: p.grossRevenue,
6247
+ commission: p.platformCommission,
6248
+ netPayable: p.netPayable,
6249
+ days: p.daysWithData,
6250
+ impressions: p.impressions
6251
+ }));
6252
+ printData(rows, [
6253
+ { key: "tenant", header: "TENANT", width: 25 },
6254
+ { key: "grossRevenue", header: "GROSS_REV", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
6255
+ { key: "commission", header: "COMMISSION", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
6256
+ { key: "netPayable", header: "NET_PAY", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
6257
+ { key: "days", header: "DAYS", width: 6 },
6258
+ { key: "impressions", header: "IMPRESSIONS", width: 12 }
6259
+ ], opts.format);
4834
6260
  });
4835
- addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
4836
- const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
6261
+ addFormatOption(statements.command("get").description("Get statement details.").argument("<id>", "Statement ID")).action(async (id, opts) => {
6262
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
4837
6263
  const json = await resp.json();
4838
6264
  if (!resp.ok) {
4839
6265
  console.error(`Error: ${json.error ?? resp.statusText}`);
@@ -4841,12 +6267,2216 @@ Examples:
4841
6267
  }
4842
6268
  printDetail(json.data ?? json, opts.format);
4843
6269
  });
6270
+ const payoutsAdmin = cmd.command("payouts").description("Payout operations");
6271
+ addFormatOption(payoutsAdmin.command("list").description("List payouts.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
6272
+ const params = new URLSearchParams({ limit: opts.limit });
6273
+ if (opts.status)
6274
+ params.set("status", opts.status);
6275
+ const resp = await apiRequest({ path: `${consolePrefix()}/payout-ops/payouts?${params}` });
6276
+ const json = await resp.json();
6277
+ if (!resp.ok) {
6278
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6279
+ process.exit(1);
6280
+ }
6281
+ const rows = (json.data ?? json).map((p) => ({
6282
+ id: p.id,
6283
+ publisher: p.publisherName ?? p.publisher_name ?? p.tenantId ?? p.tenant_id,
6284
+ amount: p.amount,
6285
+ status: p.status,
6286
+ requestedAt: p.requestedAt ?? p.requested_at ?? p.createdAt ?? p.created_at
6287
+ }));
6288
+ printData(rows, [
6289
+ { key: "id", header: "ID", width: 36 },
6290
+ { key: "publisher", header: "PUBLISHER", width: 25 },
6291
+ { key: "amount", header: "AMOUNT", width: 12 },
6292
+ { key: "status", header: "STATUS", width: 12 },
6293
+ { key: "requestedAt", header: "REQUESTED_AT", width: 22 }
6294
+ ], opts.format);
6295
+ });
6296
+ payoutsAdmin.command("approve").description("Approve a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6297
+ await confirmAction(`Approve payout ${id}?`, opts.yes);
6298
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/approve` });
6299
+ if (!resp.ok) {
6300
+ const j = await resp.json();
6301
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6302
+ process.exit(1);
6303
+ }
6304
+ console.log(`Payout ${id} approved.`);
6305
+ });
6306
+ 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) => {
6307
+ await confirmAction(`Reject payout ${id}?`, opts.yes);
6308
+ const body = {};
6309
+ if (opts.reason)
6310
+ body.reason = opts.reason;
6311
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/reject`, body });
6312
+ if (!resp.ok) {
6313
+ const j = await resp.json();
6314
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6315
+ process.exit(1);
6316
+ }
6317
+ console.log(`Payout ${id} rejected.`);
6318
+ });
6319
+ payoutsAdmin.command("process").description("Process a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6320
+ await confirmAction(`Process payout ${id}?`, opts.yes);
6321
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/process` });
6322
+ if (!resp.ok) {
6323
+ const j = await resp.json();
6324
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6325
+ process.exit(1);
6326
+ }
6327
+ console.log(`Payout ${id} processed.`);
6328
+ });
6329
+ 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) => {
6330
+ if (!opts.tenantId) {
6331
+ console.error("Error: --tenant-id is required.");
6332
+ process.exit(1);
6333
+ }
6334
+ if (!opts.amount) {
6335
+ console.error("Error: --amount is required.");
6336
+ process.exit(1);
6337
+ }
6338
+ await confirmAction(`Create manual payout of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
6339
+ const body = { tenantId: opts.tenantId, amount: opts.amount };
6340
+ if (opts.reason)
6341
+ body.reason = opts.reason;
6342
+ if (opts.wallet)
6343
+ body.wallet = opts.wallet;
6344
+ if (opts.network)
6345
+ body.network = opts.network;
6346
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-payout`, body });
6347
+ const json = await resp.json();
6348
+ if (!resp.ok) {
6349
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6350
+ process.exit(1);
6351
+ }
6352
+ console.log(`Manual payout created: ${(json.data ?? json).id ?? "OK"}`);
6353
+ });
6354
+ const deposits = cmd.command("deposits").description("Deposit operations");
6355
+ addFormatOption(deposits.command("list").description("List deposits.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
6356
+ const params = new URLSearchParams({ limit: opts.limit });
6357
+ if (opts.status)
6358
+ params.set("status", opts.status);
6359
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/deposits?${params}` });
6360
+ const json = await resp.json();
6361
+ if (!resp.ok) {
6362
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6363
+ process.exit(1);
6364
+ }
6365
+ const transactions = json.data?.transactions ?? json.transactions ?? json.data ?? json;
6366
+ const items = Array.isArray(transactions) ? transactions : [];
6367
+ const rows = items.map((d) => ({
6368
+ id: d.id,
6369
+ advertiser: d.advertiser?.name ?? d.advertiserName ?? d.advertiser_name ?? d.tenantId ?? d.tenant_id,
6370
+ amount: d.amount,
6371
+ status: d.status,
6372
+ date: d.createdAt ?? d.created_at ?? d.date
6373
+ }));
6374
+ printData(rows, [
6375
+ { key: "id", header: "ID", width: 36 },
6376
+ { key: "advertiser", header: "ADVERTISER", width: 25 },
6377
+ { key: "amount", header: "AMOUNT", width: 12 },
6378
+ { key: "status", header: "STATUS", width: 12 },
6379
+ { key: "date", header: "DATE", width: 22 }
6380
+ ], opts.format);
6381
+ });
6382
+ deposits.command("confirm").description("Confirm a deposit.").argument("<id>", "Deposit ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6383
+ await confirmAction(`Confirm deposit ${id}?`, opts.yes);
6384
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/deposits/${id}/confirm` });
6385
+ if (!resp.ok) {
6386
+ const j = await resp.json();
6387
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6388
+ process.exit(1);
6389
+ }
6390
+ console.log(`Deposit ${id} confirmed.`);
6391
+ });
6392
+ 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) => {
6393
+ if (!opts.tenantId) {
6394
+ console.error("Error: --tenant-id is required.");
6395
+ process.exit(1);
6396
+ }
6397
+ if (!opts.amount) {
6398
+ console.error("Error: --amount is required.");
6399
+ process.exit(1);
6400
+ }
6401
+ await confirmAction(`Create manual deposit of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
6402
+ const body = { tenantId: opts.tenantId, amount: opts.amount };
6403
+ if (opts.reason)
6404
+ body.reason = opts.reason;
6405
+ if (opts.reference)
6406
+ body.reference = opts.reference;
6407
+ if (opts.notes)
6408
+ body.notes = opts.notes;
6409
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-deposit`, body });
6410
+ const json = await resp.json();
6411
+ if (!resp.ok) {
6412
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6413
+ process.exit(1);
6414
+ }
6415
+ console.log(`Manual deposit created: ${(json.data ?? json).id ?? "OK"}`);
6416
+ });
6417
+ const refunds = cmd.command("refunds").description("Refund operations");
6418
+ addFormatOption(refunds.command("list").description("List refunds.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
6419
+ const params = new URLSearchParams({ limit: opts.limit });
6420
+ if (opts.status)
6421
+ params.set("status", opts.status);
6422
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds?${params}` });
6423
+ const json = await resp.json();
6424
+ if (!resp.ok) {
6425
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6426
+ process.exit(1);
6427
+ }
6428
+ const rows = (json.data ?? json).map((r) => ({
6429
+ id: r.id,
6430
+ advertiser: r.advertiserName ?? r.advertiser_name ?? r.accountId ?? r.account_id,
6431
+ amount: r.amount,
6432
+ reason: r.reason,
6433
+ status: r.status
6434
+ }));
6435
+ printData(rows, [
6436
+ { key: "id", header: "ID", width: 36 },
6437
+ { key: "advertiser", header: "ADVERTISER", width: 25 },
6438
+ { key: "amount", header: "AMOUNT", width: 12 },
6439
+ { key: "reason", header: "REASON", width: 20 },
6440
+ { key: "status", header: "STATUS", width: 12 }
6441
+ ], opts.format);
6442
+ });
6443
+ addFormatOption(refunds.command("get").description("Get refund details.").argument("<id>", "Refund ID")).action(async (id, opts) => {
6444
+ const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds/${id}` });
6445
+ const json = await resp.json();
6446
+ if (!resp.ok) {
6447
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6448
+ process.exit(1);
6449
+ }
6450
+ printDetail(json.data ?? json, opts.format);
6451
+ });
6452
+ 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) => {
6453
+ if (!opts.accountId) {
6454
+ console.error("Error: --account-id is required.");
6455
+ process.exit(1);
6456
+ }
6457
+ if (!opts.amount) {
6458
+ console.error("Error: --amount is required.");
6459
+ process.exit(1);
6460
+ }
6461
+ await confirmAction(`Create refund of ${opts.amount} for account ${opts.accountId}?`, opts.yes);
6462
+ const body = { accountId: opts.accountId, amount: opts.amount };
6463
+ if (opts.reason)
6464
+ body.reason = opts.reason;
6465
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds`, body });
6466
+ const json = await resp.json();
6467
+ if (!resp.ok) {
6468
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6469
+ process.exit(1);
6470
+ }
6471
+ console.log(`Refund created: ${(json.data ?? json).id ?? "OK"}`);
6472
+ });
6473
+ refunds.command("approve").description("Approve a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6474
+ await confirmAction(`Approve refund ${id}?`, opts.yes);
6475
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/approve` });
6476
+ if (!resp.ok) {
6477
+ const j = await resp.json();
6478
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6479
+ process.exit(1);
6480
+ }
6481
+ console.log(`Refund ${id} approved.`);
6482
+ });
6483
+ 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) => {
6484
+ await confirmAction(`Reject refund ${id}?`, opts.yes);
6485
+ const body = {};
6486
+ if (opts.reason)
6487
+ body.reason = opts.reason;
6488
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/reject`, body });
6489
+ if (!resp.ok) {
6490
+ const j = await resp.json();
6491
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6492
+ process.exit(1);
6493
+ }
6494
+ console.log(`Refund ${id} rejected.`);
6495
+ });
6496
+ refunds.command("process").description("Process an approved refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
6497
+ await confirmAction(`Process refund ${id}?`, opts.yes);
6498
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/process` });
6499
+ if (!resp.ok) {
6500
+ const j = await resp.json();
6501
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6502
+ process.exit(1);
6503
+ }
6504
+ console.log(`Refund ${id} processed.`);
6505
+ });
6506
+ const balances = cmd.command("balances").description("Account balance management");
6507
+ addFormatOption(balances.command("list").description("List account balances.")).action(async (opts) => {
6508
+ const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/balances` });
6509
+ const json = await resp.json();
6510
+ if (!resp.ok) {
6511
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6512
+ process.exit(1);
6513
+ }
6514
+ printDetail(json.data ?? json, opts.format);
6515
+ });
6516
+ 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) => {
6517
+ if (!opts.tenantId) {
6518
+ console.error("Error: --tenant-id is required.");
6519
+ process.exit(1);
6520
+ }
6521
+ if (!opts.amount) {
6522
+ console.error("Error: --amount is required.");
6523
+ process.exit(1);
6524
+ }
6525
+ await confirmAction(`Adjust balance by ${opts.amount} for tenant ${opts.tenantId} (${opts.accountType})?`, opts.yes);
6526
+ const body = { tenantId: opts.tenantId, amount: opts.amount, accountType: opts.accountType };
6527
+ if (opts.reason)
6528
+ body.reason = opts.reason;
6529
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/adjust`, body });
6530
+ const json = await resp.json();
6531
+ if (!resp.ok) {
6532
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6533
+ process.exit(1);
6534
+ }
6535
+ console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
6536
+ });
6537
+ const lease = cmd.command("lease").description("Pacer lease operator commands (v3)");
6538
+ lease.command("flush").description("Flush chunks/outbox/dedupe for a single campaign.").requiredOption("--campaign <cid>", "Campaign ID to flush").option("--region <region>", 'Region to scope (omit or "all" for every region)', "all").option("--dry-run", "Preview keys without deleting (default true)", false).option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).addHelpText("after", `
6539
+ Examples:
6540
+ $ a8techads admin lease flush --campaign cid-1
6541
+ $ a8techads admin lease flush --campaign cid-1 --region sin
6542
+ $ a8techads admin lease flush --campaign cid-1 --execute --yes`).action(async (opts) => {
6543
+ const dryRun = !opts.execute;
6544
+ if (!dryRun)
6545
+ await confirmAction(`Flush leases for ${opts.campaign} in ${opts.region}?`, opts.yes);
6546
+ const resp = await apiRequest({
6547
+ method: "POST",
6548
+ path: "/internal/pacer/ops/flush-campaign",
6549
+ body: { campaign_id: opts.campaign, region: opts.region, dry_run: dryRun }
6550
+ });
6551
+ const json = await resp.json();
6552
+ if (!resp.ok) {
6553
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6554
+ process.exit(1);
6555
+ }
6556
+ console.log(dryRun ? "[DRY RUN]" : "[APPLIED]");
6557
+ printDetail(json);
6558
+ });
6559
+ lease.command("flush-all").description("Flush all chunks for a region (bounded by active-set discovery).").requiredOption("--region <region>", 'Region to scope ("all" prohibited)').option("--shard-count <n>", "Pacer shard count (default 8)", "8").option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).action(async (opts) => {
6560
+ if (opts.region === "all") {
6561
+ console.error("--region all is prohibited for flush-all; use flush per campaign");
6562
+ process.exit(1);
6563
+ }
6564
+ const dryRun = !opts.execute;
6565
+ if (!dryRun)
6566
+ await confirmAction(`Flush ALL leases in region ${opts.region}?`, opts.yes);
6567
+ const resp = await apiRequest({
6568
+ method: "POST",
6569
+ path: "/internal/pacer/ops/flush-region",
6570
+ body: { region: opts.region, shard_count: parseInt(opts.shardCount, 10), dry_run: dryRun }
6571
+ });
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(dryRun ? "[DRY RUN]" : "[APPLIED]");
6578
+ printDetail(json);
6579
+ });
6580
+ lease.command("reset-daily").description("Reset the pacer-side daily_spent projection.").option("--shard-count <n>", "Pacer shard count (default 8)", "8").option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).action(async (opts) => {
6581
+ const dryRun = !opts.execute;
6582
+ if (!dryRun)
6583
+ await confirmAction("Reset daily_spent projection across all active campaigns?", opts.yes);
6584
+ const resp = await apiRequest({
6585
+ method: "POST",
6586
+ path: "/internal/pacer/ops/reset-daily",
6587
+ body: { shard_count: parseInt(opts.shardCount, 10), dry_run: dryRun }
6588
+ });
6589
+ const json = await resp.json();
6590
+ if (!resp.ok) {
6591
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6592
+ process.exit(1);
6593
+ }
6594
+ console.log(dryRun ? "[DRY RUN]" : "[APPLIED]");
6595
+ printDetail(json);
6596
+ });
6597
+ return cmd;
6598
+ }
6599
+
6600
+ // src/commands/external-ssp.ts
6601
+ var COLUMNS8 = [
6602
+ { key: "id", header: "ID", width: 36 },
6603
+ { key: "name", header: "NAME", width: 25 },
6604
+ { key: "code", header: "CODE", width: 15 },
6605
+ { key: "status", header: "STATUS", width: 12 },
6606
+ { key: "routingMode", header: "ROUTING", width: 12 },
6607
+ { key: "allowedRegions", header: "ALLOWED", width: 18, format: (v) => Array.isArray(v) ? v.join(", ") : "-" },
6608
+ { key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
6609
+ ];
6610
+ function parseCsvList2(value) {
6611
+ if (!value)
6612
+ return;
6613
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
6614
+ return items.length > 0 ? items : undefined;
6615
+ }
6616
+ function createExternalSspCommand() {
6617
+ const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
6618
+
6619
+ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
6620
+ Examples:
6621
+ $ a8techads external-ssp list
6622
+ $ a8techads external-ssp get <id>
6623
+ $ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
6624
+ $ a8techads external-ssp update <id> --adm-mode plain_url
6625
+ $ a8techads external-ssp activate <id>
6626
+ $ a8techads external-ssp pause <id>`);
6627
+ 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) => {
6628
+ const params = new URLSearchParams;
6629
+ if (opts.status)
6630
+ params.set("status", opts.status);
6631
+ params.set("limit", opts.limit);
6632
+ const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners?${params}` });
6633
+ const json = await resp.json();
6634
+ if (!resp.ok) {
6635
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6636
+ process.exit(1);
6637
+ }
6638
+ const rows = (json.data ?? json).map((p) => ({
6639
+ id: p.id,
6640
+ name: p.name,
6641
+ code: p.code,
6642
+ status: p.status,
6643
+ routingMode: p.routingMode ?? p.routing_mode ?? "global",
6644
+ allowedRegions: p.allowedRegions ?? p.allowed_regions ?? [],
6645
+ formats: p.supportedFormats ?? p.supported_formats ?? []
6646
+ }));
6647
+ printData(rows, COLUMNS8, opts.format);
6648
+ });
6649
+ addFormatOption(cmd.command("get").description("Get external SSP partner details (includes API key).").argument("<id>", "Partner ID")).action(async (id, opts) => {
6650
+ const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners/${id}` });
6651
+ const json = await resp.json();
6652
+ if (!resp.ok) {
6653
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6654
+ process.exit(1);
6655
+ }
6656
+ printDetail(json.data ?? json, opts.format);
6657
+ });
6658
+ 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)").option("--region <region>", "Fallback region (legacy compatibility field)").option("--routing-mode <mode>", "Routing mode (global or pinned)").option("--allowed-regions <regions>", "Allowed regions, comma-separated").option("--preferred-regions <regions>", "Preferred regions, comma-separated").option("--adm-mode <mode>", "ADM response mode (markup or plain_url)").addHelpText("after", `
6659
+ Examples:
6660
+ $ a8techads external-ssp create --name "OpenX"
6661
+ $ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO
6662
+ $ a8techads external-ssp create --name "PropellerAds" --code PROPELLERADS --formats BANNER,VIDEO,NATIVE,DIRECT_LINK --routing-mode global --allowed-regions sin,dfw --adm-mode plain_url`).action(async (opts) => {
6663
+ if (!opts.name) {
6664
+ console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
6665
+ process.exit(1);
6666
+ }
6667
+ const body = { name: opts.name };
6668
+ if (opts.code)
6669
+ body.code = opts.code;
6670
+ if (opts.formats)
6671
+ body.supportedFormats = parseCsvList2(opts.formats);
6672
+ if (opts.region)
6673
+ body.region = opts.region;
6674
+ if (opts.routingMode)
6675
+ body.routingMode = opts.routingMode;
6676
+ if (opts.allowedRegions)
6677
+ body.allowedRegions = parseCsvList2(opts.allowedRegions);
6678
+ if (opts.preferredRegions)
6679
+ body.preferredRegions = parseCsvList2(opts.preferredRegions);
6680
+ if (opts.admMode)
6681
+ body.responseTransform = { admMode: opts.admMode };
6682
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
6683
+ const json = await resp.json();
6684
+ if (!resp.ok) {
6685
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6686
+ process.exit(1);
6687
+ }
6688
+ const data = json.data ?? json;
6689
+ console.log(`Partner created: ${data.id}`);
6690
+ if (data.code)
6691
+ console.log(` Code: ${data.code}`);
6692
+ if (data.apiKey)
6693
+ console.log(` API Key: ${data.apiKey}`);
6694
+ if (data.apiSecret)
6695
+ console.log(` API Secret: ${data.apiSecret}`);
6696
+ });
6697
+ 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").option("--region <region>", "Fallback region (legacy compatibility field)").option("--routing-mode <mode>", "Routing mode (global or pinned)").option("--allowed-regions <regions>", "Allowed regions, comma-separated").option("--preferred-regions <regions>", "Preferred regions, comma-separated").option("--adm-mode <mode>", "ADM response mode (markup or plain_url)").action(async (id, opts) => {
6698
+ const body = {};
6699
+ if (opts.name)
6700
+ body.name = opts.name;
6701
+ if (opts.formats)
6702
+ body.supportedFormats = parseCsvList2(opts.formats);
6703
+ if (opts.region)
6704
+ body.region = opts.region;
6705
+ if (opts.routingMode)
6706
+ body.routingMode = opts.routingMode;
6707
+ if (opts.allowedRegions)
6708
+ body.allowedRegions = parseCsvList2(opts.allowedRegions);
6709
+ if (opts.preferredRegions)
6710
+ body.preferredRegions = parseCsvList2(opts.preferredRegions);
6711
+ if (opts.admMode)
6712
+ body.responseTransform = { admMode: opts.admMode };
6713
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
6714
+ if (!resp.ok) {
6715
+ const j = await resp.json();
6716
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6717
+ process.exit(1);
6718
+ }
6719
+ console.log(`Partner ${id} updated.`);
6720
+ });
6721
+ cmd.command("regenerate-key").description("Regenerate API credentials for an external SSP partner.").argument("<id>", "Partner ID").action(async (id) => {
6722
+ const resp = await apiRequest({
6723
+ method: "POST",
6724
+ path: `${consolePrefix()}/ssp-partners/${id}/regenerate-key`
6725
+ });
6726
+ const json = await resp.json();
6727
+ if (!resp.ok) {
6728
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6729
+ process.exit(1);
6730
+ }
6731
+ const data = json.data ?? json;
6732
+ const credentials = data.credentials ?? data;
6733
+ console.log(`Partner ${id} credentials regenerated.`);
6734
+ if (credentials.apiKey)
6735
+ console.log(` API Key: ${credentials.apiKey}`);
6736
+ if (credentials.apiSecret)
6737
+ console.log(` API Secret: ${credentials.apiSecret}`);
6738
+ });
6739
+ cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
6740
+ if (!opts.yes) {
6741
+ console.error("Add --yes to confirm deletion.");
6742
+ process.exit(1);
6743
+ }
6744
+ const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/ssp-partners/${id}` });
6745
+ if (!resp.ok && resp.status !== 204) {
6746
+ console.error(`Error: ${resp.statusText}`);
6747
+ process.exit(1);
6748
+ }
6749
+ console.log(`Partner ${id} deleted.`);
6750
+ });
6751
+ cmd.command("activate").description("Activate an external SSP partner (TESTING/SUSPENDED → ACTIVE).").argument("<id>", "Partner ID").action(async (id) => {
6752
+ const resp = await apiRequest({
6753
+ method: "PATCH",
6754
+ path: `${consolePrefix()}/ssp-partners/${id}/status`,
6755
+ body: { status: "ACTIVE" }
6756
+ });
6757
+ if (!resp.ok) {
6758
+ const j = await resp.json();
6759
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6760
+ process.exit(1);
6761
+ }
6762
+ console.log(`Partner ${id} activated.`);
6763
+ });
6764
+ cmd.command("pause").description("Pause an external SSP partner (ACTIVE → SUSPENDED).").argument("<id>", "Partner ID").action(async (id) => {
6765
+ const resp = await apiRequest({
6766
+ method: "PATCH",
6767
+ path: `${consolePrefix()}/ssp-partners/${id}/status`,
6768
+ body: { status: "SUSPENDED" }
6769
+ });
6770
+ if (!resp.ok) {
6771
+ const j = await resp.json();
6772
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6773
+ process.exit(1);
6774
+ }
6775
+ console.log(`Partner ${id} paused.`);
6776
+ });
6777
+ return cmd;
6778
+ }
6779
+
6780
+ // src/commands/settings.ts
6781
+ function settingsPrefix() {
6782
+ const ctx = loadContext();
6783
+ const context = getCurrentContext(ctx);
6784
+ const app = context?.app ?? "dsp";
6785
+ if (app === "console") {
6786
+ console.error("Error: Settings commands are not available in Console mode.");
6787
+ console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
6788
+ process.exit(1);
6789
+ }
6790
+ return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
6791
+ }
6792
+ function createSettingsCommand() {
6793
+ const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
6794
+
6795
+ Requires: ADVERTISER or PUBLISHER capability.
6796
+ Not available in Console mode.`).addHelpText("after", `
6797
+ Examples:
6798
+ $ a8techads settings show
6799
+ $ a8techads settings update --from-json settings.json
6800
+ $ a8techads settings profile
6801
+ $ a8techads settings profile-update --company-name "New Name"`);
6802
+ addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
6803
+ const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
6804
+ const json = await resp.json();
6805
+ if (!resp.ok) {
6806
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6807
+ process.exit(1);
6808
+ }
6809
+ printDetail(json.data ?? json, opts.format);
6810
+ });
6811
+ addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
6812
+ const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
6813
+ const json = await resp.json();
6814
+ if (!resp.ok) {
6815
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6816
+ process.exit(1);
6817
+ }
6818
+ printDetail(json.data ?? json, opts.format);
6819
+ });
6820
+ cmd.command("update").description(`Update tenant settings.
6821
+
6822
+ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
6823
+ if (!opts.fromJson) {
6824
+ console.error("Error: --from-json is required for settings update.");
6825
+ process.exit(1);
6826
+ }
6827
+ const { readFileSync: readFileSync5 } = await import("fs");
6828
+ const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6829
+ const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
6830
+ if (!resp.ok) {
6831
+ const j = await resp.json();
6832
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6833
+ process.exit(1);
6834
+ }
6835
+ console.log("Settings updated.");
6836
+ });
6837
+ 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) => {
6838
+ let body;
6839
+ if (opts.fromJson) {
6840
+ const { readFileSync: readFileSync5 } = await import("fs");
6841
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6842
+ } else {
6843
+ body = {};
6844
+ if (opts.companyName)
6845
+ body.companyName = opts.companyName;
6846
+ }
6847
+ const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
6848
+ if (!resp.ok) {
6849
+ const j = await resp.json();
6850
+ console.error(`Error: ${j.error ?? resp.statusText}`);
6851
+ process.exit(1);
6852
+ }
6853
+ console.log("Profile updated.");
6854
+ });
6855
+ return cmd;
6856
+ }
6857
+
6858
+ // src/commands/invoices.ts
6859
+ function createInvoicesCommand() {
6860
+ const cmd = new Command("invoices").description(`Invoice management (DSP)
6861
+
6862
+ Requires: ADVERTISER capability.`).addHelpText("after", `
6863
+ Examples:
6864
+ $ a8techads invoices list
6865
+ $ a8techads invoices get <id>
6866
+ $ a8techads invoices spending`);
6867
+ addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
6868
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
6869
+ const json = await resp.json();
6870
+ if (!resp.ok) {
6871
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6872
+ process.exit(1);
6873
+ }
6874
+ const columns = [
6875
+ { key: "id", header: "ID", width: 36 },
6876
+ { key: "period", header: "PERIOD", width: 20 },
6877
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
6878
+ { key: "status", header: "STATUS", width: 12 }
6879
+ ];
6880
+ const rows = (json.data ?? json).map((i) => ({
6881
+ id: i.id,
6882
+ period: i.period ?? i.billingPeriod,
6883
+ amount: i.amount ?? i.totalAmount,
6884
+ status: i.status
6885
+ }));
6886
+ printData(rows, columns, opts.format);
6887
+ });
6888
+ addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
6889
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
6890
+ const json = await resp.json();
6891
+ if (!resp.ok) {
6892
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6893
+ process.exit(1);
6894
+ }
6895
+ printDetail(json.data ?? json, opts.format);
6896
+ });
6897
+ addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
6898
+ const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
6899
+ const json = await resp.json();
6900
+ if (!resp.ok) {
6901
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6902
+ process.exit(1);
6903
+ }
6904
+ printDetail(json.data ?? json, opts.format);
6905
+ });
6906
+ return cmd;
6907
+ }
6908
+
6909
+ // src/commands/conversion-goals.ts
6910
+ var COLUMNS9 = [
6911
+ { key: "id", header: "ID", width: 36 },
6912
+ { key: "name", header: "NAME", width: 24 },
6913
+ { key: "goalOrder", header: "G#", width: 4 },
6914
+ { key: "conversionType", header: "TYPE", width: 20 },
6915
+ { key: "valueType", header: "VALUE", width: 10 },
6916
+ { key: "status", header: "STATUS", width: 10 }
6917
+ ];
6918
+ function createConversionGoalsCommand() {
6919
+ const cmd = new Command("conversion-goals").description(`Conversion goal management (DSP)
6920
+
6921
+ Requires: ADVERTISER capability.`).addHelpText("after", `
6922
+ Examples:
6923
+ $ a8techads conversion-goals list
6924
+ $ a8techads conversion-goals get <id>
6925
+ $ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
6926
+ $ a8techads conversion-goals pause <id>`);
6927
+ addFormatOption(cmd.command("list").description("List conversion goals.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
6928
+ const params = new URLSearchParams;
6929
+ if (opts.status)
6930
+ params.set("status", opts.status);
6931
+ params.set("limit", opts.limit);
6932
+ const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals?${params}` });
6933
+ const json = await resp.json();
6934
+ if (!resp.ok) {
6935
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6936
+ process.exit(1);
6937
+ }
6938
+ const rows = (json.data ?? json).map((g) => ({
6939
+ id: g.id,
6940
+ name: g.name,
6941
+ goalOrder: `G${g.goalOrder ?? g.goal_order}`,
6942
+ conversionType: g.conversionType ?? g.conversion_type,
6943
+ valueType: g.valueType ?? g.value_type,
6944
+ status: g.status
6945
+ }));
6946
+ printData(rows, COLUMNS9, opts.format);
6947
+ });
6948
+ addFormatOption(cmd.command("get").description("Get conversion goal details.").argument("<id>", "Goal ID")).action(async (id, opts) => {
6949
+ const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
6950
+ const json = await resp.json();
6951
+ if (!resp.ok) {
6952
+ console.error(`Error: ${json.error ?? resp.statusText}`);
6953
+ process.exit(1);
6954
+ }
6955
+ printDetail(json.data ?? json, opts.format);
6956
+ });
6957
+ cmd.command("create").description(`Create a conversion goal.
6958
+
6959
+ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6960
+ PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
6961
+ let body;
6962
+ if (opts.fromJson) {
6963
+ const { readFileSync: readFileSync5 } = await import("fs");
6964
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6965
+ } else {
6966
+ if (!opts.name) {
6967
+ console.error("Error: --name is required");
6968
+ process.exit(1);
6969
+ }
6970
+ if (!opts.conversionType) {
6971
+ console.error("Error: --conversion-type is required");
6972
+ process.exit(1);
6973
+ }
6974
+ if (!opts.goalOrder) {
6975
+ console.error("Error: --goal-order is required (1-10)");
6976
+ process.exit(1);
6977
+ }
6978
+ body = {
6979
+ name: opts.name,
6980
+ conversionType: opts.conversionType,
6981
+ goalOrder: Number(opts.goalOrder),
6982
+ valueType: opts.valueType,
6983
+ countType: opts.countType,
6984
+ conversionWindowHours: Number(opts.window)
6985
+ };
6986
+ if (opts.fixedValue)
6987
+ body.fixedValue = Number(opts.fixedValue);
6988
+ if (opts.description)
6989
+ body.description = opts.description;
6990
+ }
6991
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals`, body });
6992
+ const json = await resp.json();
6993
+ if (!resp.ok) {
6994
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6995
+ process.exit(1);
6996
+ }
6997
+ const goal = json.data ?? json;
6998
+ console.log(`Conversion goal created: ${goal.id}`);
6999
+ console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
7000
+ console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
7001
+ });
7002
+ cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7003
+ let body;
7004
+ if (opts.fromJson) {
7005
+ const { readFileSync: readFileSync5 } = await import("fs");
7006
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
7007
+ } else {
7008
+ body = {};
7009
+ if (opts.name)
7010
+ body.name = opts.name;
7011
+ if (opts.description)
7012
+ body.description = opts.description;
7013
+ if (opts.valueType)
7014
+ body.valueType = opts.valueType;
7015
+ if (opts.fixedValue)
7016
+ body.fixedValue = Number(opts.fixedValue);
7017
+ if (opts.countType)
7018
+ body.countType = opts.countType;
7019
+ if (opts.window)
7020
+ body.conversionWindowHours = Number(opts.window);
7021
+ }
7022
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}`, body });
7023
+ if (!resp.ok) {
7024
+ const j = await resp.json();
7025
+ console.error(`Error: ${j.error ?? resp.statusText}`);
7026
+ process.exit(1);
7027
+ }
7028
+ console.log(`Conversion goal ${id} updated.`);
7029
+ });
7030
+ cmd.command("delete").description("Delete a conversion goal.").argument("<id>", "Goal ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
7031
+ if (!opts.yes) {
7032
+ console.error("Add --yes to confirm deletion.");
7033
+ process.exit(1);
7034
+ }
7035
+ const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/conversion-goals/${id}` });
7036
+ if (!resp.ok && resp.status !== 204) {
7037
+ console.error(`Error: ${resp.statusText}`);
7038
+ process.exit(1);
7039
+ }
7040
+ console.log(`Conversion goal ${id} deleted.`);
7041
+ });
7042
+ for (const action of ["pause", "activate", "archive"]) {
7043
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a conversion goal.`).argument("<id>", "Goal ID").action(async (id) => {
7044
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}/${action}` });
7045
+ if (!resp.ok) {
7046
+ const j = await resp.json();
7047
+ console.error(`Error: ${j.error ?? resp.statusText}`);
7048
+ process.exit(1);
7049
+ }
7050
+ console.log(`Conversion goal ${id} ${action}d.`);
7051
+ });
7052
+ }
7053
+ cmd.command("regenerate-secret").description("Regenerate secret key and postback URL.").argument("<id>", "Goal ID").action(async (id) => {
7054
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals/${id}/regenerate-secret` });
7055
+ const json = await resp.json();
7056
+ if (!resp.ok) {
7057
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7058
+ process.exit(1);
7059
+ }
7060
+ const goal = json.data ?? json;
7061
+ console.log(`Secret regenerated for goal ${id}`);
7062
+ console.log(` New postback URL: ${goal.postbackUrl ?? goal.postback_url}`);
7063
+ });
7064
+ return cmd;
7065
+ }
7066
+
7067
+ // src/commands/algorithms.ts
7068
+ var COLUMNS10 = [
7069
+ { key: "id", header: "ID", width: 36 },
7070
+ { key: "name", header: "NAME", width: 28 },
7071
+ { key: "goal", header: "GOAL", width: 8 },
7072
+ { key: "conversionGoalId", header: "CONV_GOAL", width: 36 },
7073
+ { key: "status", header: "STATUS", width: 10 },
7074
+ { key: "campaignCount", header: "CAMPAIGNS", width: 10 }
7075
+ ];
7076
+ function createAlgorithmsCommand() {
7077
+ const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
7078
+
7079
+ Requires: ADVERTISER capability.`).addHelpText("after", `
7080
+ Examples:
7081
+ $ a8techads algorithms list
7082
+ $ a8techads algorithms get <id>
7083
+ $ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
7084
+ $ a8techads algorithms update <id> --conversion-goal-id <goal-id>
7085
+ $ a8techads algorithms pause <id>`);
7086
+ addFormatOption(cmd.command("list").description("List bidder algorithms.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
7087
+ const params = new URLSearchParams;
7088
+ if (opts.status)
7089
+ params.set("status", opts.status);
7090
+ params.set("limit", opts.limit);
7091
+ const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
7092
+ const json = await resp.json();
7093
+ if (!resp.ok) {
7094
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7095
+ process.exit(1);
7096
+ }
7097
+ const rows = (json.data ?? json).map((algo) => ({
7098
+ id: algo.id,
7099
+ name: algo.name,
7100
+ goal: algo.optimizationGoal,
7101
+ conversionGoalId: algo.conversionGoalId,
7102
+ status: algo.status,
7103
+ campaignCount: algo.campaignCount ?? 0
7104
+ }));
7105
+ printData(rows, COLUMNS10, opts.format);
7106
+ });
7107
+ addFormatOption(cmd.command("get").description("Get bidder algorithm details.").argument("<id>", "Algorithm ID")).action(async (id, opts) => {
7108
+ const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
7109
+ const json = await resp.json();
7110
+ if (!resp.ok) {
7111
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7112
+ process.exit(1);
7113
+ }
7114
+ printDetail(json.data ?? json, opts.format);
7115
+ });
7116
+ cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
7117
+ const body = await buildAlgorithmBody(opts, true);
7118
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
7119
+ const json = await resp.json();
7120
+ if (!resp.ok) {
7121
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7122
+ process.exit(1);
7123
+ }
7124
+ console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
7125
+ });
7126
+ cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7127
+ const body = await buildAlgorithmBody(opts, false);
7128
+ const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
7129
+ const json = await resp.json().catch(() => ({}));
7130
+ if (!resp.ok) {
7131
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7132
+ process.exit(1);
7133
+ }
7134
+ console.log(`Algorithm ${id} updated.`);
7135
+ });
7136
+ 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) => {
7137
+ if (!opts.yes) {
7138
+ console.error("Add --yes to confirm archiving.");
7139
+ process.exit(1);
7140
+ }
7141
+ const resp = await apiRequest({
7142
+ method: "PATCH",
7143
+ path: `${dspPrefix()}/bidder-algorithms/${id}`,
7144
+ body: { status: "ARCHIVED" }
7145
+ });
7146
+ const json = await resp.json().catch(() => ({}));
7147
+ if (!resp.ok) {
7148
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7149
+ process.exit(1);
7150
+ }
7151
+ console.log(`Algorithm ${id} archived.`);
7152
+ });
7153
+ for (const action of ["pause", "activate"]) {
7154
+ cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a bidder algorithm.`).argument("<id>", "Algorithm ID").action(async (id) => {
7155
+ const resp = await apiRequest({
7156
+ method: "PATCH",
7157
+ path: `${dspPrefix()}/bidder-algorithms/${id}/${action}`
7158
+ });
7159
+ const json = await resp.json().catch(() => ({}));
7160
+ if (!resp.ok) {
7161
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7162
+ process.exit(1);
7163
+ }
7164
+ console.log(`Algorithm ${id} ${action}d.`);
7165
+ });
7166
+ }
7167
+ return cmd;
7168
+ }
7169
+ async function buildAlgorithmBody(opts, creating) {
7170
+ if (opts.fromJson) {
7171
+ const { readFileSync: readFileSync5 } = await import("fs");
7172
+ return JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
7173
+ }
7174
+ const body = {};
7175
+ if (opts.name)
7176
+ body.name = opts.name;
7177
+ if (opts.description)
7178
+ body.description = opts.description;
7179
+ if (opts.optimizationGoal)
7180
+ body.optimizationGoal = String(opts.optimizationGoal).toUpperCase();
7181
+ if (opts.targetValue !== undefined)
7182
+ body.targetValue = Number(opts.targetValue);
7183
+ if (opts.conversionGoalId)
7184
+ body.conversionGoalId = opts.conversionGoalId;
7185
+ if (creating) {
7186
+ if (!body.name) {
7187
+ console.error("Error: --name is required.");
7188
+ process.exit(1);
7189
+ }
7190
+ if (!body.optimizationGoal) {
7191
+ console.error("Error: --optimization-goal is required.");
7192
+ process.exit(1);
7193
+ }
7194
+ if (body.targetValue === undefined) {
7195
+ console.error("Error: --target-value is required.");
7196
+ process.exit(1);
7197
+ }
7198
+ }
7199
+ return body;
7200
+ }
7201
+
7202
+ // src/commands/payouts.ts
7203
+ function createPayoutsCommand() {
7204
+ const cmd = new Command("payouts").description(`Payout management (SSP)
7205
+
7206
+ Requires: PUBLISHER capability.`).addHelpText("after", `
7207
+ Examples:
7208
+ $ a8techads payouts balance
7209
+ $ a8techads payouts account
7210
+ $ a8techads payouts list --limit 10
7211
+ $ a8techads payouts request --amount 50 --method usdt --yes`);
7212
+ addFormatOption(cmd.command("balance").description("Show current payout balance.")).action(async (opts) => {
7213
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/balance` });
7214
+ const json = await resp.json();
7215
+ if (!resp.ok) {
7216
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7217
+ process.exit(1);
7218
+ }
7219
+ printDetail(json.data ?? json, opts.format);
7220
+ });
7221
+ addFormatOption(cmd.command("account").description("Show payout account details.")).action(async (opts) => {
7222
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/account` });
7223
+ const json = await resp.json();
7224
+ if (!resp.ok) {
7225
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7226
+ process.exit(1);
7227
+ }
7228
+ printDetail(json.data ?? json, opts.format);
7229
+ });
7230
+ addFormatOption(cmd.command("list").description("List payout history.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
7231
+ const params = new URLSearchParams({ limit: opts.limit });
7232
+ if (opts.status)
7233
+ params.set("status", opts.status);
7234
+ const resp = await apiRequest({ path: `${sspPrefix()}/payouts/list?${params}` });
7235
+ const json = await resp.json();
7236
+ if (!resp.ok) {
7237
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7238
+ process.exit(1);
7239
+ }
7240
+ const columns = [
7241
+ { key: "id", header: "ID", width: 36 },
7242
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
7243
+ { key: "status", header: "STATUS", width: 14 },
7244
+ { key: "method", header: "METHOD", width: 10 },
7245
+ { key: "requestedAt", header: "REQUESTED_AT", width: 20 }
7246
+ ];
7247
+ const rows = (json.data ?? json).map((t) => ({
7248
+ id: t.id,
7249
+ amount: t.amount,
7250
+ status: t.status,
7251
+ method: t.method,
7252
+ requestedAt: t.requestedAt ?? t.requested_at
7253
+ }));
7254
+ printData(rows, columns, opts.format);
7255
+ });
7256
+ 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) => {
7257
+ if (!opts.amount) {
7258
+ console.error("Error: --amount is required.");
7259
+ process.exit(1);
7260
+ }
7261
+ const amount = Number(opts.amount);
7262
+ if (isNaN(amount) || amount <= 0) {
7263
+ console.error("Error: Amount must be a positive number.");
7264
+ process.exit(1);
7265
+ }
7266
+ if (!opts.yes) {
7267
+ const rl = await import("readline");
7268
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
7269
+ const answer = await new Promise((resolve) => iface.question(`Request payout of $${amount.toFixed(2)}? (y/N) `, resolve));
7270
+ iface.close();
7271
+ if (answer.toLowerCase() !== "y") {
7272
+ console.log("Cancelled.");
7273
+ process.exit(0);
7274
+ }
7275
+ }
7276
+ let resp;
7277
+ if (opts.method === "usdt") {
7278
+ const body = { amount, walletAddress: opts.wallet, network: opts.network };
7279
+ resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payments/usdt/payout`, body });
7280
+ } else {
7281
+ resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payouts/request`, body: { amount } });
7282
+ }
7283
+ const json = await resp.json();
7284
+ if (!resp.ok) {
7285
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7286
+ process.exit(1);
7287
+ }
7288
+ console.log(`Payout of $${amount.toFixed(2)} requested successfully.`);
7289
+ });
7290
+ return cmd;
7291
+ }
7292
+
7293
+ // src/commands/statements.ts
7294
+ function createStatementsCommand() {
7295
+ const cmd = new Command("statements").description(`Statement management (SSP)
7296
+
7297
+ Requires: PUBLISHER capability.`).addHelpText("after", `
7298
+ Examples:
7299
+ $ a8techads statements list
7300
+ $ a8techads statements get <id>`);
7301
+ addFormatOption(cmd.command("list").description("List statements.").option("--limit <n>", "Max results", "20")).action(async (opts) => {
7302
+ const params = new URLSearchParams({ limit: opts.limit });
7303
+ const resp = await apiRequest({ path: `${sspPrefix()}/statements?${params}` });
7304
+ const json = await resp.json();
7305
+ if (!resp.ok) {
7306
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7307
+ process.exit(1);
7308
+ }
7309
+ const columns = [
7310
+ { key: "id", header: "ID", width: 36 },
7311
+ { key: "number", header: "NUMBER", width: 14 },
7312
+ { key: "period", header: "PERIOD", width: 16 },
7313
+ { key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
7314
+ { key: "status", header: "STATUS", width: 12 }
7315
+ ];
7316
+ const rows = (json.data ?? json).map((t) => ({
7317
+ id: t.id,
7318
+ number: t.number,
7319
+ period: t.period,
7320
+ amount: t.amount,
7321
+ status: t.status
7322
+ }));
7323
+ printData(rows, columns, opts.format);
7324
+ });
7325
+ addFormatOption(cmd.command("get <id>").description("Show statement details.")).action(async (id, opts) => {
7326
+ const resp = await apiRequest({ path: `${sspPrefix()}/statements/${id}` });
7327
+ const json = await resp.json();
7328
+ if (!resp.ok) {
7329
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7330
+ process.exit(1);
7331
+ }
7332
+ printDetail(json.data ?? json, opts.format);
7333
+ });
7334
+ return cmd;
7335
+ }
7336
+
7337
+ // src/commands/simulator.ts
7338
+ import { readFileSync as readFileSync5 } from "fs";
7339
+ async function confirmAction2(message, yes) {
7340
+ if (yes)
7341
+ return;
7342
+ const rl = await import("readline");
7343
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
7344
+ const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
7345
+ iface.close();
7346
+ if (answer.toLowerCase() !== "y") {
7347
+ console.log("Cancelled.");
7348
+ process.exit(0);
7349
+ }
7350
+ }
7351
+ function createSimulatorCommand() {
7352
+ const cmd = new Command("simulator").description(`Simulator control and inspection
7353
+
7354
+ Used for replay and traffic verification workflows.`).addHelpText("after", `
7355
+ Examples:
7356
+ $ a8techads simulator status
7357
+ $ a8techads simulator start
7358
+ $ a8techads simulator stop --yes
7359
+ $ a8techads simulator reset --yes
7360
+ $ a8techads simulator config show
7361
+ $ a8techads simulator config update --from-json ./simulator-config.json`);
7362
+ addFormatOption(cmd.command("status").description("Show simulator runtime status and counters.")).action(async (opts) => {
7363
+ const resp = await simulatorRequest("GET", "/api/v1/simulator/status");
7364
+ const json = await resp.json();
7365
+ if (!resp.ok) {
7366
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7367
+ process.exit(1);
7368
+ }
7369
+ printDetail(json, opts.format);
7370
+ });
7371
+ cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
7372
+ let body = undefined;
7373
+ if (opts.fromJson) {
7374
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
7375
+ }
7376
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
7377
+ const json = await resp.json();
7378
+ if (!resp.ok) {
7379
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7380
+ process.exit(1);
7381
+ }
7382
+ console.log(json.message ?? "Simulator started.");
7383
+ });
7384
+ cmd.command("stop").description("Stop the simulator.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
7385
+ await confirmAction2("Stop simulator?", opts.yes);
7386
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/stop");
7387
+ const json = await resp.json();
7388
+ if (!resp.ok) {
7389
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7390
+ process.exit(1);
7391
+ }
7392
+ console.log(json.message ?? "Simulator stopped.");
7393
+ });
7394
+ cmd.command("reset").description("Reset simulator counters and stats.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
7395
+ await confirmAction2("Reset simulator stats?", opts.yes);
7396
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/reset");
7397
+ const json = await resp.json();
7398
+ if (!resp.ok) {
7399
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7400
+ process.exit(1);
7401
+ }
7402
+ console.log(json.message ?? "Simulator stats reset.");
7403
+ });
7404
+ const config = cmd.command("config").description("Simulator config management");
7405
+ addFormatOption(config.command("show").description("Show current simulator config.")).action(async (opts) => {
7406
+ const resp = await simulatorRequest("GET", "/api/v1/simulator/config");
7407
+ const json = await resp.json();
7408
+ if (!resp.ok) {
7409
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7410
+ process.exit(1);
7411
+ }
7412
+ printDetail(json, opts.format);
7413
+ });
7414
+ config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
7415
+ const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
7416
+ const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
7417
+ const json = await resp.json();
7418
+ if (!resp.ok) {
7419
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7420
+ process.exit(1);
7421
+ }
7422
+ console.log("Simulator config updated.");
7423
+ if (json.config) {
7424
+ printDetail(json.config, "table");
7425
+ }
7426
+ });
7427
+ return cmd;
7428
+ }
7429
+ async function simulatorRequest(method, path, body) {
7430
+ const creds = loadCredentials();
7431
+ const profile = getCurrentProfile(creds);
7432
+ if (!profile) {
7433
+ throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
7434
+ }
7435
+ const baseUrl = toConsoleApiBase(profile.api_url);
7436
+ const request = await buildAuthenticatedRequest({ method, path, body, baseUrl });
7437
+ return fetch(request.url, request.init);
7438
+ }
7439
+ function toConsoleApiBase(apiUrl) {
7440
+ const url = new URL(apiUrl);
7441
+ if (url.hostname.startsWith("api.")) {
7442
+ url.hostname = url.hostname.replace(/^api\./, "console.");
7443
+ }
7444
+ return url.origin;
7445
+ }
7446
+
7447
+ // src/commands/supply-ops.ts
7448
+ function formatRegion(value) {
7449
+ if (Array.isArray(value))
7450
+ return value.join(",");
7451
+ if (value === null || value === undefined)
7452
+ return "-";
7453
+ return String(value);
7454
+ }
7455
+ function formatScalar2(value) {
7456
+ if (value === undefined)
7457
+ return null;
7458
+ if (Array.isArray(value))
7459
+ return value.length > 0 ? value.join(",") : null;
7460
+ if (value === null)
7461
+ return null;
7462
+ if (typeof value === "object")
7463
+ return JSON.stringify(value);
7464
+ return value;
7465
+ }
7466
+ function formatLeaseDetail(row) {
7467
+ return {
7468
+ sectionScope: "scope",
7469
+ id: row.id,
7470
+ leaseType: row.leaseType,
7471
+ scopeId: row.scopeId,
7472
+ scopeName: row.scopeName,
7473
+ supplyProductId: row.supplyProductId,
7474
+ supplyProductName: row.supplyProductName,
7475
+ supplyContractId: row.supplyContractId,
7476
+ supplyContractName: row.supplyContractName,
7477
+ region: formatScalar2(row.region),
7478
+ status: row.status,
7479
+ sectionContractTruth: "contract_truth",
7480
+ contractType: row.contractType,
7481
+ commitmentMetric: row.commitmentMetric,
7482
+ commitmentValue: row.commitmentValue,
7483
+ commitmentWindow: row.commitmentWindow,
7484
+ remainingCommitmentValue: row.remainingCommitmentValue,
7485
+ sectionOperatorIntent: "operator_intent",
7486
+ policyMode: row.policyMode,
7487
+ operatorPriority: row.operatorPriority,
7488
+ allowed: row.allowed,
7489
+ sectionRuntimeState: "runtime_state",
7490
+ effectivePriority: row.effectivePriority ?? row.priority,
7491
+ bidAggressivenessCap: row.bidAggressivenessCap,
7492
+ bidAggressivenessEnabled: row.bidAggressivenessEnabled,
7493
+ executionControlMetric: row.executionControlMetric,
7494
+ executionQuota: row.executionQuota,
7495
+ remainingExecutionCapacity: row.remainingExecutionCapacity,
7496
+ windowSeconds: row.windowSeconds,
7497
+ expiresAt: row.expiresAt,
7498
+ sectionMeta: "meta",
7499
+ createdAt: row.createdAt,
7500
+ updatedAt: row.updatedAt
7501
+ };
7502
+ }
7503
+ function printSectionedLeaseDetail(row) {
7504
+ const sections = [
7505
+ {
7506
+ title: "Scope",
7507
+ entries: {
7508
+ id: row.id,
7509
+ leaseType: row.leaseType,
7510
+ scopeId: row.scopeId,
7511
+ scopeName: row.scopeName,
7512
+ supplyProductId: row.supplyProductId,
7513
+ supplyProductName: row.supplyProductName,
7514
+ supplyContractId: row.supplyContractId,
7515
+ supplyContractName: row.supplyContractName,
7516
+ region: formatScalar2(row.region),
7517
+ status: row.status
7518
+ }
7519
+ },
7520
+ {
7521
+ title: "Contract Truth",
7522
+ entries: {
7523
+ contractType: row.contractType,
7524
+ commitmentMetric: row.commitmentMetric,
7525
+ commitmentValue: row.commitmentValue,
7526
+ commitmentWindow: row.commitmentWindow,
7527
+ remainingCommitmentValue: row.remainingCommitmentValue
7528
+ }
7529
+ },
7530
+ {
7531
+ title: "Operator Intent",
7532
+ entries: {
7533
+ policyMode: row.policyMode,
7534
+ operatorPriority: row.operatorPriority,
7535
+ allowed: row.allowed
7536
+ }
7537
+ },
7538
+ {
7539
+ title: "Runtime State",
7540
+ entries: {
7541
+ effectivePriority: row.effectivePriority ?? row.priority,
7542
+ bidAggressivenessCap: row.bidAggressivenessCap,
7543
+ bidAggressivenessEnabled: row.bidAggressivenessEnabled,
7544
+ executionControlMetric: row.executionControlMetric,
7545
+ executionQuota: row.executionQuota,
7546
+ remainingExecutionCapacity: row.remainingExecutionCapacity,
7547
+ windowSeconds: row.windowSeconds,
7548
+ expiresAt: row.expiresAt
7549
+ }
7550
+ },
7551
+ {
7552
+ title: "Meta",
7553
+ entries: {
7554
+ createdAt: row.createdAt,
7555
+ updatedAt: row.updatedAt
7556
+ }
7557
+ }
7558
+ ];
7559
+ for (const [index, section] of sections.entries()) {
7560
+ console.log(section.title);
7561
+ printDetail(section.entries, "table");
7562
+ if (index < sections.length - 1)
7563
+ console.log("");
7564
+ }
7565
+ }
7566
+ function formatContractDetail(row) {
7567
+ return {
7568
+ sectionScope: "scope",
7569
+ id: row.id,
7570
+ name: row.name,
7571
+ supplierTenantId: row.supplierTenantId,
7572
+ supplierName: row.supplierName,
7573
+ supplyProductId: row.supplyProductId,
7574
+ supplyProductName: row.supplyProductName,
7575
+ region: formatScalar2(row.region),
7576
+ status: row.status,
7577
+ sectionContractTruth: "contract_truth",
7578
+ contractType: row.contractType,
7579
+ windowType: row.windowType,
7580
+ commitmentMetric: row.commitmentMetric,
7581
+ commitmentValue: row.commitmentValue,
7582
+ commitmentWindow: row.commitmentWindow,
7583
+ matchRules: formatScalar2(row.matchRules),
7584
+ sectionRouting: "routing",
7585
+ priority: row.priority,
7586
+ qpsCap: row.qpsCap,
7587
+ targetFillRate: row.targetFillRate,
7588
+ currency: row.currency,
7589
+ startDate: row.startDate,
7590
+ endDate: row.endDate,
7591
+ sectionMeta: "meta",
7592
+ createdAt: row.createdAt,
7593
+ updatedAt: row.updatedAt
7594
+ };
7595
+ }
7596
+ function printSectionedContractDetail(row) {
7597
+ const sections = [
7598
+ {
7599
+ title: "Scope",
7600
+ entries: {
7601
+ id: row.id,
7602
+ name: row.name,
7603
+ supplierTenantId: row.supplierTenantId,
7604
+ supplierName: row.supplierName,
7605
+ supplyProductId: row.supplyProductId,
7606
+ supplyProductName: row.supplyProductName,
7607
+ region: formatScalar2(row.region),
7608
+ status: row.status
7609
+ }
7610
+ },
7611
+ {
7612
+ title: "Contract Truth",
7613
+ entries: {
7614
+ contractType: row.contractType,
7615
+ windowType: row.windowType,
7616
+ commitmentMetric: row.commitmentMetric,
7617
+ commitmentValue: row.commitmentValue,
7618
+ commitmentWindow: row.commitmentWindow,
7619
+ matchRules: formatScalar2(row.matchRules)
7620
+ }
7621
+ },
7622
+ {
7623
+ title: "Routing",
7624
+ entries: {
7625
+ priority: row.priority,
7626
+ qpsCap: row.qpsCap,
7627
+ targetFillRate: row.targetFillRate,
7628
+ currency: row.currency,
7629
+ startDate: row.startDate,
7630
+ endDate: row.endDate
7631
+ }
7632
+ },
7633
+ {
7634
+ title: "Meta",
7635
+ entries: {
7636
+ createdAt: row.createdAt,
7637
+ updatedAt: row.updatedAt
7638
+ }
7639
+ }
7640
+ ];
7641
+ for (const [index, section] of sections.entries()) {
7642
+ console.log(section.title);
7643
+ printDetail(section.entries, "table");
7644
+ if (index < sections.length - 1)
7645
+ console.log("");
7646
+ }
7647
+ }
7648
+ function formatProductDetail(row) {
7649
+ return {
7650
+ sectionScope: "scope",
7651
+ id: row.id,
7652
+ tenantId: row.tenantId,
7653
+ name: row.name,
7654
+ region: formatScalar2(row.region),
7655
+ status: row.status,
7656
+ sectionPackaging: "packaging",
7657
+ description: row.description,
7658
+ qualityTier: row.qualityTier,
7659
+ contractCount: row.contractCount,
7660
+ sectionMeta: "meta",
7661
+ createdAt: row.createdAt,
7662
+ updatedAt: row.updatedAt
7663
+ };
7664
+ }
7665
+ function printSectionedProductDetail(row) {
7666
+ const sections = [
7667
+ {
7668
+ title: "Scope",
7669
+ entries: {
7670
+ id: row.id,
7671
+ tenantId: row.tenantId,
7672
+ name: row.name,
7673
+ region: formatScalar2(row.region),
7674
+ status: row.status
7675
+ }
7676
+ },
7677
+ {
7678
+ title: "Packaging",
7679
+ entries: {
7680
+ description: row.description,
7681
+ qualityTier: row.qualityTier,
7682
+ contractCount: row.contractCount
7683
+ }
7684
+ },
7685
+ {
7686
+ title: "Meta",
7687
+ entries: {
7688
+ createdAt: row.createdAt,
7689
+ updatedAt: row.updatedAt
7690
+ }
7691
+ }
7692
+ ];
7693
+ for (const [index, section] of sections.entries()) {
7694
+ console.log(section.title);
7695
+ printDetail(section.entries, "table");
7696
+ if (index < sections.length - 1)
7697
+ console.log("");
7698
+ }
7699
+ }
7700
+ function printSectionedSummary(kind, summary) {
7701
+ if (kind === "products") {
7702
+ const sections2 = [
7703
+ {
7704
+ title: "Packaging Layer",
7705
+ entries: {
7706
+ total: summary.total,
7707
+ totalContracts: summary.totalContracts,
7708
+ regions: formatScalar2(summary.regions)
7709
+ }
7710
+ },
7711
+ {
7712
+ title: "Status",
7713
+ entries: {
7714
+ statuses: formatScalar2(summary.statuses)
7715
+ }
7716
+ }
7717
+ ];
7718
+ for (const [index, section] of sections2.entries()) {
7719
+ console.log(section.title);
7720
+ printDetail(section.entries, "table");
7721
+ if (index < sections2.length - 1)
7722
+ console.log("");
7723
+ }
7724
+ return;
7725
+ }
7726
+ if (kind === "contracts") {
7727
+ const sections2 = [
7728
+ {
7729
+ title: "Contract Truth",
7730
+ entries: {
7731
+ total: summary.total,
7732
+ supplierCount: summary.supplierCount,
7733
+ suppliers: formatScalar2(summary.suppliers),
7734
+ regions: formatScalar2(summary.regions)
7735
+ }
7736
+ },
7737
+ {
7738
+ title: "Status",
7739
+ entries: {
7740
+ statuses: formatScalar2(summary.statuses)
7741
+ }
7742
+ }
7743
+ ];
7744
+ for (const [index, section] of sections2.entries()) {
7745
+ console.log(section.title);
7746
+ printDetail(section.entries, "table");
7747
+ if (index < sections2.length - 1)
7748
+ console.log("");
7749
+ }
7750
+ return;
7751
+ }
7752
+ const sections = [
7753
+ {
7754
+ title: "Operator Intent",
7755
+ entries: {
7756
+ total: summary.total,
7757
+ averageOperatorPriority: summary.averageOperatorPriority,
7758
+ regions: formatScalar2(summary.regions)
7759
+ }
7760
+ },
7761
+ {
7762
+ title: "Runtime State",
7763
+ entries: {
7764
+ totalQuota: summary.totalQuota,
7765
+ totalRemaining: summary.totalRemaining,
7766
+ averageEffectivePriority: summary.averageEffectivePriority,
7767
+ utilizationRatio: summary.utilizationRatio
7768
+ }
7769
+ },
7770
+ {
7771
+ title: "Status",
7772
+ entries: {
7773
+ statuses: formatScalar2(summary.statuses)
7774
+ }
7775
+ }
7776
+ ];
7777
+ for (const [index, section] of sections.entries()) {
7778
+ console.log(section.title);
7779
+ printDetail(section.entries, "table");
7780
+ if (index < sections.length - 1)
7781
+ console.log("");
7782
+ }
7783
+ }
7784
+ var PRODUCT_FULL_COLUMNS = [
7785
+ { key: "id", header: "ID", width: 36 },
7786
+ { key: "name", header: "NAME", width: 28 },
7787
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7788
+ { key: "qualityTier", header: "QUALITY", width: 10 },
7789
+ { key: "status", header: "STATUS", width: 16 },
7790
+ { key: "contractCount", header: "CONTRACTS", width: 10 }
7791
+ ];
7792
+ var PRODUCT_PACKAGING_COLUMNS = [
7793
+ { key: "id", header: "ID", width: 36 },
7794
+ { key: "name", header: "NAME", width: 28 },
7795
+ { key: "qualityTier", header: "QUALITY", width: 10 },
7796
+ { key: "status", header: "STATUS", width: 16 },
7797
+ { key: "contractCount", header: "CONTRACTS", width: 10 }
7798
+ ];
7799
+ var PRODUCT_COVERAGE_COLUMNS = [
7800
+ { key: "id", header: "ID", width: 36 },
7801
+ { key: "name", header: "NAME", width: 28 },
7802
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7803
+ { key: "status", header: "STATUS", width: 16 },
7804
+ { key: "contractCount", header: "CONTRACTS", width: 10 }
7805
+ ];
7806
+ function productColumnsForView(view) {
7807
+ switch ((view ?? "packaging").toLowerCase()) {
7808
+ case "coverage":
7809
+ return PRODUCT_COVERAGE_COLUMNS;
7810
+ case "full":
7811
+ return PRODUCT_FULL_COLUMNS;
7812
+ case "packaging":
7813
+ default:
7814
+ return PRODUCT_PACKAGING_COLUMNS;
7815
+ }
7816
+ }
7817
+ var CONTRACT_FULL_COLUMNS = [
7818
+ { key: "id", header: "ID", width: 36 },
7819
+ { key: "name", header: "NAME", width: 28 },
7820
+ { key: "supplierName", header: "SUPPLIER", width: 22 },
7821
+ { key: "supplyProductName", header: "TRAFFIC PACKAGE", width: 28 },
7822
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7823
+ { key: "contractType", header: "TYPE", width: 14 },
7824
+ { key: "commitmentMetric", header: "METRIC", width: 12 },
7825
+ { key: "commitmentValue", header: "COMMITMENT", width: 14 },
7826
+ { key: "priority", header: "ROUTING PRI", width: 12 },
7827
+ { key: "status", header: "STATUS", width: 12 }
7828
+ ];
7829
+ var CONTRACT_TRUTH_COLUMNS = [
7830
+ { key: "id", header: "ID", width: 36 },
7831
+ { key: "name", header: "NAME", width: 28 },
7832
+ { key: "supplierName", header: "SUPPLIER", width: 22 },
7833
+ { key: "supplyProductName", header: "TRAFFIC PACKAGE", width: 28 },
7834
+ { key: "contractType", header: "TYPE", width: 14 },
7835
+ { key: "commitmentMetric", header: "METRIC", width: 12 },
7836
+ { key: "commitmentValue", header: "COMMITMENT", width: 14 },
7837
+ { key: "commitmentWindow", header: "WINDOW", width: 12 },
7838
+ { key: "status", header: "STATUS", width: 12 }
7839
+ ];
7840
+ var CONTRACT_ROUTING_COLUMNS = [
7841
+ { key: "id", header: "ID", width: 36 },
7842
+ { key: "name", header: "NAME", width: 28 },
7843
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7844
+ { key: "contractType", header: "TYPE", width: 14 },
7845
+ { key: "priority", header: "ROUTING PRI", width: 12 },
7846
+ { key: "qpsCap", header: "QPS CAP", width: 10 },
7847
+ { key: "targetFillRate", header: "FILL TARGET", width: 12 },
7848
+ { key: "status", header: "STATUS", width: 12 }
7849
+ ];
7850
+ function contractColumnsForView(view) {
7851
+ switch ((view ?? "truth").toLowerCase()) {
7852
+ case "routing":
7853
+ return CONTRACT_ROUTING_COLUMNS;
7854
+ case "full":
7855
+ return CONTRACT_FULL_COLUMNS;
7856
+ case "truth":
7857
+ default:
7858
+ return CONTRACT_TRUTH_COLUMNS;
7859
+ }
7860
+ }
7861
+ var LEASE_FULL_COLUMNS = [
7862
+ { key: "id", header: "ID", width: 36 },
7863
+ { key: "scopeName", header: "SCOPE", width: 28 },
7864
+ { key: "leaseType", header: "LEASE TYPE", width: 14 },
7865
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7866
+ { key: "policyMode", header: "POLICY MODE", width: 16 },
7867
+ { key: "operatorPriority", header: "OPERATOR", width: 10 },
7868
+ { key: "effectivePriority", header: "EFFECTIVE", width: 10 },
7869
+ { key: "status", header: "STATUS", width: 16 },
7870
+ { key: "executionQuota", header: "RUNTIME QUOTA", width: 14 },
7871
+ { key: "remainingExecutionCapacity", header: "RUNTIME REM", width: 12 }
7872
+ ];
7873
+ var LEASE_RUNTIME_COLUMNS = [
7874
+ { key: "id", header: "ID", width: 36 },
7875
+ { key: "scopeName", header: "SCOPE", width: 28 },
7876
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7877
+ { key: "status", header: "STATUS", width: 16 },
7878
+ { key: "effectivePriority", header: "EFFECTIVE", width: 10 },
7879
+ { key: "executionControlMetric", header: "METRIC", width: 12 },
7880
+ { key: "executionQuota", header: "RUNTIME QUOTA", width: 14 },
7881
+ { key: "remainingExecutionCapacity", header: "RUNTIME REM", width: 12 }
7882
+ ];
7883
+ var LEASE_OPERATOR_COLUMNS = [
7884
+ { key: "id", header: "ID", width: 36 },
7885
+ { key: "scopeName", header: "SCOPE", width: 28 },
7886
+ { key: "leaseType", header: "LEASE TYPE", width: 14 },
7887
+ { key: "region", header: "REGION", width: 14, format: formatRegion },
7888
+ { key: "status", header: "STATUS", width: 16 },
7889
+ { key: "policyMode", header: "POLICY MODE", width: 16 },
7890
+ { key: "operatorPriority", header: "OPERATOR", width: 10 },
7891
+ { key: "allowed", header: "ALLOWED", width: 8 }
7892
+ ];
7893
+ function leaseColumnsForView(view) {
7894
+ switch ((view ?? "runtime").toLowerCase()) {
7895
+ case "operator":
7896
+ return LEASE_OPERATOR_COLUMNS;
7897
+ case "full":
7898
+ return LEASE_FULL_COLUMNS;
7899
+ case "runtime":
7900
+ default:
7901
+ return LEASE_RUNTIME_COLUMNS;
7902
+ }
7903
+ }
7904
+ async function listResource(path, opts, columns) {
7905
+ const rows = await fetchResource(path, opts);
7906
+ printData(rows, columns, opts.format);
7907
+ }
7908
+ async function fetchResource(path, opts) {
7909
+ const params = new URLSearchParams;
7910
+ params.set("limit", opts.limit ?? "20");
7911
+ if (opts.offset)
7912
+ params.set("offset", opts.offset);
7913
+ const resp = await apiRequest({ path: `${path}?${params.toString()}` });
7914
+ const json = await resp.json();
7915
+ if (!resp.ok) {
7916
+ const errorValue = json.error ?? json.errors ?? resp.statusText;
7917
+ console.error(`Error: ${typeof errorValue === "string" ? errorValue : JSON.stringify(errorValue)}`);
7918
+ process.exit(1);
7919
+ }
7920
+ return applyFilters(json.data ?? [], opts);
7921
+ }
7922
+ function applyFilters(rows, opts) {
7923
+ return rows.filter((row) => {
7924
+ if (opts.id && row.id !== opts.id)
7925
+ return false;
7926
+ if (opts.region) {
7927
+ const regions = Array.isArray(row.region) ? row.region.map((region) => String(region).toLowerCase()) : [String(row.region ?? "").toLowerCase()];
7928
+ if (!regions.includes(opts.region.toLowerCase()))
7929
+ return false;
7930
+ }
7931
+ if (opts.status && String(row.status ?? "").toLowerCase() !== opts.status.toLowerCase())
7932
+ return false;
7933
+ if (opts.type) {
7934
+ const candidate = String(row.leaseType ?? row.contractType ?? "").toLowerCase();
7935
+ if (candidate !== opts.type.toLowerCase())
7936
+ return false;
7937
+ }
7938
+ if (opts.supplier) {
7939
+ const supplier = String(row.supplierName ?? "").toLowerCase();
7940
+ if (!supplier.includes(opts.supplier.toLowerCase()))
7941
+ return false;
7942
+ }
7943
+ if (opts.name) {
7944
+ const name = String(row.name ?? row.scopeName ?? "").toLowerCase();
7945
+ if (!name.includes(opts.name.toLowerCase()))
7946
+ return false;
7947
+ }
7948
+ return true;
7949
+ });
7950
+ }
7951
+ function summarizeRows(rows, kind) {
7952
+ const statuses = Object.create(null);
7953
+ const regions = new Set;
7954
+ for (const row of rows) {
7955
+ const status = String(row.status ?? "UNKNOWN");
7956
+ statuses[status] = (statuses[status] ?? 0) + 1;
7957
+ const regionValues = Array.isArray(row.region) ? row.region : [row.region];
7958
+ for (const region of regionValues) {
7959
+ const text = String(region ?? "");
7960
+ if (text)
7961
+ regions.add(text);
7962
+ }
7963
+ }
7964
+ if (kind === "leases") {
7965
+ const totalQuota = rows.reduce((sum, row) => sum + Number(row.executionQuota ?? 0), 0);
7966
+ const totalRemaining = rows.reduce((sum, row) => sum + Number(row.remainingExecutionCapacity ?? 0), 0);
7967
+ const averageOperatorPriority = rows.length > 0 ? Number((rows.reduce((sum, row) => sum + Number(row.operatorPriority ?? 0), 0) / rows.length).toFixed(2)) : 0;
7968
+ const averageEffectivePriority = rows.length > 0 ? Number((rows.reduce((sum, row) => sum + Number(row.effectivePriority ?? row.priority ?? 0), 0) / rows.length).toFixed(2)) : 0;
7969
+ return {
7970
+ total: rows.length,
7971
+ regions: Array.from(regions),
7972
+ statuses,
7973
+ totalQuota,
7974
+ totalRemaining,
7975
+ averageOperatorPriority,
7976
+ averageEffectivePriority,
7977
+ utilizationRatio: totalQuota > 0 ? Number(((totalQuota - totalRemaining) / totalQuota).toFixed(4)) : 0
7978
+ };
7979
+ }
7980
+ if (kind === "contracts") {
7981
+ const suppliers = new Set(rows.map((row) => String(row.supplierName ?? "")).filter(Boolean));
7982
+ return {
7983
+ total: rows.length,
7984
+ regions: Array.from(regions),
7985
+ statuses,
7986
+ supplierCount: suppliers.size,
7987
+ suppliers: Array.from(suppliers)
7988
+ };
7989
+ }
7990
+ return {
7991
+ total: rows.length,
7992
+ regions: Array.from(regions),
7993
+ statuses,
7994
+ totalContracts: rows.reduce((sum, row) => sum + Number(row.contractCount ?? 0), 0)
7995
+ };
7996
+ }
7997
+ async function getResource(path, opts) {
7998
+ const id = opts.id;
7999
+ const resp = await apiRequest({ path: `${path}/${id}` });
8000
+ const json = await resp.json();
8001
+ if (!resp.ok) {
8002
+ const errorValue = json.error ?? json.errors ?? resp.statusText;
8003
+ console.error(`Error: ${typeof errorValue === "string" ? errorValue : JSON.stringify(errorValue)}`);
8004
+ process.exit(1);
8005
+ }
8006
+ const data = json.data ?? {};
8007
+ if (path.endsWith("/supply-products") && opts.format !== "json") {
8008
+ printSectionedProductDetail(formatProductDetail(data));
8009
+ return;
8010
+ }
8011
+ if (path.endsWith("/supply-contracts") && opts.format !== "json") {
8012
+ printSectionedContractDetail(formatContractDetail(data));
8013
+ return;
8014
+ }
8015
+ if (path.endsWith("/allocation-leases") && opts.format !== "json") {
8016
+ printSectionedLeaseDetail(formatLeaseDetail(data));
8017
+ return;
8018
+ }
8019
+ printDetail(data, opts.format);
8020
+ }
8021
+ async function summarizeResource(path, opts, kind) {
8022
+ const rows = await fetchResource(path, { ...opts, limit: opts.limit ?? "200" });
8023
+ const summary = summarizeRows(rows, kind);
8024
+ if (opts.format !== "json") {
8025
+ printSectionedSummary(kind, summary);
8026
+ return;
8027
+ }
8028
+ printDetail(summary, opts.format);
8029
+ }
8030
+ function createSupplyOpsCommand() {
8031
+ const cmd = new Command("supply-ops").description(`Supply operations (Console)
8032
+
8033
+ Aligned to Product -> Contract -> Allocation Lease -> Runtime execution.`).addHelpText("after", `
8034
+ Examples:
8035
+ $ a8techads supply-ops products list
8036
+ $ a8techads supply-ops products summary
8037
+ $ a8techads supply-ops products get <id>
8038
+ $ a8techads supply-ops contracts list --limit 50
8039
+ $ a8techads supply-ops leases list --format json
8040
+ $ a8techads supply-ops leases update <id> --operator-priority 70 --policy-mode delivery_first`);
8041
+ const products = cmd.command("products").description("Traffic package views");
8042
+ addFormatOption(products.command("list").description("List traffic packages.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--view <view>", "packaging, coverage, full", "packaging").option("--name <name>", "Filter by name contains")).action(async (opts) => {
8043
+ await listResource(`${consolePrefix()}/supply-products`, opts, productColumnsForView(opts.view));
8044
+ });
8045
+ addFormatOption(products.command("get").description("Get one traffic package by id.").argument("<id>", "Traffic package ID")).action(async (id, opts) => {
8046
+ await getResource(`${consolePrefix()}/supply-products`, { ...opts, id });
8047
+ });
8048
+ addFormatOption(products.command("summary").description("Summarize traffic packages.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status")).action(async (opts) => {
8049
+ await summarizeResource(`${consolePrefix()}/supply-products`, opts, "products");
8050
+ });
8051
+ products.command("create").description("Create a traffic package (product packaging layer).").requiredOption("--name <name>", "Traffic package name").requiredOption("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--quality-tier <tier>", "PREMIUM, STANDARD, REMNANT", "STANDARD").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "REGION_ONLY").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
8052
+ let body;
8053
+ if (opts.fromJson) {
8054
+ const { readFileSync: readFileSync6 } = await import("fs");
8055
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8056
+ } else {
8057
+ body = {
8058
+ name: opts.name,
8059
+ region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
8060
+ description: opts.description,
8061
+ qualityTier: opts.qualityTier,
8062
+ sourceMaskingMode: opts.sourceMaskingMode,
8063
+ status: opts.status
8064
+ };
8065
+ }
8066
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/supply-products`, body });
8067
+ const json = await resp.json();
8068
+ if (!resp.ok) {
8069
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8070
+ process.exit(1);
8071
+ }
8072
+ console.log(`Traffic package created: ${json.data?.id ?? json.id}`);
8073
+ });
8074
+ products.command("update").description("Update a traffic package.").argument("<id>", "Traffic package ID").option("--name <name>", "Traffic package name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--quality-tier <tier>", "PREMIUM, STANDARD, REMNANT").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
8075
+ let body;
8076
+ if (opts.fromJson) {
8077
+ const { readFileSync: readFileSync6 } = await import("fs");
8078
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8079
+ } else {
8080
+ body = {
8081
+ ...opts.name ? { name: opts.name } : {},
8082
+ ...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
8083
+ ...opts.description ? { description: opts.description } : {},
8084
+ ...opts.qualityTier ? { qualityTier: opts.qualityTier } : {},
8085
+ ...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
8086
+ ...opts.status ? { status: opts.status } : {}
8087
+ };
8088
+ }
8089
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/supply-products/${id}`, body });
8090
+ const json = await resp.json();
8091
+ if (!resp.ok) {
8092
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8093
+ process.exit(1);
8094
+ }
8095
+ console.log(`Traffic package ${id} updated.`);
8096
+ });
8097
+ products.command("delete").description("Delete a traffic package.").argument("<id>", "Traffic package ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
8098
+ if (!opts.yes) {
8099
+ console.error("Add --yes to confirm deletion.");
8100
+ process.exit(1);
8101
+ }
8102
+ const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/supply-products/${id}` });
8103
+ if (!resp.ok && resp.status !== 204) {
8104
+ const json = await resp.json();
8105
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8106
+ process.exit(1);
8107
+ }
8108
+ console.log(`Traffic package ${id} deleted.`);
8109
+ });
8110
+ const contracts = cmd.command("contracts").description("Supply contract views");
8111
+ addFormatOption(contracts.command("list").description("List supply contracts.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type").option("--view <view>", "truth, routing, full", "truth").option("--name <name>", "Filter by name contains")).action(async (opts) => {
8112
+ await listResource(`${consolePrefix()}/supply-contracts`, opts, contractColumnsForView(opts.view));
8113
+ });
8114
+ addFormatOption(contracts.command("get").description("Get one supply contract by id.").argument("<id>", "Supply contract ID")).action(async (id, opts) => {
8115
+ await getResource(`${consolePrefix()}/supply-contracts`, { ...opts, id });
8116
+ });
8117
+ addFormatOption(contracts.command("summary").description("Summarize supply contracts.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type")).action(async (opts) => {
8118
+ await summarizeResource(`${consolePrefix()}/supply-contracts`, opts, "contracts");
8119
+ });
8120
+ contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--supplier-tenant-id <id>", "Supplier tenant ID").requiredOption("--supply-product-id <id>", "Traffic package ID").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION", "OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "OPAQUE").option("--priority <n>", "Contract-level routing priority", "0").option("--match-rules <json>", 'Request match rules JSON, e.g. [{"fieldPath":"ext.alpineads.route_key","operator":"eq","value":"hilltopads-open"}]').option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency", "USD").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
8121
+ let body;
8122
+ if (opts.fromJson) {
8123
+ const { readFileSync: readFileSync6 } = await import("fs");
8124
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8125
+ } else {
8126
+ body = {
8127
+ name: opts.name,
8128
+ region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
8129
+ supplierTenantId: opts.supplierTenantId,
8130
+ supplyProductId: opts.supplyProductId,
8131
+ description: opts.description,
8132
+ contractType: opts.contractType,
8133
+ sourceMaskingMode: opts.sourceMaskingMode,
8134
+ priority: Number(opts.priority),
8135
+ ...opts.matchRules ? { matchRules: JSON.parse(opts.matchRules) } : {},
8136
+ ...opts.qpsCap ? { qpsCap: Number(opts.qpsCap) } : {},
8137
+ ...opts.targetFillRate ? { targetFillRate: opts.targetFillRate } : {},
8138
+ currency: opts.currency,
8139
+ ...opts.startDate ? { startDate: opts.startDate } : {},
8140
+ ...opts.endDate ? { endDate: opts.endDate } : {},
8141
+ status: opts.status
8142
+ };
8143
+ }
8144
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/supply-contracts`, body });
8145
+ const json = await resp.json();
8146
+ if (!resp.ok) {
8147
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8148
+ process.exit(1);
8149
+ }
8150
+ console.log(`Supply contract created: ${json.data?.id ?? json.id}`);
8151
+ });
8152
+ contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
8153
+ let body;
8154
+ if (opts.fromJson) {
8155
+ const { readFileSync: readFileSync6 } = await import("fs");
8156
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8157
+ } else {
8158
+ body = {
8159
+ ...opts.name ? { name: opts.name } : {},
8160
+ ...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
8161
+ ...opts.description ? { description: opts.description } : {},
8162
+ ...opts.contractType ? { contractType: opts.contractType } : {},
8163
+ ...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
8164
+ ...opts.priority ? { priority: Number(opts.priority) } : {},
8165
+ ...opts.matchRules ? { matchRules: JSON.parse(opts.matchRules) } : {},
8166
+ ...opts.qpsCap ? { qpsCap: Number(opts.qpsCap) } : {},
8167
+ ...opts.targetFillRate ? { targetFillRate: opts.targetFillRate } : {},
8168
+ ...opts.currency ? { currency: opts.currency } : {},
8169
+ ...opts.startDate ? { startDate: opts.startDate } : {},
8170
+ ...opts.endDate ? { endDate: opts.endDate } : {},
8171
+ ...opts.status ? { status: opts.status } : {}
8172
+ };
8173
+ }
8174
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/supply-contracts/${id}`, body });
8175
+ const json = await resp.json();
8176
+ if (!resp.ok) {
8177
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8178
+ process.exit(1);
8179
+ }
8180
+ console.log(`Supply contract ${id} updated.`);
8181
+ });
8182
+ contracts.command("delete").description("Delete a supply contract.").argument("<id>", "Supply contract ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
8183
+ if (!opts.yes) {
8184
+ console.error("Add --yes to confirm deletion.");
8185
+ process.exit(1);
8186
+ }
8187
+ const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/supply-contracts/${id}` });
8188
+ if (!resp.ok && resp.status !== 204) {
8189
+ const json = await resp.json();
8190
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8191
+ process.exit(1);
8192
+ }
8193
+ console.log(`Supply contract ${id} deleted.`);
8194
+ });
8195
+ const leases = cmd.command("leases").description("Allocation lease views (operator intent + runtime-derived state)");
8196
+ addFormatOption(leases.command("list").description("List allocation leases with operator intent and runtime-derived state.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--type <type>", "Filter by lease type").option("--view <view>", "runtime, operator, full", "runtime").option("--name <name>", "Filter by scope name contains")).action(async (opts) => {
8197
+ await listResource(`${consolePrefix()}/allocation-leases`, opts, leaseColumnsForView(opts.view));
8198
+ });
8199
+ addFormatOption(leases.command("get").description("Get one allocation lease by id.").argument("<id>", "Allocation lease ID")).action(async (id, opts) => {
8200
+ await getResource(`${consolePrefix()}/allocation-leases`, { ...opts, id });
8201
+ });
8202
+ addFormatOption(leases.command("summary").description("Summarize allocation leases.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--type <type>", "Filter by lease type")).action(async (opts) => {
8203
+ await summarizeResource(`${consolePrefix()}/allocation-leases`, opts, "leases");
8204
+ });
8205
+ leases.command("create").description("Create an allocation lease. CLI only accepts operator intent; runtime quota is allocator-derived.").requiredOption("--supply-contract-id <id>", "Supply contract ID").requiredOption("--region <regions>", "Serving regions, comma-separated").option("--allowed <bool>", "true or false", "true").option("--status <status>", "DRAFT, ACTIVE, PAUSED, EXHAUSTED, ARCHIVED", "DRAFT").option("--policy-mode <mode>", "BALANCED, PRIORITY_FIRST, DELIVERY_FIRST, SPEND_CONSERVATIVE").option("--operator-priority <n>", "Operator priority (0-100)", "50").option("--execution-quota <n>", "Optional runtime quota override for emergency/debug use").option("--window-seconds <n>", "Optional lease window seconds override").option("--expires-at <iso>", "Optional lease expiry override").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
8206
+ let body;
8207
+ if (opts.fromJson) {
8208
+ const { readFileSync: readFileSync6 } = await import("fs");
8209
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8210
+ } else {
8211
+ body = {
8212
+ supplyContractId: opts.supplyContractId,
8213
+ region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
8214
+ allowed: opts.allowed === "true",
8215
+ status: opts.status,
8216
+ ...opts.policyMode ? { policyMode: opts.policyMode } : {},
8217
+ ...opts.executionQuota ? { executionQuota: Number(opts.executionQuota) } : {},
8218
+ operatorPriority: Number(opts.operatorPriority),
8219
+ ...opts.windowSeconds ? { windowSeconds: Number(opts.windowSeconds) } : {},
8220
+ ...opts.expiresAt ? { expiresAt: opts.expiresAt } : {}
8221
+ };
8222
+ }
8223
+ const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/allocation-leases`, body });
8224
+ const json = await resp.json();
8225
+ if (!resp.ok) {
8226
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8227
+ process.exit(1);
8228
+ }
8229
+ console.log(`Allocation lease created: ${json.data?.id ?? json.id}`);
8230
+ });
8231
+ leases.command("update").description("Update an allocation lease. Prefer operator intent fields; runtime fields are override-only.").argument("<id>", "Allocation lease ID").option("--region <regions>", "Serving regions, comma-separated").option("--allowed <bool>", "true or false").option("--status <status>", "DRAFT, ACTIVE, PAUSED, EXHAUSTED, ARCHIVED").option("--policy-mode <mode>", "BALANCED, PRIORITY_FIRST, DELIVERY_FIRST, SPEND_CONSERVATIVE").option("--operator-priority <n>", "Operator priority (0-100)").option("--execution-quota <n>", "Optional runtime quota override for emergency/debug use").option("--window-seconds <n>", "Optional lease window seconds override").option("--expires-at <iso>", "Optional lease expiry override").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
8232
+ let body;
8233
+ if (opts.fromJson) {
8234
+ const { readFileSync: readFileSync6 } = await import("fs");
8235
+ body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8236
+ } else {
8237
+ body = {
8238
+ ...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
8239
+ ...opts.allowed ? { allowed: opts.allowed === "true" } : {},
8240
+ ...opts.status ? { status: opts.status } : {},
8241
+ ...opts.policyMode ? { policyMode: opts.policyMode } : {},
8242
+ ...opts.executionQuota ? { executionQuota: Number(opts.executionQuota) } : {},
8243
+ ...opts.operatorPriority ? { operatorPriority: Number(opts.operatorPriority) } : {},
8244
+ ...opts.windowSeconds ? { windowSeconds: Number(opts.windowSeconds) } : {},
8245
+ ...opts.expiresAt ? { expiresAt: opts.expiresAt } : {}
8246
+ };
8247
+ }
8248
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/allocation-leases/${id}`, body });
8249
+ const json = await resp.json();
8250
+ if (!resp.ok) {
8251
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8252
+ process.exit(1);
8253
+ }
8254
+ console.log(`Allocation lease ${id} updated.`);
8255
+ });
8256
+ leases.command("delete").description("Delete an allocation lease.").argument("<id>", "Allocation lease ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
8257
+ if (!opts.yes) {
8258
+ console.error("Add --yes to confirm deletion.");
8259
+ process.exit(1);
8260
+ }
8261
+ const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/allocation-leases/${id}` });
8262
+ if (!resp.ok && resp.status !== 204) {
8263
+ const json = await resp.json();
8264
+ console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
8265
+ process.exit(1);
8266
+ }
8267
+ console.log(`Allocation lease ${id} deleted.`);
8268
+ });
8269
+ return cmd;
8270
+ }
8271
+
8272
+ // src/commands/validate.ts
8273
+ function unwrapData(json) {
8274
+ if (json && typeof json === "object" && "data" in json) {
8275
+ return json.data;
8276
+ }
8277
+ return json;
8278
+ }
8279
+ function getPath(value, path) {
8280
+ return path.reduce((current, key) => {
8281
+ if (!current || typeof current !== "object")
8282
+ return;
8283
+ return current[key];
8284
+ }, value);
8285
+ }
8286
+ function describe(value) {
8287
+ if (value === null)
8288
+ return "null";
8289
+ if (value === undefined)
8290
+ return "missing";
8291
+ if (typeof value === "string")
8292
+ return value;
8293
+ if (typeof value === "number" || typeof value === "boolean")
8294
+ return String(value);
8295
+ return JSON.stringify(value);
8296
+ }
8297
+ function addRow(rows, check, status, detail) {
8298
+ rows.push({ check, status, detail });
8299
+ }
8300
+ function maybeSuspiciousZero(value) {
8301
+ return typeof value === "number" && value === 0;
8302
+ }
8303
+ async function fetchJson(path) {
8304
+ const resp = await apiRequest({ path });
8305
+ const json = await resp.json().catch(() => null);
8306
+ if (!resp.ok) {
8307
+ const message = json && typeof json === "object" && "error" in json ? describe(json.error) : resp.statusText;
8308
+ throw new Error(`${path} failed: ${message}`);
8309
+ }
8310
+ return json;
8311
+ }
8312
+ async function requestJson(method, path, body) {
8313
+ const resp = await apiRequest({ method, path, body });
8314
+ const json = await resp.json().catch(() => null);
8315
+ return { status: resp.status, ok: resp.ok, json };
8316
+ }
8317
+ function summarizeError(json) {
8318
+ const body = json ?? {};
8319
+ if (body.error)
8320
+ return describe(body.error);
8321
+ if (body.errors)
8322
+ return describe(body.errors);
8323
+ return "no error body";
8324
+ }
8325
+ function parseCsv(value) {
8326
+ return value.split(",").map((part) => part.trim()).filter(Boolean);
8327
+ }
8328
+ async function runDataHonestyChecks() {
8329
+ const rows = [];
8330
+ const [summaryRaw, financeRaw, billingRaw, systemHealthRaw, systemQueuesRaw] = await Promise.all([
8331
+ fetchJson("/api/v1/console/summary"),
8332
+ fetchJson("/api/v1/console/billing-ops/overview"),
8333
+ fetchJson("/api/v1/console/billing-ops/summary"),
8334
+ fetchJson("/api/v1/console/system/health"),
8335
+ fetchJson("/api/v1/console/system/queues")
8336
+ ]);
8337
+ const summary = unwrapData(summaryRaw);
8338
+ const finance = unwrapData(financeRaw);
8339
+ const billing = unwrapData(billingRaw);
8340
+ const systemHealth = unwrapData(systemHealthRaw);
8341
+ const systemQueues = unwrapData(systemQueuesRaw);
8342
+ const pending = getPath(finance, ["pending"]);
8343
+ const depositsCount = pending?.depositsCount;
8344
+ const depositsAmount = pending?.depositsAmount;
8345
+ if (depositsCount === null && depositsAmount === null) {
8346
+ addRow(rows, "finance.pendingDeposits", "pass", "pending deposits disclosed as unavailable");
8347
+ } else if (maybeSuspiciousZero(depositsCount) && maybeSuspiciousZero(depositsAmount)) {
8348
+ addRow(rows, "finance.pendingDeposits", "fail", "still returning 0 / 0.0 placeholder values");
8349
+ } else {
8350
+ addRow(rows, "finance.pendingDeposits", "warn", `returned count=${describe(depositsCount)} amount=${describe(depositsAmount)}`);
8351
+ }
8352
+ const overdue = getPath(billing, ["receivables", "overdue"]);
8353
+ if (overdue === null) {
8354
+ addRow(rows, "billing.receivables.overdue", "pass", "overdue disclosed as unavailable");
8355
+ } else {
8356
+ addRow(rows, "billing.receivables.overdue", "warn", `returned ${describe(overdue)}`);
8357
+ }
8358
+ const pendingPayouts = getPath(systemHealth, ["stats", "pendingPayouts"]);
8359
+ if (pendingPayouts === null) {
8360
+ addRow(rows, "system.pendingPayouts", "pass", "pending payouts disclosed as unavailable");
8361
+ } else if (pendingPayouts === undefined) {
8362
+ addRow(rows, "system.pendingPayouts", "warn", "system/health does not currently expose stats.pendingPayouts");
8363
+ } else {
8364
+ addRow(rows, "system.pendingPayouts", "warn", `returned ${describe(pendingPayouts)}`);
8365
+ }
8366
+ const advertiserChange = getPath(summary, ["platformHealth", "advertiserChange"]);
8367
+ const publisherChange = getPath(summary, ["platformHealth", "publisherChange"]);
8368
+ const clicksChange = getPath(summary, ["changes", "clicks"]);
8369
+ addRow(rows, "summary.platformHealth.advertiserChange", advertiserChange === null ? "pass" : "warn", describe(advertiserChange));
8370
+ addRow(rows, "summary.platformHealth.publisherChange", publisherChange === null ? "pass" : "warn", describe(publisherChange));
8371
+ addRow(rows, "summary.changes.clicks", clicksChange === null ? "pass" : "warn", describe(clicksChange));
8372
+ const queueRows = Array.isArray(systemQueues) ? systemQueues : [];
8373
+ const fallbackQueue = queueRows.find((row) => {
8374
+ if (!row || typeof row !== "object")
8375
+ return false;
8376
+ return row.available === false;
8377
+ });
8378
+ if (!fallbackQueue) {
8379
+ addRow(rows, "system.queues.unavailableFallback", "warn", "no unavailable queue row found");
8380
+ } else {
8381
+ const hasNilMetrics = ["pending", "processing", "completed", "failed", "avgProcessingTime"].every((key) => fallbackQueue[key] === null);
8382
+ addRow(rows, "system.queues.unavailableFallback", hasNilMetrics ? "pass" : "warn", hasNilMetrics ? "queue fallback row marks metrics unavailable" : `queue row=${JSON.stringify(fallbackQueue)}`);
8383
+ }
8384
+ return rows;
8385
+ }
8386
+ function createValidateCommand() {
8387
+ const cmd = new Command("validate").description("Deploy and integration validation helpers").addHelpText("after", `
8388
+ Examples:
8389
+ $ a8techads validate health
8390
+ $ a8techads validate data-honesty
8391
+ $ a8techads validate data-honesty --format json`);
8392
+ cmd.command("health").description("Public API health check.").option("--base-url <url>", "Base API URL", "https://api.a8.tech").action(async (opts) => {
8393
+ const url = new URL("/health", opts.baseUrl).toString();
8394
+ const resp = await fetch(url);
8395
+ if (!resp.ok) {
8396
+ console.error(`Health check failed: ${url} -> ${resp.status}`);
8397
+ process.exit(1);
8398
+ }
8399
+ console.log(`OK ${url} -> ${resp.status}`);
8400
+ });
8401
+ addFormatOption(cmd.command("data-honesty").description("Check post-deploy nullable/disclosure behavior for Console data honesty changes.")).action(async (opts) => {
8402
+ const rows = await runDataHonestyChecks();
8403
+ const columns = [
8404
+ { key: "check", header: "CHECK", width: 38 },
8405
+ { key: "status", header: "STATUS", width: 8 },
8406
+ { key: "detail", header: "DETAIL", width: 80 }
8407
+ ];
8408
+ printData(rows, columns, opts.format);
8409
+ if (rows.some((row) => row.status === "fail")) {
8410
+ process.exit(1);
8411
+ }
8412
+ });
8413
+ addFormatOption(cmd.command("tenant-boundary").description("Assert that current tenant context cannot access known foreign resources.").requiredOption("--campaign <id>", "Campaign ID that must not be visible from current tenant context").option("--supply-product <id>", "Active DSP-visible supply product ID to verify shared catalog access").option("--tenant <id>", "Tenant ID that current user does not belong to; used for tenant switch rejection check")).action(async (opts) => {
8414
+ const rows = [];
8415
+ const campaignId = String(opts.campaign);
8416
+ const campaignShow = await requestJson("GET", `/api/v1/dsp/campaigns/${campaignId}`);
8417
+ addRow(rows, "campaign.show.foreign", campaignShow.status === 404 ? "pass" : "fail", `expected 404, got ${campaignShow.status}${campaignShow.ok ? "" : ` (${summarizeError(campaignShow.json)})`}`);
8418
+ const campaignUpdate = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}`, {
8419
+ name: `cross-tenant-check-${Date.now()}`
8420
+ });
8421
+ addRow(rows, "campaign.update.foreign", campaignUpdate.status === 404 ? "pass" : "fail", `expected 404, got ${campaignUpdate.status}${campaignUpdate.ok ? "" : ` (${summarizeError(campaignUpdate.json)})`}`);
8422
+ const campaignDelete = await requestJson("DELETE", `/api/v1/dsp/campaigns/${campaignId}`);
8423
+ addRow(rows, "campaign.delete.foreign", campaignDelete.status === 404 ? "pass" : "fail", `expected 404, got ${campaignDelete.status}${campaignDelete.ok ? "" : ` (${summarizeError(campaignDelete.json)})`}`);
8424
+ if (opts.supplyProduct) {
8425
+ const supplyProductId = String(opts.supplyProduct);
8426
+ const supplyProductShow = await requestJson("GET", `/api/v1/dsp/supply-products/${supplyProductId}`);
8427
+ addRow(rows, "supplyProduct.sharedCatalog", supplyProductShow.status === 200 ? "pass" : "warn", supplyProductShow.status === 200 ? "active supply product is visible through DSP shared catalog" : `expected shared catalog access, got ${supplyProductShow.status}${supplyProductShow.ok ? "" : ` (${summarizeError(supplyProductShow.json)})`}`);
8428
+ }
8429
+ if (opts.tenant) {
8430
+ const tenantId = String(opts.tenant);
8431
+ const tenantSwitch = await requestJson("POST", "/api/v1/me/switch-tenant", { tenant_id: tenantId });
8432
+ addRow(rows, "tenantSwitch.foreignTenant", tenantSwitch.status === 403 ? "pass" : "fail", `expected 403, got ${tenantSwitch.status}${tenantSwitch.ok ? "" : ` (${summarizeError(tenantSwitch.json)})`}`);
8433
+ }
8434
+ const columns = [
8435
+ { key: "check", header: "CHECK", width: 32 },
8436
+ { key: "status", header: "STATUS", width: 8 },
8437
+ { key: "detail", header: "DETAIL", width: 80 }
8438
+ ];
8439
+ printData(rows, columns, opts.format);
8440
+ if (rows.some((row) => row.status === "fail")) {
8441
+ process.exit(1);
8442
+ }
8443
+ });
8444
+ addFormatOption(cmd.command("campaign-binding-flow").description("Run pause -> update supply binding -> resume against a real DSP campaign.").argument("<campaign-id>", "Campaign ID to mutate during the validation flow").requiredOption("--supply-products <ids>", "Comma-separated supply product IDs to apply while paused").option("--yes", "Allow live mutation of campaign state", false)).action(async (campaignId, opts) => {
8445
+ if (!opts.yes) {
8446
+ console.error("Error: this command mutates live campaign state. Re-run with --yes.");
8447
+ process.exit(1);
8448
+ }
8449
+ const rows = [];
8450
+ const supplyProductIds = parseCsv(String(opts.supplyProducts));
8451
+ const pauseResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}/status`, { status: "paused" });
8452
+ addRow(rows, "campaign.pause", pauseResp.ok ? "pass" : "fail", pauseResp.ok ? "pause request accepted" : `status ${pauseResp.status} (${summarizeError(pauseResp.json)})`);
8453
+ const updateResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}`, { supplyProductIds });
8454
+ addRow(rows, "campaign.updateSupplyBinding", updateResp.ok ? "pass" : "fail", updateResp.ok ? `updated supplyProductIds=${supplyProductIds.join(",")}` : `status ${updateResp.status} (${summarizeError(updateResp.json)})`);
8455
+ const resumeResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}/status`, { status: "active" });
8456
+ addRow(rows, "campaign.resume", resumeResp.ok ? "pass" : "fail", resumeResp.ok ? "resume request accepted" : `status ${resumeResp.status} (${summarizeError(resumeResp.json)})`);
8457
+ const verifyResp = await requestJson("GET", `/api/v1/dsp/campaigns/${campaignId}`);
8458
+ const verify = unwrapData(verifyResp.json);
8459
+ const actualStatus = getPath(verify, ["status"]);
8460
+ const actualSupplyProducts = getPath(verify, ["supplyProductIds"]);
8461
+ const supplyBindingMatches = Array.isArray(actualSupplyProducts) && actualSupplyProducts.length === supplyProductIds.length && supplyProductIds.every((id) => actualSupplyProducts.includes(id));
8462
+ addRow(rows, "campaign.verifyStatus", String(actualStatus).toUpperCase() === "ACTIVE" ? "pass" : "fail", `status=${describe(actualStatus)}`);
8463
+ addRow(rows, "campaign.verifySupplyBinding", supplyBindingMatches ? "pass" : "fail", `supplyProductIds=${describe(actualSupplyProducts)}`);
8464
+ const columns = [
8465
+ { key: "check", header: "CHECK", width: 32 },
8466
+ { key: "status", header: "STATUS", width: 8 },
8467
+ { key: "detail", header: "DETAIL", width: 80 }
8468
+ ];
8469
+ printData(rows, columns, opts.format);
8470
+ if (rows.some((row) => row.status === "fail")) {
8471
+ process.exit(1);
8472
+ }
8473
+ });
4844
8474
  return cmd;
4845
8475
  }
4846
8476
 
4847
8477
  // src/index.ts
4848
8478
  function createProgram() {
4849
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.0").addHelpText("after", `
8479
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
4850
8480
  Command Groups:
4851
8481
  auth Authentication (login, logout, token, status)
4852
8482
  profile Multi-profile management
@@ -4854,13 +8484,23 @@ Command Groups:
4854
8484
  audiences Audience management (DSP)
4855
8485
  campaigns Campaign management (DSP)
4856
8486
  variations Ad variation management (DSP)
8487
+ landing-pages Landing page groups and pages (DSP)
8488
+ conversion-goals Conversion goal management (DSP)
8489
+ media-assets Media library management (DSP)
8490
+ algorithms Bidder algorithm management (DSP)
4857
8491
  sites Site management (SSP)
4858
8492
  zones Ad zone management (SSP)
4859
8493
  reports Analytics and reporting
4860
8494
  billing Billing and payments (DSP)
4861
8495
  invoices Invoice management (DSP)
8496
+ payouts Payout management (SSP)
8497
+ statements Statement management (SSP)
8498
+ simulator Simulator control and replay support
4862
8499
  users Team member management
4863
8500
  settings Tenant settings
8501
+ admin Platform administration (Console)
8502
+ external-ssp External SSP partner management (Console)
8503
+ supply-ops Internal supply operations views (Console)
4864
8504
 
4865
8505
  Getting Started:
4866
8506
  $ a8techads auth login # Authenticate
@@ -4874,14 +8514,24 @@ Getting Started:
4874
8514
  program2.addCommand(createAudiencesCommand());
4875
8515
  program2.addCommand(createCampaignsCommand());
4876
8516
  program2.addCommand(createVariationsCommand());
8517
+ program2.addCommand(createMediaAssetsCommand());
8518
+ program2.addCommand(createLandingPagesCommand());
8519
+ program2.addCommand(createConversionGoalsCommand());
8520
+ program2.addCommand(createAlgorithmsCommand());
4877
8521
  program2.addCommand(createSitesCommand());
4878
8522
  program2.addCommand(createZonesCommand());
4879
8523
  program2.addCommand(createReportsCommand());
4880
8524
  program2.addCommand(createBillingCommand());
8525
+ program2.addCommand(createPayoutsCommand());
8526
+ program2.addCommand(createStatementsCommand());
8527
+ program2.addCommand(createSimulatorCommand());
4881
8528
  program2.addCommand(createUsersCommand());
4882
8529
  program2.addCommand(createSettingsCommand());
4883
8530
  program2.addCommand(createInvoicesCommand());
4884
8531
  program2.addCommand(createAdminCommand());
8532
+ program2.addCommand(createExternalSspCommand());
8533
+ program2.addCommand(createSupplyOpsCommand());
8534
+ program2.addCommand(createValidateCommand());
4885
8535
  return program2;
4886
8536
  }
4887
8537