@a8techads/cli 0.4.1 → 0.4.3

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 +1969 -129
  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";
@@ -2843,7 +2829,9 @@ async function loginClientCredentials(opts) {
2843
2829
  const apiUrl = opts.apiUrl ?? DEFAULT_API_URL2;
2844
2830
  const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL2;
2845
2831
  const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
2846
- console.log("Authenticating with client credentials...");
2832
+ if (!opts.silent) {
2833
+ console.log("Authenticating with client credentials...");
2834
+ }
2847
2835
  const resp = await fetch(tokenEndpoint, {
2848
2836
  method: "POST",
2849
2837
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -2861,7 +2849,7 @@ async function loginClientCredentials(opts) {
2861
2849
  const tokens = await resp.json();
2862
2850
  const claims = decodeJwt(tokens.access_token);
2863
2851
  const authClaims = claims ? getAuthClaims(claims) : null;
2864
- const profileName = `client:${opts.clientId}`;
2852
+ const profileName = opts.profile ?? `client:${opts.clientId}`;
2865
2853
  const tenants = await fetchTenantsForClient(apiUrl, tokens.access_token);
2866
2854
  const expiresAt = new Date(Date.now() + tokens.expires_in * 1000).toISOString();
2867
2855
  const profileCreds = {
@@ -2893,19 +2881,21 @@ async function loginClientCredentials(opts) {
2893
2881
  ctx.current_profile = profileName;
2894
2882
  ctx.profiles[profileName] = profileCtx;
2895
2883
  saveContext(ctx);
2896
- console.log(`
2884
+ if (!opts.silent) {
2885
+ console.log(`
2897
2886
  Authenticated as ${profileName}`);
2898
- console.log(`Profile: ${profileName}`);
2899
- if (tenant) {
2900
- console.log(`Tenant: ${tenant.tenant_name} (${tenant.tenant_id})`);
2901
- }
2902
- console.log(`App: ${app}`);
2903
- if (capability) {
2904
- console.log(`Capability: ${capability}`);
2905
- }
2906
- console.log(`Token expires: ${expiresAt}`);
2907
- 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(`
2908
2897
  Note: Client credentials tokens cannot be refreshed. Re-authenticate on expiry.`);
2898
+ }
2909
2899
  }
2910
2900
  function deriveApp2(tenant) {
2911
2901
  if (!tenant)
@@ -2970,16 +2960,124 @@ function logout(profileName) {
2970
2960
  console.log(`Logged out from profile "${name}".`);
2971
2961
  }
2972
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
+
2973
3066
  // src/auth/refresh.ts
2974
3067
  async function refreshTokenIfNeeded(profileName) {
2975
3068
  const creds = loadCredentials();
3069
+ const effectiveProfileName = profileName ?? creds.current_profile;
2976
3070
  const profile = profileName ? getProfile(creds, profileName) : getCurrentProfile(creds);
2977
- if (!profile)
2978
- return false;
3071
+ if (!profile) {
3072
+ return await loginFromConfiguredOAuthClient(effectiveProfileName);
3073
+ }
2979
3074
  const claims = decodeJwt(profile.access_token);
2980
3075
  if (!claims || !isTokenExpired(claims))
2981
3076
  return false;
2982
3077
  if (!profile.refresh_token) {
3078
+ const refreshed = await loginFromConfiguredOAuthClient(effectiveProfileName, profile.client_id);
3079
+ if (refreshed)
3080
+ return true;
2983
3081
  throw new Error('Token expired and no refresh token available. Run "a8techads auth login" to re-authenticate.');
2984
3082
  }
2985
3083
  const response = await fetch(profile.token_endpoint, {
@@ -2993,6 +3091,12 @@ async function refreshTokenIfNeeded(profileName) {
2993
3091
  });
2994
3092
  if (!response.ok) {
2995
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
+ }
2996
3100
  throw new Error(`Token refresh failed (${response.status}): ${text}
2997
3101
  Run "a8techads auth login" to re-authenticate.`);
2998
3102
  }
@@ -3006,16 +3110,31 @@ Run "a8techads auth login" to re-authenticate.`);
3006
3110
  saveCredentials(creds);
3007
3111
  return true;
3008
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
+ }
3009
3129
 
3010
3130
  // src/auth/token.ts
3011
3131
  async function outputToken(profileName) {
3012
- const creds = loadCredentials();
3013
- const name = profileName ?? creds.current_profile;
3014
- await refreshTokenIfNeeded(name);
3132
+ await refreshTokenIfNeeded(profileName);
3015
3133
  const freshCreds = loadCredentials();
3016
- const profile = getProfile(freshCreds, name);
3134
+ const freshName = profileName ?? freshCreds.current_profile;
3135
+ const profile = getProfile(freshCreds, freshName);
3017
3136
  if (!profile) {
3018
- 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.`);
3019
3138
  process.exit(1);
3020
3139
  }
3021
3140
  process.stdout.write(profile.access_token);
@@ -3030,8 +3149,16 @@ function showAuthStatus(profileNameOverride) {
3030
3149
  const profileCtx = ctx.profiles[profileName] ?? null;
3031
3150
  console.log(`Profile: ${profileName}`);
3032
3151
  if (!profile) {
3033
- console.log("Auth mode: none");
3034
- 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
+ }
3035
3162
  return;
3036
3163
  }
3037
3164
  const isClientCreds = profileName.startsWith("client:");
@@ -3090,6 +3217,7 @@ function createAuthCommand() {
3090
3217
  Examples:
3091
3218
  $ a8techads auth login # Browser OAuth + PKCE
3092
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
3093
3221
  $ a8techads auth status # Show current auth state
3094
3222
  $ a8techads auth token # Print access token for scripting
3095
3223
  $ a8techads auth logout # Clear stored tokens`);
@@ -3098,10 +3226,10 @@ Examples:
3098
3226
  Browser mode (default): Opens browser for OAuth 2.1 Authorization Code + PKCE.
3099
3227
  Client credentials mode: Non-interactive auth using --client-id and --client-secret.
3100
3228
 
3101
- 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", `
3102
3230
  Examples:
3103
3231
  $ a8techads auth login # Interactive browser login
3104
- $ a8techads auth login -p staging --api-url https://api.staging.a8.tech
3232
+ $ a8techads auth login -p pilot --api-url https://api.a8.tech
3105
3233
  $ a8techads auth login --client-id svc-001 --client-secret s3cret
3106
3234
  $ a8techads auth login -p owner --force-login # Switch to different user
3107
3235
 
@@ -3113,7 +3241,8 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3113
3241
  clientId: opts.clientId,
3114
3242
  clientSecret: opts.clientSecret,
3115
3243
  apiUrl: opts.apiUrl,
3116
- authUrl: opts.authUrl
3244
+ authUrl: opts.authUrl,
3245
+ profile: opts.profile
3117
3246
  });
3118
3247
  } else if (opts.clientId || opts.clientSecret) {
3119
3248
  console.error("Error: Both --client-id and --client-secret are required for client credentials flow.");
@@ -3121,7 +3250,7 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3121
3250
  process.exit(1);
3122
3251
  } else {
3123
3252
  await login({
3124
- profile: opts.profile,
3253
+ profile: opts.profile ?? "default",
3125
3254
  apiUrl: opts.apiUrl,
3126
3255
  authUrl: opts.authUrl,
3127
3256
  forceLogin: opts.forceLogin
@@ -3133,12 +3262,41 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
3133
3262
  process.exit(1);
3134
3263
  }
3135
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
+ });
3136
3294
  auth.command("logout").description(`Clear stored tokens for a profile.
3137
3295
 
3138
3296
  Requires: an existing profile.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
3139
3297
  Examples:
3140
3298
  $ a8techads auth logout # Logout current profile
3141
- $ a8techads auth logout -p staging # Logout specific profile`).action((opts) => {
3299
+ $ a8techads auth logout -p pilot # Logout specific profile`).action((opts) => {
3142
3300
  logout(opts.profile);
3143
3301
  });
3144
3302
  auth.command("token").description(`Output current access token to stdout.
@@ -3149,7 +3307,7 @@ cannot be refreshed — re-authenticate if expired.
3149
3307
  Requires: an active profile with valid credentials.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
3150
3308
  Examples:
3151
3309
  $ a8techads auth token # Print token for current profile
3152
- $ a8techads auth token -p staging # Print token for specific profile
3310
+ $ a8techads auth token -p pilot # Print token for specific profile
3153
3311
  $ curl -H "Authorization: Bearer $(a8techads auth token)" https://api.a8.tech/api/v1/dsp/me`).action(async (opts) => {
3154
3312
  try {
3155
3313
  await outputToken(opts.profile);
@@ -3176,20 +3334,31 @@ function createProfileCommand() {
3176
3334
  const profile = new Command("profile").description("Profile management");
3177
3335
  profile.command("list").description("List all profiles").action(() => {
3178
3336
  const creds = loadCredentials();
3179
- 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
+ ]));
3180
3342
  if (profiles.length === 0) {
3181
- 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.');
3182
3344
  return;
3183
3345
  }
3184
3346
  console.log(`Profiles:
3185
3347
  `);
3186
3348
  for (const name of profiles) {
3187
3349
  const p = creds.profiles[name];
3350
+ const clientConfig = loadOAuthClientConfig(name);
3188
3351
  const marker = name === creds.current_profile ? " (active)" : "";
3189
3352
  console.log(` ${name}${marker}`);
3190
- console.log(` Email: ${p.email}`);
3191
- console.log(` API: ${p.api_url}`);
3192
- 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) {
3193
3362
  console.log(` Tenants: ${p.tenants.map((t) => `${t.tenant_name} (${t.tenant_id})`).join(", ")}`);
3194
3363
  }
3195
3364
  console.log();
@@ -3197,7 +3366,8 @@ function createProfileCommand() {
3197
3366
  });
3198
3367
  profile.command("use").description("Switch active profile").argument("<name>", "Profile name to switch to").action((name) => {
3199
3368
  const creds = loadCredentials();
3200
- if (!creds.profiles[name]) {
3369
+ const clientConfigExists = hasConfiguredOAuthClientProfile(name);
3370
+ if (!creds.profiles[name] && !clientConfigExists) {
3201
3371
  console.error(`Profile "${name}" not found. Run "a8techads profile list" to see available profiles.`);
3202
3372
  process.exit(1);
3203
3373
  }
@@ -3207,7 +3377,11 @@ function createProfileCommand() {
3207
3377
  ctx.current_profile = name;
3208
3378
  saveContext(ctx);
3209
3379
  const p = creds.profiles[name];
3210
- 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
+ }
3211
3385
  });
3212
3386
  return profile;
3213
3387
  }
@@ -3687,8 +3861,8 @@ Examples:
3687
3861
  $ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
3688
3862
  let body;
3689
3863
  if (opts.fromJson) {
3690
- const { readFileSync: readFileSync3 } = await import("fs");
3691
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
3864
+ const { readFileSync: readFileSync4 } = await import("fs");
3865
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3692
3866
  } else {
3693
3867
  if (!opts.name) {
3694
3868
  console.error('Error: --name is required. Run "a8techads audiences create --help".');
@@ -3794,10 +3968,10 @@ File format (one per line):
3794
3968
  console.error("Error: --file is required.");
3795
3969
  process.exit(1);
3796
3970
  }
3797
- const { readFileSync: readFileSync3 } = await import("fs");
3971
+ const { readFileSync: readFileSync4 } = await import("fs");
3798
3972
  let identifiers;
3799
3973
  try {
3800
- const content = readFileSync3(opts.file, "utf-8").trim();
3974
+ const content = readFileSync4(opts.file, "utf-8").trim();
3801
3975
  try {
3802
3976
  identifiers = JSON.parse(content);
3803
3977
  if (!Array.isArray(identifiers))
@@ -3868,9 +4042,89 @@ var COLUMNS2 = [
3868
4042
  { key: "id", header: "ID", width: 36 },
3869
4043
  { key: "name", header: "NAME", width: 30 },
3870
4044
  { key: "status", header: "STATUS", width: 12 },
4045
+ { key: "supplyProductCount", header: "SUPPLY", width: 8 },
3871
4046
  { key: "budget", header: "BUDGET", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
3872
4047
  { key: "spent", header: "SPENT", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" }
3873
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
+ }
3874
4128
  function createCampaignsCommand() {
3875
4129
  const cmd = new Command("campaigns").description(`Campaign management (DSP)
3876
4130
 
@@ -3879,6 +4133,7 @@ Examples:
3879
4133
  $ a8techads campaigns list
3880
4134
  $ a8techads campaigns get <id>
3881
4135
  $ a8techads campaigns create --name "Summer Sale" --budget 500
4136
+ $ a8techads campaigns update <id> --max-bid 6
3882
4137
  $ a8techads campaigns pause <id>`);
3883
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) => {
3884
4139
  const params = new URLSearchParams;
@@ -3895,6 +4150,7 @@ Examples:
3895
4150
  id: c.id,
3896
4151
  name: c.name,
3897
4152
  status: c.status,
4153
+ supplyProductCount: c.supplyProductCount ?? (Array.isArray(c.supplyProductIds) ? c.supplyProductIds.length : 0),
3898
4154
  budget: c.budget ?? c.dailyBudget,
3899
4155
  spent: c.stats?.spend ?? c.spent
3900
4156
  }));
@@ -3907,7 +4163,12 @@ Examples:
3907
4163
  console.error(`Error: ${json.error ?? resp.statusText}`);
3908
4164
  process.exit(1);
3909
4165
  }
3910
- 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);
3911
4172
  });
3912
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) => {
3913
4174
  const params = new URLSearchParams;
@@ -3925,14 +4186,15 @@ Examples:
3925
4186
  });
3926
4187
  cmd.command("create").description(`Create a new campaign.
3927
4188
 
3928
- 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", `
3929
4190
  Examples:
3930
4191
  $ a8techads campaigns create --name "Summer Sale" --budget 500
4192
+ $ a8techads campaigns create --name "Pilot Supply Binding" --budget 500 --supply-products <supply-product-id>
3931
4193
  $ a8techads campaigns create --from-json campaign.json`).action(async (opts) => {
3932
4194
  let body;
3933
4195
  if (opts.fromJson) {
3934
- const { readFileSync: readFileSync3 } = await import("fs");
3935
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4196
+ const { readFileSync: readFileSync4 } = await import("fs");
4197
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3936
4198
  } else {
3937
4199
  if (!opts.name) {
3938
4200
  console.error('Error: --name is required. Run "a8techads campaigns create --help".');
@@ -3941,6 +4203,11 @@ Examples:
3941
4203
  body = { name: opts.name };
3942
4204
  if (opts.budget)
3943
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);
3944
4211
  }
3945
4212
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/campaigns`, body });
3946
4213
  const json = await resp.json();
@@ -3950,17 +4217,52 @@ Examples:
3950
4217
  }
