@a8techads/cli 0.4.1 → 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 +1968 -128
- 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";
|
|
@@ -2843,7 +2829,9 @@ async function loginClientCredentials(opts) {
|
|
|
2843
2829
|
const apiUrl = opts.apiUrl ?? DEFAULT_API_URL2;
|
|
2844
2830
|
const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL2;
|
|
2845
2831
|
const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
|
|
2846
|
-
|
|
2832
|
+
if (!opts.silent) {
|
|
2833
|
+
console.log("Authenticating with client credentials...");
|
|
2834
|
+
}
|
|
2847
2835
|
const resp = await fetch(tokenEndpoint, {
|
|
2848
2836
|
method: "POST",
|
|
2849
2837
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -2861,7 +2849,7 @@ async function loginClientCredentials(opts) {
|
|
|
2861
2849
|
const tokens = await resp.json();
|
|
2862
2850
|
const claims = decodeJwt(tokens.access_token);
|
|
2863
2851
|
const authClaims = claims ? getAuthClaims(claims) : null;
|
|
2864
|
-
const profileName = `client:${opts.clientId}`;
|
|
2852
|
+
const profileName = opts.profile ?? `client:${opts.clientId}`;
|
|
2865
2853
|
const tenants = await fetchTenantsForClient(apiUrl, tokens.access_token);
|
|
2866
2854
|
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000).toISOString();
|
|
2867
2855
|
const profileCreds = {
|
|
@@ -2893,19 +2881,21 @@ async function loginClientCredentials(opts) {
|
|
|
2893
2881
|
ctx.current_profile = profileName;
|
|
2894
2882
|
ctx.profiles[profileName] = profileCtx;
|
|
2895
2883
|
saveContext(ctx);
|
|
2896
|
-
|
|
2884
|
+
if (!opts.silent) {
|
|
2885
|
+
console.log(`
|
|
2897
2886
|
Authenticated as ${profileName}`);
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2887
|
+
console.log(`Profile: ${profileName}`);
|
|
2888
|
+
if (tenant) {
|
|
2889
|
+
console.log(`Tenant: ${tenant.tenant_name} (${tenant.tenant_id})`);
|
|
2890
|
+
}
|
|
2891
|
+
console.log(`App: ${app}`);
|
|
2892
|
+
if (capability) {
|
|
2893
|
+
console.log(`Capability: ${capability}`);
|
|
2894
|
+
}
|
|
2895
|
+
console.log(`Token expires: ${expiresAt}`);
|
|
2896
|
+
console.log(`
|
|
2908
2897
|
Note: Client credentials tokens cannot be refreshed. Re-authenticate on expiry.`);
|
|
2898
|
+
}
|
|
2909
2899
|
}
|
|
2910
2900
|
function deriveApp2(tenant) {
|
|
2911
2901
|
if (!tenant)
|
|
@@ -2970,16 +2960,124 @@ function logout(profileName) {
|
|
|
2970
2960
|
console.log(`Logged out from profile "${name}".`);
|
|
2971
2961
|
}
|
|
2972
2962
|
|
|
2963
|
+
// src/store/oauth-client.ts
|
|
2964
|
+
import { join as join3 } from "path";
|
|
2965
|
+
import { homedir as homedir3 } from "os";
|
|
2966
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "fs";
|
|
2967
|
+
var CONFIG_DIR3 = join3(homedir3(), ".alpineads");
|
|
2968
|
+
var OAUTH_CLIENT_PATH = join3(CONFIG_DIR3, "oauth-client.json");
|
|
2969
|
+
var DEFAULT_API_URL3 = "https://api.a8.tech";
|
|
2970
|
+
var DEFAULT_AUTH_URL3 = "https://auth.a8.tech";
|
|
2971
|
+
function ensureDir3() {
|
|
2972
|
+
if (!existsSync3(CONFIG_DIR3)) {
|
|
2973
|
+
mkdirSync3(CONFIG_DIR3, { recursive: true, mode: 448 });
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function defaultClientProfile(clientId) {
|
|
2977
|
+
return `client:${clientId}`;
|
|
2978
|
+
}
|
|
2979
|
+
function loadOAuthClientConfig(profileName) {
|
|
2980
|
+
const fromEnv = loadOAuthClientConfigFromEnv(profileName);
|
|
2981
|
+
if (fromEnv)
|
|
2982
|
+
return fromEnv;
|
|
2983
|
+
const file = loadOAuthClientConfigFile();
|
|
2984
|
+
if (!file)
|
|
2985
|
+
return null;
|
|
2986
|
+
const selectedProfile = profileName ?? file.current_profile;
|
|
2987
|
+
const selected = file.profiles[selectedProfile];
|
|
2988
|
+
if (!selected)
|
|
2989
|
+
return null;
|
|
2990
|
+
return {
|
|
2991
|
+
profile: selectedProfile,
|
|
2992
|
+
...selected
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
function loadOAuthClientConfigFile() {
|
|
2996
|
+
if (!existsSync3(OAUTH_CLIENT_PATH))
|
|
2997
|
+
return null;
|
|
2998
|
+
const raw = JSON.parse(readFileSync3(OAUTH_CLIENT_PATH, "utf-8"));
|
|
2999
|
+
if ("profiles" in raw)
|
|
3000
|
+
return raw;
|
|
3001
|
+
return {
|
|
3002
|
+
version: 1,
|
|
3003
|
+
current_profile: raw.profile,
|
|
3004
|
+
profiles: {
|
|
3005
|
+
[raw.profile]: {
|
|
3006
|
+
client_id: raw.client_id,
|
|
3007
|
+
client_secret: raw.client_secret,
|
|
3008
|
+
api_url: raw.api_url,
|
|
3009
|
+
auth_url: raw.auth_url
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function saveOAuthClientConfig(input) {
|
|
3015
|
+
const profile = input.profile ?? defaultClientProfile(input.clientId);
|
|
3016
|
+
const file = loadOAuthClientConfigFile() ?? {
|
|
3017
|
+
version: 1,
|
|
3018
|
+
current_profile: profile,
|
|
3019
|
+
profiles: {}
|
|
3020
|
+
};
|
|
3021
|
+
file.current_profile = profile;
|
|
3022
|
+
file.profiles[profile] = {
|
|
3023
|
+
client_id: input.clientId,
|
|
3024
|
+
client_secret: input.clientSecret,
|
|
3025
|
+
api_url: input.apiUrl ?? DEFAULT_API_URL3,
|
|
3026
|
+
auth_url: input.authUrl ?? DEFAULT_AUTH_URL3
|
|
3027
|
+
};
|
|
3028
|
+
ensureDir3();
|
|
3029
|
+
writeFileSync3(OAUTH_CLIENT_PATH, JSON.stringify(file, null, 2), {
|
|
3030
|
+
mode: 384
|
|
3031
|
+
});
|
|
3032
|
+
chmodSync3(OAUTH_CLIENT_PATH, 384);
|
|
3033
|
+
return {
|
|
3034
|
+
profile,
|
|
3035
|
+
...file.profiles[profile]
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
function getConfiguredOAuthClientProfiles() {
|
|
3039
|
+
const envConfig = loadOAuthClientConfigFromEnv();
|
|
3040
|
+
const file = loadOAuthClientConfigFile();
|
|
3041
|
+
return Array.from(new Set([
|
|
3042
|
+
...file ? Object.keys(file.profiles) : [],
|
|
3043
|
+
...envConfig ? [envConfig.profile] : []
|
|
3044
|
+
]));
|
|
3045
|
+
}
|
|
3046
|
+
function hasConfiguredOAuthClientProfile(profileName) {
|
|
3047
|
+
return loadOAuthClientConfig(profileName) !== null;
|
|
3048
|
+
}
|
|
3049
|
+
function loadOAuthClientConfigFromEnv(profileName) {
|
|
3050
|
+
const clientId = process.env.A8TECHADS_CLIENT_ID ?? process.env.A8TECHADS_OAUTH_CLIENT_ID;
|
|
3051
|
+
const clientSecret = process.env.A8TECHADS_CLIENT_SECRET ?? process.env.A8TECHADS_OAUTH_CLIENT_SECRET;
|
|
3052
|
+
if (!clientId || !clientSecret)
|
|
3053
|
+
return null;
|
|
3054
|
+
const profile = process.env.A8TECHADS_PROFILE ?? process.env.A8TECHADS_OAUTH_PROFILE ?? defaultClientProfile(clientId);
|
|
3055
|
+
if (profileName && profileName !== profile)
|
|
3056
|
+
return null;
|
|
3057
|
+
return {
|
|
3058
|
+
profile,
|
|
3059
|
+
client_id: clientId,
|
|
3060
|
+
client_secret: clientSecret,
|
|
3061
|
+
api_url: process.env.A8TECHADS_API_URL ?? DEFAULT_API_URL3,
|
|
3062
|
+
auth_url: process.env.A8TECHADS_AUTH_URL ?? DEFAULT_AUTH_URL3
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2973
3066
|
// src/auth/refresh.ts
|
|
2974
3067
|
async function refreshTokenIfNeeded(profileName) {
|
|
2975
3068
|
const creds = loadCredentials();
|
|
3069
|
+
const effectiveProfileName = profileName ?? creds.current_profile;
|
|
2976
3070
|
const profile = profileName ? getProfile(creds, profileName) : getCurrentProfile(creds);
|
|
2977
|
-
if (!profile)
|
|
2978
|
-
return
|
|
3071
|
+
if (!profile) {
|
|
3072
|
+
return await loginFromConfiguredOAuthClient(effectiveProfileName);
|
|
3073
|
+
}
|
|
2979
3074
|
const claims = decodeJwt(profile.access_token);
|
|
2980
3075
|
if (!claims || !isTokenExpired(claims))
|
|
2981
3076
|
return false;
|
|
2982
3077
|
if (!profile.refresh_token) {
|
|
3078
|
+
const refreshed = await loginFromConfiguredOAuthClient(effectiveProfileName, profile.client_id);
|
|
3079
|
+
if (refreshed)
|
|
3080
|
+
return true;
|
|
2983
3081
|
throw new Error('Token expired and no refresh token available. Run "a8techads auth login" to re-authenticate.');
|
|
2984
3082
|
}
|
|
2985
3083
|
const response = await fetch(profile.token_endpoint, {
|
|
@@ -2993,6 +3091,12 @@ async function refreshTokenIfNeeded(profileName) {
|
|
|
2993
3091
|
});
|
|
2994
3092
|
if (!response.ok) {
|
|
2995
3093
|
const text = await response.text();
|
|
3094
|
+
const refreshedClaims = decodeJwt(profile.access_token);
|
|
3095
|
+
const expiresAtMs = profile.expires_at ? Date.parse(profile.expires_at) : NaN;
|
|
3096
|
+
const tokenStillUsable = refreshedClaims && !isTokenExpired(refreshedClaims) || !Number.isNaN(expiresAtMs) && expiresAtMs - Date.now() > 30000;
|
|
3097
|
+
if (tokenStillUsable) {
|
|
3098
|
+
return false;
|
|
3099
|
+
}
|
|
2996
3100
|
throw new Error(`Token refresh failed (${response.status}): ${text}
|
|
2997
3101
|
Run "a8techads auth login" to re-authenticate.`);
|
|
2998
3102
|
}
|
|
@@ -3006,16 +3110,31 @@ Run "a8techads auth login" to re-authenticate.`);
|
|
|
3006
3110
|
saveCredentials(creds);
|
|
3007
3111
|
return true;
|
|
3008
3112
|
}
|
|
3113
|
+
async function loginFromConfiguredOAuthClient(profileName, expectedClientId) {
|
|
3114
|
+
const config = loadOAuthClientConfig(profileName);
|
|
3115
|
+
if (!config)
|
|
3116
|
+
return false;
|
|
3117
|
+
if (expectedClientId && expectedClientId !== config.client_id)
|
|
3118
|
+
return false;
|
|
3119
|
+
await loginClientCredentials({
|
|
3120
|
+
clientId: config.client_id,
|
|
3121
|
+
clientSecret: config.client_secret,
|
|
3122
|
+
apiUrl: config.api_url,
|
|
3123
|
+
authUrl: config.auth_url,
|
|
3124
|
+
profile: config.profile,
|
|
3125
|
+
silent: true
|
|
3126
|
+
});
|
|
3127
|
+
return true;
|
|
3128
|
+
}
|
|
3009
3129
|
|
|
3010
3130
|
// src/auth/token.ts
|
|
3011
3131
|
async function outputToken(profileName) {
|
|
3012
|
-
|
|
3013
|
-
const name = profileName ?? creds.current_profile;
|
|
3014
|
-
await refreshTokenIfNeeded(name);
|
|
3132
|
+
await refreshTokenIfNeeded(profileName);
|
|
3015
3133
|
const freshCreds = loadCredentials();
|
|
3016
|
-
const
|
|
3134
|
+
const freshName = profileName ?? freshCreds.current_profile;
|
|
3135
|
+
const profile = getProfile(freshCreds, freshName);
|
|
3017
3136
|
if (!profile) {
|
|
3018
|
-
console.error(`No credentials for profile "${
|
|
3137
|
+
console.error(`No credentials for profile "${freshName}". Run "a8techads auth login --profile ${freshName}" first.`);
|
|
3019
3138
|
process.exit(1);
|
|
3020
3139
|
}
|
|
3021
3140
|
process.stdout.write(profile.access_token);
|
|
@@ -3030,8 +3149,16 @@ function showAuthStatus(profileNameOverride) {
|
|
|
3030
3149
|
const profileCtx = ctx.profiles[profileName] ?? null;
|
|
3031
3150
|
console.log(`Profile: ${profileName}`);
|
|
3032
3151
|
if (!profile) {
|
|
3033
|
-
|
|
3034
|
-
|
|
3152
|
+
const clientConfig = loadOAuthClientConfig(profileName);
|
|
3153
|
+
if (clientConfig) {
|
|
3154
|
+
console.log("Auth mode: client_credentials (configured)");
|
|
3155
|
+
console.log(`Client ID: ${clientConfig.client_id}`);
|
|
3156
|
+
console.log("Token: not cached (will be obtained on next command)");
|
|
3157
|
+
console.log(`API URL: ${clientConfig.api_url}`);
|
|
3158
|
+
} else {
|
|
3159
|
+
console.log("Auth mode: none");
|
|
3160
|
+
console.log('Token: not authenticated (run "a8techads auth login")');
|
|
3161
|
+
}
|
|
3035
3162
|
return;
|
|
3036
3163
|
}
|
|
3037
3164
|
const isClientCreds = profileName.startsWith("client:");
|
|
@@ -3090,6 +3217,7 @@ function createAuthCommand() {
|
|
|
3090
3217
|
Examples:
|
|
3091
3218
|
$ a8techads auth login # Browser OAuth + PKCE
|
|
3092
3219
|
$ a8techads auth login --client-id X --client-secret Y # Client credentials
|
|
3220
|
+
$ a8techads auth configure-client --client-id X --client-secret Y # Save non-interactive client
|
|
3093
3221
|
$ a8techads auth status # Show current auth state
|
|
3094
3222
|
$ a8techads auth token # Print access token for scripting
|
|
3095
3223
|
$ a8techads auth logout # Clear stored tokens`);
|
|
@@ -3098,10 +3226,10 @@ Examples:
|
|
|
3098
3226
|
Browser mode (default): Opens browser for OAuth 2.1 Authorization Code + PKCE.
|
|
3099
3227
|
Client credentials mode: Non-interactive auth using --client-id and --client-secret.
|
|
3100
3228
|
|
|
3101
|
-
Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser
|
|
3229
|
+
Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser default: default; client default: client:<client-id>)").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").option("--client-id <id>", "OAuth client ID (enables client_credentials flow)").option("--client-secret <secret>", "OAuth client secret (requires --client-id)").option("--force-login", "Force login prompt even if browser session exists (use to switch users)").addHelpText("after", `
|
|
3102
3230
|
Examples:
|
|
3103
3231
|
$ a8techads auth login # Interactive browser login
|
|
3104
|
-
$ a8techads auth login -p
|
|
3232
|
+
$ a8techads auth login -p pilot --api-url https://api.a8.tech
|
|
3105
3233
|
$ a8techads auth login --client-id svc-001 --client-secret s3cret
|
|
3106
3234
|
$ a8techads auth login -p owner --force-login # Switch to different user
|
|
3107
3235
|
|
|
@@ -3113,7 +3241,8 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
|
|
|
3113
3241
|
clientId: opts.clientId,
|
|
3114
3242
|
clientSecret: opts.clientSecret,
|
|
3115
3243
|
apiUrl: opts.apiUrl,
|
|
3116
|
-
authUrl: opts.authUrl
|
|
3244
|
+
authUrl: opts.authUrl,
|
|
3245
|
+
profile: opts.profile
|
|
3117
3246
|
});
|
|
3118
3247
|
} else if (opts.clientId || opts.clientSecret) {
|
|
3119
3248
|
console.error("Error: Both --client-id and --client-secret are required for client credentials flow.");
|
|
@@ -3121,7 +3250,7 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
|
|
|
3121
3250
|
process.exit(1);
|
|
3122
3251
|
} else {
|
|
3123
3252
|
await login({
|
|
3124
|
-
profile: opts.profile,
|
|
3253
|
+
profile: opts.profile ?? "default",
|
|
3125
3254
|
apiUrl: opts.apiUrl,
|
|
3126
3255
|
authUrl: opts.authUrl,
|
|
3127
3256
|
forceLogin: opts.forceLogin
|
|
@@ -3133,12 +3262,41 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
|
|
|
3133
3262
|
process.exit(1);
|
|
3134
3263
|
}
|
|
3135
3264
|
});
|
|
3265
|
+
auth.command("configure-client").description(`Save OAuth client credentials for non-interactive CLI use.
|
|
3266
|
+
|
|
3267
|
+
After configuration, normal CLI commands can obtain and renew access tokens via client_credentials without running auth login.`).requiredOption("--client-id <id>", "OAuth client ID").requiredOption("--client-secret <secret>", "OAuth client secret").option("-p, --profile <name>", "Profile name (default: client:<client-id>)").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").addHelpText("after", `
|
|
3268
|
+
Examples:
|
|
3269
|
+
$ a8techads auth configure-client --client-id svc-001 --client-secret s3cret
|
|
3270
|
+
$ a8techads campaigns list
|
|
3271
|
+
|
|
3272
|
+
Environment-only alternative:
|
|
3273
|
+
$ export A8TECHADS_CLIENT_ID=svc-001
|
|
3274
|
+
$ export A8TECHADS_CLIENT_SECRET=s3cret
|
|
3275
|
+
$ a8techads campaigns list`).action((opts) => {
|
|
3276
|
+
const config = saveOAuthClientConfig({
|
|
3277
|
+
clientId: opts.clientId,
|
|
3278
|
+
clientSecret: opts.clientSecret,
|
|
3279
|
+
apiUrl: opts.apiUrl,
|
|
3280
|
+
authUrl: opts.authUrl,
|
|
3281
|
+
profile: opts.profile
|
|
3282
|
+
});
|
|
3283
|
+
const creds = loadCredentials();
|
|
3284
|
+
creds.current_profile = config.profile;
|
|
3285
|
+
saveCredentials(creds);
|
|
3286
|
+
const ctx = loadContext();
|
|
3287
|
+
ctx.current_profile = config.profile;
|
|
3288
|
+
saveContext(ctx);
|
|
3289
|
+
console.log("OAuth client configured for non-interactive CLI use.");
|
|
3290
|
+
console.log(`Profile: ${config.profile}`);
|
|
3291
|
+
console.log(`API URL: ${config.api_url}`);
|
|
3292
|
+
console.log("The client secret is stored in ~/.alpineads/oauth-client.json with mode 600.");
|
|
3293
|
+
});
|
|
3136
3294
|
auth.command("logout").description(`Clear stored tokens for a profile.
|
|
3137
3295
|
|
|
3138
3296
|
Requires: an existing profile.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
|
|
3139
3297
|
Examples:
|
|
3140
3298
|
$ a8techads auth logout # Logout current profile
|
|
3141
|
-
$ a8techads auth logout -p
|
|
3299
|
+
$ a8techads auth logout -p pilot # Logout specific profile`).action((opts) => {
|
|
3142
3300
|
logout(opts.profile);
|
|
3143
3301
|
});
|
|
3144
3302
|
auth.command("token").description(`Output current access token to stdout.
|
|
@@ -3149,7 +3307,7 @@ cannot be refreshed — re-authenticate if expired.
|
|
|
3149
3307
|
Requires: an active profile with valid credentials.`).option("-p, --profile <name>", "Profile name (default: current profile)").addHelpText("after", `
|
|
3150
3308
|
Examples:
|
|
3151
3309
|
$ a8techads auth token # Print token for current profile
|
|
3152
|
-
$ a8techads auth token -p
|
|
3310
|
+
$ a8techads auth token -p pilot # Print token for specific profile
|
|
3153
3311
|
$ curl -H "Authorization: Bearer $(a8techads auth token)" https://api.a8.tech/api/v1/dsp/me`).action(async (opts) => {
|
|
3154
3312
|
try {
|
|
3155
3313
|
await outputToken(opts.profile);
|
|
@@ -3176,20 +3334,31 @@ function createProfileCommand() {
|
|
|
3176
3334
|
const profile = new Command("profile").description("Profile management");
|
|
3177
3335
|
profile.command("list").description("List all profiles").action(() => {
|
|
3178
3336
|
const creds = loadCredentials();
|
|
3179
|
-
const
|
|
3337
|
+
const clientProfiles = getConfiguredOAuthClientProfiles();
|
|
3338
|
+
const profiles = Array.from(new Set([
|
|
3339
|
+
...Object.keys(creds.profiles),
|
|
3340
|
+
...clientProfiles
|
|
3341
|
+
]));
|
|
3180
3342
|
if (profiles.length === 0) {
|
|
3181
|
-
console.log('No profiles. Run "a8techads auth login" to create one.');
|
|
3343
|
+
console.log('No profiles. Run "a8techads auth login" or "a8techads auth configure-client" to create one.');
|
|
3182
3344
|
return;
|
|
3183
3345
|
}
|
|
3184
3346
|
console.log(`Profiles:
|
|
3185
3347
|
`);
|
|
3186
3348
|
for (const name of profiles) {
|
|
3187
3349
|
const p = creds.profiles[name];
|
|
3350
|
+
const clientConfig = loadOAuthClientConfig(name);
|
|
3188
3351
|
const marker = name === creds.current_profile ? " (active)" : "";
|
|
3189
3352
|
console.log(` ${name}${marker}`);
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3353
|
+
if (p) {
|
|
3354
|
+
console.log(` Email: ${p.email}`);
|
|
3355
|
+
console.log(` API: ${p.api_url}`);
|
|
3356
|
+
} else if (clientConfig) {
|
|
3357
|
+
console.log(` Auth: client_credentials (configured)`);
|
|
3358
|
+
console.log(` Client ID: ${clientConfig.client_id}`);
|
|
3359
|
+
console.log(` API: ${clientConfig.api_url}`);
|
|
3360
|
+
}
|
|
3361
|
+
if (p?.tenants.length > 0) {
|
|
3193
3362
|
console.log(` Tenants: ${p.tenants.map((t) => `${t.tenant_name} (${t.tenant_id})`).join(", ")}`);
|
|
3194
3363
|
}
|
|
3195
3364
|
console.log();
|
|
@@ -3197,7 +3366,8 @@ function createProfileCommand() {
|
|
|
3197
3366
|
});
|
|
3198
3367
|
profile.command("use").description("Switch active profile").argument("<name>", "Profile name to switch to").action((name) => {
|
|
3199
3368
|
const creds = loadCredentials();
|
|
3200
|
-
|
|
3369
|
+
const clientConfigExists = hasConfiguredOAuthClientProfile(name);
|
|
3370
|
+
if (!creds.profiles[name] && !clientConfigExists) {
|
|
3201
3371
|
console.error(`Profile "${name}" not found. Run "a8techads profile list" to see available profiles.`);
|
|
3202
3372
|
process.exit(1);
|
|
3203
3373
|
}
|
|
@@ -3207,7 +3377,11 @@ function createProfileCommand() {
|
|
|
3207
3377
|
ctx.current_profile = name;
|
|
3208
3378
|
saveContext(ctx);
|
|
3209
3379
|
const p = creds.profiles[name];
|
|
3210
|
-
|
|
3380
|
+
if (p) {
|
|
3381
|
+
console.log(`Switched to profile "${name}" (${p.email})`);
|
|
3382
|
+
} else {
|
|
3383
|
+
console.log(`Switched to profile "${name}" (client_credentials configured)`);
|
|
3384
|
+
}
|
|
3211
3385
|
});
|
|
3212
3386
|
return profile;
|
|
3213
3387
|
}
|
|
@@ -3687,8 +3861,8 @@ Examples:
|
|
|
3687
3861
|
$ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
|
|
3688
3862
|
let body;
|
|
3689
3863
|
if (opts.fromJson) {
|
|
3690
|
-
const { readFileSync:
|
|
3691
|
-
body = JSON.parse(
|
|
3864
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
3865
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
3692
3866
|
} else {
|
|
3693
3867
|
if (!opts.name) {
|
|
3694
3868
|
console.error('Error: --name is required. Run "a8techads audiences create --help".');
|
|
@@ -3794,10 +3968,10 @@ File format (one per line):
|
|
|
3794
3968
|
console.error("Error: --file is required.");
|
|
3795
3969
|
process.exit(1);
|
|
3796
3970
|
}
|
|
3797
|
-
const { readFileSync:
|
|
3971
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
3798
3972
|
let identifiers;
|
|
3799
3973
|
try {
|
|
3800
|
-
const content =
|
|
3974
|
+
const content = readFileSync4(opts.file, "utf-8").trim();
|
|
3801
3975
|
try {
|
|
3802
3976
|
identifiers = JSON.parse(content);
|
|
3803
3977
|
if (!Array.isArray(identifiers))
|
|
@@ -3868,9 +4042,89 @@ var COLUMNS2 = [
|
|
|
3868
4042
|
{ key: "id", header: "ID", width: 36 },
|
|
3869
4043
|
{ key: "name", header: "NAME", width: 30 },
|
|
3870
4044
|
{ key: "status", header: "STATUS", width: 12 },
|
|
4045
|
+
{ key: "supplyProductCount", header: "SUPPLY", width: 8 },
|
|
3871
4046
|
{ key: "budget", header: "BUDGET", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
3872
4047
|
{ key: "spent", header: "SPENT", width: 10, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" }
|
|
3873
4048
|
];
|
|
4049
|
+
function formatScalar(value) {
|
|
4050
|
+
if (value === undefined || value === null)
|
|
4051
|
+
return null;
|
|
4052
|
+
if (Array.isArray(value))
|
|
4053
|
+
return value.length > 0 ? value.join(",") : null;
|
|
4054
|
+
if (typeof value === "object")
|
|
4055
|
+
return JSON.stringify(value);
|
|
4056
|
+
return value;
|
|
4057
|
+
}
|
|
4058
|
+
function formatSupplyProducts(value) {
|
|
4059
|
+
if (!Array.isArray(value))
|
|
4060
|
+
return null;
|
|
4061
|
+
const names = value.map((item) => {
|
|
4062
|
+
if (item && typeof item === "object" && "name" in item)
|
|
4063
|
+
return String(item.name);
|
|
4064
|
+
return null;
|
|
4065
|
+
}).filter(Boolean);
|
|
4066
|
+
return names.length > 0 ? names.join(", ") : null;
|
|
4067
|
+
}
|
|
4068
|
+
function printSectionedCampaignDetail(campaign) {
|
|
4069
|
+
const sections = [
|
|
4070
|
+
{
|
|
4071
|
+
title: "Scope",
|
|
4072
|
+
entries: {
|
|
4073
|
+
id: campaign.id,
|
|
4074
|
+
name: campaign.name,
|
|
4075
|
+
status: campaign.status,
|
|
4076
|
+
campaignMode: campaign.campaignMode,
|
|
4077
|
+
categories: formatScalar(campaign.categories),
|
|
4078
|
+
adFormat: campaign.adFormat,
|
|
4079
|
+
adSize: formatScalar(campaign.adSize),
|
|
4080
|
+
zoneTypes: formatScalar(campaign.zoneTypes)
|
|
4081
|
+
}
|
|
4082
|
+
},
|
|
4083
|
+
{
|
|
4084
|
+
title: "Budget And Pricing",
|
|
4085
|
+
entries: {
|
|
4086
|
+
dailyBudget: campaign.dailyBudget,
|
|
4087
|
+
totalBudget: campaign.totalBudget,
|
|
4088
|
+
pricingModel: campaign.pricingModel,
|
|
4089
|
+
priceSettings: formatScalar(campaign.priceSettings)
|
|
4090
|
+
}
|
|
4091
|
+
},
|
|
4092
|
+
{
|
|
4093
|
+
title: "Supply Binding",
|
|
4094
|
+
entries: {
|
|
4095
|
+
supplyProductIds: formatScalar(campaign.supplyProductIds),
|
|
4096
|
+
supplyProductCount: campaign.supplyProductCount ?? (Array.isArray(campaign.supplyProductIds) ? campaign.supplyProductIds.length : 0),
|
|
4097
|
+
supplyProducts: formatSupplyProducts(campaign.supplyProducts),
|
|
4098
|
+
sspPartnerIds: formatScalar(campaign.sspPartnerIds),
|
|
4099
|
+
sourceMasking: formatScalar(campaign.sourceMasking)
|
|
4100
|
+
}
|
|
4101
|
+
},
|
|
4102
|
+
{
|
|
4103
|
+
title: "Delivery",
|
|
4104
|
+
entries: {
|
|
4105
|
+
startDate: campaign.startDate,
|
|
4106
|
+
endDate: campaign.endDate,
|
|
4107
|
+
timezone: campaign.timezone,
|
|
4108
|
+
frequencyCap: formatScalar(campaign.frequencyCap),
|
|
4109
|
+
targeting: formatScalar(campaign.targeting)
|
|
4110
|
+
}
|
|
4111
|
+
},
|
|
4112
|
+
{
|
|
4113
|
+
title: "Meta",
|
|
4114
|
+
entries: {
|
|
4115
|
+
createdAt: campaign.createdAt,
|
|
4116
|
+
updatedAt: campaign.updatedAt,
|
|
4117
|
+
version: campaign.version
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
];
|
|
4121
|
+
for (const [index, section] of sections.entries()) {
|
|
4122
|
+
console.log(section.title);
|
|
4123
|
+
printDetail(section.entries, "table");
|
|
4124
|
+
if (index < sections.length - 1)
|
|
4125
|
+
console.log("");
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
3874
4128
|
function createCampaignsCommand() {
|
|
3875
4129
|
const cmd = new Command("campaigns").description(`Campaign management (DSP)
|
|
3876
4130
|
|
|
@@ -3879,6 +4133,7 @@ Examples:
|
|
|
3879
4133
|
$ a8techads campaigns list
|
|
3880
4134
|
$ a8techads campaigns get <id>
|
|
3881
4135
|
$ a8techads campaigns create --name "Summer Sale" --budget 500
|
|
4136
|
+
$ a8techads campaigns update <id> --max-bid 6
|
|
3882
4137
|
$ a8techads campaigns pause <id>`);
|
|
3883
4138
|
addFormatOption(cmd.command("list").description("List campaigns with optional filters.").option("--status <status>", "Filter by status (draft, active, paused, etc.)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
3884
4139
|
const params = new URLSearchParams;
|
|
@@ -3895,6 +4150,7 @@ Examples:
|
|
|
3895
4150
|
id: c.id,
|
|
3896
4151
|
name: c.name,
|
|
3897
4152
|
status: c.status,
|
|
4153
|
+
supplyProductCount: c.supplyProductCount ?? (Array.isArray(c.supplyProductIds) ? c.supplyProductIds.length : 0),
|
|
3898
4154
|
budget: c.budget ?? c.dailyBudget,
|
|
3899
4155
|
spent: c.stats?.spend ?? c.spent
|
|
3900
4156
|
}));
|
|
@@ -3907,7 +4163,12 @@ Examples:
|
|
|
3907
4163
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3908
4164
|
process.exit(1);
|
|
3909
4165
|
}
|
|
3910
|
-
|
|
4166
|
+
const data = json.data ?? json;
|
|
4167
|
+
if (opts.format !== "json") {
|
|
4168
|
+
printSectionedCampaignDetail(data);
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
printDetail(data, opts.format);
|
|
3911
4172
|
});
|
|
3912
4173
|
addFormatOption(cmd.command("stats").description("Get campaign performance statistics.").argument("<id>", "Campaign ID").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")).action(async (id, opts) => {
|
|
3913
4174
|
const params = new URLSearchParams;
|
|
@@ -3925,14 +4186,15 @@ Examples:
|
|
|
3925
4186
|
});
|
|
3926
4187
|
cmd.command("create").description(`Create a new campaign.
|
|
3927
4188
|
|
|
3928
|
-
Requires: ADVERTISER capability, advertiser_admin or advertiser_member role.`).option("--name <name>", "Campaign name (required)").option("--budget <amount>", "Daily budget in dollars").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
|
|
4189
|
+
Requires: ADVERTISER capability, advertiser_admin or advertiser_member role.`).option("--name <name>", "Campaign name (required)").option("--budget <amount>", "Daily budget in dollars").option("--max-bid <amount>", "Maximum bid ceiling to store in priceSettings.maxBid").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
|
|
3929
4190
|
Examples:
|
|
3930
4191
|
$ a8techads campaigns create --name "Summer Sale" --budget 500
|
|
4192
|
+
$ a8techads campaigns create --name "Pilot Supply Binding" --budget 500 --supply-products <supply-product-id>
|
|
3931
4193
|
$ a8techads campaigns create --from-json campaign.json`).action(async (opts) => {
|
|
3932
4194
|
let body;
|
|
3933
4195
|
if (opts.fromJson) {
|
|
3934
|
-
const { readFileSync:
|
|
3935
|
-
body = JSON.parse(
|
|
4196
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4197
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
3936
4198
|
} else {
|
|
3937
4199
|
if (!opts.name) {
|
|
3938
4200
|
console.error('Error: --name is required. Run "a8techads campaigns create --help".');
|
|
@@ -3941,6 +4203,11 @@ Examples:
|
|
|
3941
4203
|
body = { name: opts.name };
|
|
3942
4204
|
if (opts.budget)
|
|
3943
4205
|
body.dailyBudget = Number(opts.budget);
|
|
4206
|
+
if (opts.maxBid)
|
|
4207
|
+
body.priceSettings = { maxBid: Number(opts.maxBid) };
|
|
4208
|
+
const supplyProducts = opts.supplyProducts ?? opts.preferredSupply;
|
|
4209
|
+
if (supplyProducts)
|
|
4210
|
+
body.supplyProductIds = parseCsvList(supplyProducts);
|
|
3944
4211
|
}
|
|
3945
4212
|
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/campaigns`, body });
|
|
3946
4213
|
const json = await resp.json();
|
|
@@ -3950,17 +4217,52 @@ Examples:
|
|
|
3950
4217
|
}
|
|
3951
4218
|
console.log(`Campaign created: ${json.data?.id ?? json.id}`);
|
|
3952
4219
|
});
|
|
3953
|
-
cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4220
|
+
cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
3954
4221
|
let body;
|
|
3955
4222
|
if (opts.fromJson) {
|
|
3956
|
-
const { readFileSync:
|
|
3957
|
-
body = JSON.parse(
|
|
4223
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4224
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
3958
4225
|
} else {
|
|
3959
4226
|
body = {};
|
|
3960
4227
|
if (opts.name)
|
|
3961
4228
|
body.name = opts.name;
|
|
3962
4229
|
if (opts.budget)
|
|
3963
4230
|
body.dailyBudget = Number(opts.budget);
|
|
4231
|
+
if (opts.maxBid && opts.clearMaxBid) {
|
|
4232
|
+
console.error("Error: use either --max-bid or --clear-max-bid, not both.");
|
|
4233
|
+
process.exit(1);
|
|
4234
|
+
}
|
|
4235
|
+
const supplyProducts = opts.supplyProducts ?? opts.preferredSupply;
|
|
4236
|
+
const clearSupplyProducts = opts.clearSupplyProducts || opts.clearPreferredSupply;
|
|
4237
|
+
if (supplyProducts && clearSupplyProducts) {
|
|
4238
|
+
console.error("Error: use either --supply-products or --clear-supply-products, not both.");
|
|
4239
|
+
process.exit(1);
|
|
4240
|
+
}
|
|
4241
|
+
const updatesSupplyBindings = Boolean(supplyProducts || clearSupplyProducts);
|
|
4242
|
+
if (updatesSupplyBindings) {
|
|
4243
|
+
const campaign = await fetchCampaign(id);
|
|
4244
|
+
if (String(campaign.status || "").toUpperCase() === "ACTIVE") {
|
|
4245
|
+
console.error("Error: active campaigns cannot change supply product bindings via update.");
|
|
4246
|
+
console.error(`Next steps:`);
|
|
4247
|
+
console.error(` a8techads campaigns pause ${id}`);
|
|
4248
|
+
console.error(` a8techads campaigns update ${id} ${supplyProducts ? `--supply-products ${supplyProducts}` : "--clear-supply-products"}`);
|
|
4249
|
+
console.error(` a8techads campaigns resume ${id}`);
|
|
4250
|
+
process.exit(1);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
if (opts.maxBid || opts.clearMaxBid) {
|
|
4254
|
+
const campaign = await fetchCampaign(id);
|
|
4255
|
+
const priceSettings = normalizePriceSettings(campaign.priceSettings);
|
|
4256
|
+
if (opts.maxBid)
|
|
4257
|
+
priceSettings.maxBid = Number(opts.maxBid);
|
|
4258
|
+
if (opts.clearMaxBid)
|
|
4259
|
+
delete priceSettings.maxBid;
|
|
4260
|
+
body.priceSettings = priceSettings;
|
|
4261
|
+
}
|
|
4262
|
+
if (supplyProducts)
|
|
4263
|
+
body.supplyProductIds = parseCsvList(supplyProducts);
|
|
4264
|
+
if (clearSupplyProducts)
|
|
4265
|
+
body.supplyProductIds = [];
|
|
3964
4266
|
}
|
|
3965
4267
|
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
|
|
3966
4268
|
if (!resp.ok) {
|
|
@@ -4073,8 +4375,8 @@ Examples:
|
|
|
4073
4375
|
if (opts.disable) {
|
|
4074
4376
|
dayParting = null;
|
|
4075
4377
|
} else {
|
|
4076
|
-
const { readFileSync:
|
|
4077
|
-
dayParting = JSON.parse(
|
|
4378
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4379
|
+
dayParting = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4078
4380
|
}
|
|
4079
4381
|
await patchCampaign(id, {
|
|
4080
4382
|
dayParting,
|
|
@@ -4175,6 +4477,9 @@ function normalizeListTargeting(value) {
|
|
|
4175
4477
|
list: Array.isArray(value?.list) ? value.list : []
|
|
4176
4478
|
};
|
|
4177
4479
|
}
|
|
4480
|
+
function normalizePriceSettings(value) {
|
|
4481
|
+
return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4482
|
+
}
|
|
4178
4483
|
function camelizeOption(value) {
|
|
4179
4484
|
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
4180
4485
|
}
|
|
@@ -4225,8 +4530,8 @@ Examples:
|
|
|
4225
4530
|
cmd.command("create").description("Create a new ad variation.").option("--campaign <id>", "Campaign ID (required)").option("--name <name>", "Variation name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4226
4531
|
let body;
|
|
4227
4532
|
if (opts.fromJson) {
|
|
4228
|
-
const { readFileSync:
|
|
4229
|
-
body = JSON.parse(
|
|
4533
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4534
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4230
4535
|
} else {
|
|
4231
4536
|
if (!opts.campaign || !opts.name) {
|
|
4232
4537
|
console.error("Error: --campaign and --name are required.");
|
|
@@ -4245,8 +4550,8 @@ Examples:
|
|
|
4245
4550
|
cmd.command("update").description("Update a variation.").argument("<id>", "Variation ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4246
4551
|
let body;
|
|
4247
4552
|
if (opts.fromJson) {
|
|
4248
|
-
const { readFileSync:
|
|
4249
|
-
body = JSON.parse(
|
|
4553
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4554
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4250
4555
|
} else {
|
|
4251
4556
|
body = {};
|
|
4252
4557
|
if (opts.name)
|
|
@@ -4272,12 +4577,48 @@ Examples:
|
|
|
4272
4577
|
}
|
|
4273
4578
|
console.log(`Variation ${id} deleted.`);
|
|
4274
4579
|
});
|
|
4580
|
+
cmd.command("submit").description("Submit a variation for approval.").argument("<id>", "Variation ID").action(async (id) => {
|
|
4581
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/submit` });
|
|
4582
|
+
if (!resp.ok) {
|
|
4583
|
+
const j = await resp.json();
|
|
4584
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4585
|
+
process.exit(1);
|
|
4586
|
+
}
|
|
4587
|
+
console.log(`Variation ${id} submitted.`);
|
|
4588
|
+
});
|
|
4589
|
+
cmd.command("pause").description("Pause an active variation.").argument("<id>", "Variation ID").action(async (id) => {
|
|
4590
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/pause` });
|
|
4591
|
+
if (!resp.ok) {
|
|
4592
|
+
const j = await resp.json();
|
|
4593
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4594
|
+
process.exit(1);
|
|
4595
|
+
}
|
|
4596
|
+
console.log(`Variation ${id} paused.`);
|
|
4597
|
+
});
|
|
4598
|
+
cmd.command("resume").description("Resume a paused variation.").argument("<id>", "Variation ID").action(async (id) => {
|
|
4599
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/resume` });
|
|
4600
|
+
if (!resp.ok) {
|
|
4601
|
+
const j = await resp.json();
|
|
4602
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4603
|
+
process.exit(1);
|
|
4604
|
+
}
|
|
4605
|
+
console.log(`Variation ${id} resumed.`);
|
|
4606
|
+
});
|
|
4607
|
+
cmd.command("archive").description("Archive a variation.").argument("<id>", "Variation ID").action(async (id) => {
|
|
4608
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/variations/${id}/archive` });
|
|
4609
|
+
if (!resp.ok) {
|
|
4610
|
+
const j = await resp.json();
|
|
4611
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4612
|
+
process.exit(1);
|
|
4613
|
+
}
|
|
4614
|
+
console.log(`Variation ${id} archived.`);
|
|
4615
|
+
});
|
|
4275
4616
|
return cmd;
|
|
4276
4617
|
}
|
|
4277
4618
|
|
|
4278
4619
|
// src/commands/media-assets.ts
|
|
4279
4620
|
import { basename, extname } from "node:path";
|
|
4280
|
-
import { readFileSync as
|
|
4621
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4281
4622
|
var COLUMNS4 = [
|
|
4282
4623
|
{ key: "id", header: "ID", width: 36 },
|
|
4283
4624
|
{ key: "name", header: "NAME", width: 24 },
|
|
@@ -4401,7 +4742,7 @@ Examples:
|
|
|
4401
4742
|
const headers = new Headers(request.init.headers);
|
|
4402
4743
|
headers.delete("Content-Type");
|
|
4403
4744
|
const body = new FormData;
|
|
4404
|
-
const fileBytes =
|
|
4745
|
+
const fileBytes = readFileSync4(filePath);
|
|
4405
4746
|
body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
|
|
4406
4747
|
if (opts.name)
|
|
4407
4748
|
body.append("name", opts.name);
|
|
@@ -4428,7 +4769,7 @@ Examples:
|
|
|
4428
4769
|
cmd.command("update").description("Update media asset metadata.").argument("<id>", "Media asset ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--product-category <value>", "Product category").option("--target-audience <value>", "Target audience").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4429
4770
|
let body = {};
|
|
4430
4771
|
if (opts.fromJson) {
|
|
4431
|
-
body = JSON.parse(
|
|
4772
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4432
4773
|
} else {
|
|
4433
4774
|
if (opts.name)
|
|
4434
4775
|
body.name = opts.name;
|
|
@@ -4485,6 +4826,94 @@ Examples:
|
|
|
4485
4826
|
return cmd;
|
|
4486
4827
|
}
|
|
4487
4828
|
|
|
4829
|
+
// src/commands/landing-pages.ts
|
|
4830
|
+
var GROUP_COLUMNS = [
|
|
4831
|
+
{ key: "id", header: "ID", width: 36 },
|
|
4832
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
4833
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
4834
|
+
{ key: "distributionAlgorithm", header: "DISTRIBUTION", width: 14 },
|
|
4835
|
+
{ key: "pagesCount", header: "PAGES", width: 8 }
|
|
4836
|
+
];
|
|
4837
|
+
var PAGE_COLUMNS = [
|
|
4838
|
+
{ key: "id", header: "ID", width: 36 },
|
|
4839
|
+
{ key: "name", header: "NAME", width: 24 },
|
|
4840
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
4841
|
+
{ key: "weight", header: "WEIGHT", width: 8 },
|
|
4842
|
+
{ key: "url", header: "URL", width: 60 }
|
|
4843
|
+
];
|
|
4844
|
+
function createLandingPagesCommand() {
|
|
4845
|
+
const cmd = new Command("landing-pages").description(`Landing page groups and pages (DSP)
|
|
4846
|
+
|
|
4847
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
4848
|
+
Examples:
|
|
4849
|
+
$ a8techads landing-pages list
|
|
4850
|
+
$ a8techads landing-pages active
|
|
4851
|
+
$ a8techads landing-pages get <group-id>
|
|
4852
|
+
$ a8techads landing-pages pages <group-id>`);
|
|
4853
|
+
addFormatOption(cmd.command("list").description("List landing page groups.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "50")).action(async (opts) => {
|
|
4854
|
+
const params = new URLSearchParams;
|
|
4855
|
+
if (opts.status)
|
|
4856
|
+
params.set("status", opts.status);
|
|
4857
|
+
params.set("limit", opts.limit);
|
|
4858
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups?${params}` });
|
|
4859
|
+
const json = await resp.json();
|
|
4860
|
+
if (!resp.ok) {
|
|
4861
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4862
|
+
process.exit(1);
|
|
4863
|
+
}
|
|
4864
|
+
const rows = (json.data ?? json).map((g) => ({
|
|
4865
|
+
id: g.id,
|
|
4866
|
+
name: g.name,
|
|
4867
|
+
status: g.status,
|
|
4868
|
+
distributionAlgorithm: g.distributionAlgorithm ?? g.distribution_algorithm,
|
|
4869
|
+
pagesCount: g.pagesCount ?? g.pages_count ?? 0
|
|
4870
|
+
}));
|
|
4871
|
+
printData(rows, GROUP_COLUMNS, opts.format);
|
|
4872
|
+
});
|
|
4873
|
+
addFormatOption(cmd.command("active").description("List active landing page groups.")).action(async (_arg, opts) => {
|
|
4874
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/active` });
|
|
4875
|
+
const json = await resp.json();
|
|
4876
|
+
if (!resp.ok) {
|
|
4877
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4878
|
+
process.exit(1);
|
|
4879
|
+
}
|
|
4880
|
+
const rows = (json.data ?? json).map((g) => ({
|
|
4881
|
+
id: g.id,
|
|
4882
|
+
name: g.name,
|
|
4883
|
+
status: g.status,
|
|
4884
|
+
distributionAlgorithm: g.distributionAlgorithm ?? g.distribution_algorithm,
|
|
4885
|
+
pagesCount: g.pagesCount ?? g.pages_count ?? 0
|
|
4886
|
+
}));
|
|
4887
|
+
printData(rows, GROUP_COLUMNS, opts.format);
|
|
4888
|
+
});
|
|
4889
|
+
addFormatOption(cmd.command("get").description("Get landing page group details.").argument("<id>", "Landing page group ID")).action(async (id, opts) => {
|
|
4890
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/${id}` });
|
|
4891
|
+
const json = await resp.json();
|
|
4892
|
+
if (!resp.ok) {
|
|
4893
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4894
|
+
process.exit(1);
|
|
4895
|
+
}
|
|
4896
|
+
printDetail(json.data ?? json, opts.format);
|
|
4897
|
+
});
|
|
4898
|
+
addFormatOption(cmd.command("pages").description("List landing pages in a landing page group.").argument("<group-id>", "Landing page group ID")).action(async (groupId, opts) => {
|
|
4899
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/landing-page-groups/${groupId}/pages` });
|
|
4900
|
+
const json = await resp.json();
|
|
4901
|
+
if (!resp.ok) {
|
|
4902
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4903
|
+
process.exit(1);
|
|
4904
|
+
}
|
|
4905
|
+
const rows = (json.data ?? json).map((page) => ({
|
|
4906
|
+
id: page.id,
|
|
4907
|
+
name: page.name,
|
|
4908
|
+
status: page.status,
|
|
4909
|
+
weight: page.weight,
|
|
4910
|
+
url: page.url
|
|
4911
|
+
}));
|
|
4912
|
+
printData(rows, PAGE_COLUMNS, opts.format);
|
|
4913
|
+
});
|
|
4914
|
+
return cmd;
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4488
4917
|
// src/commands/sites.ts
|
|
4489
4918
|
var COLUMNS5 = [
|
|
4490
4919
|
{ key: "id", header: "ID", width: 36 },
|
|
@@ -4533,8 +4962,8 @@ Examples:
|
|
|
4533
4962
|
cmd.command("create").description("Create a new site.").option("--name <name>", "Site name (required)").option("--domain <domain>", "Site domain (required)").option("--type <type>", "Site type: WEB, APP, CTV", "WEB").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4534
4963
|
let body;
|
|
4535
4964
|
if (opts.fromJson) {
|
|
4536
|
-
const { readFileSync:
|
|
4537
|
-
body = JSON.parse(
|
|
4965
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
4966
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4538
4967
|
} else {
|
|
4539
4968
|
if (!opts.name || !opts.domain) {
|
|
4540
4969
|
console.error("Error: --name and --domain are required.");
|
|
@@ -4553,8 +4982,8 @@ Examples:
|
|
|
4553
4982
|
cmd.command("update").description("Update a site.").argument("<id>", "Site ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4554
4983
|
let body;
|
|
4555
4984
|
if (opts.fromJson) {
|
|
4556
|
-
const { readFileSync:
|
|
4557
|
-
body = JSON.parse(
|
|
4985
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
4986
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4558
4987
|
} else {
|
|
4559
4988
|
body = {};
|
|
4560
4989
|
if (opts.name)
|
|
@@ -4651,8 +5080,8 @@ Examples:
|
|
|
4651
5080
|
cmd.command("create").description("Create a new ad zone.").option("--site <id>", "Site ID (required)").option("--name <name>", "Zone name (required)").option("--format <format>", "Ad format (e.g., banner_300x250)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4652
5081
|
let body;
|
|
4653
5082
|
if (opts.fromJson) {
|
|
4654
|
-
const { readFileSync:
|
|
4655
|
-
body = JSON.parse(
|
|
5083
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
5084
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4656
5085
|
} else {
|
|
4657
5086
|
if (!opts.site || !opts.name) {
|
|
4658
5087
|
console.error("Error: --site and --name are required.");
|
|
@@ -4673,8 +5102,8 @@ Examples:
|
|
|
4673
5102
|
cmd.command("update").description("Update a zone.").argument("<id>", "Zone ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4674
5103
|
let body;
|
|
4675
5104
|
if (opts.fromJson) {
|
|
4676
|
-
const { readFileSync:
|
|
4677
|
-
body = JSON.parse(
|
|
5105
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
5106
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4678
5107
|
} else {
|
|
4679
5108
|
body = {};
|
|
4680
5109
|
if (opts.name)
|
|
@@ -4810,8 +5239,8 @@ Summary:`);
|
|
|
4810
5239
|
cmd.command("create").description("Create a saved report.").option("--name <name>", "Report name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4811
5240
|
let body;
|
|
4812
5241
|
if (opts.fromJson) {
|
|
4813
|
-
const { readFileSync:
|
|
4814
|
-
body = JSON.parse(
|
|
5242
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
5243
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4815
5244
|
} else {
|
|
4816
5245
|
if (!opts.name) {
|
|
4817
5246
|
console.error("Error: --name is required.");
|
|
@@ -4913,8 +5342,8 @@ Examples:
|
|
|
4913
5342
|
cmd.command("settings-update").description("Update billing settings.").option("--auto-recharge <bool>", "Enable/disable auto-recharge (true/false)").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
4914
5343
|
let body;
|
|
4915
5344
|
if (opts.fromJson) {
|
|
4916
|
-
const { readFileSync:
|
|
4917
|
-
body = JSON.parse(
|
|
5345
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
5346
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
4918
5347
|
} else {
|
|
4919
5348
|
body = {};
|
|
4920
5349
|
if (opts.autoRecharge !== undefined)
|
|
@@ -5109,6 +5538,28 @@ Examples:
|
|
|
5109
5538
|
}
|
|
5110
5539
|
|
|
5111
5540
|
// src/commands/admin.ts
|
|
5541
|
+
function parseDurationMs(input) {
|
|
5542
|
+
const trimmed = input.trim();
|
|
5543
|
+
const match = /^(\d+)\s*(ms|s|m|h|d)?$/i.exec(trimmed);
|
|
5544
|
+
if (!match)
|
|
5545
|
+
return 60 * 60 * 1000;
|
|
5546
|
+
const value = parseInt(match[1], 10);
|
|
5547
|
+
const unit = (match[2] || "s").toLowerCase();
|
|
5548
|
+
switch (unit) {
|
|
5549
|
+
case "ms":
|
|
5550
|
+
return value;
|
|
5551
|
+
case "s":
|
|
5552
|
+
return value * 1000;
|
|
5553
|
+
case "m":
|
|
5554
|
+
return value * 60 * 1000;
|
|
5555
|
+
case "h":
|
|
5556
|
+
return value * 60 * 60 * 1000;
|
|
5557
|
+
case "d":
|
|
5558
|
+
return value * 24 * 60 * 60 * 1000;
|
|
5559
|
+
default:
|
|
5560
|
+
return value * 1000;
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5112
5563
|
async function confirmAction(message, yes) {
|
|
5113
5564
|
if (yes)
|
|
5114
5565
|
return;
|
|
@@ -5128,6 +5579,8 @@ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
|
|
|
5128
5579
|
Examples:
|
|
5129
5580
|
$ a8techads admin tenants list
|
|
5130
5581
|
$ a8techads admin tenants get <id>
|
|
5582
|
+
$ a8techads admin campaign-reviews pending
|
|
5583
|
+
$ a8techads admin campaign-reviews approve-all <campaign-id> --yes
|
|
5131
5584
|
$ a8techads admin audit-logs
|
|
5132
5585
|
$ a8techads admin system health`);
|
|
5133
5586
|
const tenants = cmd.command("tenants").description("Tenant management");
|
|
@@ -5245,6 +5698,24 @@ Examples:
|
|
|
5245
5698
|
}
|
|
5246
5699
|
console.log(`Tenant ${id} activated.`);
|
|
5247
5700
|
});
|
|
5701
|
+
tenants.command("delete").description("Delete a tenant, its members, and associated data.").argument("<id>", "Tenant ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5702
|
+
await confirmAction(`Delete tenant ${id}? This removes the organization, its members, and associated data.`, opts.yes);
|
|
5703
|
+
const resp = await apiRequest({
|
|
5704
|
+
method: "DELETE",
|
|
5705
|
+
path: `${consolePrefix()}/tenants/${id}`
|
|
5706
|
+
});
|
|
5707
|
+
const json = await resp.json().catch(() => null);
|
|
5708
|
+
if (!resp.ok) {
|
|
5709
|
+
console.error(`Error: ${json?.error ?? json?.errors ?? resp.statusText}`);
|
|
5710
|
+
process.exit(1);
|
|
5711
|
+
}
|
|
5712
|
+
const data = json?.data ?? json ?? {};
|
|
5713
|
+
console.log(`Tenant ${id} deleted.`);
|
|
5714
|
+
if (data.deletedUserCount !== undefined)
|
|
5715
|
+
console.log(` Deleted users: ${data.deletedUserCount}`);
|
|
5716
|
+
if (data.removedMembershipCount !== undefined)
|
|
5717
|
+
console.log(` Removed memberships: ${data.removedMembershipCount}`);
|
|
5718
|
+
});
|
|
5248
5719
|
const members = tenants.command("members").description("Tenant member management");
|
|
5249
5720
|
addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
|
|
5250
5721
|
if (!opts.tenant) {
|
|
@@ -5293,6 +5764,102 @@ Examples:
|
|
|
5293
5764
|
{ key: "time", header: "TIME", width: 22 }
|
|
5294
5765
|
], opts.format);
|
|
5295
5766
|
});
|
|
5767
|
+
const campaignReviews = cmd.command("campaign-reviews").description("Campaign review operations (Console).").addHelpText("after", `
|
|
5768
|
+
Examples:
|
|
5769
|
+
$ a8techads admin campaign-reviews pending
|
|
5770
|
+
$ a8techads admin campaign-reviews show <campaign-id>
|
|
5771
|
+
$ a8techads admin campaign-reviews approve-all <campaign-id> --yes
|
|
5772
|
+
$ a8techads admin campaign-reviews decline <campaign-id> --reason-code policy --feedback "Needs changes" --yes`);
|
|
5773
|
+
const reviewColumns = [
|
|
5774
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5775
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
5776
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5777
|
+
{ key: "tenantName", header: "TENANT", width: 24 },
|
|
5778
|
+
{ key: "adFormat", header: "FORMAT", width: 12 },
|
|
5779
|
+
{ key: "pendingVariations", header: "PENDING", width: 8 },
|
|
5780
|
+
{ key: "totalVariations", header: "TOTAL", width: 8 }
|
|
5781
|
+
];
|
|
5782
|
+
function normalizeReviewCampaign(campaign) {
|
|
5783
|
+
return {
|
|
5784
|
+
id: campaign.id ?? campaign.campaignId,
|
|
5785
|
+
name: campaign.name,
|
|
5786
|
+
status: campaign.status ?? campaign.reviewStatus,
|
|
5787
|
+
tenantName: campaign.tenantName ?? campaign.tenant?.companyName ?? campaign.tenant?.company_name,
|
|
5788
|
+
adFormat: campaign.adFormat ?? campaign.ad_format,
|
|
5789
|
+
pendingVariations: campaign.pendingVariations ?? campaign.pending_variations,
|
|
5790
|
+
totalVariations: campaign.totalVariations ?? campaign.total_variations,
|
|
5791
|
+
submittedAt: campaign.submittedAt ?? campaign.submitted_at
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
5794
|
+
addFormatOption(campaignReviews.command("pending").description("List campaigns pending review.").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
|
|
5795
|
+
const params = new URLSearchParams({ limit: opts.limit, offset: opts.offset });
|
|
5796
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/campaigns/pending?${params}` });
|
|
5797
|
+
const json = await resp.json();
|
|
5798
|
+
if (!resp.ok) {
|
|
5799
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5800
|
+
process.exit(1);
|
|
5801
|
+
}
|
|
5802
|
+
const rows = (json.data ?? json).map(normalizeReviewCampaign);
|
|
5803
|
+
printData(rows, reviewColumns, opts.format);
|
|
5804
|
+
});
|
|
5805
|
+
addFormatOption(campaignReviews.command("show").description("Show campaign review details, including variations.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
|
|
5806
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/campaigns/${id}/review` });
|
|
5807
|
+
const json = await resp.json();
|
|
5808
|
+
if (!resp.ok) {
|
|
5809
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5810
|
+
process.exit(1);
|
|
5811
|
+
}
|
|
5812
|
+
const data = json.data ?? json;
|
|
5813
|
+
if (opts.format === "json") {
|
|
5814
|
+
printDetail(data, "json");
|
|
5815
|
+
return;
|
|
5816
|
+
}
|
|
5817
|
+
const summary = normalizeReviewCampaign(data);
|
|
5818
|
+
printDetail(summary, "table");
|
|
5819
|
+
const variations = Array.isArray(data.variations) ? data.variations : [];
|
|
5820
|
+
if (variations.length > 0) {
|
|
5821
|
+
console.log(`
|
|
5822
|
+
Variations`);
|
|
5823
|
+
printData(variations.map((variation) => ({
|
|
5824
|
+
id: variation.id,
|
|
5825
|
+
name: variation.name,
|
|
5826
|
+
type: variation.type,
|
|
5827
|
+
status: variation.reviewStatus ?? variation.status,
|
|
5828
|
+
mediaAsset: variation.mediaAsset?.name ?? variation.media_asset?.name
|
|
5829
|
+
})), [
|
|
5830
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5831
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
5832
|
+
{ key: "type", header: "TYPE", width: 12 },
|
|
5833
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5834
|
+
{ key: "mediaAsset", header: "MEDIA", width: 24 }
|
|
5835
|
+
], "table");
|
|
5836
|
+
}
|
|
5837
|
+
});
|
|
5838
|
+
addFormatOption(campaignReviews.command("approve-all").description("Approve all pending variations and activate the campaign atomically.").argument("<id>", "Campaign ID").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
|
|
5839
|
+
await confirmAction(`Approve all pending variations and activate campaign ${id}?`, opts.yes);
|
|
5840
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/campaigns/${id}/approve-all` });
|
|
5841
|
+
const json = await resp.json();
|
|
5842
|
+
if (!resp.ok) {
|
|
5843
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5844
|
+
process.exit(1);
|
|
5845
|
+
}
|
|
5846
|
+
printDetail(json.data ?? json, opts.format);
|
|
5847
|
+
});
|
|
5848
|
+
addFormatOption(campaignReviews.command("decline").description("Decline all pending variations for a campaign atomically.").argument("<id>", "Campaign ID").option("--feedback <text>", "Review feedback").option("--reason-code <code>", "Review reason code").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
|
|
5849
|
+
await confirmAction(`Decline campaign ${id}?`, opts.yes);
|
|
5850
|
+
const body = {};
|
|
5851
|
+
if (opts.feedback)
|
|
5852
|
+
body.feedback = opts.feedback;
|
|
5853
|
+
if (opts.reasonCode)
|
|
5854
|
+
body.reasonCode = opts.reasonCode;
|
|
5855
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/campaigns/${id}/decline`, body });
|
|
5856
|
+
const json = await resp.json();
|
|
5857
|
+
if (!resp.ok) {
|
|
5858
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5859
|
+
process.exit(1);
|
|
5860
|
+
}
|
|
5861
|
+
printDetail(json.data ?? json, opts.format);
|
|
5862
|
+
});
|
|
5296
5863
|
const system = cmd.command("system").description("System operations");
|
|
5297
5864
|
addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
|
|
5298
5865
|
const resp = await apiRequest({ path: "/api/v1/console/system/health" });
|
|
@@ -5312,27 +5879,149 @@ Examples:
|
|
|
5312
5879
|
}
|
|
5313
5880
|
console.log("Cache cleared.");
|
|
5314
5881
|
});
|
|
5315
|
-
const
|
|
5316
|
-
|
|
5317
|
-
const
|
|
5882
|
+
const biddingGate = system.command("bidding-gate").description("Manage bidder runtime gate without redeploy.");
|
|
5883
|
+
addFormatOption(biddingGate.command("show").description("Show bidder runtime gate state.").option("--partner-code <code>", "Show gate state for one supply partner code")).action(async (opts) => {
|
|
5884
|
+
const path = opts.partnerCode ? `/api/v1/console/system/bidding-gate/${encodeURIComponent(String(opts.partnerCode))}` : "/api/v1/console/system/bidding-gate";
|
|
5885
|
+
const resp = await apiRequest({ path });
|
|
5318
5886
|
const json = await resp.json();
|
|
5319
5887
|
if (!resp.ok) {
|
|
5320
5888
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5321
5889
|
process.exit(1);
|
|
5322
5890
|
}
|
|
5323
|
-
|
|
5324
|
-
console.log(`Tracking Identity Stats:`);
|
|
5325
|
-
console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
|
|
5326
|
-
console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
|
|
5327
|
-
console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
|
|
5328
|
-
console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
|
|
5329
|
-
if (data.earliest_seen)
|
|
5330
|
-
console.log(` Earliest Seen: ${data.earliest_seen}`);
|
|
5331
|
-
if (data.latest_seen)
|
|
5332
|
-
console.log(` Latest Seen: ${data.latest_seen}`);
|
|
5891
|
+
printDetail(json.data ?? json, opts.format);
|
|
5333
5892
|
});
|
|
5334
|
-
|
|
5335
|
-
|
|
5893
|
+
biddingGate.command("set").description("Set bidder runtime gate mode.").argument("<mode>", "active | observe_only").option("--partner-code <code>", "Set gate mode for one supply partner code").option("--yes", "Skip confirmation", false).addHelpText("after", `
|
|
5894
|
+
Examples:
|
|
5895
|
+
$ a8techads admin system bidding-gate show
|
|
5896
|
+
$ a8techads admin system bidding-gate set observe_only --yes
|
|
5897
|
+
$ a8techads admin system bidding-gate set active --yes
|
|
5898
|
+
$ a8techads admin system bidding-gate set observe_only --partner-code PROPELLERADS --yes`).action(async (mode, opts) => {
|
|
5899
|
+
const normalizedMode = String(mode).trim();
|
|
5900
|
+
if (!["active", "observe_only"].includes(normalizedMode)) {
|
|
5901
|
+
console.error("Error: mode must be one of: active, observe_only");
|
|
5902
|
+
process.exit(1);
|
|
5903
|
+
}
|
|
5904
|
+
const partnerCode = opts.partnerCode ? String(opts.partnerCode).trim() : "";
|
|
5905
|
+
await confirmAction(partnerCode ? `Set bidder runtime gate for ${partnerCode} to ${normalizedMode}?` : `Set bidder runtime gate to ${normalizedMode}?`, opts.yes);
|
|
5906
|
+
const resp = await apiRequest({
|
|
5907
|
+
method: "PUT",
|
|
5908
|
+
path: partnerCode ? `/api/v1/console/system/bidding-gate/${encodeURIComponent(partnerCode)}` : "/api/v1/console/system/bidding-gate",
|
|
5909
|
+
body: { mode: normalizedMode }
|
|
5910
|
+
});
|
|
5911
|
+
const json = await resp.json();
|
|
5912
|
+
if (!resp.ok) {
|
|
5913
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5914
|
+
process.exit(1);
|
|
5915
|
+
}
|
|
5916
|
+
printDetail(json.data ?? json);
|
|
5917
|
+
});
|
|
5918
|
+
const debug = biddingGate.command("debug").description("Phase 4 per-request pacer debug funnel.");
|
|
5919
|
+
debug.command("arm").description("Arm N debug slot(s) for a partner. Each consumed slot routes one request via the pacer path.").requiredOption("--partner <code>", "Partner code (e.g. HILLTOPADS)").option("--count <n>", "Number of slots to arm", "1").option("--region <region>", "sin | dfw | all", "dfw").addHelpText("after", `
|
|
5920
|
+
Examples:
|
|
5921
|
+
$ a8techads admin system bidding-gate debug arm --partner HILLTOPADS
|
|
5922
|
+
$ a8techads admin system bidding-gate debug arm --partner HILLTOPADS --count 10
|
|
5923
|
+
$ a8techads admin system bidding-gate debug arm --partner HILLTOPADS --region sin --count 1`).action(async (opts) => {
|
|
5924
|
+
const body = {
|
|
5925
|
+
partner_code: String(opts.partner).trim(),
|
|
5926
|
+
count: parseInt(String(opts.count), 10),
|
|
5927
|
+
region: String(opts.region).trim()
|
|
5928
|
+
};
|
|
5929
|
+
const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/arm", body });
|
|
5930
|
+
const json = await resp.json();
|
|
5931
|
+
if (!resp.ok) {
|
|
5932
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5933
|
+
process.exit(1);
|
|
5934
|
+
}
|
|
5935
|
+
printDetail(json.data ?? json);
|
|
5936
|
+
});
|
|
5937
|
+
debug.command("disarm").description("Disarm debug slots. --all wipes every armed partner across regions.").option("--partner <code>", "Partner code to disarm").option("--region <region>", "sin | dfw | all", "all").option("--all", "Disarm every armed partner across all regions", false).action(async (opts) => {
|
|
5938
|
+
if (!opts.all && !opts.partner) {
|
|
5939
|
+
console.error("Error: provide --partner <code> or --all");
|
|
5940
|
+
process.exit(1);
|
|
5941
|
+
}
|
|
5942
|
+
const body = opts.all ? { all: true } : { partner_code: String(opts.partner).trim(), region: String(opts.region).trim() };
|
|
5943
|
+
const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/disarm", body });
|
|
5944
|
+
const json = await resp.json();
|
|
5945
|
+
if (!resp.ok) {
|
|
5946
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5947
|
+
process.exit(1);
|
|
5948
|
+
}
|
|
5949
|
+
printDetail(json.data ?? json);
|
|
5950
|
+
});
|
|
5951
|
+
debug.command("show").description("Show armed counters and cumulative tallies for a partner across regions.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw | all", "all").action(async (opts) => {
|
|
5952
|
+
const params = new URLSearchParams({
|
|
5953
|
+
partner_code: String(opts.partner).trim(),
|
|
5954
|
+
region: String(opts.region).trim()
|
|
5955
|
+
});
|
|
5956
|
+
const resp = await apiRequest({ path: `/api/v1/console/system/bidding-gate/debug/show?${params}` });
|
|
5957
|
+
const json = await resp.json();
|
|
5958
|
+
if (!resp.ok) {
|
|
5959
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5960
|
+
process.exit(1);
|
|
5961
|
+
}
|
|
5962
|
+
printDetail(json.data ?? json);
|
|
5963
|
+
});
|
|
5964
|
+
debug.command("tail").description("Tail captured fallback entries for a partner. Each entry is one request that came back as non-2xx-non-204 from the bidder.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw", "dfw").option("--since <duration>", "How far back to read (e.g. 5m, 1h)", "1h").option("--max <n>", "Cap on entries returned", "100").action(async (opts) => {
|
|
5965
|
+
const sinceMs = Date.now() - parseDurationMs(String(opts.since));
|
|
5966
|
+
const params = new URLSearchParams({
|
|
5967
|
+
partner_code: String(opts.partner).trim(),
|
|
5968
|
+
region: String(opts.region).trim(),
|
|
5969
|
+
since_ms: String(sinceMs),
|
|
5970
|
+
max_count: String(opts.max)
|
|
5971
|
+
});
|
|
5972
|
+
const resp = await apiRequest({ path: `/api/v1/console/system/bidding-gate/debug/tail?${params}` });
|
|
5973
|
+
const json = await resp.json();
|
|
5974
|
+
if (!resp.ok) {
|
|
5975
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5976
|
+
process.exit(1);
|
|
5977
|
+
}
|
|
5978
|
+
printDetail(json.data ?? json);
|
|
5979
|
+
});
|
|
5980
|
+
debug.command("prune").description("Trim captured fallback stream entries older than --max-age.").requiredOption("--partner <code>", "Partner code").option("--region <region>", "sin | dfw | all", "all").option("--max-age <duration>", "Drop entries older than this", "24h").action(async (opts) => {
|
|
5981
|
+
const cutoffMs = Date.now() - parseDurationMs(String(opts.maxAge));
|
|
5982
|
+
const body = {
|
|
5983
|
+
partner_code: String(opts.partner).trim(),
|
|
5984
|
+
region: String(opts.region).trim(),
|
|
5985
|
+
cutoff_ms: cutoffMs
|
|
5986
|
+
};
|
|
5987
|
+
const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/bidding-gate/debug/prune", body });
|
|
5988
|
+
const json = await resp.json();
|
|
5989
|
+
if (!resp.ok) {
|
|
5990
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5991
|
+
process.exit(1);
|
|
5992
|
+
}
|
|
5993
|
+
printDetail(json.data ?? json);
|
|
5994
|
+
});
|
|
5995
|
+
debug.command("preflight").description("Dependency preflight: control-plane reachable, runtime Redis healthy, BILLING_CONFIRM stream alive. Touches no real billing.").action(async () => {
|
|
5996
|
+
const resp = await apiRequest({ path: "/api/v1/console/system/bidding-gate/debug/preflight" });
|
|
5997
|
+
const json = await resp.json();
|
|
5998
|
+
if (!resp.ok) {
|
|
5999
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6000
|
+
process.exit(1);
|
|
6001
|
+
}
|
|
6002
|
+
printDetail(json.data ?? json);
|
|
6003
|
+
});
|
|
6004
|
+
const tracking = cmd.command("tracking-identities").description("User identity graph stats");
|
|
6005
|
+
tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
|
|
6006
|
+
const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
|
|
6007
|
+
const json = await resp.json();
|
|
6008
|
+
if (!resp.ok) {
|
|
6009
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6010
|
+
process.exit(1);
|
|
6011
|
+
}
|
|
6012
|
+
const data = json.data ?? json;
|
|
6013
|
+
console.log(`Tracking Identity Stats:`);
|
|
6014
|
+
console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
|
|
6015
|
+
console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
|
|
6016
|
+
console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
|
|
6017
|
+
console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
|
|
6018
|
+
if (data.earliest_seen)
|
|
6019
|
+
console.log(` Earliest Seen: ${data.earliest_seen}`);
|
|
6020
|
+
if (data.latest_seen)
|
|
6021
|
+
console.log(` Latest Seen: ${data.latest_seen}`);
|
|
6022
|
+
});
|
|
6023
|
+
const finance = cmd.command("finance").description("Financial overview and audit");
|
|
6024
|
+
addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
|
|
5336
6025
|
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
|
|
5337
6026
|
const json = await resp.json();
|
|
5338
6027
|
if (!resp.ok) {
|
|
@@ -5425,10 +6114,12 @@ Examples:
|
|
|
5425
6114
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5426
6115
|
process.exit(1);
|
|
5427
6116
|
}
|
|
5428
|
-
const
|
|
6117
|
+
const payload = json.data ?? json;
|
|
6118
|
+
const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
|
|
6119
|
+
const rows = invoiceRows.map((i) => ({
|
|
5429
6120
|
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5430
6121
|
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5431
|
-
amount: i.amount,
|
|
6122
|
+
amount: i.amount ?? i.totalAmount ?? i.total_amount,
|
|
5432
6123
|
status: i.status,
|
|
5433
6124
|
dueDate: i.dueDate ?? i.due_date
|
|
5434
6125
|
}));
|
|
@@ -5520,10 +6211,12 @@ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Es
|
|
|
5520
6211
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5521
6212
|
process.exit(1);
|
|
5522
6213
|
}
|
|
5523
|
-
const
|
|
6214
|
+
const payload = json.data ?? json;
|
|
6215
|
+
const invoiceRows = Array.isArray(payload) ? payload : payload.invoices ?? [];
|
|
6216
|
+
const rows = invoiceRows.map((i) => ({
|
|
5524
6217
|
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5525
6218
|
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5526
|
-
amount: i.amount,
|
|
6219
|
+
amount: i.amount ?? i.totalAmount ?? i.total_amount,
|
|
5527
6220
|
status: i.status,
|
|
5528
6221
|
dueDate: i.dueDate ?? i.due_date
|
|
5529
6222
|
}));
|
|
@@ -5841,6 +6534,66 @@ Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Es
|
|
|
5841
6534
|
}
|
|
5842
6535
|
console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
|
|
5843
6536
|
});
|
|
6537
|
+
const lease = cmd.command("lease").description("Pacer lease operator commands (v3)");
|
|
6538
|
+
lease.command("flush").description("Flush chunks/outbox/dedupe for a single campaign.").requiredOption("--campaign <cid>", "Campaign ID to flush").option("--region <region>", 'Region to scope (omit or "all" for every region)', "all").option("--dry-run", "Preview keys without deleting (default true)", false).option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).addHelpText("after", `
|
|
6539
|
+
Examples:
|
|
6540
|
+
$ a8techads admin lease flush --campaign cid-1
|
|
6541
|
+
$ a8techads admin lease flush --campaign cid-1 --region sin
|
|
6542
|
+
$ a8techads admin lease flush --campaign cid-1 --execute --yes`).action(async (opts) => {
|
|
6543
|
+
const dryRun = !opts.execute;
|
|
6544
|
+
if (!dryRun)
|
|
6545
|
+
await confirmAction(`Flush leases for ${opts.campaign} in ${opts.region}?`, opts.yes);
|
|
6546
|
+
const resp = await apiRequest({
|
|
6547
|
+
method: "POST",
|
|
6548
|
+
path: "/internal/pacer/ops/flush-campaign",
|
|
6549
|
+
body: { campaign_id: opts.campaign, region: opts.region, dry_run: dryRun }
|
|
6550
|
+
});
|
|
6551
|
+
const json = await resp.json();
|
|
6552
|
+
if (!resp.ok) {
|
|
6553
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6554
|
+
process.exit(1);
|
|
6555
|
+
}
|
|
6556
|
+
console.log(dryRun ? "[DRY RUN]" : "[APPLIED]");
|
|
6557
|
+
printDetail(json);
|
|
6558
|
+
});
|
|
6559
|
+
lease.command("flush-all").description("Flush all chunks for a region (bounded by active-set discovery).").requiredOption("--region <region>", 'Region to scope ("all" prohibited)').option("--shard-count <n>", "Pacer shard count (default 8)", "8").option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).action(async (opts) => {
|
|
6560
|
+
if (opts.region === "all") {
|
|
6561
|
+
console.error("--region all is prohibited for flush-all; use flush per campaign");
|
|
6562
|
+
process.exit(1);
|
|
6563
|
+
}
|
|
6564
|
+
const dryRun = !opts.execute;
|
|
6565
|
+
if (!dryRun)
|
|
6566
|
+
await confirmAction(`Flush ALL leases in region ${opts.region}?`, opts.yes);
|
|
6567
|
+
const resp = await apiRequest({
|
|
6568
|
+
method: "POST",
|
|
6569
|
+
path: "/internal/pacer/ops/flush-region",
|
|
6570
|
+
body: { region: opts.region, shard_count: parseInt(opts.shardCount, 10), dry_run: dryRun }
|
|
6571
|
+
});
|
|
6572
|
+
const json = await resp.json();
|
|
6573
|
+
if (!resp.ok) {
|
|
6574
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6575
|
+
process.exit(1);
|
|
6576
|
+
}
|
|
6577
|
+
console.log(dryRun ? "[DRY RUN]" : "[APPLIED]");
|
|
6578
|
+
printDetail(json);
|
|
6579
|
+
});
|
|
6580
|
+
lease.command("reset-daily").description("Reset the pacer-side daily_spent projection.").option("--shard-count <n>", "Pacer shard count (default 8)", "8").option("--execute", "Actually delete; otherwise dry-run is forced", false).option("--yes", "Skip confirmation when --execute is given", false).action(async (opts) => {
|
|
6581
|
+
const dryRun = !opts.execute;
|
|
6582
|
+
if (!dryRun)
|
|
6583
|
+
await confirmAction("Reset daily_spent projection across all active campaigns?", opts.yes);
|
|
6584
|
+
const resp = await apiRequest({
|
|
6585
|
+
method: "POST",
|
|
6586
|
+
path: "/internal/pacer/ops/reset-daily",
|
|
6587
|
+
body: { shard_count: parseInt(opts.shardCount, 10), dry_run: dryRun }
|
|
6588
|
+
});
|
|
6589
|
+
const json = await resp.json();
|
|
6590
|
+
if (!resp.ok) {
|
|
6591
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6592
|
+
process.exit(1);
|
|
6593
|
+
}
|
|
6594
|
+
console.log(dryRun ? "[DRY RUN]" : "[APPLIED]");
|
|
6595
|
+
printDetail(json);
|
|
6596
|
+
});
|
|
5844
6597
|
return cmd;
|
|
5845
6598
|
}
|
|
5846
6599
|
|
|
@@ -5850,8 +6603,16 @@ var COLUMNS8 = [
|
|
|
5850
6603
|
{ key: "name", header: "NAME", width: 25 },
|
|
5851
6604
|
{ key: "code", header: "CODE", width: 15 },
|
|
5852
6605
|
{ key: "status", header: "STATUS", width: 12 },
|
|
6606
|
+
{ key: "routingMode", header: "ROUTING", width: 12 },
|
|
6607
|
+
{ key: "allowedRegions", header: "ALLOWED", width: 18, format: (v) => Array.isArray(v) ? v.join(", ") : "-" },
|
|
5853
6608
|
{ key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
|
|
5854
6609
|
];
|
|
6610
|
+
function parseCsvList2(value) {
|
|
6611
|
+
if (!value)
|
|
6612
|
+
return;
|
|
6613
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
6614
|
+
return items.length > 0 ? items : undefined;
|
|
6615
|
+
}
|
|
5855
6616
|
function createExternalSspCommand() {
|
|
5856
6617
|
const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
|
|
5857
6618
|
|
|
@@ -5860,6 +6621,7 @@ Examples:
|
|
|
5860
6621
|
$ a8techads external-ssp list
|
|
5861
6622
|
$ a8techads external-ssp get <id>
|
|
5862
6623
|
$ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
|
|
6624
|
+
$ a8techads external-ssp update <id> --adm-mode plain_url
|
|
5863
6625
|
$ a8techads external-ssp activate <id>
|
|
5864
6626
|
$ a8techads external-ssp pause <id>`);
|
|
5865
6627
|
addFormatOption(cmd.command("list").description("List all external SSP partners.").option("--status <status>", "Filter by status (TESTING, ACTIVE, SUSPENDED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
@@ -5878,6 +6640,8 @@ Examples:
|
|
|
5878
6640
|
name: p.name,
|
|
5879
6641
|
code: p.code,
|
|
5880
6642
|
status: p.status,
|
|
6643
|
+
routingMode: p.routingMode ?? p.routing_mode ?? "global",
|
|
6644
|
+
allowedRegions: p.allowedRegions ?? p.allowed_regions ?? [],
|
|
5881
6645
|
formats: p.supportedFormats ?? p.supported_formats ?? []
|
|
5882
6646
|
}));
|
|
5883
6647
|
printData(rows, COLUMNS8, opts.format);
|
|
@@ -5891,10 +6655,11 @@ Examples:
|
|
|
5891
6655
|
}
|
|
5892
6656
|
printDetail(json.data ?? json, opts.format);
|
|
5893
6657
|
});
|
|
5894
|
-
cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").addHelpText("after", `
|
|
6658
|
+
cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").option("--region <region>", "Fallback region (legacy compatibility field)").option("--routing-mode <mode>", "Routing mode (global or pinned)").option("--allowed-regions <regions>", "Allowed regions, comma-separated").option("--preferred-regions <regions>", "Preferred regions, comma-separated").option("--adm-mode <mode>", "ADM response mode (markup or plain_url)").addHelpText("after", `
|
|
5895
6659
|
Examples:
|
|
5896
6660
|
$ a8techads external-ssp create --name "OpenX"
|
|
5897
|
-
$ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO
|
|
6661
|
+
$ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO
|
|
6662
|
+
$ a8techads external-ssp create --name "PropellerAds" --code PROPELLERADS --formats BANNER,VIDEO,NATIVE,DIRECT_LINK --routing-mode global --allowed-regions sin,dfw --adm-mode plain_url`).action(async (opts) => {
|
|
5898
6663
|
if (!opts.name) {
|
|
5899
6664
|
console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
|
|
5900
6665
|
process.exit(1);
|
|
@@ -5903,7 +6668,17 @@ Examples:
|
|
|
5903
6668
|
if (opts.code)
|
|
5904
6669
|
body.code = opts.code;
|
|
5905
6670
|
if (opts.formats)
|
|
5906
|
-
body.supportedFormats = opts.formats
|
|
6671
|
+
body.supportedFormats = parseCsvList2(opts.formats);
|
|
6672
|
+
if (opts.region)
|
|
6673
|
+
body.region = opts.region;
|
|
6674
|
+
if (opts.routingMode)
|
|
6675
|
+
body.routingMode = opts.routingMode;
|
|
6676
|
+
if (opts.allowedRegions)
|
|
6677
|
+
body.allowedRegions = parseCsvList2(opts.allowedRegions);
|
|
6678
|
+
if (opts.preferredRegions)
|
|
6679
|
+
body.preferredRegions = parseCsvList2(opts.preferredRegions);
|
|
6680
|
+
if (opts.admMode)
|
|
6681
|
+
body.responseTransform = { admMode: opts.admMode };
|
|
5907
6682
|
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
|
|
5908
6683
|
const json = await resp.json();
|
|
5909
6684
|
if (!resp.ok) {
|
|
@@ -5916,13 +6691,25 @@ Examples:
|
|
|
5916
6691
|
console.log(` Code: ${data.code}`);
|
|
5917
6692
|
if (data.apiKey)
|
|
5918
6693
|
console.log(` API Key: ${data.apiKey}`);
|
|
6694
|
+
if (data.apiSecret)
|
|
6695
|
+
console.log(` API Secret: ${data.apiSecret}`);
|
|
5919
6696
|
});
|
|
5920
|
-
cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").action(async (id, opts) => {
|
|
6697
|
+
cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").option("--region <region>", "Fallback region (legacy compatibility field)").option("--routing-mode <mode>", "Routing mode (global or pinned)").option("--allowed-regions <regions>", "Allowed regions, comma-separated").option("--preferred-regions <regions>", "Preferred regions, comma-separated").option("--adm-mode <mode>", "ADM response mode (markup or plain_url)").action(async (id, opts) => {
|
|
5921
6698
|
const body = {};
|
|
5922
6699
|
if (opts.name)
|
|
5923
6700
|
body.name = opts.name;
|
|
5924
6701
|
if (opts.formats)
|
|
5925
|
-
body.supportedFormats = opts.formats
|
|
6702
|
+
body.supportedFormats = parseCsvList2(opts.formats);
|
|
6703
|
+
if (opts.region)
|
|
6704
|
+
body.region = opts.region;
|
|
6705
|
+
if (opts.routingMode)
|
|
6706
|
+
body.routingMode = opts.routingMode;
|
|
6707
|
+
if (opts.allowedRegions)
|
|
6708
|
+
body.allowedRegions = parseCsvList2(opts.allowedRegions);
|
|
6709
|
+
if (opts.preferredRegions)
|
|
6710
|
+
body.preferredRegions = parseCsvList2(opts.preferredRegions);
|
|
6711
|
+
if (opts.admMode)
|
|
6712
|
+
body.responseTransform = { admMode: opts.admMode };
|
|
5926
6713
|
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
|
|
5927
6714
|
if (!resp.ok) {
|
|
5928
6715
|
const j = await resp.json();
|
|
@@ -5931,6 +6718,24 @@ Examples:
|
|
|
5931
6718
|
}
|
|
5932
6719
|
console.log(`Partner ${id} updated.`);
|
|
5933
6720
|
});
|
|
6721
|
+
cmd.command("regenerate-key").description("Regenerate API credentials for an external SSP partner.").argument("<id>", "Partner ID").action(async (id) => {
|
|
6722
|
+
const resp = await apiRequest({
|
|
6723
|
+
method: "POST",
|
|
6724
|
+
path: `${consolePrefix()}/ssp-partners/${id}/regenerate-key`
|
|
6725
|
+
});
|
|
6726
|
+
const json = await resp.json();
|
|
6727
|
+
if (!resp.ok) {
|
|
6728
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6729
|
+
process.exit(1);
|
|
6730
|
+
}
|
|
6731
|
+
const data = json.data ?? json;
|
|
6732
|
+
const credentials = data.credentials ?? data;
|
|
6733
|
+
console.log(`Partner ${id} credentials regenerated.`);
|
|
6734
|
+
if (credentials.apiKey)
|
|
6735
|
+
console.log(` API Key: ${credentials.apiKey}`);
|
|
6736
|
+
if (credentials.apiSecret)
|
|
6737
|
+
console.log(` API Secret: ${credentials.apiSecret}`);
|
|
6738
|
+
});
|
|
5934
6739
|
cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
5935
6740
|
if (!opts.yes) {
|
|
5936
6741
|
console.error("Add --yes to confirm deletion.");
|
|
@@ -6019,8 +6824,8 @@ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").ac
|
|
|
6019
6824
|
console.error("Error: --from-json is required for settings update.");
|
|
6020
6825
|
process.exit(1);
|
|
6021
6826
|
}
|
|
6022
|
-
const { readFileSync:
|
|
6023
|
-
const body = JSON.parse(
|
|
6827
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
6828
|
+
const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6024
6829
|
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
|
|
6025
6830
|
if (!resp.ok) {
|
|
6026
6831
|
const j = await resp.json();
|
|
@@ -6032,8 +6837,8 @@ Requires: admin role.`).option("--from-json <file>", "Update from JSON file").ac
|
|
|
6032
6837
|
cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
6033
6838
|
let body;
|
|
6034
6839
|
if (opts.fromJson) {
|
|
6035
|
-
const { readFileSync:
|
|
6036
|
-
body = JSON.parse(
|
|
6840
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
6841
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6037
6842
|
} else {
|
|
6038
6843
|
body = {};
|
|
6039
6844
|
if (opts.companyName)
|
|
@@ -6155,8 +6960,8 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
6155
6960
|
PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6156
6961
|
let body;
|
|
6157
6962
|
if (opts.fromJson) {
|
|
6158
|
-
const { readFileSync:
|
|
6159
|
-
body = JSON.parse(
|
|
6963
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
6964
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6160
6965
|
} else {
|
|
6161
6966
|
if (!opts.name) {
|
|
6162
6967
|
console.error("Error: --name is required");
|
|
@@ -6197,8 +7002,8 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
|
6197
7002
|
cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
6198
7003
|
let body;
|
|
6199
7004
|
if (opts.fromJson) {
|
|
6200
|
-
const { readFileSync:
|
|
6201
|
-
body = JSON.parse(
|
|
7005
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
7006
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6202
7007
|
} else {
|
|
6203
7008
|
body = {};
|
|
6204
7009
|
if (opts.name)
|
|
@@ -6363,8 +7168,8 @@ Examples:
|
|
|
6363
7168
|
}
|
|
6364
7169
|
async function buildAlgorithmBody(opts, creating) {
|
|
6365
7170
|
if (opts.fromJson) {
|
|
6366
|
-
const { readFileSync:
|
|
6367
|
-
return JSON.parse(
|
|
7171
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
7172
|
+
return JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6368
7173
|
}
|
|
6369
7174
|
const body = {};
|
|
6370
7175
|
if (opts.name)
|
|
@@ -6530,7 +7335,7 @@ Examples:
|
|
|
6530
7335
|
}
|
|
6531
7336
|
|
|
6532
7337
|
// src/commands/simulator.ts
|
|
6533
|
-
import { readFileSync as
|
|
7338
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
6534
7339
|
async function confirmAction2(message, yes) {
|
|
6535
7340
|
if (yes)
|
|
6536
7341
|
return;
|
|
@@ -6566,7 +7371,7 @@ Examples:
|
|
|
6566
7371
|
cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
|
|
6567
7372
|
let body = undefined;
|
|
6568
7373
|
if (opts.fromJson) {
|
|
6569
|
-
body = JSON.parse(
|
|
7374
|
+
body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6570
7375
|
}
|
|
6571
7376
|
const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
|
|
6572
7377
|
const json = await resp.json();
|
|
@@ -6607,7 +7412,7 @@ Examples:
|
|
|
6607
7412
|
printDetail(json, opts.format);
|
|
6608
7413
|
});
|
|
6609
7414
|
config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
|
|
6610
|
-
const body = JSON.parse(
|
|
7415
|
+
const body = JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
|
|
6611
7416
|
const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
|
|
6612
7417
|
const json = await resp.json();
|
|
6613
7418
|
if (!resp.ok) {
|
|
@@ -6639,6 +7444,1036 @@ function toConsoleApiBase(apiUrl) {
|
|
|
6639
7444
|
return url.origin;
|
|
6640
7445
|
}
|
|
6641
7446
|
|
|
7447
|
+
// src/commands/supply-ops.ts
|
|
7448
|
+
function formatRegion(value) {
|
|
7449
|
+
if (Array.isArray(value))
|
|
7450
|
+
return value.join(",");
|
|
7451
|
+
if (value === null || value === undefined)
|
|
7452
|
+
return "-";
|
|
7453
|
+
return String(value);
|
|
7454
|
+
}
|
|
7455
|
+
function formatScalar2(value) {
|
|
7456
|
+
if (value === undefined)
|
|
7457
|
+
return null;
|
|
7458
|
+
if (Array.isArray(value))
|
|
7459
|
+
return value.length > 0 ? value.join(",") : null;
|
|
7460
|
+
if (value === null)
|
|
7461
|
+
return null;
|
|
7462
|
+
if (typeof value === "object")
|
|
7463
|
+
return JSON.stringify(value);
|
|
7464
|
+
return value;
|
|
7465
|
+
}
|
|
7466
|
+
function formatLeaseDetail(row) {
|
|
7467
|
+
return {
|
|
7468
|
+
sectionScope: "scope",
|
|
7469
|
+
id: row.id,
|
|
7470
|
+
leaseType: row.leaseType,
|
|
7471
|
+
scopeId: row.scopeId,
|
|
7472
|
+
scopeName: row.scopeName,
|
|
7473
|
+
supplyProductId: row.supplyProductId,
|
|
7474
|
+
supplyProductName: row.supplyProductName,
|
|
7475
|
+
supplyContractId: row.supplyContractId,
|
|
7476
|
+
supplyContractName: row.supplyContractName,
|
|
7477
|
+
region: formatScalar2(row.region),
|
|
7478
|
+
status: row.status,
|
|
7479
|
+
sectionContractTruth: "contract_truth",
|
|
7480
|
+
contractType: row.contractType,
|
|
7481
|
+
commitmentMetric: row.commitmentMetric,
|
|
7482
|
+
commitmentValue: row.commitmentValue,
|
|
7483
|
+
commitmentWindow: row.commitmentWindow,
|
|
7484
|
+
remainingCommitmentValue: row.remainingCommitmentValue,
|
|
7485
|
+
sectionOperatorIntent: "operator_intent",
|
|
7486
|
+
policyMode: row.policyMode,
|
|
7487
|
+
operatorPriority: row.operatorPriority,
|
|
7488
|
+
allowed: row.allowed,
|
|
7489
|
+
sectionRuntimeState: "runtime_state",
|
|
7490
|
+
effectivePriority: row.effectivePriority ?? row.priority,
|
|
7491
|
+
bidAggressivenessCap: row.bidAggressivenessCap,
|
|
7492
|
+
bidAggressivenessEnabled: row.bidAggressivenessEnabled,
|
|
7493
|
+
executionControlMetric: row.executionControlMetric,
|
|
7494
|
+
executionQuota: row.executionQuota,
|
|
7495
|
+
remainingExecutionCapacity: row.remainingExecutionCapacity,
|
|
7496
|
+
windowSeconds: row.windowSeconds,
|
|
7497
|
+
expiresAt: row.expiresAt,
|
|
7498
|
+
sectionMeta: "meta",
|
|
7499
|
+
createdAt: row.createdAt,
|
|
7500
|
+
updatedAt: row.updatedAt
|
|
7501
|
+
};
|
|
7502
|
+
}
|
|
7503
|
+
function printSectionedLeaseDetail(row) {
|
|
7504
|
+
const sections = [
|
|
7505
|
+
{
|
|
7506
|
+
title: "Scope",
|
|
7507
|
+
entries: {
|
|
7508
|
+
id: row.id,
|
|
7509
|
+
leaseType: row.leaseType,
|
|
7510
|
+
scopeId: row.scopeId,
|
|
7511
|
+
scopeName: row.scopeName,
|
|
7512
|
+
supplyProductId: row.supplyProductId,
|
|
7513
|
+
supplyProductName: row.supplyProductName,
|
|
7514
|
+
supplyContractId: row.supplyContractId,
|
|
7515
|
+
supplyContractName: row.supplyContractName,
|
|
7516
|
+
region: formatScalar2(row.region),
|
|
7517
|
+
status: row.status
|
|
7518
|
+
}
|
|
7519
|
+
},
|
|
7520
|
+
{
|
|
7521
|
+
title: "Contract Truth",
|
|
7522
|
+
entries: {
|
|
7523
|
+
contractType: row.contractType,
|
|
7524
|
+
commitmentMetric: row.commitmentMetric,
|
|
7525
|
+
commitmentValue: row.commitmentValue,
|
|
7526
|
+
commitmentWindow: row.commitmentWindow,
|
|
7527
|
+
remainingCommitmentValue: row.remainingCommitmentValue
|
|
7528
|
+
}
|
|
7529
|
+
},
|
|
7530
|
+
{
|
|
7531
|
+
title: "Operator Intent",
|
|
7532
|
+
entries: {
|
|
7533
|
+
policyMode: row.policyMode,
|
|
7534
|
+
operatorPriority: row.operatorPriority,
|
|
7535
|
+
allowed: row.allowed
|
|
7536
|
+
}
|
|
7537
|
+
},
|
|
7538
|
+
{
|
|
7539
|
+
title: "Runtime State",
|
|
7540
|
+
entries: {
|
|
7541
|
+
effectivePriority: row.effectivePriority ?? row.priority,
|
|
7542
|
+
bidAggressivenessCap: row.bidAggressivenessCap,
|
|
7543
|
+
bidAggressivenessEnabled: row.bidAggressivenessEnabled,
|
|
7544
|
+
executionControlMetric: row.executionControlMetric,
|
|
7545
|
+
executionQuota: row.executionQuota,
|
|
7546
|
+
remainingExecutionCapacity: row.remainingExecutionCapacity,
|
|
7547
|
+
windowSeconds: row.windowSeconds,
|
|
7548
|
+
expiresAt: row.expiresAt
|
|
7549
|
+
}
|
|
7550
|
+
},
|
|
7551
|
+
{
|
|
7552
|
+
title: "Meta",
|
|
7553
|
+
entries: {
|
|
7554
|
+
createdAt: row.createdAt,
|
|
7555
|
+
updatedAt: row.updatedAt
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
];
|
|
7559
|
+
for (const [index, section] of sections.entries()) {
|
|
7560
|
+
console.log(section.title);
|
|
7561
|
+
printDetail(section.entries, "table");
|
|
7562
|
+
if (index < sections.length - 1)
|
|
7563
|
+
console.log("");
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7566
|
+
function formatContractDetail(row) {
|
|
7567
|
+
return {
|
|
7568
|
+
sectionScope: "scope",
|
|
7569
|
+
id: row.id,
|
|
7570
|
+
name: row.name,
|
|
7571
|
+
supplierTenantId: row.supplierTenantId,
|
|
7572
|
+
supplierName: row.supplierName,
|
|
7573
|
+
supplyProductId: row.supplyProductId,
|
|
7574
|
+
supplyProductName: row.supplyProductName,
|
|
7575
|
+
region: formatScalar2(row.region),
|
|
7576
|
+
status: row.status,
|
|
7577
|
+
sectionContractTruth: "contract_truth",
|
|
7578
|
+
contractType: row.contractType,
|
|
7579
|
+
windowType: row.windowType,
|
|
7580
|
+
commitmentMetric: row.commitmentMetric,
|
|
7581
|
+
commitmentValue: row.commitmentValue,
|
|
7582
|
+
commitmentWindow: row.commitmentWindow,
|
|
7583
|
+
matchRules: formatScalar2(row.matchRules),
|
|
7584
|
+
sectionRouting: "routing",
|
|
7585
|
+
priority: row.priority,
|
|
7586
|
+
qpsCap: row.qpsCap,
|
|
7587
|
+
targetFillRate: row.targetFillRate,
|
|
7588
|
+
currency: row.currency,
|
|
7589
|
+
startDate: row.startDate,
|
|
7590
|
+
endDate: row.endDate,
|
|
7591
|
+
sectionMeta: "meta",
|
|
7592
|
+
createdAt: row.createdAt,
|
|
7593
|
+
updatedAt: row.updatedAt
|
|
7594
|
+
};
|
|
7595
|
+
}
|
|
7596
|
+
function printSectionedContractDetail(row) {
|
|
7597
|
+
const sections = [
|
|
7598
|
+
{
|
|
7599
|
+
title: "Scope",
|
|
7600
|
+
entries: {
|
|
7601
|
+
id: row.id,
|
|
7602
|
+
name: row.name,
|
|
7603
|
+
supplierTenantId: row.supplierTenantId,
|
|
7604
|
+
supplierName: row.supplierName,
|
|
7605
|
+
supplyProductId: row.supplyProductId,
|
|
7606
|
+
supplyProductName: row.supplyProductName,
|
|
7607
|
+
region: formatScalar2(row.region),
|
|
7608
|
+
status: row.status
|
|
7609
|
+
}
|
|
7610
|
+
},
|
|
7611
|
+
{
|
|
7612
|
+
title: "Contract Truth",
|
|
7613
|
+
entries: {
|
|
7614
|
+
contractType: row.contractType,
|
|
7615
|
+
windowType: row.windowType,
|
|
7616
|
+
commitmentMetric: row.commitmentMetric,
|
|
7617
|
+
commitmentValue: row.commitmentValue,
|
|
7618
|
+
commitmentWindow: row.commitmentWindow,
|
|
7619
|
+
matchRules: formatScalar2(row.matchRules)
|
|
7620
|
+
}
|
|
7621
|
+
},
|
|
7622
|
+
{
|
|
7623
|
+
title: "Routing",
|
|
7624
|
+
entries: {
|
|
7625
|
+
priority: row.priority,
|
|
7626
|
+
qpsCap: row.qpsCap,
|
|
7627
|
+
targetFillRate: row.targetFillRate,
|
|
7628
|
+
currency: row.currency,
|
|
7629
|
+
startDate: row.startDate,
|
|
7630
|
+
endDate: row.endDate
|
|
7631
|
+
}
|
|
7632
|
+
},
|
|
7633
|
+
{
|
|
7634
|
+
title: "Meta",
|
|
7635
|
+
entries: {
|
|
7636
|
+
createdAt: row.createdAt,
|
|
7637
|
+
updatedAt: row.updatedAt
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
];
|
|
7641
|
+
for (const [index, section] of sections.entries()) {
|
|
7642
|
+
console.log(section.title);
|
|
7643
|
+
printDetail(section.entries, "table");
|
|
7644
|
+
if (index < sections.length - 1)
|
|
7645
|
+
console.log("");
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
function formatProductDetail(row) {
|
|
7649
|
+
return {
|
|
7650
|
+
sectionScope: "scope",
|
|
7651
|
+
id: row.id,
|
|
7652
|
+
tenantId: row.tenantId,
|
|
7653
|
+
name: row.name,
|
|
7654
|
+
region: formatScalar2(row.region),
|
|
7655
|
+
status: row.status,
|
|
7656
|
+
sectionPackaging: "packaging",
|
|
7657
|
+
description: row.description,
|
|
7658
|
+
qualityTier: row.qualityTier,
|
|
7659
|
+
contractCount: row.contractCount,
|
|
7660
|
+
sectionMeta: "meta",
|
|
7661
|
+
createdAt: row.createdAt,
|
|
7662
|
+
updatedAt: row.updatedAt
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7665
|
+
function printSectionedProductDetail(row) {
|
|
7666
|
+
const sections = [
|
|
7667
|
+
{
|
|
7668
|
+
title: "Scope",
|
|
7669
|
+
entries: {
|
|
7670
|
+
id: row.id,
|
|
7671
|
+
tenantId: row.tenantId,
|
|
7672
|
+
name: row.name,
|
|
7673
|
+
region: formatScalar2(row.region),
|
|
7674
|
+
status: row.status
|
|
7675
|
+
}
|
|
7676
|
+
},
|
|
7677
|
+
{
|
|
7678
|
+
title: "Packaging",
|
|
7679
|
+
entries: {
|
|
7680
|
+
description: row.description,
|
|
7681
|
+
qualityTier: row.qualityTier,
|
|
7682
|
+
contractCount: row.contractCount
|
|
7683
|
+
}
|
|
7684
|
+
},
|
|
7685
|
+
{
|
|
7686
|
+
title: "Meta",
|
|
7687
|
+
entries: {
|
|
7688
|
+
createdAt: row.createdAt,
|
|
7689
|
+
updatedAt: row.updatedAt
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
];
|
|
7693
|
+
for (const [index, section] of sections.entries()) {
|
|
7694
|
+
console.log(section.title);
|
|
7695
|
+
printDetail(section.entries, "table");
|
|
7696
|
+
if (index < sections.length - 1)
|
|
7697
|
+
console.log("");
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
7700
|
+
function printSectionedSummary(kind, summary) {
|
|
7701
|
+
if (kind === "products") {
|
|
7702
|
+
const sections2 = [
|
|
7703
|
+
{
|
|
7704
|
+
title: "Packaging Layer",
|
|
7705
|
+
entries: {
|
|
7706
|
+
total: summary.total,
|
|
7707
|
+
totalContracts: summary.totalContracts,
|
|
7708
|
+
regions: formatScalar2(summary.regions)
|
|
7709
|
+
}
|
|
7710
|
+
},
|
|
7711
|
+
{
|
|
7712
|
+
title: "Status",
|
|
7713
|
+
entries: {
|
|
7714
|
+
statuses: formatScalar2(summary.statuses)
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
];
|
|
7718
|
+
for (const [index, section] of sections2.entries()) {
|
|
7719
|
+
console.log(section.title);
|
|
7720
|
+
printDetail(section.entries, "table");
|
|
7721
|
+
if (index < sections2.length - 1)
|
|
7722
|
+
console.log("");
|
|
7723
|
+
}
|
|
7724
|
+
return;
|
|
7725
|
+
}
|
|
7726
|
+
if (kind === "contracts") {
|
|
7727
|
+
const sections2 = [
|
|
7728
|
+
{
|
|
7729
|
+
title: "Contract Truth",
|
|
7730
|
+
entries: {
|
|
7731
|
+
total: summary.total,
|
|
7732
|
+
supplierCount: summary.supplierCount,
|
|
7733
|
+
suppliers: formatScalar2(summary.suppliers),
|
|
7734
|
+
regions: formatScalar2(summary.regions)
|
|
7735
|
+
}
|
|
7736
|
+
},
|
|
7737
|
+
{
|
|
7738
|
+
title: "Status",
|
|
7739
|
+
entries: {
|
|
7740
|
+
statuses: formatScalar2(summary.statuses)
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
];
|
|
7744
|
+
for (const [index, section] of sections2.entries()) {
|
|
7745
|
+
console.log(section.title);
|
|
7746
|
+
printDetail(section.entries, "table");
|
|
7747
|
+
if (index < sections2.length - 1)
|
|
7748
|
+
console.log("");
|
|
7749
|
+
}
|
|
7750
|
+
return;
|
|
7751
|
+
}
|
|
7752
|
+
const sections = [
|
|
7753
|
+
{
|
|
7754
|
+
title: "Operator Intent",
|
|
7755
|
+
entries: {
|
|
7756
|
+
total: summary.total,
|
|
7757
|
+
averageOperatorPriority: summary.averageOperatorPriority,
|
|
7758
|
+
regions: formatScalar2(summary.regions)
|
|
7759
|
+
}
|
|
7760
|
+
},
|
|
7761
|
+
{
|
|
7762
|
+
title: "Runtime State",
|
|
7763
|
+
entries: {
|
|
7764
|
+
totalQuota: summary.totalQuota,
|
|
7765
|
+
totalRemaining: summary.totalRemaining,
|
|
7766
|
+
averageEffectivePriority: summary.averageEffectivePriority,
|
|
7767
|
+
utilizationRatio: summary.utilizationRatio
|
|
7768
|
+
}
|
|
7769
|
+
},
|
|
7770
|
+
{
|
|
7771
|
+
title: "Status",
|
|
7772
|
+
entries: {
|
|
7773
|
+
statuses: formatScalar2(summary.statuses)
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
];
|
|
7777
|
+
for (const [index, section] of sections.entries()) {
|
|
7778
|
+
console.log(section.title);
|
|
7779
|
+
printDetail(section.entries, "table");
|
|
7780
|
+
if (index < sections.length - 1)
|
|
7781
|
+
console.log("");
|
|
7782
|
+
}
|
|
7783
|
+
}
|
|
7784
|
+
var PRODUCT_FULL_COLUMNS = [
|
|
7785
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7786
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7787
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7788
|
+
{ key: "qualityTier", header: "QUALITY", width: 10 },
|
|
7789
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7790
|
+
{ key: "contractCount", header: "CONTRACTS", width: 10 }
|
|
7791
|
+
];
|
|
7792
|
+
var PRODUCT_PACKAGING_COLUMNS = [
|
|
7793
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7794
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7795
|
+
{ key: "qualityTier", header: "QUALITY", width: 10 },
|
|
7796
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7797
|
+
{ key: "contractCount", header: "CONTRACTS", width: 10 }
|
|
7798
|
+
];
|
|
7799
|
+
var PRODUCT_COVERAGE_COLUMNS = [
|
|
7800
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7801
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7802
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7803
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7804
|
+
{ key: "contractCount", header: "CONTRACTS", width: 10 }
|
|
7805
|
+
];
|
|
7806
|
+
function productColumnsForView(view) {
|
|
7807
|
+
switch ((view ?? "packaging").toLowerCase()) {
|
|
7808
|
+
case "coverage":
|
|
7809
|
+
return PRODUCT_COVERAGE_COLUMNS;
|
|
7810
|
+
case "full":
|
|
7811
|
+
return PRODUCT_FULL_COLUMNS;
|
|
7812
|
+
case "packaging":
|
|
7813
|
+
default:
|
|
7814
|
+
return PRODUCT_PACKAGING_COLUMNS;
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
var CONTRACT_FULL_COLUMNS = [
|
|
7818
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7819
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7820
|
+
{ key: "supplierName", header: "SUPPLIER", width: 22 },
|
|
7821
|
+
{ key: "supplyProductName", header: "TRAFFIC PACKAGE", width: 28 },
|
|
7822
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7823
|
+
{ key: "contractType", header: "TYPE", width: 14 },
|
|
7824
|
+
{ key: "commitmentMetric", header: "METRIC", width: 12 },
|
|
7825
|
+
{ key: "commitmentValue", header: "COMMITMENT", width: 14 },
|
|
7826
|
+
{ key: "priority", header: "ROUTING PRI", width: 12 },
|
|
7827
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
7828
|
+
];
|
|
7829
|
+
var CONTRACT_TRUTH_COLUMNS = [
|
|
7830
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7831
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7832
|
+
{ key: "supplierName", header: "SUPPLIER", width: 22 },
|
|
7833
|
+
{ key: "supplyProductName", header: "TRAFFIC PACKAGE", width: 28 },
|
|
7834
|
+
{ key: "contractType", header: "TYPE", width: 14 },
|
|
7835
|
+
{ key: "commitmentMetric", header: "METRIC", width: 12 },
|
|
7836
|
+
{ key: "commitmentValue", header: "COMMITMENT", width: 14 },
|
|
7837
|
+
{ key: "commitmentWindow", header: "WINDOW", width: 12 },
|
|
7838
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
7839
|
+
];
|
|
7840
|
+
var CONTRACT_ROUTING_COLUMNS = [
|
|
7841
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7842
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
7843
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7844
|
+
{ key: "contractType", header: "TYPE", width: 14 },
|
|
7845
|
+
{ key: "priority", header: "ROUTING PRI", width: 12 },
|
|
7846
|
+
{ key: "qpsCap", header: "QPS CAP", width: 10 },
|
|
7847
|
+
{ key: "targetFillRate", header: "FILL TARGET", width: 12 },
|
|
7848
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
7849
|
+
];
|
|
7850
|
+
function contractColumnsForView(view) {
|
|
7851
|
+
switch ((view ?? "truth").toLowerCase()) {
|
|
7852
|
+
case "routing":
|
|
7853
|
+
return CONTRACT_ROUTING_COLUMNS;
|
|
7854
|
+
case "full":
|
|
7855
|
+
return CONTRACT_FULL_COLUMNS;
|
|
7856
|
+
case "truth":
|
|
7857
|
+
default:
|
|
7858
|
+
return CONTRACT_TRUTH_COLUMNS;
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
var LEASE_FULL_COLUMNS = [
|
|
7862
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7863
|
+
{ key: "scopeName", header: "SCOPE", width: 28 },
|
|
7864
|
+
{ key: "leaseType", header: "LEASE TYPE", width: 14 },
|
|
7865
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7866
|
+
{ key: "policyMode", header: "POLICY MODE", width: 16 },
|
|
7867
|
+
{ key: "operatorPriority", header: "OPERATOR", width: 10 },
|
|
7868
|
+
{ key: "effectivePriority", header: "EFFECTIVE", width: 10 },
|
|
7869
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7870
|
+
{ key: "executionQuota", header: "RUNTIME QUOTA", width: 14 },
|
|
7871
|
+
{ key: "remainingExecutionCapacity", header: "RUNTIME REM", width: 12 }
|
|
7872
|
+
];
|
|
7873
|
+
var LEASE_RUNTIME_COLUMNS = [
|
|
7874
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7875
|
+
{ key: "scopeName", header: "SCOPE", width: 28 },
|
|
7876
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7877
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7878
|
+
{ key: "effectivePriority", header: "EFFECTIVE", width: 10 },
|
|
7879
|
+
{ key: "executionControlMetric", header: "METRIC", width: 12 },
|
|
7880
|
+
{ key: "executionQuota", header: "RUNTIME QUOTA", width: 14 },
|
|
7881
|
+
{ key: "remainingExecutionCapacity", header: "RUNTIME REM", width: 12 }
|
|
7882
|
+
];
|
|
7883
|
+
var LEASE_OPERATOR_COLUMNS = [
|
|
7884
|
+
{ key: "id", header: "ID", width: 36 },
|
|
7885
|
+
{ key: "scopeName", header: "SCOPE", width: 28 },
|
|
7886
|
+
{ key: "leaseType", header: "LEASE TYPE", width: 14 },
|
|
7887
|
+
{ key: "region", header: "REGION", width: 14, format: formatRegion },
|
|
7888
|
+
{ key: "status", header: "STATUS", width: 16 },
|
|
7889
|
+
{ key: "policyMode", header: "POLICY MODE", width: 16 },
|
|
7890
|
+
{ key: "operatorPriority", header: "OPERATOR", width: 10 },
|
|
7891
|
+
{ key: "allowed", header: "ALLOWED", width: 8 }
|
|
7892
|
+
];
|
|
7893
|
+
function leaseColumnsForView(view) {
|
|
7894
|
+
switch ((view ?? "runtime").toLowerCase()) {
|
|
7895
|
+
case "operator":
|
|
7896
|
+
return LEASE_OPERATOR_COLUMNS;
|
|
7897
|
+
case "full":
|
|
7898
|
+
return LEASE_FULL_COLUMNS;
|
|
7899
|
+
case "runtime":
|
|
7900
|
+
default:
|
|
7901
|
+
return LEASE_RUNTIME_COLUMNS;
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
async function listResource(path, opts, columns) {
|
|
7905
|
+
const rows = await fetchResource(path, opts);
|
|
7906
|
+
printData(rows, columns, opts.format);
|
|
7907
|
+
}
|
|
7908
|
+
async function fetchResource(path, opts) {
|
|
7909
|
+
const params = new URLSearchParams;
|
|
7910
|
+
params.set("limit", opts.limit ?? "20");
|
|
7911
|
+
if (opts.offset)
|
|
7912
|
+
params.set("offset", opts.offset);
|
|
7913
|
+
const resp = await apiRequest({ path: `${path}?${params.toString()}` });
|
|
7914
|
+
const json = await resp.json();
|
|
7915
|
+
if (!resp.ok) {
|
|
7916
|
+
const errorValue = json.error ?? json.errors ?? resp.statusText;
|
|
7917
|
+
console.error(`Error: ${typeof errorValue === "string" ? errorValue : JSON.stringify(errorValue)}`);
|
|
7918
|
+
process.exit(1);
|
|
7919
|
+
}
|
|
7920
|
+
return applyFilters(json.data ?? [], opts);
|
|
7921
|
+
}
|
|
7922
|
+
function applyFilters(rows, opts) {
|
|
7923
|
+
return rows.filter((row) => {
|
|
7924
|
+
if (opts.id && row.id !== opts.id)
|
|
7925
|
+
return false;
|
|
7926
|
+
if (opts.region) {
|
|
7927
|
+
const regions = Array.isArray(row.region) ? row.region.map((region) => String(region).toLowerCase()) : [String(row.region ?? "").toLowerCase()];
|
|
7928
|
+
if (!regions.includes(opts.region.toLowerCase()))
|
|
7929
|
+
return false;
|
|
7930
|
+
}
|
|
7931
|
+
if (opts.status && String(row.status ?? "").toLowerCase() !== opts.status.toLowerCase())
|
|
7932
|
+
return false;
|
|
7933
|
+
if (opts.type) {
|
|
7934
|
+
const candidate = String(row.leaseType ?? row.contractType ?? "").toLowerCase();
|
|
7935
|
+
if (candidate !== opts.type.toLowerCase())
|
|
7936
|
+
return false;
|
|
7937
|
+
}
|
|
7938
|
+
if (opts.supplier) {
|
|
7939
|
+
const supplier = String(row.supplierName ?? "").toLowerCase();
|
|
7940
|
+
if (!supplier.includes(opts.supplier.toLowerCase()))
|
|
7941
|
+
return false;
|
|
7942
|
+
}
|
|
7943
|
+
if (opts.name) {
|
|
7944
|
+
const name = String(row.name ?? row.scopeName ?? "").toLowerCase();
|
|
7945
|
+
if (!name.includes(opts.name.toLowerCase()))
|
|
7946
|
+
return false;
|
|
7947
|
+
}
|
|
7948
|
+
return true;
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
function summarizeRows(rows, kind) {
|
|
7952
|
+
const statuses = Object.create(null);
|
|
7953
|
+
const regions = new Set;
|
|
7954
|
+
for (const row of rows) {
|
|
7955
|
+
const status = String(row.status ?? "UNKNOWN");
|
|
7956
|
+
statuses[status] = (statuses[status] ?? 0) + 1;
|
|
7957
|
+
const regionValues = Array.isArray(row.region) ? row.region : [row.region];
|
|
7958
|
+
for (const region of regionValues) {
|
|
7959
|
+
const text = String(region ?? "");
|
|
7960
|
+
if (text)
|
|
7961
|
+
regions.add(text);
|
|
7962
|
+
}
|
|
7963
|
+
}
|
|
7964
|
+
if (kind === "leases") {
|
|
7965
|
+
const totalQuota = rows.reduce((sum, row) => sum + Number(row.executionQuota ?? 0), 0);
|
|
7966
|
+
const totalRemaining = rows.reduce((sum, row) => sum + Number(row.remainingExecutionCapacity ?? 0), 0);
|
|
7967
|
+
const averageOperatorPriority = rows.length > 0 ? Number((rows.reduce((sum, row) => sum + Number(row.operatorPriority ?? 0), 0) / rows.length).toFixed(2)) : 0;
|
|
7968
|
+
const averageEffectivePriority = rows.length > 0 ? Number((rows.reduce((sum, row) => sum + Number(row.effectivePriority ?? row.priority ?? 0), 0) / rows.length).toFixed(2)) : 0;
|
|
7969
|
+
return {
|
|
7970
|
+
total: rows.length,
|
|
7971
|
+
regions: Array.from(regions),
|
|
7972
|
+
statuses,
|
|
7973
|
+
totalQuota,
|
|
7974
|
+
totalRemaining,
|
|
7975
|
+
averageOperatorPriority,
|
|
7976
|
+
averageEffectivePriority,
|
|
7977
|
+
utilizationRatio: totalQuota > 0 ? Number(((totalQuota - totalRemaining) / totalQuota).toFixed(4)) : 0
|
|
7978
|
+
};
|
|
7979
|
+
}
|
|
7980
|
+
if (kind === "contracts") {
|
|
7981
|
+
const suppliers = new Set(rows.map((row) => String(row.supplierName ?? "")).filter(Boolean));
|
|
7982
|
+
return {
|
|
7983
|
+
total: rows.length,
|
|
7984
|
+
regions: Array.from(regions),
|
|
7985
|
+
statuses,
|
|
7986
|
+
supplierCount: suppliers.size,
|
|
7987
|
+
suppliers: Array.from(suppliers)
|
|
7988
|
+
};
|
|
7989
|
+
}
|
|
7990
|
+
return {
|
|
7991
|
+
total: rows.length,
|
|
7992
|
+
regions: Array.from(regions),
|
|
7993
|
+
statuses,
|
|
7994
|
+
totalContracts: rows.reduce((sum, row) => sum + Number(row.contractCount ?? 0), 0)
|
|
7995
|
+
};
|
|
7996
|
+
}
|
|
7997
|
+
async function getResource(path, opts) {
|
|
7998
|
+
const id = opts.id;
|
|
7999
|
+
const resp = await apiRequest({ path: `${path}/${id}` });
|
|
8000
|
+
const json = await resp.json();
|
|
8001
|
+
if (!resp.ok) {
|
|
8002
|
+
const errorValue = json.error ?? json.errors ?? resp.statusText;
|
|
8003
|
+
console.error(`Error: ${typeof errorValue === "string" ? errorValue : JSON.stringify(errorValue)}`);
|
|
8004
|
+
process.exit(1);
|
|
8005
|
+
}
|
|
8006
|
+
const data = json.data ?? {};
|
|
8007
|
+
if (path.endsWith("/supply-products") && opts.format !== "json") {
|
|
8008
|
+
printSectionedProductDetail(formatProductDetail(data));
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
8011
|
+
if (path.endsWith("/supply-contracts") && opts.format !== "json") {
|
|
8012
|
+
printSectionedContractDetail(formatContractDetail(data));
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
if (path.endsWith("/allocation-leases") && opts.format !== "json") {
|
|
8016
|
+
printSectionedLeaseDetail(formatLeaseDetail(data));
|
|
8017
|
+
return;
|
|
8018
|
+
}
|
|
8019
|
+
printDetail(data, opts.format);
|
|
8020
|
+
}
|
|
8021
|
+
async function summarizeResource(path, opts, kind) {
|
|
8022
|
+
const rows = await fetchResource(path, { ...opts, limit: opts.limit ?? "200" });
|
|
8023
|
+
const summary = summarizeRows(rows, kind);
|
|
8024
|
+
if (opts.format !== "json") {
|
|
8025
|
+
printSectionedSummary(kind, summary);
|
|
8026
|
+
return;
|
|
8027
|
+
}
|
|
8028
|
+
printDetail(summary, opts.format);
|
|
8029
|
+
}
|
|
8030
|
+
function createSupplyOpsCommand() {
|
|
8031
|
+
const cmd = new Command("supply-ops").description(`Supply operations (Console)
|
|
8032
|
+
|
|
8033
|
+
Aligned to Product -> Contract -> Allocation Lease -> Runtime execution.`).addHelpText("after", `
|
|
8034
|
+
Examples:
|
|
8035
|
+
$ a8techads supply-ops products list
|
|
8036
|
+
$ a8techads supply-ops products summary
|
|
8037
|
+
$ a8techads supply-ops products get <id>
|
|
8038
|
+
$ a8techads supply-ops contracts list --limit 50
|
|
8039
|
+
$ a8techads supply-ops leases list --format json
|
|
8040
|
+
$ a8techads supply-ops leases update <id> --operator-priority 70 --policy-mode delivery_first`);
|
|
8041
|
+
const products = cmd.command("products").description("Traffic package views");
|
|
8042
|
+
addFormatOption(products.command("list").description("List traffic packages.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--view <view>", "packaging, coverage, full", "packaging").option("--name <name>", "Filter by name contains")).action(async (opts) => {
|
|
8043
|
+
await listResource(`${consolePrefix()}/supply-products`, opts, productColumnsForView(opts.view));
|
|
8044
|
+
});
|
|
8045
|
+
addFormatOption(products.command("get").description("Get one traffic package by id.").argument("<id>", "Traffic package ID")).action(async (id, opts) => {
|
|
8046
|
+
await getResource(`${consolePrefix()}/supply-products`, { ...opts, id });
|
|
8047
|
+
});
|
|
8048
|
+
addFormatOption(products.command("summary").description("Summarize traffic packages.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
8049
|
+
await summarizeResource(`${consolePrefix()}/supply-products`, opts, "products");
|
|
8050
|
+
});
|
|
8051
|
+
products.command("create").description("Create a traffic package (product packaging layer).").requiredOption("--name <name>", "Traffic package name").requiredOption("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--quality-tier <tier>", "PREMIUM, STANDARD, REMNANT", "STANDARD").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "REGION_ONLY").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
8052
|
+
let body;
|
|
8053
|
+
if (opts.fromJson) {
|
|
8054
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8055
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8056
|
+
} else {
|
|
8057
|
+
body = {
|
|
8058
|
+
name: opts.name,
|
|
8059
|
+
region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
|
|
8060
|
+
description: opts.description,
|
|
8061
|
+
qualityTier: opts.qualityTier,
|
|
8062
|
+
sourceMaskingMode: opts.sourceMaskingMode,
|
|
8063
|
+
status: opts.status
|
|
8064
|
+
};
|
|
8065
|
+
}
|
|
8066
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/supply-products`, body });
|
|
8067
|
+
const json = await resp.json();
|
|
8068
|
+
if (!resp.ok) {
|
|
8069
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8070
|
+
process.exit(1);
|
|
8071
|
+
}
|
|
8072
|
+
console.log(`Traffic package created: ${json.data?.id ?? json.id}`);
|
|
8073
|
+
});
|
|
8074
|
+
products.command("update").description("Update a traffic package.").argument("<id>", "Traffic package ID").option("--name <name>", "Traffic package name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--quality-tier <tier>", "PREMIUM, STANDARD, REMNANT").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
8075
|
+
let body;
|
|
8076
|
+
if (opts.fromJson) {
|
|
8077
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8078
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8079
|
+
} else {
|
|
8080
|
+
body = {
|
|
8081
|
+
...opts.name ? { name: opts.name } : {},
|
|
8082
|
+
...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
|
|
8083
|
+
...opts.description ? { description: opts.description } : {},
|
|
8084
|
+
...opts.qualityTier ? { qualityTier: opts.qualityTier } : {},
|
|
8085
|
+
...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
|
|
8086
|
+
...opts.status ? { status: opts.status } : {}
|
|
8087
|
+
};
|
|
8088
|
+
}
|
|
8089
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/supply-products/${id}`, body });
|
|
8090
|
+
const json = await resp.json();
|
|
8091
|
+
if (!resp.ok) {
|
|
8092
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8093
|
+
process.exit(1);
|
|
8094
|
+
}
|
|
8095
|
+
console.log(`Traffic package ${id} updated.`);
|
|
8096
|
+
});
|
|
8097
|
+
products.command("delete").description("Delete a traffic package.").argument("<id>", "Traffic package ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
8098
|
+
if (!opts.yes) {
|
|
8099
|
+
console.error("Add --yes to confirm deletion.");
|
|
8100
|
+
process.exit(1);
|
|
8101
|
+
}
|
|
8102
|
+
const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/supply-products/${id}` });
|
|
8103
|
+
if (!resp.ok && resp.status !== 204) {
|
|
8104
|
+
const json = await resp.json();
|
|
8105
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8106
|
+
process.exit(1);
|
|
8107
|
+
}
|
|
8108
|
+
console.log(`Traffic package ${id} deleted.`);
|
|
8109
|
+
});
|
|
8110
|
+
const contracts = cmd.command("contracts").description("Supply contract views");
|
|
8111
|
+
addFormatOption(contracts.command("list").description("List supply contracts.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type").option("--view <view>", "truth, routing, full", "truth").option("--name <name>", "Filter by name contains")).action(async (opts) => {
|
|
8112
|
+
await listResource(`${consolePrefix()}/supply-contracts`, opts, contractColumnsForView(opts.view));
|
|
8113
|
+
});
|
|
8114
|
+
addFormatOption(contracts.command("get").description("Get one supply contract by id.").argument("<id>", "Supply contract ID")).action(async (id, opts) => {
|
|
8115
|
+
await getResource(`${consolePrefix()}/supply-contracts`, { ...opts, id });
|
|
8116
|
+
});
|
|
8117
|
+
addFormatOption(contracts.command("summary").description("Summarize supply contracts.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type")).action(async (opts) => {
|
|
8118
|
+
await summarizeResource(`${consolePrefix()}/supply-contracts`, opts, "contracts");
|
|
8119
|
+
});
|
|
8120
|
+
contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--supplier-tenant-id <id>", "Supplier tenant ID").requiredOption("--supply-product-id <id>", "Traffic package ID").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION", "OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "OPAQUE").option("--priority <n>", "Contract-level routing priority", "0").option("--match-rules <json>", 'Request match rules JSON, e.g. [{"fieldPath":"ext.alpineads.route_key","operator":"eq","value":"hilltopads-open"}]').option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency", "USD").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
8121
|
+
let body;
|
|
8122
|
+
if (opts.fromJson) {
|
|
8123
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8124
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8125
|
+
} else {
|
|
8126
|
+
body = {
|
|
8127
|
+
name: opts.name,
|
|
8128
|
+
region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
|
|
8129
|
+
supplierTenantId: opts.supplierTenantId,
|
|
8130
|
+
supplyProductId: opts.supplyProductId,
|
|
8131
|
+
description: opts.description,
|
|
8132
|
+
contractType: opts.contractType,
|
|
8133
|
+
sourceMaskingMode: opts.sourceMaskingMode,
|
|
8134
|
+
priority: Number(opts.priority),
|
|
8135
|
+
...opts.matchRules ? { matchRules: JSON.parse(opts.matchRules) } : {},
|
|
8136
|
+
...opts.qpsCap ? { qpsCap: Number(opts.qpsCap) } : {},
|
|
8137
|
+
...opts.targetFillRate ? { targetFillRate: opts.targetFillRate } : {},
|
|
8138
|
+
currency: opts.currency,
|
|
8139
|
+
...opts.startDate ? { startDate: opts.startDate } : {},
|
|
8140
|
+
...opts.endDate ? { endDate: opts.endDate } : {},
|
|
8141
|
+
status: opts.status
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/supply-contracts`, body });
|
|
8145
|
+
const json = await resp.json();
|
|
8146
|
+
if (!resp.ok) {
|
|
8147
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8148
|
+
process.exit(1);
|
|
8149
|
+
}
|
|
8150
|
+
console.log(`Supply contract created: ${json.data?.id ?? json.id}`);
|
|
8151
|
+
});
|
|
8152
|
+
contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
8153
|
+
let body;
|
|
8154
|
+
if (opts.fromJson) {
|
|
8155
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8156
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8157
|
+
} else {
|
|
8158
|
+
body = {
|
|
8159
|
+
...opts.name ? { name: opts.name } : {},
|
|
8160
|
+
...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
|
|
8161
|
+
...opts.description ? { description: opts.description } : {},
|
|
8162
|
+
...opts.contractType ? { contractType: opts.contractType } : {},
|
|
8163
|
+
...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
|
|
8164
|
+
...opts.priority ? { priority: Number(opts.priority) } : {},
|
|
8165
|
+
...opts.matchRules ? { matchRules: JSON.parse(opts.matchRules) } : {},
|
|
8166
|
+
...opts.qpsCap ? { qpsCap: Number(opts.qpsCap) } : {},
|
|
8167
|
+
...opts.targetFillRate ? { targetFillRate: opts.targetFillRate } : {},
|
|
8168
|
+
...opts.currency ? { currency: opts.currency } : {},
|
|
8169
|
+
...opts.startDate ? { startDate: opts.startDate } : {},
|
|
8170
|
+
...opts.endDate ? { endDate: opts.endDate } : {},
|
|
8171
|
+
...opts.status ? { status: opts.status } : {}
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8174
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/supply-contracts/${id}`, body });
|
|
8175
|
+
const json = await resp.json();
|
|
8176
|
+
if (!resp.ok) {
|
|
8177
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8178
|
+
process.exit(1);
|
|
8179
|
+
}
|
|
8180
|
+
console.log(`Supply contract ${id} updated.`);
|
|
8181
|
+
});
|
|
8182
|
+
contracts.command("delete").description("Delete a supply contract.").argument("<id>", "Supply contract ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
8183
|
+
if (!opts.yes) {
|
|
8184
|
+
console.error("Add --yes to confirm deletion.");
|
|
8185
|
+
process.exit(1);
|
|
8186
|
+
}
|
|
8187
|
+
const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/supply-contracts/${id}` });
|
|
8188
|
+
if (!resp.ok && resp.status !== 204) {
|
|
8189
|
+
const json = await resp.json();
|
|
8190
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8191
|
+
process.exit(1);
|
|
8192
|
+
}
|
|
8193
|
+
console.log(`Supply contract ${id} deleted.`);
|
|
8194
|
+
});
|
|
8195
|
+
const leases = cmd.command("leases").description("Allocation lease views (operator intent + runtime-derived state)");
|
|
8196
|
+
addFormatOption(leases.command("list").description("List allocation leases with operator intent and runtime-derived state.").option("--limit <n>", "Max results", "20").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--type <type>", "Filter by lease type").option("--view <view>", "runtime, operator, full", "runtime").option("--name <name>", "Filter by scope name contains")).action(async (opts) => {
|
|
8197
|
+
await listResource(`${consolePrefix()}/allocation-leases`, opts, leaseColumnsForView(opts.view));
|
|
8198
|
+
});
|
|
8199
|
+
addFormatOption(leases.command("get").description("Get one allocation lease by id.").argument("<id>", "Allocation lease ID")).action(async (id, opts) => {
|
|
8200
|
+
await getResource(`${consolePrefix()}/allocation-leases`, { ...opts, id });
|
|
8201
|
+
});
|
|
8202
|
+
addFormatOption(leases.command("summary").description("Summarize allocation leases.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--type <type>", "Filter by lease type")).action(async (opts) => {
|
|
8203
|
+
await summarizeResource(`${consolePrefix()}/allocation-leases`, opts, "leases");
|
|
8204
|
+
});
|
|
8205
|
+
leases.command("create").description("Create an allocation lease. CLI only accepts operator intent; runtime quota is allocator-derived.").requiredOption("--supply-contract-id <id>", "Supply contract ID").requiredOption("--region <regions>", "Serving regions, comma-separated").option("--allowed <bool>", "true or false", "true").option("--status <status>", "DRAFT, ACTIVE, PAUSED, EXHAUSTED, ARCHIVED", "DRAFT").option("--policy-mode <mode>", "BALANCED, PRIORITY_FIRST, DELIVERY_FIRST, SPEND_CONSERVATIVE").option("--operator-priority <n>", "Operator priority (0-100)", "50").option("--execution-quota <n>", "Optional runtime quota override for emergency/debug use").option("--window-seconds <n>", "Optional lease window seconds override").option("--expires-at <iso>", "Optional lease expiry override").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
8206
|
+
let body;
|
|
8207
|
+
if (opts.fromJson) {
|
|
8208
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8209
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8210
|
+
} else {
|
|
8211
|
+
body = {
|
|
8212
|
+
supplyContractId: opts.supplyContractId,
|
|
8213
|
+
region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
|
|
8214
|
+
allowed: opts.allowed === "true",
|
|
8215
|
+
status: opts.status,
|
|
8216
|
+
...opts.policyMode ? { policyMode: opts.policyMode } : {},
|
|
8217
|
+
...opts.executionQuota ? { executionQuota: Number(opts.executionQuota) } : {},
|
|
8218
|
+
operatorPriority: Number(opts.operatorPriority),
|
|
8219
|
+
...opts.windowSeconds ? { windowSeconds: Number(opts.windowSeconds) } : {},
|
|
8220
|
+
...opts.expiresAt ? { expiresAt: opts.expiresAt } : {}
|
|
8221
|
+
};
|
|
8222
|
+
}
|
|
8223
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/allocation-leases`, body });
|
|
8224
|
+
const json = await resp.json();
|
|
8225
|
+
if (!resp.ok) {
|
|
8226
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8227
|
+
process.exit(1);
|
|
8228
|
+
}
|
|
8229
|
+
console.log(`Allocation lease created: ${json.data?.id ?? json.id}`);
|
|
8230
|
+
});
|
|
8231
|
+
leases.command("update").description("Update an allocation lease. Prefer operator intent fields; runtime fields are override-only.").argument("<id>", "Allocation lease ID").option("--region <regions>", "Serving regions, comma-separated").option("--allowed <bool>", "true or false").option("--status <status>", "DRAFT, ACTIVE, PAUSED, EXHAUSTED, ARCHIVED").option("--policy-mode <mode>", "BALANCED, PRIORITY_FIRST, DELIVERY_FIRST, SPEND_CONSERVATIVE").option("--operator-priority <n>", "Operator priority (0-100)").option("--execution-quota <n>", "Optional runtime quota override for emergency/debug use").option("--window-seconds <n>", "Optional lease window seconds override").option("--expires-at <iso>", "Optional lease expiry override").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
8232
|
+
let body;
|
|
8233
|
+
if (opts.fromJson) {
|
|
8234
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
8235
|
+
body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
|
|
8236
|
+
} else {
|
|
8237
|
+
body = {
|
|
8238
|
+
...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
|
|
8239
|
+
...opts.allowed ? { allowed: opts.allowed === "true" } : {},
|
|
8240
|
+
...opts.status ? { status: opts.status } : {},
|
|
8241
|
+
...opts.policyMode ? { policyMode: opts.policyMode } : {},
|
|
8242
|
+
...opts.executionQuota ? { executionQuota: Number(opts.executionQuota) } : {},
|
|
8243
|
+
...opts.operatorPriority ? { operatorPriority: Number(opts.operatorPriority) } : {},
|
|
8244
|
+
...opts.windowSeconds ? { windowSeconds: Number(opts.windowSeconds) } : {},
|
|
8245
|
+
...opts.expiresAt ? { expiresAt: opts.expiresAt } : {}
|
|
8246
|
+
};
|
|
8247
|
+
}
|
|
8248
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/allocation-leases/${id}`, body });
|
|
8249
|
+
const json = await resp.json();
|
|
8250
|
+
if (!resp.ok) {
|
|
8251
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8252
|
+
process.exit(1);
|
|
8253
|
+
}
|
|
8254
|
+
console.log(`Allocation lease ${id} updated.`);
|
|
8255
|
+
});
|
|
8256
|
+
leases.command("delete").description("Delete an allocation lease.").argument("<id>", "Allocation lease ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
8257
|
+
if (!opts.yes) {
|
|
8258
|
+
console.error("Add --yes to confirm deletion.");
|
|
8259
|
+
process.exit(1);
|
|
8260
|
+
}
|
|
8261
|
+
const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/allocation-leases/${id}` });
|
|
8262
|
+
if (!resp.ok && resp.status !== 204) {
|
|
8263
|
+
const json = await resp.json();
|
|
8264
|
+
console.error(`Error: ${JSON.stringify(json.error ?? json.errors ?? resp.statusText)}`);
|
|
8265
|
+
process.exit(1);
|
|
8266
|
+
}
|
|
8267
|
+
console.log(`Allocation lease ${id} deleted.`);
|
|
8268
|
+
});
|
|
8269
|
+
return cmd;
|
|
8270
|
+
}
|
|
8271
|
+
|
|
8272
|
+
// src/commands/validate.ts
|
|
8273
|
+
function unwrapData(json) {
|
|
8274
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
8275
|
+
return json.data;
|
|
8276
|
+
}
|
|
8277
|
+
return json;
|
|
8278
|
+
}
|
|
8279
|
+
function getPath(value, path) {
|
|
8280
|
+
return path.reduce((current, key) => {
|
|
8281
|
+
if (!current || typeof current !== "object")
|
|
8282
|
+
return;
|
|
8283
|
+
return current[key];
|
|
8284
|
+
}, value);
|
|
8285
|
+
}
|
|
8286
|
+
function describe(value) {
|
|
8287
|
+
if (value === null)
|
|
8288
|
+
return "null";
|
|
8289
|
+
if (value === undefined)
|
|
8290
|
+
return "missing";
|
|
8291
|
+
if (typeof value === "string")
|
|
8292
|
+
return value;
|
|
8293
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
8294
|
+
return String(value);
|
|
8295
|
+
return JSON.stringify(value);
|
|
8296
|
+
}
|
|
8297
|
+
function addRow(rows, check, status, detail) {
|
|
8298
|
+
rows.push({ check, status, detail });
|
|
8299
|
+
}
|
|
8300
|
+
function maybeSuspiciousZero(value) {
|
|
8301
|
+
return typeof value === "number" && value === 0;
|
|
8302
|
+
}
|
|
8303
|
+
async function fetchJson(path) {
|
|
8304
|
+
const resp = await apiRequest({ path });
|
|
8305
|
+
const json = await resp.json().catch(() => null);
|
|
8306
|
+
if (!resp.ok) {
|
|
8307
|
+
const message = json && typeof json === "object" && "error" in json ? describe(json.error) : resp.statusText;
|
|
8308
|
+
throw new Error(`${path} failed: ${message}`);
|
|
8309
|
+
}
|
|
8310
|
+
return json;
|
|
8311
|
+
}
|
|
8312
|
+
async function requestJson(method, path, body) {
|
|
8313
|
+
const resp = await apiRequest({ method, path, body });
|
|
8314
|
+
const json = await resp.json().catch(() => null);
|
|
8315
|
+
return { status: resp.status, ok: resp.ok, json };
|
|
8316
|
+
}
|
|
8317
|
+
function summarizeError(json) {
|
|
8318
|
+
const body = json ?? {};
|
|
8319
|
+
if (body.error)
|
|
8320
|
+
return describe(body.error);
|
|
8321
|
+
if (body.errors)
|
|
8322
|
+
return describe(body.errors);
|
|
8323
|
+
return "no error body";
|
|
8324
|
+
}
|
|
8325
|
+
function parseCsv(value) {
|
|
8326
|
+
return value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
8327
|
+
}
|
|
8328
|
+
async function runDataHonestyChecks() {
|
|
8329
|
+
const rows = [];
|
|
8330
|
+
const [summaryRaw, financeRaw, billingRaw, systemHealthRaw, systemQueuesRaw] = await Promise.all([
|
|
8331
|
+
fetchJson("/api/v1/console/summary"),
|
|
8332
|
+
fetchJson("/api/v1/console/billing-ops/overview"),
|
|
8333
|
+
fetchJson("/api/v1/console/billing-ops/summary"),
|
|
8334
|
+
fetchJson("/api/v1/console/system/health"),
|
|
8335
|
+
fetchJson("/api/v1/console/system/queues")
|
|
8336
|
+
]);
|
|
8337
|
+
const summary = unwrapData(summaryRaw);
|
|
8338
|
+
const finance = unwrapData(financeRaw);
|
|
8339
|
+
const billing = unwrapData(billingRaw);
|
|
8340
|
+
const systemHealth = unwrapData(systemHealthRaw);
|
|
8341
|
+
const systemQueues = unwrapData(systemQueuesRaw);
|
|
8342
|
+
const pending = getPath(finance, ["pending"]);
|
|
8343
|
+
const depositsCount = pending?.depositsCount;
|
|
8344
|
+
const depositsAmount = pending?.depositsAmount;
|
|
8345
|
+
if (depositsCount === null && depositsAmount === null) {
|
|
8346
|
+
addRow(rows, "finance.pendingDeposits", "pass", "pending deposits disclosed as unavailable");
|
|
8347
|
+
} else if (maybeSuspiciousZero(depositsCount) && maybeSuspiciousZero(depositsAmount)) {
|
|
8348
|
+
addRow(rows, "finance.pendingDeposits", "fail", "still returning 0 / 0.0 placeholder values");
|
|
8349
|
+
} else {
|
|
8350
|
+
addRow(rows, "finance.pendingDeposits", "warn", `returned count=${describe(depositsCount)} amount=${describe(depositsAmount)}`);
|
|
8351
|
+
}
|
|
8352
|
+
const overdue = getPath(billing, ["receivables", "overdue"]);
|
|
8353
|
+
if (overdue === null) {
|
|
8354
|
+
addRow(rows, "billing.receivables.overdue", "pass", "overdue disclosed as unavailable");
|
|
8355
|
+
} else {
|
|
8356
|
+
addRow(rows, "billing.receivables.overdue", "warn", `returned ${describe(overdue)}`);
|
|
8357
|
+
}
|
|
8358
|
+
const pendingPayouts = getPath(systemHealth, ["stats", "pendingPayouts"]);
|
|
8359
|
+
if (pendingPayouts === null) {
|
|
8360
|
+
addRow(rows, "system.pendingPayouts", "pass", "pending payouts disclosed as unavailable");
|
|
8361
|
+
} else if (pendingPayouts === undefined) {
|
|
8362
|
+
addRow(rows, "system.pendingPayouts", "warn", "system/health does not currently expose stats.pendingPayouts");
|
|
8363
|
+
} else {
|
|
8364
|
+
addRow(rows, "system.pendingPayouts", "warn", `returned ${describe(pendingPayouts)}`);
|
|
8365
|
+
}
|
|
8366
|
+
const advertiserChange = getPath(summary, ["platformHealth", "advertiserChange"]);
|
|
8367
|
+
const publisherChange = getPath(summary, ["platformHealth", "publisherChange"]);
|
|
8368
|
+
const clicksChange = getPath(summary, ["changes", "clicks"]);
|
|
8369
|
+
addRow(rows, "summary.platformHealth.advertiserChange", advertiserChange === null ? "pass" : "warn", describe(advertiserChange));
|
|
8370
|
+
addRow(rows, "summary.platformHealth.publisherChange", publisherChange === null ? "pass" : "warn", describe(publisherChange));
|
|
8371
|
+
addRow(rows, "summary.changes.clicks", clicksChange === null ? "pass" : "warn", describe(clicksChange));
|
|
8372
|
+
const queueRows = Array.isArray(systemQueues) ? systemQueues : [];
|
|
8373
|
+
const fallbackQueue = queueRows.find((row) => {
|
|
8374
|
+
if (!row || typeof row !== "object")
|
|
8375
|
+
return false;
|
|
8376
|
+
return row.available === false;
|
|
8377
|
+
});
|
|
8378
|
+
if (!fallbackQueue) {
|
|
8379
|
+
addRow(rows, "system.queues.unavailableFallback", "warn", "no unavailable queue row found");
|
|
8380
|
+
} else {
|
|
8381
|
+
const hasNilMetrics = ["pending", "processing", "completed", "failed", "avgProcessingTime"].every((key) => fallbackQueue[key] === null);
|
|
8382
|
+
addRow(rows, "system.queues.unavailableFallback", hasNilMetrics ? "pass" : "warn", hasNilMetrics ? "queue fallback row marks metrics unavailable" : `queue row=${JSON.stringify(fallbackQueue)}`);
|
|
8383
|
+
}
|
|
8384
|
+
return rows;
|
|
8385
|
+
}
|
|
8386
|
+
function createValidateCommand() {
|
|
8387
|
+
const cmd = new Command("validate").description("Deploy and integration validation helpers").addHelpText("after", `
|
|
8388
|
+
Examples:
|
|
8389
|
+
$ a8techads validate health
|
|
8390
|
+
$ a8techads validate data-honesty
|
|
8391
|
+
$ a8techads validate data-honesty --format json`);
|
|
8392
|
+
cmd.command("health").description("Public API health check.").option("--base-url <url>", "Base API URL", "https://api.a8.tech").action(async (opts) => {
|
|
8393
|
+
const url = new URL("/health", opts.baseUrl).toString();
|
|
8394
|
+
const resp = await fetch(url);
|
|
8395
|
+
if (!resp.ok) {
|
|
8396
|
+
console.error(`Health check failed: ${url} -> ${resp.status}`);
|
|
8397
|
+
process.exit(1);
|
|
8398
|
+
}
|
|
8399
|
+
console.log(`OK ${url} -> ${resp.status}`);
|
|
8400
|
+
});
|
|
8401
|
+
addFormatOption(cmd.command("data-honesty").description("Check post-deploy nullable/disclosure behavior for Console data honesty changes.")).action(async (opts) => {
|
|
8402
|
+
const rows = await runDataHonestyChecks();
|
|
8403
|
+
const columns = [
|
|
8404
|
+
{ key: "check", header: "CHECK", width: 38 },
|
|
8405
|
+
{ key: "status", header: "STATUS", width: 8 },
|
|
8406
|
+
{ key: "detail", header: "DETAIL", width: 80 }
|
|
8407
|
+
];
|
|
8408
|
+
printData(rows, columns, opts.format);
|
|
8409
|
+
if (rows.some((row) => row.status === "fail")) {
|
|
8410
|
+
process.exit(1);
|
|
8411
|
+
}
|
|
8412
|
+
});
|
|
8413
|
+
addFormatOption(cmd.command("tenant-boundary").description("Assert that current tenant context cannot access known foreign resources.").requiredOption("--campaign <id>", "Campaign ID that must not be visible from current tenant context").option("--supply-product <id>", "Active DSP-visible supply product ID to verify shared catalog access").option("--tenant <id>", "Tenant ID that current user does not belong to; used for tenant switch rejection check")).action(async (opts) => {
|
|
8414
|
+
const rows = [];
|
|
8415
|
+
const campaignId = String(opts.campaign);
|
|
8416
|
+
const campaignShow = await requestJson("GET", `/api/v1/dsp/campaigns/${campaignId}`);
|
|
8417
|
+
addRow(rows, "campaign.show.foreign", campaignShow.status === 404 ? "pass" : "fail", `expected 404, got ${campaignShow.status}${campaignShow.ok ? "" : ` (${summarizeError(campaignShow.json)})`}`);
|
|
8418
|
+
const campaignUpdate = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}`, {
|
|
8419
|
+
name: `cross-tenant-check-${Date.now()}`
|
|
8420
|
+
});
|
|
8421
|
+
addRow(rows, "campaign.update.foreign", campaignUpdate.status === 404 ? "pass" : "fail", `expected 404, got ${campaignUpdate.status}${campaignUpdate.ok ? "" : ` (${summarizeError(campaignUpdate.json)})`}`);
|
|
8422
|
+
const campaignDelete = await requestJson("DELETE", `/api/v1/dsp/campaigns/${campaignId}`);
|
|
8423
|
+
addRow(rows, "campaign.delete.foreign", campaignDelete.status === 404 ? "pass" : "fail", `expected 404, got ${campaignDelete.status}${campaignDelete.ok ? "" : ` (${summarizeError(campaignDelete.json)})`}`);
|
|
8424
|
+
if (opts.supplyProduct) {
|
|
8425
|
+
const supplyProductId = String(opts.supplyProduct);
|
|
8426
|
+
const supplyProductShow = await requestJson("GET", `/api/v1/dsp/supply-products/${supplyProductId}`);
|
|
8427
|
+
addRow(rows, "supplyProduct.sharedCatalog", supplyProductShow.status === 200 ? "pass" : "warn", supplyProductShow.status === 200 ? "active supply product is visible through DSP shared catalog" : `expected shared catalog access, got ${supplyProductShow.status}${supplyProductShow.ok ? "" : ` (${summarizeError(supplyProductShow.json)})`}`);
|
|
8428
|
+
}
|
|
8429
|
+
if (opts.tenant) {
|
|
8430
|
+
const tenantId = String(opts.tenant);
|
|
8431
|
+
const tenantSwitch = await requestJson("POST", "/api/v1/me/switch-tenant", { tenant_id: tenantId });
|
|
8432
|
+
addRow(rows, "tenantSwitch.foreignTenant", tenantSwitch.status === 403 ? "pass" : "fail", `expected 403, got ${tenantSwitch.status}${tenantSwitch.ok ? "" : ` (${summarizeError(tenantSwitch.json)})`}`);
|
|
8433
|
+
}
|
|
8434
|
+
const columns = [
|
|
8435
|
+
{ key: "check", header: "CHECK", width: 32 },
|
|
8436
|
+
{ key: "status", header: "STATUS", width: 8 },
|
|
8437
|
+
{ key: "detail", header: "DETAIL", width: 80 }
|
|
8438
|
+
];
|
|
8439
|
+
printData(rows, columns, opts.format);
|
|
8440
|
+
if (rows.some((row) => row.status === "fail")) {
|
|
8441
|
+
process.exit(1);
|
|
8442
|
+
}
|
|
8443
|
+
});
|
|
8444
|
+
addFormatOption(cmd.command("campaign-binding-flow").description("Run pause -> update supply binding -> resume against a real DSP campaign.").argument("<campaign-id>", "Campaign ID to mutate during the validation flow").requiredOption("--supply-products <ids>", "Comma-separated supply product IDs to apply while paused").option("--yes", "Allow live mutation of campaign state", false)).action(async (campaignId, opts) => {
|
|
8445
|
+
if (!opts.yes) {
|
|
8446
|
+
console.error("Error: this command mutates live campaign state. Re-run with --yes.");
|
|
8447
|
+
process.exit(1);
|
|
8448
|
+
}
|
|
8449
|
+
const rows = [];
|
|
8450
|
+
const supplyProductIds = parseCsv(String(opts.supplyProducts));
|
|
8451
|
+
const pauseResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}/status`, { status: "paused" });
|
|
8452
|
+
addRow(rows, "campaign.pause", pauseResp.ok ? "pass" : "fail", pauseResp.ok ? "pause request accepted" : `status ${pauseResp.status} (${summarizeError(pauseResp.json)})`);
|
|
8453
|
+
const updateResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}`, { supplyProductIds });
|
|
8454
|
+
addRow(rows, "campaign.updateSupplyBinding", updateResp.ok ? "pass" : "fail", updateResp.ok ? `updated supplyProductIds=${supplyProductIds.join(",")}` : `status ${updateResp.status} (${summarizeError(updateResp.json)})`);
|
|
8455
|
+
const resumeResp = await requestJson("PATCH", `/api/v1/dsp/campaigns/${campaignId}/status`, { status: "active" });
|
|
8456
|
+
addRow(rows, "campaign.resume", resumeResp.ok ? "pass" : "fail", resumeResp.ok ? "resume request accepted" : `status ${resumeResp.status} (${summarizeError(resumeResp.json)})`);
|
|
8457
|
+
const verifyResp = await requestJson("GET", `/api/v1/dsp/campaigns/${campaignId}`);
|
|
8458
|
+
const verify = unwrapData(verifyResp.json);
|
|
8459
|
+
const actualStatus = getPath(verify, ["status"]);
|
|
8460
|
+
const actualSupplyProducts = getPath(verify, ["supplyProductIds"]);
|
|
8461
|
+
const supplyBindingMatches = Array.isArray(actualSupplyProducts) && actualSupplyProducts.length === supplyProductIds.length && supplyProductIds.every((id) => actualSupplyProducts.includes(id));
|
|
8462
|
+
addRow(rows, "campaign.verifyStatus", String(actualStatus).toUpperCase() === "ACTIVE" ? "pass" : "fail", `status=${describe(actualStatus)}`);
|
|
8463
|
+
addRow(rows, "campaign.verifySupplyBinding", supplyBindingMatches ? "pass" : "fail", `supplyProductIds=${describe(actualSupplyProducts)}`);
|
|
8464
|
+
const columns = [
|
|
8465
|
+
{ key: "check", header: "CHECK", width: 32 },
|
|
8466
|
+
{ key: "status", header: "STATUS", width: 8 },
|
|
8467
|
+
{ key: "detail", header: "DETAIL", width: 80 }
|
|
8468
|
+
];
|
|
8469
|
+
printData(rows, columns, opts.format);
|
|
8470
|
+
if (rows.some((row) => row.status === "fail")) {
|
|
8471
|
+
process.exit(1);
|
|
8472
|
+
}
|
|
8473
|
+
});
|
|
8474
|
+
return cmd;
|
|
8475
|
+
}
|
|
8476
|
+
|
|
6642
8477
|
// src/index.ts
|
|
6643
8478
|
function createProgram() {
|
|
6644
8479
|
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
|
|
@@ -6649,6 +8484,7 @@ Command Groups:
|
|
|
6649
8484
|
audiences Audience management (DSP)
|
|
6650
8485
|
campaigns Campaign management (DSP)
|
|
6651
8486
|
variations Ad variation management (DSP)
|
|
8487
|
+
landing-pages Landing page groups and pages (DSP)
|
|
6652
8488
|
conversion-goals Conversion goal management (DSP)
|
|
6653
8489
|
media-assets Media library management (DSP)
|
|
6654
8490
|
algorithms Bidder algorithm management (DSP)
|
|
@@ -6664,6 +8500,7 @@ Command Groups:
|
|
|
6664
8500
|
settings Tenant settings
|
|
6665
8501
|
admin Platform administration (Console)
|
|
6666
8502
|
external-ssp External SSP partner management (Console)
|
|
8503
|
+
supply-ops Internal supply operations views (Console)
|
|
6667
8504
|
|
|
6668
8505
|
Getting Started:
|
|
6669
8506
|
$ a8techads auth login # Authenticate
|
|
@@ -6678,6 +8515,7 @@ Getting Started:
|
|
|
6678
8515
|
program2.addCommand(createCampaignsCommand());
|
|
6679
8516
|
program2.addCommand(createVariationsCommand());
|
|
6680
8517
|
program2.addCommand(createMediaAssetsCommand());
|
|
8518
|
+
program2.addCommand(createLandingPagesCommand());
|
|
6681
8519
|
program2.addCommand(createConversionGoalsCommand());
|
|
6682
8520
|
program2.addCommand(createAlgorithmsCommand());
|
|
6683
8521
|
program2.addCommand(createSitesCommand());
|
|
@@ -6692,6 +8530,8 @@ Getting Started:
|
|
|
6692
8530
|
program2.addCommand(createInvoicesCommand());
|
|
6693
8531
|
program2.addCommand(createAdminCommand());
|
|
6694
8532
|
program2.addCommand(createExternalSspCommand());
|
|
8533
|
+
program2.addCommand(createSupplyOpsCommand());
|
|
8534
|
+
program2.addCommand(createValidateCommand());
|
|
6695
8535
|
return program2;
|
|
6696
8536
|
}
|
|
6697
8537
|
|