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