3951
4218
  console.log(`Campaign created: ${json.data?.id ?? json.id}`);
3952
4219
  });
3953
- 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) => {
3954
4221
  let body;
3955
4222
  if (opts.fromJson) {
3956
- const { readFileSync: readFileSync3 } = await import("fs");
3957
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4223
+ const { readFileSync: readFileSync4 } = await import("fs");
4224
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
3958
4225
  } else {
3959
4226
  body = {};
3960
4227
  if (opts.name)
3961
4228
  body.name = opts.name;
3962
4229
  if (opts.budget)
3963
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 = [];
3964
4266
  }
3965
4267
  const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
3966
4268
  if (!resp.ok) {
@@ -4073,8 +4375,8 @@ Examples:
4073
4375
  if (opts.disable) {
4074
4376
  dayParting = null;
4075
4377
  } else {
4076
- const { readFileSync: readFileSync3 } = await import("fs");
4077
- dayParting = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4378
+ const { readFileSync: readFileSync4 } = await import("fs");
4379
+ dayParting = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4078
4380
  }
4079
4381
  await patchCampaign(id, {
4080
4382
  dayParting,
@@ -4175,6 +4477,9 @@ function normalizeListTargeting(value) {
4175
4477
  list: Array.isArray(value?.list) ? value.list : []
4176
4478
  };
4177
4479
  }
4480
+ function normalizePriceSettings(value) {
4481
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
4482
+ }
4178
4483
  function camelizeOption(value) {
4179
4484
  return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
4180
4485
  }
@@ -4225,8 +4530,8 @@ Examples:
4225
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) => {
4226
4531
  let body;
4227
4532
  if (opts.fromJson) {
4228
- const { readFileSync: readFileSync3 } = await import("fs");
4229
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4533
+ const { readFileSync: readFileSync4 } = await import("fs");
4534
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4230
4535
  } else {
4231
4536
  if (!opts.campaign || !opts.name) {
4232
4537
  console.error("Error: --campaign and --name are required.");
@@ -4245,8 +4550,8 @@ Examples:
4245
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) => {
4246
4551
  let body;
4247
4552
  if (opts.fromJson) {
4248
- const { readFileSync: readFileSync3 } = await import("fs");
4249
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4553
+ const { readFileSync: readFileSync4 } = await import("fs");
4554
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4250
4555
  } else {
4251
4556
  body = {};
4252
4557
  if (opts.name)
@@ -4272,12 +4577,48 @@ Examples:
4272
4577
  }
4273
4578
  console.log(`Variation ${id} deleted.`);
4274
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
+ });
4275
4616
  return cmd;
4276
4617
  }
4277
4618
 
4278
4619
  // src/commands/media-assets.ts
4279
4620
  import { basename, extname } from "node:path";
4280
- import { readFileSync as readFileSync3 } from "node:fs";
4621
+ import { readFileSync as readFileSync4 } from "node:fs";
4281
4622
  var COLUMNS4 = [
4282
4623
  { key: "id", header: "ID", width: 36 },
4283
4624
  { key: "name", header: "NAME", width: 24 },
@@ -4401,7 +4742,7 @@ Examples:
4401
4742
  const headers = new Headers(request.init.headers);
4402
4743
  headers.delete("Content-Type");
4403
4744
  const body = new FormData;
4404
- const fileBytes = readFileSync3(filePath);
4745
+ const fileBytes = readFileSync4(filePath);
4405
4746
  body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
4406
4747
  if (opts.name)
4407
4748
  body.append("name", opts.name);
@@ -4428,7 +4769,7 @@ Examples:
4428
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) => {
4429
4770
  let body = {};
4430
4771
  if (opts.fromJson) {
4431
- body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
4772
+ body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4432
4773
  } else {
4433
4774
  if (opts.name)
4434
4775
  body.name = opts.name;
@@ -4485,6 +4826,94 @@ Examples:
4485
4826
  return cmd;
4486
4827
  }
4487
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
+
4488
4917
  // src/commands/sites.ts
4489
4918
  var COLUMNS5 = [
4490
4919
  { key: "id", header: "ID", width: 36 },
@@ -4533,8 +4962,8 @@ Examples:
4533
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) => {
4534
4963
  let body;
4535
4964
  if (opts.fromJson) {
4536
- const { readFileSync: readFileSync4 } = await import("fs");
4537
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4965
+ const { readFileSync: readFileSync5 } = await import("fs");
4966
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4538
4967
  } else {
4539
4968
  if (!opts.name || !opts.domain) {
4540
4969
  console.error("Error: --name and --domain are required.");
@@ -4553,8 +4982,8 @@ Examples:
4553
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) => {
4554
4983
  let body;
4555
4984
  if (opts.fromJson) {
4556
- const { readFileSync: readFileSync4 } = await import("fs");
4557
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
4985
+ const { readFileSync: readFileSync5 } = await import("fs");
4986
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4558
4987
  } else {
4559
4988
  body = {};
4560
4989
  if (opts.name)
@@ -4651,8 +5080,8 @@ Examples:
4651
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) => {
4652
5081
  let body;
4653
5082
  if (opts.fromJson) {
4654
- const { readFileSync: readFileSync4 } = await import("fs");
4655
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
5083
+ const { readFileSync: readFileSync5 } = await import("fs");
5084
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4656
5085
  } else {
4657
5086
  if (!opts.site || !opts.name) {
4658
5087
  console.error("Error: --site and --name are required.");
@@ -4673,8 +5102,8 @@ Examples:
4673
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) => {
4674
5103
  let body;
4675
5104
  if (opts.fromJson) {
4676
- const { readFileSync: readFileSync4 } = await import("fs");
4677
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
5105
+ const { readFileSync: readFileSync5 } = await import("fs");
5106
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4678
5107
  } else {
4679
5108
  body = {};
4680
5109
  if (opts.name)
@@ -4810,8 +5239,8 @@ Summary:`);
4810
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) => {
4811
5240
  let body;
4812
5241
  if (opts.fromJson) {
4813
- const { readFileSync: readFileSync4 } = await import("fs");
4814
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
5242
+ const { readFileSync: readFileSync5 } = await import("fs");
5243
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4815
5244
  } else {
4816
5245
  if (!opts.name) {
4817
5246
  console.error("Error: --name is required.");
@@ -4913,8 +5342,8 @@ Examples:
4913
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) => {
4914
5343
  let body;
4915
5344
  if (opts.fromJson) {
4916
- const { readFileSync: readFileSync4 } = await import("fs");
4917
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
5345
+ const { readFileSync: readFileSync5 } = await import("fs");
5346
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
4918
5347
  } else {
4919
5348
  body = {};
4920
5349
  if (opts.autoRecharge !== undefined)
@@ -5109,6 +5538,28 @@ Examples:
5109
5538
  }
5110
5539
 
5111
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
+ }
5112
5563
  async function confirmAction(message, yes) {
5113
5564
  if (yes)
5114
5565
  return;
@@ -5128,6 +5579,8 @@ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
5128
5579
  Examples:
5129
5580
  $ a8techads admin tenants list
5130
5581
  $ a8techads admin tenants get <id>
5582
+ $ a8techads admin campaign-reviews pending
5583
+ $ a8techads admin campaign-reviews approve-all <campaign-id> --yes
5131
5584
  $ a8techads admin audit-logs
5132
5585
  $ a8techads admin system health`);
5133
5586
  const tenants = cmd.command("tenants").description("Tenant management");
@@ -5245,6 +5698,24 @@ Examples:
5245
5698
  }
5246
5699
  console.log(`Tenant ${id} activated.`);
5247
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
+ });
5248
5719
  const members = tenants.command("members").description("Tenant member management");
5249
5720
  addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
5250
5721
  if (!opts.tenant) {
@@ -5293,6 +5764,102 @@ Examples:
5293
5764
  { key: "time", header: "TIME", width: 22 }
5294
5765
  ], opts.format);
5295
5766
  });
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}` });
5797
+ const json = await resp.json();
5798
+ if (!resp.ok) {
5799
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5800
+ process.exit(1);
5801
+ }
5802
+ const rows = (json.data ?? json).map(normalizeReviewCampaign);
5803
+ printData(rows, reviewColumns, opts.format);
5804
+ });
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
+ });
5296
5863
  const system = cmd.command("system").description("System operations");
5297
5864
  addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
5298
5865
  const resp = await apiRequest({ path: "/api/v1/console/system/health" });
@@ -5312,27 +5879,149 @@ Examples:
5312
5879
  }
5313
5880
  console.log("Cache cleared.");
5314
5881
  });
5315
- const tracking = cmd.command("tracking-identities").description("User identity graph stats");
5316
- tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
5317
- const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
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 });
5318
5886
  const json = await resp.json();
5319
5887
  if (!resp.ok) {
5320
5888
  console.error(`Error: ${json.error ?? resp.statusText}`);
5321
5889
  process.exit(1);
5322
5890
  }
5323
- const data = json.data ?? json;
5324
- console.log(`Tracking Identity Stats:`);
5325
- console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
5326
- console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
5327
- console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
5328
- console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
5329
- if (data.earliest_seen)
5330
- console.log(` Earliest Seen: ${data.earliest_seen}`);
5331
- if (data.latest_seen)
5332
- console.log(` Latest Seen: ${data.latest_seen}`);
5891
+ printDetail(json.data ?? json, opts.format);
5333
5892
  });
5334
- const finance = cmd.command("finance").description("Financial overview and audit");
5335
- addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
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) => {
5336
6025
  const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
5337
6026
  const json = await resp.json();
5338
6027
  if (!resp.ok) {
@@ -5425,10 +6114,12 @@ Examples:
5425
6114
  console.error(`Error: ${json.error ?? resp.statusText}`);
5426
6115
  process.exit(1);
5427
6116
  }
5428
- const rows = (json.data ?? json).map((i) => ({
6117
+ const payload = json.data ?? json;
6118
+ const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
6119
+ const rows = invoiceRows.map((i) => ({
5429
6120
  invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
5430
6121
  tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
5431
- amount: i.amount,
6122
+ amount: i.amount ?? i.totalAmount ?? i.total_amount,
5432
6123
  status: i.status,
5433
6124
  dueDate: i.dueDate ?? i.due_date
5434
6125
  }));
@@ -5520,10 +6211,12 @@ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Es
5520
6211
  console.error(`Error: ${json.error ?? resp.statusText}`);
5521
6212
  process.exit(1);
5522
6213
  }
5523
- const rows = (json.data ?? json).map((i) => ({
6214
+ const payload = json.data ?? json;
6215
+ const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
6216
+ const rows = invoiceRows.map((i) => ({
5524
6217
  invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
5525
6218
  tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
5526
- amount: i.amount,
6219
+ amount: i.amount ?? i.totalAmount ?? i.total_amount,
5527
6220
  status: i.status,
5528
6221
  dueDate: i.dueDate ?? i.due_date
5529
6222
  }));
@@ -5841,6 +6534,66 @@ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Es
5841
6534
  }
5842
6535
  console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
5843
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
+ });
5844
6597
  return cmd;
5845
6598
  }
5846
6599
 
@@ -5850,8 +6603,16 @@ var COLUMNS8 = [
5850
6603
  { key: "name", header: "NAME", width: 25 },
5851
6604
  { key: "code", header: "CODE", width: 15 },
5852
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(", ") : "-" },
5853
6608
  { key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
5854
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
+ }
5855
6616
  function createExternalSspCommand() {
5856
6617
  const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
5857
6618
 
@@ -5860,6 +6621,7 @@ Examples:
5860
6621
  $ a8techads external-ssp list
5861
6622
  $ a8techads external-ssp get <id>
5862
6623
  $ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
6624
+ $ a8techads external-ssp update <id> --adm-mode plain_url
5863
6625
  $ a8techads external-ssp activate <id>
5864
6626
  $ a8techads external-ssp pause <id>`);
5865
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) => {
@@ -5878,6 +6640,8 @@ Examples:
5878
6640
  name: p.name,
5879
6641
  code: p.code,
5880
6642
  status: p.status,
6643
+ routingMode: p.routingMode ?? p.routing_mode ?? "global",
6644
+ allowedRegions: p.allowedRegions ?? p.allowed_regions ?? [],
5881
6645
  formats: p.supportedFormats ?? p.supported_formats ?? []
5882
6646
  }));
5883
6647
  printData(rows, COLUMNS8, opts.format);
@@ -5891,10 +6655,11 @@ Examples:
5891
6655
  }
5892
6656
  printDetail(json.data ?? json, opts.format);
5893
6657
  });
5894
- cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").addHelpText("after", `
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", `
5895
6659
  Examples:
5896
6660
  $ a8techads external-ssp create --name "OpenX"
5897
- $ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO`).action(async (opts) => {
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) => {
5898
6663
  if (!opts.name) {
5899
6664
  console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
5900
6665
  process.exit(1);
@@ -5903,7 +6668,17 @@ Examples:
5903
6668
  if (opts.code)
5904
6669
  body.code = opts.code;
5905
6670
  if (opts.formats)
5906
- body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
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 };
5907
6682
  const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
5908
6683
  const json = await resp.json();
5909
6684
  if (!resp.ok) {
@@ -5916,13 +6691,25 @@ Examples:
5916
6691
  console.log(` Code: ${data.code}`);
5917
6692
  if (data.apiKey)
5918
6693
  console.log(` API Key: ${data.apiKey}`);
6694
+ if (data.apiSecret)
6695
+ console.log(` API Secret: ${data.apiSecret}`);
5919
6696
  });
5920
- cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").action(async (id, opts) => {
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) => {
5921
6698
  const body = {};
5922
6699
  if (opts.name)
5923
6700
  body.name = opts.name;
5924
6701
  if (opts.formats)
5925
- body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
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 };
5926
6713
  const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
5927
6714
  if (!resp.ok) {
5928
6715
  const j = await resp.json();
@@ -5931,6 +6718,24 @@ Examples:
5931
6718
  }
5932
6719
  console.log(`Partner ${id} updated.`);
5933
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
+ });
5934
6739
  cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
5935
6740
  if (!opts.yes) {
5936
6741
  console.error("Add --yes to confirm deletion.");
@@ -6019,8 +6824,8 @@ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").ac
6019
6824
  console.error("Error: --from-json is required for settings update.");
6020
6825
  process.exit(1);
6021
6826
  }
6022
- const { readFileSync: readFileSync4 } = await import("fs");
6023
- const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6827
+ const { readFileSync: readFileSync5 } = await import("fs");
6828
+ const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6024
6829
  const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
6025
6830
  if (!resp.ok) {
6026
6831
  const j = await resp.json();
@@ -6032,8 +6837,8 @@ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").ac
6032
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) => {
6033
6838
  let body;
6034
6839
  if (opts.fromJson) {
6035
- const { readFileSync: readFileSync4 } = await import("fs");
6036
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6840
+ const { readFileSync: readFileSync5 } = await import("fs");
6841
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6037
6842
  } else {
6038
6843
  body = {};
6039
6844
  if (opts.companyName)
@@ -6155,8 +6960,8 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6155
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) => {
6156
6961
  let body;
6157
6962
  if (opts.fromJson) {
6158
- const { readFileSync: readFileSync4 } = await import("fs");
6159
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
6963
+ const { readFileSync: readFileSync5 } = await import("fs");
6964
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6160
6965
  } else {
6161
6966
  if (!opts.name) {
6162
6967
  console.error("Error: --name is required");
@@ -6197,8 +7002,8 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6197
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) => {
6198
7003
  let body;
6199
7004
  if (opts.fromJson) {
6200
- const { readFileSync: readFileSync4 } = await import("fs");
6201
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
7005
+ const { readFileSync: readFileSync5 } = await import("fs");
7006
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6202
7007
  } else {
6203
7008
  body = {};
6204
7009
  if (opts.name)
@@ -6363,8 +7168,8 @@ Examples:
6363
7168
  }
6364
7169
  async function buildAlgorithmBody(opts, creating) {
6365
7170
  if (opts.fromJson) {
6366
- const { readFileSync: readFileSync4 } = await import("fs");
6367
- return JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
7171
+ const { readFileSync: readFileSync5 } = await import("fs");
7172
+ return JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6368
7173
  }
6369
7174
  const body = {};
6370
7175
  if (opts.name)
@@ -6530,7 +7335,7 @@ Examples:
6530
7335
  }
6531
7336
 
6532
7337
  // src/commands/simulator.ts
6533
- import { readFileSync as readFileSync4 } from "fs";
7338
+ import { readFileSync as readFileSync5 } from "fs";
6534
7339
  async function confirmAction2(message, yes) {
6535
7340
  if (yes)
6536
7341
  return;
@@ -6566,7 +7371,7 @@ Examples:
6566
7371
  cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
6567
7372
  let body = undefined;
6568
7373
  if (opts.fromJson) {
6569
- body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
7374
+ body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6570
7375
  }
6571
7376
  const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
6572
7377
  const json = await resp.json();
@@ -6607,7 +7412,7 @@ Examples:
6607
7412
  printDetail(json, opts.format);
6608
7413
  });
6609
7414
  config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
6610
- const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
7415
+ const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
6611
7416
  const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
6612
7417
  const json = await resp.json();
6613
7418
  if (!resp.ok) {
@@ -6639,9 +7444,1039 @@ function toConsoleApiBase(apiUrl) {
6639
7444
  return url.origin;
6640
7445
  }
6641
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
+ });
8474
+ return cmd;
8475
+ }
8476
+
6642
8477
  // src/index.ts
6643
8478
  function createProgram() {
6644
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
8479
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.3").addHelpText("after", `
6645
8480
  Command Groups:
6646
8481
  auth Authentication (login, logout, token, status)
6647
8482
  profile Multi-profile management
@@ -6649,6 +8484,7 @@ Command Groups:
6649
8484
  audiences Audience management (DSP)
6650
8485
  campaigns Campaign management (DSP)
6651
8486
  variations Ad variation management (DSP)
8487
+ landing-pages Landing page groups and pages (DSP)
6652
8488
  conversion-goals Conversion goal management (DSP)
6653
8489
  media-assets Media library management (DSP)
6654
8490
  algorithms Bidder algorithm management (DSP)
@@ -6664,6 +8500,7 @@ Command Groups:
6664
8500
  settings Tenant settings
6665
8501
  admin Platform administration (Console)
6666
8502
  external-ssp External SSP partner management (Console)
8503
+ supply-ops Internal supply operations views (Console)
6667
8504
 
6668
8505
  Getting Started:
6669
8506
  $ a8techads auth login # Authenticate
@@ -6678,6 +8515,7 @@ Getting Started:
6678
8515
  program2.addCommand(createCampaignsCommand());
6679
8516
  program2.addCommand(createVariationsCommand());
6680
8517
  program2.addCommand(createMediaAssetsCommand());
8518
+ program2.addCommand(createLandingPagesCommand());
6681
8519
  program2.addCommand(createConversionGoalsCommand());
6682
8520
  program2.addCommand(createAlgorithmsCommand());
6683
8521
  program2.addCommand(createSitesCommand());
@@ -6692,6 +8530,8 @@ Getting Started:
6692
8530
  program2.addCommand(createInvoicesCommand());
6693
8531
  program2.addCommand(createAdminCommand());
6694
8532
  program2.addCommand(createExternalSspCommand());
8533
+ program2.addCommand(createSupplyOpsCommand());
8534
+ program2.addCommand(createValidateCommand());
6695
8535
  return program2;
6696
8536
  }
6697
8537