@a8techads/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a8techads.js +2239 -201
- package/package.json +1 -1
package/dist/a8techads.js
CHANGED
|
@@ -2628,6 +2628,18 @@ async function login(opts = {}) {
|
|
|
2628
2628
|
const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL;
|
|
2629
2629
|
const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
|
|
2630
2630
|
const authorizationEndpoint = `${authUrl}/.ory/hydra/oauth2/auth`;
|
|
2631
|
+
if (opts.forceLogin) {
|
|
2632
|
+
const existingCreds = loadCredentials();
|
|
2633
|
+
if (existingCreds.profiles[profileName]) {
|
|
2634
|
+
delete existingCreds.profiles[profileName];
|
|
2635
|
+
saveCredentials(existingCreds);
|
|
2636
|
+
}
|
|
2637
|
+
const existingCtx = loadContext();
|
|
2638
|
+
if (existingCtx.profiles[profileName]) {
|
|
2639
|
+
delete existingCtx.profiles[profileName];
|
|
2640
|
+
saveContext(existingCtx);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2631
2643
|
const codeVerifier = generateRandomCodeVerifier();
|
|
2632
2644
|
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
2633
2645
|
const state = generateRandomState();
|
|
@@ -3200,6 +3212,58 @@ function createProfileCommand() {
|
|
|
3200
3212
|
return profile;
|
|
3201
3213
|
}
|
|
3202
3214
|
|
|
3215
|
+
// src/utils/http.ts
|
|
3216
|
+
async function apiRequest(opts) {
|
|
3217
|
+
const request = await buildAuthenticatedRequest(opts);
|
|
3218
|
+
return fetch(request.url, request.init);
|
|
3219
|
+
}
|
|
3220
|
+
async function buildAuthenticatedRequest(opts) {
|
|
3221
|
+
await refreshTokenIfNeeded();
|
|
3222
|
+
const creds = loadCredentials();
|
|
3223
|
+
const profile = getCurrentProfile(creds);
|
|
3224
|
+
if (!profile) {
|
|
3225
|
+
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
3226
|
+
}
|
|
3227
|
+
const ctx = loadContext();
|
|
3228
|
+
const context = getCurrentContext(ctx);
|
|
3229
|
+
validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
|
|
3230
|
+
const headers = {
|
|
3231
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
3232
|
+
"Content-Type": "application/json",
|
|
3233
|
+
...opts.headers
|
|
3234
|
+
};
|
|
3235
|
+
if (context?.impersonation) {
|
|
3236
|
+
headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
|
|
3237
|
+
if (context.impersonation.effective_capability) {
|
|
3238
|
+
headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
|
|
3239
|
+
}
|
|
3240
|
+
} else if (context?.current_capability) {
|
|
3241
|
+
headers["X-Effective-Capability"] = context.current_capability;
|
|
3242
|
+
}
|
|
3243
|
+
const url = `${opts.baseUrl ?? profile.api_url}${opts.path}`;
|
|
3244
|
+
return {
|
|
3245
|
+
url,
|
|
3246
|
+
init: {
|
|
3247
|
+
method: opts.method ?? "GET",
|
|
3248
|
+
headers,
|
|
3249
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
function validateTenantMatch(accessToken, contextTenantId) {
|
|
3254
|
+
const claims = decodeJwt(accessToken);
|
|
3255
|
+
if (!claims)
|
|
3256
|
+
return;
|
|
3257
|
+
const authClaims = getAuthClaims(claims);
|
|
3258
|
+
const tokenTenantId = authClaims?.tenant_id ?? null;
|
|
3259
|
+
if (!contextTenantId || !tokenTenantId)
|
|
3260
|
+
return;
|
|
3261
|
+
if (contextTenantId !== tokenTenantId) {
|
|
3262
|
+
throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
|
|
3263
|
+
` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3203
3267
|
// src/commands/context.ts
|
|
3204
3268
|
function createContextCommand() {
|
|
3205
3269
|
const context = new Command("context").description("Workspace context — tenant, capability, and app selection").addHelpText("after", `
|
|
@@ -3242,6 +3306,16 @@ Examples:
|
|
|
3242
3306
|
}
|
|
3243
3307
|
console.log(`App: ${profileCtx?.app ?? "none"}`);
|
|
3244
3308
|
console.log(`Capability: ${profileCtx?.current_capability ?? "null (platform)"}`);
|
|
3309
|
+
if (profileCtx?.impersonation) {
|
|
3310
|
+
const imp = profileCtx.impersonation;
|
|
3311
|
+
console.log(`
|
|
3312
|
+
--- Managed Access Session ---`);
|
|
3313
|
+
console.log(`Target: ${imp.target_tenant_name} (${imp.target_tenant_id})`);
|
|
3314
|
+
console.log(`Capability: ${imp.effective_capability}`);
|
|
3315
|
+
console.log(`Role: ${imp.effective_role}`);
|
|
3316
|
+
console.log(`Grant: ${imp.managed_grant_id ?? "none"}`);
|
|
3317
|
+
console.log(`Started: ${imp.started_at}`);
|
|
3318
|
+
}
|
|
3245
3319
|
});
|
|
3246
3320
|
context.command("set-tenant").description(`Switch to a different tenant. Triggers re-authentication if the
|
|
3247
3321
|
tenant differs from the current token tenant.
|
|
@@ -3376,6 +3450,74 @@ Shortcuts:
|
|
|
3376
3450
|
console.log("Capability: PUBLISHER");
|
|
3377
3451
|
console.log("App: ssp");
|
|
3378
3452
|
});
|
|
3453
|
+
context.command("assume-role").description(`Start a managed access session as a platform operator.
|
|
3454
|
+
|
|
3455
|
+
Requires: platform_owner or platform_admin role.`).option("--tenant <id>", "Target tenant ID (required)").option("--capability <cap>", "Effective capability: ADVERTISER or PUBLISHER").addHelpText("after", `
|
|
3456
|
+
Examples:
|
|
3457
|
+
$ a8techads context assume-role --tenant 00000000-... --capability ADVERTISER
|
|
3458
|
+
$ a8techads context assume-role --tenant <id>`).action(async (opts) => {
|
|
3459
|
+
if (!opts.tenant) {
|
|
3460
|
+
console.error("Error: --tenant is required.");
|
|
3461
|
+
console.error('Run "a8techads context assume-role --help" for usage.');
|
|
3462
|
+
process.exit(1);
|
|
3463
|
+
}
|
|
3464
|
+
try {
|
|
3465
|
+
const body = { target_tenant_id: opts.tenant };
|
|
3466
|
+
if (opts.capability)
|
|
3467
|
+
body.target_capability = opts.capability;
|
|
3468
|
+
const resp = await apiRequest({
|
|
3469
|
+
method: "POST",
|
|
3470
|
+
path: "/api/v1/console/impersonation/start",
|
|
3471
|
+
body
|
|
3472
|
+
});
|
|
3473
|
+
if (!resp.ok) {
|
|
3474
|
+
const err = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
3475
|
+
console.error(`Error: ${err.error || resp.statusText}`);
|
|
3476
|
+
process.exit(1);
|
|
3477
|
+
}
|
|
3478
|
+
const data = await resp.json();
|
|
3479
|
+
const targetTenant = data.target_tenant || {};
|
|
3480
|
+
const ctx = loadContext();
|
|
3481
|
+
const impersonation = {
|
|
3482
|
+
target_tenant_id: opts.tenant,
|
|
3483
|
+
target_tenant_name: targetTenant.company_name || "Unknown",
|
|
3484
|
+
effective_capability: data.effective_capability || opts.capability || "",
|
|
3485
|
+
effective_role: data.effective_role || "",
|
|
3486
|
+
managed_grant_id: data.managed_grant_id || null,
|
|
3487
|
+
started_at: new Date().toISOString()
|
|
3488
|
+
};
|
|
3489
|
+
setCurrentContext(ctx, { impersonation });
|
|
3490
|
+
saveContext(ctx);
|
|
3491
|
+
console.log("Managed access session started.");
|
|
3492
|
+
console.log(`Target: ${impersonation.target_tenant_name} (${opts.tenant})`);
|
|
3493
|
+
console.log(`Capability: ${impersonation.effective_capability}`);
|
|
3494
|
+
console.log(`Grant: ${impersonation.managed_grant_id || "auto-created"}`);
|
|
3495
|
+
} catch (err) {
|
|
3496
|
+
console.error(`Failed: ${err.message}`);
|
|
3497
|
+
process.exit(1);
|
|
3498
|
+
}
|
|
3499
|
+
});
|
|
3500
|
+
context.command("end-session").description(`End the current managed access session.
|
|
3501
|
+
|
|
3502
|
+
Requires: an active managed access session.`).addHelpText("after", `
|
|
3503
|
+
Examples:
|
|
3504
|
+
$ a8techads context end-session`).action(async () => {
|
|
3505
|
+
const ctx = loadContext();
|
|
3506
|
+
const profileCtx = getCurrentContext(ctx);
|
|
3507
|
+
if (!profileCtx?.impersonation) {
|
|
3508
|
+
console.log("No active managed access session.");
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
try {
|
|
3512
|
+
await apiRequest({
|
|
3513
|
+
method: "POST",
|
|
3514
|
+
path: "/api/v1/console/impersonation/end"
|
|
3515
|
+
});
|
|
3516
|
+
} catch {}
|
|
3517
|
+
setCurrentContext(ctx, { impersonation: null });
|
|
3518
|
+
saveContext(ctx);
|
|
3519
|
+
console.log("Managed access session ended.");
|
|
3520
|
+
});
|
|
3379
3521
|
return context;
|
|
3380
3522
|
}
|
|
3381
3523
|
function deriveAppFromTenant(tenant) {
|
|
@@ -3392,46 +3534,6 @@ function deriveAuthUrl(tokenEndpoint) {
|
|
|
3392
3534
|
return url.origin;
|
|
3393
3535
|
}
|
|
3394
3536
|
|
|
3395
|
-
// src/utils/http.ts
|
|
3396
|
-
async function apiRequest(opts) {
|
|
3397
|
-
await refreshTokenIfNeeded();
|
|
3398
|
-
const creds = loadCredentials();
|
|
3399
|
-
const profile = getCurrentProfile(creds);
|
|
3400
|
-
if (!profile) {
|
|
3401
|
-
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
3402
|
-
}
|
|
3403
|
-
const ctx = loadContext();
|
|
3404
|
-
const context = getCurrentContext(ctx);
|
|
3405
|
-
validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
|
|
3406
|
-
const headers = {
|
|
3407
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
3408
|
-
"Content-Type": "application/json",
|
|
3409
|
-
...opts.headers
|
|
3410
|
-
};
|
|
3411
|
-
if (context?.current_capability) {
|
|
3412
|
-
headers["X-Effective-Capability"] = context.current_capability;
|
|
3413
|
-
}
|
|
3414
|
-
const url = `${profile.api_url}${opts.path}`;
|
|
3415
|
-
return fetch(url, {
|
|
3416
|
-
method: opts.method ?? "GET",
|
|
3417
|
-
headers,
|
|
3418
|
-
body: opts.body ? JSON.stringify(opts.body) : undefined
|
|
3419
|
-
});
|
|
3420
|
-
}
|
|
3421
|
-
function validateTenantMatch(accessToken, contextTenantId) {
|
|
3422
|
-
const claims = decodeJwt(accessToken);
|
|
3423
|
-
if (!claims)
|
|
3424
|
-
return;
|
|
3425
|
-
const authClaims = getAuthClaims(claims);
|
|
3426
|
-
const tokenTenantId = authClaims?.tenant_id ?? null;
|
|
3427
|
-
if (!contextTenantId || !tokenTenantId)
|
|
3428
|
-
return;
|
|
3429
|
-
if (contextTenantId !== tokenTenantId) {
|
|
3430
|
-
throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
|
|
3431
|
-
` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
3537
|
// src/utils/api-prefix.ts
|
|
3436
3538
|
function getApiPrefix() {
|
|
3437
3539
|
const ctx = loadContext();
|
|
@@ -3454,6 +3556,9 @@ function dspPrefix() {
|
|
|
3454
3556
|
function sspPrefix() {
|
|
3455
3557
|
return "/api/v1/ssp";
|
|
3456
3558
|
}
|
|
3559
|
+
function consolePrefix() {
|
|
3560
|
+
return "/api/v1/console";
|
|
3561
|
+
}
|
|
3457
3562
|
|
|
3458
3563
|
// src/utils/output.ts
|
|
3459
3564
|
function printData(data, columns, format = "table") {
|
|
@@ -3562,21 +3667,24 @@ Examples:
|
|
|
3562
3667
|
printData(rows, COLUMNS, opts.format);
|
|
3563
3668
|
});
|
|
3564
3669
|
addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
|
|
3565
|
-
const
|
|
3566
|
-
|
|
3567
|
-
if (!resp.ok) {
|
|
3568
|
-
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3569
|
-
process.exit(1);
|
|
3570
|
-
}
|
|
3571
|
-
printDetail(json.data ?? json, opts.format);
|
|
3670
|
+
const audience = await fetchAudience(id);
|
|
3671
|
+
printDetail(audience, opts.format);
|
|
3572
3672
|
});
|
|
3573
|
-
cmd.command("
|
|
3574
|
-
|
|
3575
|
-
|
|
3673
|
+
addFormatOption(cmd.command("size").description("Show the current estimated audience size.").argument("<id>", "Audience ID")).action(async (id, opts) => {
|
|
3674
|
+
const audience = await fetchAudience(id);
|
|
3675
|
+
const detail = {
|
|
3676
|
+
id: audience.id,
|
|
3677
|
+
name: audience.name,
|
|
3678
|
+
status: audience.status,
|
|
3679
|
+
estimatedSize: audience.estimatedSize ?? audience.estimated_size ?? null
|
|
3680
|
+
};
|
|
3681
|
+
printDetail(detail, opts.format);
|
|
3682
|
+
});
|
|
3683
|
+
cmd.command("create").description("Create a new audience.").option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST, RETARGETING, or LOOKALIKE", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--seed <id>", "Seed audience ID (required for LOOKALIKE type)").option("--ratio <n>", "Expansion ratio for LOOKALIKE (default: 0.05)", "0.05").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
|
|
3576
3684
|
Examples:
|
|
3577
3685
|
$ a8techads audiences create --name "High-Value Customers"
|
|
3578
|
-
$ a8techads audiences create --name "Retarget Pool" --
|
|
3579
|
-
$ a8techads audiences create --
|
|
3686
|
+
$ a8techads audiences create --name "Retarget Pool" --type RETARGETING --goal-id <id>
|
|
3687
|
+
$ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
|
|
3580
3688
|
let body;
|
|
3581
3689
|
if (opts.fromJson) {
|
|
3582
3690
|
const { readFileSync: readFileSync3 } = await import("fs");
|
|
@@ -3586,6 +3694,14 @@ Examples:
|
|
|
3586
3694
|
console.error('Error: --name is required. Run "a8techads audiences create --help".');
|
|
3587
3695
|
process.exit(1);
|
|
3588
3696
|
}
|
|
3697
|
+
if (opts.type === "RETARGETING" && !opts.goalId) {
|
|
3698
|
+
console.error("Error: --goal-id is required for RETARGETING type.");
|
|
3699
|
+
process.exit(1);
|
|
3700
|
+
}
|
|
3701
|
+
if (opts.type === "LOOKALIKE" && !opts.seed) {
|
|
3702
|
+
console.error("Error: --seed is required for LOOKALIKE type.");
|
|
3703
|
+
process.exit(1);
|
|
3704
|
+
}
|
|
3589
3705
|
body = {
|
|
3590
3706
|
name: opts.name,
|
|
3591
3707
|
type: opts.type,
|
|
@@ -3593,6 +3709,12 @@ Examples:
|
|
|
3593
3709
|
};
|
|
3594
3710
|
if (opts.description)
|
|
3595
3711
|
body.description = opts.description;
|
|
3712
|
+
if (opts.goalId) {
|
|
3713
|
+
body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
|
|
3714
|
+
}
|
|
3715
|
+
if (opts.seed) {
|
|
3716
|
+
body.rules = { seed_audience_id: opts.seed, expansion_ratio: Number(opts.ratio) };
|
|
3717
|
+
}
|
|
3596
3718
|
}
|
|
3597
3719
|
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
|
|
3598
3720
|
const json = await resp.json();
|
|
@@ -3657,31 +3779,89 @@ Examples:
|
|
|
3657
3779
|
}
|
|
3658
3780
|
console.log(`Audience ${id} archived.`);
|
|
3659
3781
|
});
|
|
3660
|
-
cmd.command("upload").description(`Upload
|
|
3782
|
+
cmd.command("upload").description(`Upload user identifiers to populate audience membership.
|
|
3661
3783
|
|
|
3662
|
-
|
|
3784
|
+
Reads a file with one identifier per line (email hash, device ID, etc.)
|
|
3785
|
+
and uploads to the audience membership store.`).argument("<id>", "Audience ID").option("--file <path>", "File path (one identifier per line, or JSON array)").option("--identifier-type <type>", "Identifier type: EMAIL_HASH or DEVICE_ID", "EMAIL_HASH").addHelpText("after", `
|
|
3663
3786
|
Examples:
|
|
3664
|
-
$ a8techads audiences upload <id> --file users.
|
|
3665
|
-
$ a8techads audiences upload <id> --file devices.
|
|
3787
|
+
$ a8techads audiences upload <id> --file users.txt --identifier-type EMAIL_HASH
|
|
3788
|
+
$ a8techads audiences upload <id> --file devices.txt --identifier-type DEVICE_ID
|
|
3789
|
+
|
|
3790
|
+
File format (one per line):
|
|
3791
|
+
a1b2c3d4e5f6...
|
|
3792
|
+
f6e5d4c3b2a1...`).action(async (id, opts) => {
|
|
3666
3793
|
if (!opts.file) {
|
|
3667
3794
|
console.error("Error: --file is required.");
|
|
3668
3795
|
process.exit(1);
|
|
3669
3796
|
}
|
|
3797
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
3798
|
+
let identifiers;
|
|
3799
|
+
try {
|
|
3800
|
+
const content = readFileSync3(opts.file, "utf-8").trim();
|
|
3801
|
+
try {
|
|
3802
|
+
identifiers = JSON.parse(content);
|
|
3803
|
+
if (!Array.isArray(identifiers))
|
|
3804
|
+
throw new Error("not array");
|
|
3805
|
+
} catch {
|
|
3806
|
+
identifiers = content.split(`
|
|
3807
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3808
|
+
}
|
|
3809
|
+
} catch (err) {
|
|
3810
|
+
console.error(`Error reading file: ${err.message}`);
|
|
3811
|
+
process.exit(1);
|
|
3812
|
+
}
|
|
3813
|
+
if (identifiers.length === 0) {
|
|
3814
|
+
console.error("Error: file contains no identifiers.");
|
|
3815
|
+
process.exit(1);
|
|
3816
|
+
}
|
|
3817
|
+
console.log(`Uploading ${identifiers.length} identifiers...`);
|
|
3670
3818
|
const body = {
|
|
3819
|
+
identifiers,
|
|
3671
3820
|
identifierType: opts.identifierType
|
|
3672
3821
|
};
|
|
3673
|
-
if (opts.estimatedSize)
|
|
3674
|
-
body.estimatedSize = Number(opts.estimatedSize);
|
|
3675
3822
|
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences/${id}/upload`, body });
|
|
3676
3823
|
const json = await resp.json();
|
|
3677
3824
|
if (!resp.ok) {
|
|
3678
3825
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3679
3826
|
process.exit(1);
|
|
3680
3827
|
}
|
|
3681
|
-
console.log(`Upload
|
|
3828
|
+
console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
|
|
3829
|
+
});
|
|
3830
|
+
cmd.command("wait-ready").description("Poll audience status until it becomes READY, ACTIVE, or ERROR.").argument("<id>", "Audience ID").option("--timeout <seconds>", "Timeout in seconds", "120").option("--interval <seconds>", "Polling interval in seconds", "3").action(async (id, opts) => {
|
|
3831
|
+
const timeoutMs = Number(opts.timeout) * 1000;
|
|
3832
|
+
const intervalMs = Number(opts.interval) * 1000;
|
|
3833
|
+
const started = Date.now();
|
|
3834
|
+
while (true) {
|
|
3835
|
+
const audience = await fetchAudience(id);
|
|
3836
|
+
const status = audience.status;
|
|
3837
|
+
const size = audience.estimatedSize ?? audience.estimated_size ?? null;
|
|
3838
|
+
console.log(`Status: ${status}${size != null ? ` | Size: ${Number(size).toLocaleString()}` : ""}`);
|
|
3839
|
+
if (status === "READY" || status === "ACTIVE") {
|
|
3840
|
+
console.log(`Audience ${id} is ready.`);
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (status === "ERROR" || status === "ARCHIVED") {
|
|
3844
|
+
console.error(`Audience ${id} reached terminal status: ${status}`);
|
|
3845
|
+
process.exit(1);
|
|
3846
|
+
}
|
|
3847
|
+
if (Date.now() - started >= timeoutMs) {
|
|
3848
|
+
console.error(`Timed out waiting for audience ${id}.`);
|
|
3849
|
+
process.exit(1);
|
|
3850
|
+
}
|
|
3851
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3852
|
+
}
|
|
3682
3853
|
});
|
|
3683
3854
|
return cmd;
|
|
3684
3855
|
}
|
|
3856
|
+
async function fetchAudience(id) {
|
|
3857
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
|
|
3858
|
+
const json = await resp.json();
|
|
3859
|
+
if (!resp.ok) {
|
|
3860
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3861
|
+
process.exit(1);
|
|
3862
|
+
}
|
|
3863
|
+
return json.data ?? json;
|
|
3864
|
+
}
|
|
3685
3865
|
|
|
3686
3866
|
// src/commands/campaigns.ts
|
|
3687
3867
|
var COLUMNS2 = [
|
|
@@ -3826,8 +4006,178 @@ Examples:
|
|
|
3826
4006
|
}
|
|
3827
4007
|
console.log(`Campaign duplicated. New ID: ${json.data?.id ?? json.id}`);
|
|
3828
4008
|
});
|
|
4009
|
+
const targetingCmd = new Command("targeting").description("Show or update campaign targeting without editing full JSON.").addHelpText("after", `
|
|
4010
|
+
Examples:
|
|
4011
|
+
$ a8techads campaigns targeting show <campaign-id>
|
|
4012
|
+
$ a8techads campaigns targeting set-geo <campaign-id> --countries US,CA --mode target
|
|
4013
|
+
$ a8techads campaigns targeting add-audience <campaign-id> --audience <id> --mode include
|
|
4014
|
+
$ a8techads campaigns targeting set-day-parting <campaign-id> --timezone UTC --from-json schedule.json`);
|
|
4015
|
+
addFormatOption(targetingCmd.command("show").description("Show campaign targeting, audience include/exclude, and day parting.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
|
|
4016
|
+
const campaign = await fetchCampaign(id);
|
|
4017
|
+
const detail = {
|
|
4018
|
+
campaignId: campaign.id,
|
|
4019
|
+
campaignName: campaign.name,
|
|
4020
|
+
targeting: campaign.targeting ?? {},
|
|
4021
|
+
timezone: campaign.timezone ?? "UTC",
|
|
4022
|
+
dayParting: campaign.dayParting ?? null
|
|
4023
|
+
};
|
|
4024
|
+
printDetail(detail, opts.format);
|
|
4025
|
+
});
|
|
4026
|
+
targetingCmd.command("set-geo").description("Replace campaign geo countries and mode.").argument("<id>", "Campaign ID").requiredOption("--countries <codes>", "Comma-separated country codes, e.g. US,CA").option("--mode <mode>", "target or block", "target").action(async (id, opts) => {
|
|
4027
|
+
const campaign = await fetchCampaign(id);
|
|
4028
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4029
|
+
const countries = parseCsvList(opts.countries);
|
|
4030
|
+
const mode = opts.mode === "block" ? "block" : "target";
|
|
4031
|
+
targeting.geo = {
|
|
4032
|
+
...targeting.geo ?? {},
|
|
4033
|
+
countries,
|
|
4034
|
+
mode,
|
|
4035
|
+
countriesMode: mode
|
|
4036
|
+
};
|
|
4037
|
+
await patchCampaign(id, { targeting });
|
|
4038
|
+
console.log(`Campaign ${id} geo targeting updated.`);
|
|
4039
|
+
});
|
|
4040
|
+
targetingCmd.command("add-audience").description("Add an audience to include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
|
|
4041
|
+
const mode = normalizeAudienceMode(opts.mode);
|
|
4042
|
+
const campaign = await fetchCampaign(id);
|
|
4043
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4044
|
+
const audiences = normalizeAudiences(targeting.audiences);
|
|
4045
|
+
const list = new Set(mode === "include" ? audiences.include : audiences.exclude);
|
|
4046
|
+
list.add(opts.audience);
|
|
4047
|
+
targeting.audiences = {
|
|
4048
|
+
...audiences,
|
|
4049
|
+
[mode]: Array.from(list)
|
|
4050
|
+
};
|
|
4051
|
+
await patchCampaign(id, { targeting });
|
|
4052
|
+
console.log(`Campaign ${id} audience ${opts.audience} added to ${mode}.`);
|
|
4053
|
+
});
|
|
4054
|
+
targetingCmd.command("remove-audience").description("Remove an audience from include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
|
|
4055
|
+
const mode = normalizeAudienceMode(opts.mode);
|
|
4056
|
+
const campaign = await fetchCampaign(id);
|
|
4057
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4058
|
+
const audiences = normalizeAudiences(targeting.audiences);
|
|
4059
|
+
targeting.audiences = {
|
|
4060
|
+
...audiences,
|
|
4061
|
+
[mode]: (mode === "include" ? audiences.include : audiences.exclude).filter((audId) => audId !== opts.audience)
|
|
4062
|
+
};
|
|
4063
|
+
await patchCampaign(id, { targeting });
|
|
4064
|
+
console.log(`Campaign ${id} audience ${opts.audience} removed from ${mode}.`);
|
|
4065
|
+
});
|
|
4066
|
+
targetingCmd.command("set-day-parting").description("Set or disable day parting schedule.").argument("<id>", "Campaign ID").option("--timezone <tz>", "Timezone (default: keep existing or UTC)").option("--from-json <file>", "Schedule JSON file").option("--disable", "Disable day parting").action(async (id, opts) => {
|
|
4067
|
+
if (!opts.disable && !opts.fromJson) {
|
|
4068
|
+
console.error("Error: provide --from-json <file> or --disable.");
|
|
4069
|
+
process.exit(1);
|
|
4070
|
+
}
|
|
4071
|
+
const campaign = await fetchCampaign(id);
|
|
4072
|
+
let dayParting;
|
|
4073
|
+
if (opts.disable) {
|
|
4074
|
+
dayParting = null;
|
|
4075
|
+
} else {
|
|
4076
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
4077
|
+
dayParting = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
4078
|
+
}
|
|
4079
|
+
await patchCampaign(id, {
|
|
4080
|
+
dayParting,
|
|
4081
|
+
timezone: opts.timezone ?? campaign.timezone ?? "UTC"
|
|
4082
|
+
});
|
|
4083
|
+
console.log(`Campaign ${id} day parting updated.`);
|
|
4084
|
+
});
|
|
4085
|
+
for (const [resourceName, field] of [
|
|
4086
|
+
["domain", "domains"],
|
|
4087
|
+
["keyword", "keywords"],
|
|
4088
|
+
["ip-range", "ipRanges"]
|
|
4089
|
+
]) {
|
|
4090
|
+
targetingCmd.command(`add-${resourceName}`).description(`Add a ${resourceName} to targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to add`).option("--mode <mode>", "target or block (default: keep current or target)", "target").action(async (id, opts) => {
|
|
4091
|
+
const campaign = await fetchCampaign(id);
|
|
4092
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4093
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4094
|
+
const value = String(opts[camelizeOption(resourceName)]).trim();
|
|
4095
|
+
if (!value) {
|
|
4096
|
+
console.error(`Error: --${resourceName} is required.`);
|
|
4097
|
+
process.exit(1);
|
|
4098
|
+
}
|
|
4099
|
+
const list = new Set(existing.list);
|
|
4100
|
+
list.add(value);
|
|
4101
|
+
targeting[field] = {
|
|
4102
|
+
mode: opts.mode === "block" ? "block" : existing.mode,
|
|
4103
|
+
list: Array.from(list)
|
|
4104
|
+
};
|
|
4105
|
+
await patchCampaign(id, { targeting });
|
|
4106
|
+
console.log(`Campaign ${id} ${resourceName} added.`);
|
|
4107
|
+
});
|
|
4108
|
+
targetingCmd.command(`remove-${resourceName}`).description(`Remove a ${resourceName} from targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to remove`).action(async (id, opts) => {
|
|
4109
|
+
const campaign = await fetchCampaign(id);
|
|
4110
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4111
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4112
|
+
const value = String(opts[camelizeOption(resourceName)]).trim();
|
|
4113
|
+
targeting[field] = {
|
|
4114
|
+
...existing,
|
|
4115
|
+
list: existing.list.filter((item) => item !== value)
|
|
4116
|
+
};
|
|
4117
|
+
await patchCampaign(id, { targeting });
|
|
4118
|
+
console.log(`Campaign ${id} ${resourceName} removed.`);
|
|
4119
|
+
});
|
|
4120
|
+
targetingCmd.command(`set-${resourceName}-mode`).description(`Set ${resourceName} targeting mode.`).argument("<id>", "Campaign ID").requiredOption("--mode <mode>", "target or block").action(async (id, opts) => {
|
|
4121
|
+
const campaign = await fetchCampaign(id);
|
|
4122
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4123
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4124
|
+
targeting[field] = {
|
|
4125
|
+
...existing,
|
|
4126
|
+
mode: opts.mode === "block" ? "block" : "target"
|
|
4127
|
+
};
|
|
4128
|
+
await patchCampaign(id, { targeting });
|
|
4129
|
+
console.log(`Campaign ${id} ${resourceName} mode updated.`);
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
cmd.addCommand(targetingCmd);
|
|
3829
4133
|
return cmd;
|
|
3830
4134
|
}
|
|
4135
|
+
async function fetchCampaign(id) {
|
|
4136
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/campaigns/${id}` });
|
|
4137
|
+
const json = await resp.json();
|
|
4138
|
+
if (!resp.ok) {
|
|
4139
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4140
|
+
process.exit(1);
|
|
4141
|
+
}
|
|
4142
|
+
return json.data ?? json;
|
|
4143
|
+
}
|
|
4144
|
+
async function patchCampaign(id, body) {
|
|
4145
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
|
|
4146
|
+
const json = await resp.json().catch(() => ({}));
|
|
4147
|
+
if (!resp.ok) {
|
|
4148
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
4149
|
+
process.exit(1);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function parseCsvList(value) {
|
|
4153
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
4154
|
+
}
|
|
4155
|
+
function normalizeAudienceMode(value) {
|
|
4156
|
+
const normalized = String(value).toLowerCase();
|
|
4157
|
+
if (normalized !== "include" && normalized !== "exclude") {
|
|
4158
|
+
console.error("Error: --mode must be include or exclude.");
|
|
4159
|
+
process.exit(1);
|
|
4160
|
+
}
|
|
4161
|
+
return normalized;
|
|
4162
|
+
}
|
|
4163
|
+
function normalizeTargeting(targeting) {
|
|
4164
|
+
return targeting && typeof targeting === "object" ? { ...targeting } : {};
|
|
4165
|
+
}
|
|
4166
|
+
function normalizeAudiences(audiences) {
|
|
4167
|
+
return {
|
|
4168
|
+
include: Array.isArray(audiences?.include) ? audiences.include : [],
|
|
4169
|
+
exclude: Array.isArray(audiences?.exclude) ? audiences.exclude : []
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
function normalizeListTargeting(value) {
|
|
4173
|
+
return {
|
|
4174
|
+
mode: value?.mode === "block" ? "block" : "target",
|
|
4175
|
+
list: Array.isArray(value?.list) ? value.list : []
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
function camelizeOption(value) {
|
|
4179
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
4180
|
+
}
|
|
3831
4181
|
|
|
3832
4182
|
// src/commands/variations.ts
|
|
3833
4183
|
var COLUMNS3 = [
|
|
@@ -3925,85 +4275,295 @@ Examples:
|
|
|
3925
4275
|
return cmd;
|
|
3926
4276
|
}
|
|
3927
4277
|
|
|
3928
|
-
// src/commands/
|
|
4278
|
+
// src/commands/media-assets.ts
|
|
4279
|
+
import { basename, extname } from "node:path";
|
|
4280
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3929
4281
|
var COLUMNS4 = [
|
|
3930
4282
|
{ key: "id", header: "ID", width: 36 },
|
|
3931
|
-
{ key: "name", header: "NAME", width:
|
|
3932
|
-
{ key: "
|
|
3933
|
-
{ key: "status", header: "STATUS", width:
|
|
3934
|
-
{ key: "
|
|
4283
|
+
{ key: "name", header: "NAME", width: 24 },
|
|
4284
|
+
{ key: "type", header: "TYPE", width: 10 },
|
|
4285
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
4286
|
+
{ key: "adFormat", header: "FORMAT", width: 12 },
|
|
4287
|
+
{ key: "mimeType", header: "MIME", width: 20 }
|
|
3935
4288
|
];
|
|
3936
|
-
function
|
|
3937
|
-
|
|
4289
|
+
function inferMimeType(filePath) {
|
|
4290
|
+
switch (extname(filePath).toLowerCase()) {
|
|
4291
|
+
case ".png":
|
|
4292
|
+
return "image/png";
|
|
4293
|
+
case ".jpg":
|
|
4294
|
+
case ".jpeg":
|
|
4295
|
+
return "image/jpeg";
|
|
4296
|
+
case ".gif":
|
|
4297
|
+
return "image/gif";
|
|
4298
|
+
case ".webp":
|
|
4299
|
+
return "image/webp";
|
|
4300
|
+
case ".svg":
|
|
4301
|
+
return "image/svg+xml";
|
|
4302
|
+
case ".mp4":
|
|
4303
|
+
return "video/mp4";
|
|
4304
|
+
case ".mov":
|
|
4305
|
+
return "video/quicktime";
|
|
4306
|
+
case ".html":
|
|
4307
|
+
return "text/html";
|
|
4308
|
+
case ".txt":
|
|
4309
|
+
return "text/plain";
|
|
4310
|
+
default:
|
|
4311
|
+
return "application/octet-stream";
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function normalizeAsset(asset) {
|
|
4315
|
+
return {
|
|
4316
|
+
id: asset.id,
|
|
4317
|
+
name: asset.name,
|
|
4318
|
+
type: asset.type,
|
|
4319
|
+
status: asset.status,
|
|
4320
|
+
adFormat: asset.adFormat ?? asset.ad_format,
|
|
4321
|
+
mimeType: asset.mimeType ?? asset.mime_type
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
function createMediaAssetsCommand() {
|
|
4325
|
+
const cmd = new Command("media-assets").description(`Media library management (DSP)
|
|
3938
4326
|
|
|
3939
|
-
Requires:
|
|
4327
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
3940
4328
|
Examples:
|
|
3941
|
-
$ a8techads
|
|
3942
|
-
$ a8techads
|
|
3943
|
-
$ a8techads
|
|
3944
|
-
|
|
4329
|
+
$ a8techads media-assets list
|
|
4330
|
+
$ a8techads media-assets get <id>
|
|
4331
|
+
$ a8techads media-assets upload --file ./banner.png --ad-format BANNER
|
|
4332
|
+
$ a8techads media-assets pause <id>`);
|
|
4333
|
+
addFormatOption(cmd.command("list").description("List media assets.").option("--status <status>", "Filter by status").option("--type <type>", "Filter by type").option("--ad-format <format>", "Filter by ad format").option("--search <text>", "Search by name").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
|
|
3945
4334
|
const params = new URLSearchParams;
|
|
4335
|
+
params.set("limit", opts.limit);
|
|
4336
|
+
params.set("offset", opts.offset);
|
|
3946
4337
|
if (opts.status)
|
|
3947
4338
|
params.set("status", opts.status);
|
|
3948
|
-
|
|
3949
|
-
|
|
4339
|
+
if (opts.type)
|
|
4340
|
+
params.set("type", opts.type);
|
|
4341
|
+
if (opts.adFormat)
|
|
4342
|
+
params.set("adFormat", opts.adFormat);
|
|
4343
|
+
if (opts.search)
|
|
4344
|
+
params.set("search", opts.search);
|
|
4345
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets?${params}` });
|
|
3950
4346
|
const json = await resp.json();
|
|
3951
4347
|
if (!resp.ok) {
|
|
3952
4348
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3953
4349
|
process.exit(1);
|
|
3954
4350
|
}
|
|
3955
|
-
const rows = (json.data ?? json).map(
|
|
3956
|
-
id: s.id,
|
|
3957
|
-
name: s.name,
|
|
3958
|
-
domain: s.domain,
|
|
3959
|
-
status: s.status,
|
|
3960
|
-
zoneCount: s.zoneCount ?? s.zone_count ?? "-"
|
|
3961
|
-
}));
|
|
4351
|
+
const rows = (json.data ?? json).map(normalizeAsset);
|
|
3962
4352
|
printData(rows, COLUMNS4, opts.format);
|
|
3963
4353
|
});
|
|
3964
|
-
addFormatOption(cmd.command("
|
|
3965
|
-
const
|
|
4354
|
+
addFormatOption(cmd.command("active").description("List active media assets.").option("--ad-format <format>", "Filter by ad format").option("--width <n>", "Required width").option("--height <n>", "Required height").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
|
|
4355
|
+
const params = new URLSearchParams;
|
|
4356
|
+
params.set("limit", opts.limit);
|
|
4357
|
+
params.set("offset", opts.offset);
|
|
4358
|
+
if (opts.adFormat)
|
|
4359
|
+
params.set("adFormat", opts.adFormat);
|
|
4360
|
+
if (opts.width)
|
|
4361
|
+
params.set("width", opts.width);
|
|
4362
|
+
if (opts.height)
|
|
4363
|
+
params.set("height", opts.height);
|
|
4364
|
+
const resp = await apiRequest({
|
|
4365
|
+
path: `${dspPrefix()}/media-assets/active?${params}`
|
|
4366
|
+
});
|
|
3966
4367
|
const json = await resp.json();
|
|
3967
4368
|
if (!resp.ok) {
|
|
3968
4369
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3969
4370
|
process.exit(1);
|
|
3970
4371
|
}
|
|
3971
|
-
|
|
4372
|
+
const rows = (json.data ?? json).map(normalizeAsset);
|
|
4373
|
+
printData(rows, COLUMNS4, opts.format);
|
|
3972
4374
|
});
|
|
3973
|
-
cmd.command("
|
|
3974
|
-
|
|
3975
|
-
if (opts.fromJson) {
|
|
3976
|
-
const { readFileSync: readFileSync3 } = await import("fs");
|
|
3977
|
-
body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
3978
|
-
} else {
|
|
3979
|
-
if (!opts.name || !opts.domain) {
|
|
3980
|
-
console.error("Error: --name and --domain are required.");
|
|
3981
|
-
process.exit(1);
|
|
3982
|
-
}
|
|
3983
|
-
body = { name: opts.name, domain: opts.domain, type: opts.type };
|
|
3984
|
-
}
|
|
3985
|
-
const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
|
|
4375
|
+
addFormatOption(cmd.command("get").description("Get media asset details.").argument("<id>", "Media asset ID")).action(async (id, opts) => {
|
|
4376
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/${id}` });
|
|
3986
4377
|
const json = await resp.json();
|
|
3987
4378
|
if (!resp.ok) {
|
|
3988
4379
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3989
4380
|
process.exit(1);
|
|
3990
4381
|
}
|
|
3991
|
-
|
|
4382
|
+
printDetail(json.data ?? json, opts.format);
|
|
3992
4383
|
});
|
|
3993
|
-
cmd.command("
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
const { readFileSync: readFileSync3 } = await import("fs");
|
|
3997
|
-
body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
3998
|
-
} else {
|
|
3999
|
-
body = {};
|
|
4000
|
-
if (opts.name)
|
|
4001
|
-
body.name = opts.name;
|
|
4002
|
-
}
|
|
4003
|
-
const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
|
|
4384
|
+
addFormatOption(cmd.command("size-limits").description("Get media asset size limits.")).action(async (opts) => {
|
|
4385
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/size-limits` });
|
|
4386
|
+
const json = await resp.json();
|
|
4004
4387
|
if (!resp.ok) {
|
|
4005
|
-
|
|
4006
|
-
|
|
4388
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4389
|
+
process.exit(1);
|
|
4390
|
+
}
|
|
4391
|
+
printDetail(json.data ?? json, opts.format);
|
|
4392
|
+
});
|
|
4393
|
+
cmd.command("upload").description("Upload a media asset file.").requiredOption("--file <path>", "Path to local file").option("--name <name>", "Override asset name").option("--ad-format <format>", "Ad format, e.g. BANNER, NATIVE, VIDEO").action(async (opts) => {
|
|
4394
|
+
const filePath = opts.file;
|
|
4395
|
+
const fileName = basename(filePath);
|
|
4396
|
+
const request = await buildAuthenticatedRequest({
|
|
4397
|
+
method: "POST",
|
|
4398
|
+
path: `${dspPrefix()}/media-assets/upload`,
|
|
4399
|
+
headers: {}
|
|
4400
|
+
});
|
|
4401
|
+
const headers = new Headers(request.init.headers);
|
|
4402
|
+
headers.delete("Content-Type");
|
|
4403
|
+
const body = new FormData;
|
|
4404
|
+
const fileBytes = readFileSync3(filePath);
|
|
4405
|
+
body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
|
|
4406
|
+
if (opts.name)
|
|
4407
|
+
body.append("name", opts.name);
|
|
4408
|
+
if (opts.adFormat)
|
|
4409
|
+
body.append("adFormat", opts.adFormat);
|
|
4410
|
+
const resp = await fetch(request.url, {
|
|
4411
|
+
...request.init,
|
|
4412
|
+
headers,
|
|
4413
|
+
body
|
|
4414
|
+
});
|
|
4415
|
+
const json = await resp.json();
|
|
4416
|
+
if (!resp.ok) {
|
|
4417
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
4418
|
+
process.exit(1);
|
|
4419
|
+
}
|
|
4420
|
+
const asset = json.data ?? json;
|
|
4421
|
+
console.log(`Media asset uploaded: ${asset.id}`);
|
|
4422
|
+
if (asset.name)
|
|
4423
|
+
console.log(` Name: ${asset.name}`);
|
|
4424
|
+
if (asset.cdnUrl ?? asset.cdn_url) {
|
|
4425
|
+
console.log(` CDN URL: ${asset.cdnUrl ?? asset.cdn_url}`);
|
|
4426
|
+
}
|
|
4427
|
+
});
|
|
4428
|
+
cmd.command("update").description("Update media asset metadata.").argument("<id>", "Media asset ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--product-category <value>", "Product category").option("--target-audience <value>", "Target audience").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4429
|
+
let body = {};
|
|
4430
|
+
if (opts.fromJson) {
|
|
4431
|
+
body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
4432
|
+
} else {
|
|
4433
|
+
if (opts.name)
|
|
4434
|
+
body.name = opts.name;
|
|
4435
|
+
if (opts.description)
|
|
4436
|
+
body.description = opts.description;
|
|
4437
|
+
if (opts.productCategory)
|
|
4438
|
+
body.productCategory = opts.productCategory;
|
|
4439
|
+
if (opts.targetAudience)
|
|
4440
|
+
body.targetAudience = opts.targetAudience;
|
|
4441
|
+
}
|
|
4442
|
+
const resp = await apiRequest({
|
|
4443
|
+
method: "PATCH",
|
|
4444
|
+
path: `${dspPrefix()}/media-assets/${id}`,
|
|
4445
|
+
body
|
|
4446
|
+
});
|
|
4447
|
+
const json = await resp.json();
|
|
4448
|
+
if (!resp.ok) {
|
|
4449
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4450
|
+
process.exit(1);
|
|
4451
|
+
}
|
|
4452
|
+
console.log(`Media asset ${id} updated.`);
|
|
4453
|
+
});
|
|
4454
|
+
cmd.command("delete").description("Delete a media asset.").argument("<id>", "Media asset ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
4455
|
+
if (!opts.yes) {
|
|
4456
|
+
console.error("Add --yes to confirm deletion.");
|
|
4457
|
+
process.exit(1);
|
|
4458
|
+
}
|
|
4459
|
+
const resp = await apiRequest({
|
|
4460
|
+
method: "DELETE",
|
|
4461
|
+
path: `${dspPrefix()}/media-assets/${id}`
|
|
4462
|
+
});
|
|
4463
|
+
if (!resp.ok && resp.status !== 204) {
|
|
4464
|
+
const json = await resp.json();
|
|
4465
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4466
|
+
process.exit(1);
|
|
4467
|
+
}
|
|
4468
|
+
console.log(`Media asset ${id} deleted.`);
|
|
4469
|
+
});
|
|
4470
|
+
for (const action of ["pause", "resume", "archive", "unarchive"]) {
|
|
4471
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a media asset.`).argument("<id>", "Media asset ID").action(async (id) => {
|
|
4472
|
+
const resp = await apiRequest({
|
|
4473
|
+
method: "PATCH",
|
|
4474
|
+
path: `${dspPrefix()}/media-assets/${id}/${action}`,
|
|
4475
|
+
body: {}
|
|
4476
|
+
});
|
|
4477
|
+
const json = await resp.json();
|
|
4478
|
+
if (!resp.ok) {
|
|
4479
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4480
|
+
process.exit(1);
|
|
4481
|
+
}
|
|
4482
|
+
console.log(`Media asset ${id} ${action}d.`);
|
|
4483
|
+
});
|
|
4484
|
+
}
|
|
4485
|
+
return cmd;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
// src/commands/sites.ts
|
|
4489
|
+
var COLUMNS5 = [
|
|
4490
|
+
{ key: "id", header: "ID", width: 36 },
|
|
4491
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
4492
|
+
{ key: "domain", header: "DOMAIN", width: 25 },
|
|
4493
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
4494
|
+
{ key: "zoneCount", header: "ZONES", width: 6 }
|
|
4495
|
+
];
|
|
4496
|
+
function createSitesCommand() {
|
|
4497
|
+
const cmd = new Command("sites").description(`Site management (SSP)
|
|
4498
|
+
|
|
4499
|
+
Requires: PUBLISHER capability.`).addHelpText("after", `
|
|
4500
|
+
Examples:
|
|
4501
|
+
$ a8techads sites list
|
|
4502
|
+
$ a8techads sites get <id>
|
|
4503
|
+
$ a8techads sites create --name "My Blog" --domain blog.example.com`);
|
|
4504
|
+
addFormatOption(cmd.command("list").description("List publisher sites.").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
4505
|
+
const params = new URLSearchParams;
|
|
4506
|
+
if (opts.status)
|
|
4507
|
+
params.set("status", opts.status);
|
|
4508
|
+
params.set("limit", opts.limit);
|
|
4509
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/sites?${params}` });
|
|
4510
|
+
const json = await resp.json();
|
|
4511
|
+
if (!resp.ok) {
|
|
4512
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4513
|
+
process.exit(1);
|
|
4514
|
+
}
|
|
4515
|
+
const rows = (json.data ?? json).map((s) => ({
|
|
4516
|
+
id: s.id,
|
|
4517
|
+
name: s.name,
|
|
4518
|
+
domain: s.domain,
|
|
4519
|
+
status: s.status,
|
|
4520
|
+
zoneCount: s.zoneCount ?? s.zone_count ?? "-"
|
|
4521
|
+
}));
|
|
4522
|
+
printData(rows, COLUMNS5, opts.format);
|
|
4523
|
+
});
|
|
4524
|
+
addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
|
|
4525
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
|
|
4526
|
+
const json = await resp.json();
|
|
4527
|
+
if (!resp.ok) {
|
|
4528
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4529
|
+
process.exit(1);
|
|
4530
|
+
}
|
|
4531
|
+
printDetail(json.data ?? json, opts.format);
|
|
4532
|
+
});
|
|
4533
|
+
cmd.command("create").description("Create a new site.").option("--name <name>", "Site name (required)").option("--domain <domain>", "Site domain (required)").option("--type <type>", "Site type: WEB, APP, CTV", "WEB").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4534
|
+
let body;
|
|
4535
|
+
if (opts.fromJson) {
|
|
4536
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4537
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4538
|
+
} else {
|
|
4539
|
+
if (!opts.name || !opts.domain) {
|
|
4540
|
+
console.error("Error: --name and --domain are required.");
|
|
4541
|
+
process.exit(1);
|
|
4542
|
+
}
|
|
4543
|
+
body = { name: opts.name, domain: opts.domain, type: opts.type };
|
|
4544
|
+
}
|
|
4545
|
+
const resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/sites`, body });
|
|
4546
|
+
const json = await resp.json();
|
|
4547
|
+
if (!resp.ok) {
|
|
4548
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4549
|
+
process.exit(1);
|
|
4550
|
+
}
|
|
4551
|
+
console.log(`Site created: ${json.data?.id ?? json.id}`);
|
|
4552
|
+
});
|
|
4553
|
+
cmd.command("update").description("Update a site.").argument("<id>", "Site ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4554
|
+
let body;
|
|
4555
|
+
if (opts.fromJson) {
|
|
4556
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4557
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4558
|
+
} else {
|
|
4559
|
+
body = {};
|
|
4560
|
+
if (opts.name)
|
|
4561
|
+
body.name = opts.name;
|
|
4562
|
+
}
|
|
4563
|
+
const resp = await apiRequest({ method: "PATCH", path: `${sspPrefix()}/sites/${id}`, body });
|
|
4564
|
+
if (!resp.ok) {
|
|
4565
|
+
const j = await resp.json();
|
|
4566
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4007
4567
|
process.exit(1);
|
|
4008
4568
|
}
|
|
4009
4569
|
console.log(`Site ${id} updated.`);
|
|
@@ -4045,7 +4605,7 @@ Examples:
|
|
|
4045
4605
|
}
|
|
4046
4606
|
|
|
4047
4607
|
// src/commands/zones.ts
|
|
4048
|
-
var
|
|
4608
|
+
var COLUMNS6 = [
|
|
4049
4609
|
{ key: "id", header: "ID", width: 36 },
|
|
4050
4610
|
{ key: "name", header: "NAME", width: 25 },
|
|
4051
4611
|
{ key: "format", header: "FORMAT", width: 18 },
|
|
@@ -4077,7 +4637,7 @@ Examples:
|
|
|
4077
4637
|
format: z.adFormat ?? z.ad_format ?? "-",
|
|
4078
4638
|
status: z.status
|
|
4079
4639
|
}));
|
|
4080
|
-
printData(rows,
|
|
4640
|
+
printData(rows, COLUMNS6, opts.format);
|
|
4081
4641
|
});
|
|
4082
4642
|
addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
|
|
4083
4643
|
const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
|
|
@@ -4091,8 +4651,8 @@ Examples:
|
|
|
4091
4651
|
cmd.command("create").description("Create a new ad zone.").option("--site <id>", "Site ID (required)").option("--name <name>", "Zone name (required)").option("--format <format>", "Ad format (e.g., banner_300x250)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4092
4652
|
let body;
|
|
4093
4653
|
if (opts.fromJson) {
|
|
4094
|
-
const { readFileSync:
|
|
4095
|
-
body = JSON.parse(
|
|
4654
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4655
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4096
4656
|
} else {
|
|
4097
4657
|
if (!opts.site || !opts.name) {
|
|
4098
4658
|
console.error("Error: --site and --name are required.");
|
|
@@ -4113,8 +4673,8 @@ Examples:
|
|
|
4113
4673
|
cmd.command("update").description("Update a zone.").argument("<id>", "Zone ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4114
4674
|
let body;
|
|
4115
4675
|
if (opts.fromJson) {
|
|
4116
|
-
const { readFileSync:
|
|
4117
|
-
body = JSON.parse(
|
|
4676
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4677
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4118
4678
|
} else {
|
|
4119
4679
|
body = {};
|
|
4120
4680
|
if (opts.name)
|
|
@@ -4163,11 +4723,12 @@ Examples:
|
|
|
4163
4723
|
$ a8techads reports templates`);
|
|
4164
4724
|
addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
|
|
4165
4725
|
|
|
4166
|
-
Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
|
|
4726
|
+
Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo,goal,goal_id,goal_order)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
|
|
4167
4727
|
Examples:
|
|
4168
4728
|
$ a8techads reports query --metrics spend,impressions --dimensions date --from 2026-03-01 --to 2026-03-20
|
|
4169
4729
|
$ a8techads reports query --metrics revenue --dimensions publisher --format json
|
|
4170
|
-
$ a8techads reports query --dimensions geo --limit 10
|
|
4730
|
+
$ a8techads reports query --dimensions geo --limit 10
|
|
4731
|
+
$ a8techads reports query --dimensions goal,date --metrics conversions,spend,cpa --from 2026-03-01`)).action(async (opts) => {
|
|
4171
4732
|
const body = {
|
|
4172
4733
|
metrics: opts.metrics.split(","),
|
|
4173
4734
|
dimensions: opts.dimensions.split(","),
|
|
@@ -4183,19 +4744,20 @@ Examples:
|
|
|
4183
4744
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4184
4745
|
process.exit(1);
|
|
4185
4746
|
}
|
|
4186
|
-
const
|
|
4747
|
+
const data = json.data ?? json;
|
|
4748
|
+
const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
|
|
4187
4749
|
if (rows.length === 0) {
|
|
4188
4750
|
console.log("No results.");
|
|
4189
4751
|
return;
|
|
4190
4752
|
}
|
|
4191
4753
|
if (opts.format === "json") {
|
|
4192
|
-
console.log(JSON.stringify(
|
|
4754
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4193
4755
|
return;
|
|
4194
4756
|
}
|
|
4195
4757
|
const keys = Object.keys(rows[0]);
|
|
4196
4758
|
const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
|
|
4197
4759
|
printData(rows, columns, opts.format);
|
|
4198
|
-
const summary = json.
|
|
4760
|
+
const summary = data.summary ?? data.totals ?? json.summary ?? json.totals;
|
|
4199
4761
|
if (summary && opts.format === "table") {
|
|
4200
4762
|
console.log(`
|
|
4201
4763
|
Summary:`);
|
|
@@ -4211,7 +4773,9 @@ Summary:`);
|
|
|
4211
4773
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4212
4774
|
process.exit(1);
|
|
4213
4775
|
}
|
|
4214
|
-
const
|
|
4776
|
+
const data = json.data ?? json;
|
|
4777
|
+
const reports = data.reports ?? json.reports ?? data;
|
|
4778
|
+
const rows = (Array.isArray(reports) ? reports : []).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
|
|
4215
4779
|
printData(rows, [
|
|
4216
4780
|
{ key: "id", header: "ID", width: 36 },
|
|
4217
4781
|
{ key: "name", header: "NAME", width: 30 },
|
|
@@ -4234,7 +4798,9 @@ Summary:`);
|
|
|
4234
4798
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4235
4799
|
process.exit(1);
|
|
4236
4800
|
}
|
|
4237
|
-
const
|
|
4801
|
+
const data = json.data ?? json;
|
|
4802
|
+
const templates = data.templates ?? json.templates ?? data;
|
|
4803
|
+
const rows = (Array.isArray(templates) ? templates : []).map((t) => ({ id: t.id, name: t.name, description: t.description }));
|
|
4238
4804
|
printData(rows, [
|
|
4239
4805
|
{ key: "id", header: "ID", width: 20 },
|
|
4240
4806
|
{ key: "name", header: "NAME", width: 25 },
|
|
@@ -4244,8 +4810,8 @@ Summary:`);
|
|
|
4244
4810
|
cmd.command("create").description("Create a saved report.").option("--name <name>", "Report name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4245
4811
|
let body;
|
|
4246
4812
|
if (opts.fromJson) {
|
|
4247
|
-
const { readFileSync:
|
|
4248
|
-
body = JSON.parse(
|
|
4813
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4814
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4249
4815
|
} else {
|
|
4250
4816
|
if (!opts.name) {
|
|
4251
4817
|
console.error("Error: --name is required.");
|
|
@@ -4347,8 +4913,8 @@ Examples:
|
|
|
4347
4913
|
cmd.command("settings-update").description("Update billing settings.").option("--auto-recharge <bool>", "Enable/disable auto-recharge (true/false)").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
4348
4914
|
let body;
|
|
4349
4915
|
if (opts.fromJson) {
|
|
4350
|
-
const { readFileSync:
|
|
4351
|
-
body = JSON.parse(
|
|
4916
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4917
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4352
4918
|
} else {
|
|
4353
4919
|
body = {};
|
|
4354
4920
|
if (opts.autoRecharge !== undefined)
|
|
@@ -4362,6 +4928,60 @@ Examples:
|
|
|
4362
4928
|
}
|
|
4363
4929
|
console.log("Billing settings updated.");
|
|
4364
4930
|
});
|
|
4931
|
+
addFormatOption(cmd.command("payment-methods").description("List saved payment methods.")).action(async (opts) => {
|
|
4932
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/payments/stripe/payment-methods` });
|
|
4933
|
+
const json = await resp.json();
|
|
4934
|
+
if (!resp.ok) {
|
|
4935
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4936
|
+
process.exit(1);
|
|
4937
|
+
}
|
|
4938
|
+
const columns = [
|
|
4939
|
+
{ key: "id", header: "ID", width: 30 },
|
|
4940
|
+
{ key: "type", header: "TYPE", width: 10 },
|
|
4941
|
+
{ key: "last4", header: "LAST4", width: 6 },
|
|
4942
|
+
{ key: "brand", header: "BRAND", width: 12 },
|
|
4943
|
+
{ key: "expires", header: "EXPIRES", width: 10 }
|
|
4944
|
+
];
|
|
4945
|
+
const rows = (json.data ?? json).map((pm) => ({
|
|
4946
|
+
id: pm.id,
|
|
4947
|
+
type: pm.type,
|
|
4948
|
+
last4: pm.last4 ?? pm.card?.last4,
|
|
4949
|
+
brand: pm.brand ?? pm.card?.brand,
|
|
4950
|
+
expires: pm.expires ?? (pm.card ? `${pm.card.exp_month}/${pm.card.exp_year}` : "-")
|
|
4951
|
+
}));
|
|
4952
|
+
printData(rows, columns, opts.format);
|
|
4953
|
+
});
|
|
4954
|
+
cmd.command("usdt-deposit").description("Initiate a USDT deposit.").option("--amount <dollars>", "Deposit amount in dollars (required)").option("--network <network>", "Blockchain network", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
4955
|
+
if (!opts.amount) {
|
|
4956
|
+
console.error("Error: --amount is required.");
|
|
4957
|
+
process.exit(1);
|
|
4958
|
+
}
|
|
4959
|
+
const amount = Number(opts.amount);
|
|
4960
|
+
if (isNaN(amount) || amount <= 0) {
|
|
4961
|
+
console.error("Error: Amount must be a positive number.");
|
|
4962
|
+
process.exit(1);
|
|
4963
|
+
}
|
|
4964
|
+
if (!opts.yes) {
|
|
4965
|
+
const rl = await import("readline");
|
|
4966
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
4967
|
+
const answer = await new Promise((resolve) => iface.question(`Initiate USDT deposit of $${amount.toFixed(2)}? (y/N) `, resolve));
|
|
4968
|
+
iface.close();
|
|
4969
|
+
if (answer.toLowerCase() !== "y") {
|
|
4970
|
+
console.log("Cancelled.");
|
|
4971
|
+
process.exit(0);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/payments/usdt/checkout`, body: { amount, network: opts.network } });
|
|
4975
|
+
const json = await resp.json();
|
|
4976
|
+
if (!resp.ok) {
|
|
4977
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4978
|
+
process.exit(1);
|
|
4979
|
+
}
|
|
4980
|
+
console.log(`USDT deposit of $${amount.toFixed(2)} initiated.`);
|
|
4981
|
+
if (json.data?.paymentUrl || json.paymentUrl) {
|
|
4982
|
+
console.log(`Payment URL: ${json.data?.paymentUrl ?? json.paymentUrl}`);
|
|
4983
|
+
}
|
|
4984
|
+
});
|
|
4365
4985
|
return cmd;
|
|
4366
4986
|
}
|
|
4367
4987
|
|
|
@@ -4378,7 +4998,7 @@ function usersPrefix() {
|
|
|
4378
4998
|
}
|
|
4379
4999
|
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
4380
5000
|
}
|
|
4381
|
-
var
|
|
5001
|
+
var COLUMNS7 = [
|
|
4382
5002
|
{ key: "id", header: "ID", width: 36 },
|
|
4383
5003
|
{ key: "email", header: "EMAIL", width: 30 },
|
|
4384
5004
|
{ key: "name", header: "NAME", width: 20 },
|
|
@@ -4409,7 +5029,7 @@ Examples:
|
|
|
4409
5029
|
role: u.role,
|
|
4410
5030
|
status: u.status
|
|
4411
5031
|
}));
|
|
4412
|
-
printData(rows,
|
|
5032
|
+
printData(rows, COLUMNS7, opts.format);
|
|
4413
5033
|
});
|
|
4414
5034
|
addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
|
|
4415
5035
|
const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
|
|
@@ -4488,39 +5108,52 @@ Examples:
|
|
|
4488
5108
|
return cmd;
|
|
4489
5109
|
}
|
|
4490
5110
|
|
|
4491
|
-
// src/commands/
|
|
4492
|
-
function
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
const
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
5111
|
+
// src/commands/admin.ts
|
|
5112
|
+
async function confirmAction(message, yes) {
|
|
5113
|
+
if (yes)
|
|
5114
|
+
return;
|
|
5115
|
+
const rl = await import("readline");
|
|
5116
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
5117
|
+
const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
|
|
5118
|
+
iface.close();
|
|
5119
|
+
if (answer.toLowerCase() !== "y") {
|
|
5120
|
+
console.log("Cancelled.");
|
|
5121
|
+
process.exit(0);
|
|
4500
5122
|
}
|
|
4501
|
-
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
4502
5123
|
}
|
|
4503
|
-
function
|
|
4504
|
-
const cmd = new Command("
|
|
5124
|
+
function createAdminCommand() {
|
|
5125
|
+
const cmd = new Command("admin").description(`Platform administration (Console)
|
|
4505
5126
|
|
|
4506
|
-
Requires:
|
|
4507
|
-
Not available in Console mode.`).addHelpText("after", `
|
|
5127
|
+
Requires: platform_owner or platform_admin role.`).addHelpText("after", `
|
|
4508
5128
|
Examples:
|
|
4509
|
-
$ a8techads
|
|
4510
|
-
$ a8techads
|
|
4511
|
-
$ a8techads
|
|
4512
|
-
$ a8techads
|
|
4513
|
-
|
|
4514
|
-
|
|
5129
|
+
$ a8techads admin tenants list
|
|
5130
|
+
$ a8techads admin tenants get <id>
|
|
5131
|
+
$ a8techads admin audit-logs
|
|
5132
|
+
$ a8techads admin system health`);
|
|
5133
|
+
const tenants = cmd.command("tenants").description("Tenant management");
|
|
5134
|
+
addFormatOption(tenants.command("list").description("List all tenants.")).action(async (opts) => {
|
|
5135
|
+
const resp = await apiRequest({ path: "/api/v1/console/tenants" });
|
|
4515
5136
|
const json = await resp.json();
|
|
4516
5137
|
if (!resp.ok) {
|
|
4517
5138
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4518
5139
|
process.exit(1);
|
|
4519
5140
|
}
|
|
4520
|
-
|
|
5141
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
5142
|
+
id: t.id,
|
|
5143
|
+
name: t.companyName ?? t.company_name,
|
|
5144
|
+
type: t.tenantType ?? t.tenant_type,
|
|
5145
|
+
status: t.status
|
|
5146
|
+
}));
|
|
5147
|
+
const columns = [
|
|
5148
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5149
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
5150
|
+
{ key: "type", header: "TYPE", width: 12 },
|
|
5151
|
+
{ key: "status", header: "STATUS", width: 10 }
|
|
5152
|
+
];
|
|
5153
|
+
printData(rows, columns, opts.format);
|
|
4521
5154
|
});
|
|
4522
|
-
addFormatOption(
|
|
4523
|
-
const resp = await apiRequest({ path:
|
|
5155
|
+
addFormatOption(tenants.command("get").description("Get tenant details.").argument("<id>", "Tenant ID")).action(async (id, opts) => {
|
|
5156
|
+
const resp = await apiRequest({ path: `/api/v1/console/tenants/${id}` });
|
|
4524
5157
|
const json = await resp.json();
|
|
4525
5158
|
if (!resp.ok) {
|
|
4526
5159
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
@@ -4528,76 +5161,1364 @@ Examples:
|
|
|
4528
5161
|
}
|
|
4529
5162
|
printDetail(json.data ?? json, opts.format);
|
|
4530
5163
|
});
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
5164
|
+
tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required)").option("--admin-name <name>", "Admin user name").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
|
|
5165
|
+
Examples:
|
|
5166
|
+
$ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
|
|
5167
|
+
$ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
|
|
5168
|
+
if (!opts.name) {
|
|
5169
|
+
console.error("Error: --name is required.");
|
|
4536
5170
|
process.exit(1);
|
|
4537
5171
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
5172
|
+
if (!opts.email) {
|
|
5173
|
+
console.error("Error: --email is required.");
|
|
5174
|
+
process.exit(1);
|
|
5175
|
+
}
|
|
5176
|
+
const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
|
|
5177
|
+
const body = {
|
|
5178
|
+
tenant: {
|
|
5179
|
+
companyName: opts.name,
|
|
5180
|
+
capabilities
|
|
5181
|
+
},
|
|
5182
|
+
admin: {
|
|
5183
|
+
email: opts.email,
|
|
5184
|
+
name: opts.adminName ?? undefined
|
|
5185
|
+
}
|
|
5186
|
+
};
|
|
5187
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/tenants`, body });
|
|
5188
|
+
const json = await resp.json();
|
|
5189
|
+
if (!resp.ok) {
|
|
5190
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5191
|
+
process.exit(1);
|
|
5192
|
+
}
|
|
5193
|
+
const data = json.data ?? json;
|
|
5194
|
+
const tenant = data.tenant ?? data;
|
|
5195
|
+
console.log(`Tenant created: ${tenant.id}`);
|
|
5196
|
+
if (tenant.companyName)
|
|
5197
|
+
console.log(` Name: ${tenant.companyName}`);
|
|
5198
|
+
if (tenant.status)
|
|
5199
|
+
console.log(` Status: ${tenant.status}`);
|
|
5200
|
+
if (data.admin) {
|
|
5201
|
+
console.log(` Admin: ${data.admin.email} (userId: ${data.admin.userId})`);
|
|
5202
|
+
}
|
|
5203
|
+
});
|
|
5204
|
+
tenants.command("update").description("Update a tenant.").argument("<id>", "Tenant ID").option("--name <name>", "New company name").option("--status <status>", "New status: ACTIVE, SUSPENDED, TESTING").action(async (id, opts) => {
|
|
5205
|
+
const body = {};
|
|
5206
|
+
if (opts.name)
|
|
5207
|
+
body.companyName = opts.name;
|
|
5208
|
+
if (opts.status)
|
|
5209
|
+
body.status = opts.status.toUpperCase();
|
|
5210
|
+
if (Object.keys(body).length === 0) {
|
|
5211
|
+
console.error("Error: at least one of --name or --status is required.");
|
|
5212
|
+
process.exit(1);
|
|
5213
|
+
}
|
|
5214
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/tenants/${id}`, body });
|
|
4541
5215
|
if (!resp.ok) {
|
|
4542
5216
|
const j = await resp.json();
|
|
4543
5217
|
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4544
5218
|
process.exit(1);
|
|
4545
5219
|
}
|
|
4546
|
-
console.log(
|
|
5220
|
+
console.log(`Tenant ${id} updated.`);
|
|
4547
5221
|
});
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
body
|
|
4553
|
-
}
|
|
4554
|
-
body = {};
|
|
4555
|
-
if (opts.companyName)
|
|
4556
|
-
body.companyName = opts.companyName;
|
|
4557
|
-
}
|
|
4558
|
-
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
|
|
5222
|
+
tenants.command("suspend").description("Suspend a tenant (set status to SUSPENDED).").argument("<id>", "Tenant ID").action(async (id) => {
|
|
5223
|
+
const resp = await apiRequest({
|
|
5224
|
+
method: "PATCH",
|
|
5225
|
+
path: `${consolePrefix()}/tenants/${id}`,
|
|
5226
|
+
body: { status: "SUSPENDED" }
|
|
5227
|
+
});
|
|
4559
5228
|
if (!resp.ok) {
|
|
4560
5229
|
const j = await resp.json();
|
|
4561
5230
|
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4562
5231
|
process.exit(1);
|
|
4563
5232
|
}
|
|
4564
|
-
console.log(
|
|
5233
|
+
console.log(`Tenant ${id} suspended.`);
|
|
4565
5234
|
});
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
4574
|
-
Examples:
|
|
4575
|
-
$ a8techads invoices list
|
|
4576
|
-
$ a8techads invoices get <id>
|
|
4577
|
-
$ a8techads invoices spending`);
|
|
4578
|
-
addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
|
|
4579
|
-
const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
|
|
4580
|
-
const json = await resp.json();
|
|
5235
|
+
tenants.command("activate").description("Activate a tenant (set status to ACTIVE).").argument("<id>", "Tenant ID").action(async (id) => {
|
|
5236
|
+
const resp = await apiRequest({
|
|
5237
|
+
method: "PATCH",
|
|
5238
|
+
path: `${consolePrefix()}/tenants/${id}`,
|
|
5239
|
+
body: { status: "ACTIVE" }
|
|
5240
|
+
});
|
|
4581
5241
|
if (!resp.ok) {
|
|
4582
|
-
|
|
5242
|
+
const j = await resp.json();
|
|
5243
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5244
|
+
process.exit(1);
|
|
5245
|
+
}
|
|
5246
|
+
console.log(`Tenant ${id} activated.`);
|
|
5247
|
+
});
|
|
5248
|
+
const members = tenants.command("members").description("Tenant member management");
|
|
5249
|
+
addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
|
|
5250
|
+
if (!opts.tenant) {
|
|
5251
|
+
console.error("Error: --tenant is required.");
|
|
5252
|
+
process.exit(1);
|
|
5253
|
+
}
|
|
5254
|
+
const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
|
|
5255
|
+
const json = await resp.json();
|
|
5256
|
+
if (!resp.ok) {
|
|
5257
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5258
|
+
process.exit(1);
|
|
5259
|
+
}
|
|
5260
|
+
const rows = (json.data ?? json).map((m) => ({
|
|
5261
|
+
id: m.userId ?? m.user_id ?? m.id,
|
|
5262
|
+
email: m.email,
|
|
5263
|
+
role: m.role,
|
|
5264
|
+
status: m.status
|
|
5265
|
+
}));
|
|
5266
|
+
printData(rows, [
|
|
5267
|
+
{ key: "id", header: "USER ID", width: 36 },
|
|
5268
|
+
{ key: "email", header: "EMAIL", width: 30 },
|
|
5269
|
+
{ key: "role", header: "ROLE", width: 20 }
|
|
5270
|
+
], opts.format);
|
|
5271
|
+
});
|
|
5272
|
+
addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
5273
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5274
|
+
if (opts.tenant)
|
|
5275
|
+
params.set("tenant_id", opts.tenant);
|
|
5276
|
+
const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
|
|
5277
|
+
const json = await resp.json();
|
|
5278
|
+
if (!resp.ok) {
|
|
5279
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5280
|
+
process.exit(1);
|
|
5281
|
+
}
|
|
5282
|
+
const rows = (json.data ?? json).map((l) => ({
|
|
5283
|
+
action: l.action,
|
|
5284
|
+
target: l.targetType ?? l.target_type,
|
|
5285
|
+
targetId: l.targetId ?? l.target_id,
|
|
5286
|
+
user: l.adminUserId ?? l.admin_user_id,
|
|
5287
|
+
time: l.createdAt ?? l.created_at
|
|
5288
|
+
}));
|
|
5289
|
+
printData(rows, [
|
|
5290
|
+
{ key: "action", header: "ACTION", width: 25 },
|
|
5291
|
+
{ key: "target", header: "TARGET", width: 12 },
|
|
5292
|
+
{ key: "targetId", header: "TARGET ID", width: 36 },
|
|
5293
|
+
{ key: "time", header: "TIME", width: 22 }
|
|
5294
|
+
], opts.format);
|
|
5295
|
+
});
|
|
5296
|
+
const system = cmd.command("system").description("System operations");
|
|
5297
|
+
addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
|
|
5298
|
+
const resp = await apiRequest({ path: "/api/v1/console/system/health" });
|
|
5299
|
+
const json = await resp.json();
|
|
5300
|
+
if (!resp.ok) {
|
|
5301
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5302
|
+
process.exit(1);
|
|
5303
|
+
}
|
|
5304
|
+
printDetail(json.data ?? json, opts.format);
|
|
5305
|
+
});
|
|
5306
|
+
system.command("cache-clear").description("Clear system cache.").action(async () => {
|
|
5307
|
+
const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
|
|
5308
|
+
if (!resp.ok) {
|
|
5309
|
+
const j = await resp.json();
|
|
5310
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5311
|
+
process.exit(1);
|
|
5312
|
+
}
|
|
5313
|
+
console.log("Cache cleared.");
|
|
5314
|
+
});
|
|
5315
|
+
const tracking = cmd.command("tracking-identities").description("User identity graph stats");
|
|
5316
|
+
tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
|
|
5317
|
+
const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
|
|
5318
|
+
const json = await resp.json();
|
|
5319
|
+
if (!resp.ok) {
|
|
5320
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5321
|
+
process.exit(1);
|
|
5322
|
+
}
|
|
5323
|
+
const data = json.data ?? json;
|
|
5324
|
+
console.log(`Tracking Identity Stats:`);
|
|
5325
|
+
console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
|
|
5326
|
+
console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
|
|
5327
|
+
console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
|
|
5328
|
+
console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
|
|
5329
|
+
if (data.earliest_seen)
|
|
5330
|
+
console.log(` Earliest Seen: ${data.earliest_seen}`);
|
|
5331
|
+
if (data.latest_seen)
|
|
5332
|
+
console.log(` Latest Seen: ${data.latest_seen}`);
|
|
5333
|
+
});
|
|
5334
|
+
const finance = cmd.command("finance").description("Financial overview and audit");
|
|
5335
|
+
addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
|
|
5336
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
|
|
5337
|
+
const json = await resp.json();
|
|
5338
|
+
if (!resp.ok) {
|
|
5339
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5340
|
+
process.exit(1);
|
|
5341
|
+
}
|
|
5342
|
+
printDetail(json.data ?? json, opts.format);
|
|
5343
|
+
});
|
|
5344
|
+
finance.command("finalize-daily").description("Run daily billing finalization for a date.").option("--date <yyyy-mm-dd>", "Date to finalize (default: today UTC)").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5345
|
+
const targetDate = opts.date || new Date().toISOString().slice(0, 10);
|
|
5346
|
+
await confirmAction(`Run daily billing finalization for ${targetDate}?`, opts.yes);
|
|
5347
|
+
const body = {};
|
|
5348
|
+
if (opts.date)
|
|
5349
|
+
body.date = opts.date;
|
|
5350
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/finalize-daily`, body });
|
|
5351
|
+
const json = await resp.json();
|
|
5352
|
+
if (!resp.ok) {
|
|
5353
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5354
|
+
process.exit(1);
|
|
5355
|
+
}
|
|
5356
|
+
const data = json.data ?? json;
|
|
5357
|
+
console.log(`Daily billing finalized for ${data.date ?? targetDate}.`);
|
|
5358
|
+
});
|
|
5359
|
+
addFormatOption(finance.command("events").description("List billing events.").option("--limit <n>", "Max results", "20").option("--aggregate-type <type>", "Filter by aggregate type").option("--event-type <type>", "Filter by event type")).action(async (opts) => {
|
|
5360
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5361
|
+
if (opts.aggregateType)
|
|
5362
|
+
params.set("aggregate_type", opts.aggregateType);
|
|
5363
|
+
if (opts.eventType)
|
|
5364
|
+
params.set("event_type", opts.eventType);
|
|
5365
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/events?${params}` });
|
|
5366
|
+
const json = await resp.json();
|
|
5367
|
+
if (!resp.ok) {
|
|
5368
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5369
|
+
process.exit(1);
|
|
5370
|
+
}
|
|
5371
|
+
const payload = json.data ?? json;
|
|
5372
|
+
const rows = (payload.events ?? payload).map((e) => ({
|
|
5373
|
+
id: e.id,
|
|
5374
|
+
eventType: e.eventType ?? e.event_type,
|
|
5375
|
+
aggregateType: e.aggregateType ?? e.aggregate_type,
|
|
5376
|
+
createdAt: e.createdAt ?? e.created_at
|
|
5377
|
+
}));
|
|
5378
|
+
printData(rows, [
|
|
5379
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5380
|
+
{ key: "eventType", header: "EVENT_TYPE", width: 25 },
|
|
5381
|
+
{ key: "aggregateType", header: "AGGREGATE_TYPE", width: 20 },
|
|
5382
|
+
{ key: "createdAt", header: "CREATED_AT", width: 22 }
|
|
5383
|
+
], opts.format);
|
|
5384
|
+
});
|
|
5385
|
+
addFormatOption(finance.command("operations").description("List billing operations.").option("--limit <n>", "Max results", "20").option("--type <type>", "Filter by operation type").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5386
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5387
|
+
if (opts.type)
|
|
5388
|
+
params.set("type", opts.type);
|
|
5389
|
+
if (opts.status)
|
|
5390
|
+
params.set("status", opts.status);
|
|
5391
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/operations?${params}` });
|
|
5392
|
+
const json = await resp.json();
|
|
5393
|
+
if (!resp.ok) {
|
|
5394
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5395
|
+
process.exit(1);
|
|
5396
|
+
}
|
|
5397
|
+
const payload = json.data ?? json;
|
|
5398
|
+
const rows = (payload.operations ?? payload).map((o) => ({
|
|
5399
|
+
id: o.id,
|
|
5400
|
+
type: o.type,
|
|
5401
|
+
target: o.target ?? o.targetId ?? o.target_id,
|
|
5402
|
+
amount: o.amount,
|
|
5403
|
+
status: o.status,
|
|
5404
|
+
createdAt: o.createdAt ?? o.created_at
|
|
5405
|
+
}));
|
|
5406
|
+
printData(rows, [
|
|
5407
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5408
|
+
{ key: "type", header: "TYPE", width: 18 },
|
|
5409
|
+
{ key: "target", header: "TARGET", width: 20 },
|
|
5410
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5411
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5412
|
+
{ key: "createdAt", header: "CREATED_AT", width: 22 }
|
|
5413
|
+
], opts.format);
|
|
5414
|
+
});
|
|
5415
|
+
const invoices = cmd.command("invoices").description("Invoice management");
|
|
5416
|
+
addFormatOption(invoices.command("list").description("List invoices.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status").option("--tenant-type <type>", "Filter by tenant type")).action(async (opts) => {
|
|
5417
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5418
|
+
if (opts.status)
|
|
5419
|
+
params.set("status", opts.status);
|
|
5420
|
+
if (opts.tenantType)
|
|
5421
|
+
params.set("tenantType", opts.tenantType);
|
|
5422
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
|
|
5423
|
+
const json = await resp.json();
|
|
5424
|
+
if (!resp.ok) {
|
|
5425
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5426
|
+
process.exit(1);
|
|
5427
|
+
}
|
|
5428
|
+
const rows = (json.data ?? json).map((i) => ({
|
|
5429
|
+
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5430
|
+
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5431
|
+
amount: i.amount,
|
|
5432
|
+
status: i.status,
|
|
5433
|
+
dueDate: i.dueDate ?? i.due_date
|
|
5434
|
+
}));
|
|
5435
|
+
printData(rows, [
|
|
5436
|
+
{ key: "invoiceNumber", header: "INVOICE#", width: 20 },
|
|
5437
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5438
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5439
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5440
|
+
{ key: "dueDate", header: "DUE_DATE", width: 16 }
|
|
5441
|
+
], opts.format);
|
|
5442
|
+
});
|
|
5443
|
+
addFormatOption(invoices.command("preview").description("Preview unbilled invoices for current period.")).action(async (opts) => {
|
|
5444
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=advertiser` });
|
|
5445
|
+
const json = await resp.json();
|
|
5446
|
+
if (!resp.ok) {
|
|
5447
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5448
|
+
process.exit(1);
|
|
5449
|
+
}
|
|
5450
|
+
const data = json.data ?? json;
|
|
5451
|
+
if (data.summary) {
|
|
5452
|
+
console.log(`
|
|
5453
|
+
Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Total Due: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
|
|
5454
|
+
`);
|
|
5455
|
+
}
|
|
5456
|
+
const rows = (data.previews ?? []).map((p) => ({
|
|
5457
|
+
tenant: p.tenantName,
|
|
5458
|
+
adSpend: p.adSpend,
|
|
5459
|
+
platformFee: p.platformFee,
|
|
5460
|
+
totalDue: p.totalDue,
|
|
5461
|
+
days: p.daysWithData,
|
|
5462
|
+
impressions: p.impressions
|
|
5463
|
+
}));
|
|
5464
|
+
printData(rows, [
|
|
5465
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5466
|
+
{ key: "adSpend", header: "AD_SPEND", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5467
|
+
{ key: "platformFee", header: "PLATFORM_FEE", width: 14, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5468
|
+
{ key: "totalDue", header: "TOTAL_DUE", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5469
|
+
{ key: "days", header: "DAYS", width: 6 },
|
|
5470
|
+
{ key: "impressions", header: "IMPRESSIONS", width: 12 }
|
|
5471
|
+
], opts.format);
|
|
5472
|
+
});
|
|
5473
|
+
addFormatOption(invoices.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
|
|
5474
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
|
|
5475
|
+
const json = await resp.json();
|
|
5476
|
+
if (!resp.ok) {
|
|
5477
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5478
|
+
process.exit(1);
|
|
5479
|
+
}
|
|
5480
|
+
printDetail(json.data ?? json, opts.format);
|
|
5481
|
+
});
|
|
5482
|
+
invoices.command("issue").description("Issue an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5483
|
+
await confirmAction(`Issue invoice ${id}?`, opts.yes);
|
|
5484
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/issue` });
|
|
5485
|
+
if (!resp.ok) {
|
|
5486
|
+
const j = await resp.json();
|
|
5487
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5488
|
+
process.exit(1);
|
|
5489
|
+
}
|
|
5490
|
+
console.log(`Invoice ${id} issued.`);
|
|
5491
|
+
});
|
|
5492
|
+
invoices.command("void").description("Void an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5493
|
+
await confirmAction(`Void invoice ${id}?`, opts.yes);
|
|
5494
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/void` });
|
|
5495
|
+
if (!resp.ok) {
|
|
5496
|
+
const j = await resp.json();
|
|
5497
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5498
|
+
process.exit(1);
|
|
5499
|
+
}
|
|
5500
|
+
console.log(`Invoice ${id} voided.`);
|
|
5501
|
+
});
|
|
5502
|
+
invoices.command("mark-paid").description("Mark an invoice as paid.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5503
|
+
await confirmAction(`Mark invoice ${id} as paid?`, opts.yes);
|
|
5504
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/mark-paid` });
|
|
5505
|
+
if (!resp.ok) {
|
|
5506
|
+
const j = await resp.json();
|
|
5507
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5508
|
+
process.exit(1);
|
|
5509
|
+
}
|
|
5510
|
+
console.log(`Invoice ${id} marked as paid.`);
|
|
5511
|
+
});
|
|
5512
|
+
const statements = cmd.command("statements").description("Publisher statement management");
|
|
5513
|
+
addFormatOption(statements.command("list").description("List publisher statements.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5514
|
+
const params = new URLSearchParams({ limit: opts.limit, tenantType: "publisher" });
|
|
5515
|
+
if (opts.status)
|
|
5516
|
+
params.set("status", opts.status);
|
|
5517
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
|
|
5518
|
+
const json = await resp.json();
|
|
5519
|
+
if (!resp.ok) {
|
|
5520
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5521
|
+
process.exit(1);
|
|
5522
|
+
}
|
|
5523
|
+
const rows = (json.data ?? json).map((i) => ({
|
|
5524
|
+
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5525
|
+
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5526
|
+
amount: i.amount,
|
|
5527
|
+
status: i.status,
|
|
5528
|
+
dueDate: i.dueDate ?? i.due_date
|
|
5529
|
+
}));
|
|
5530
|
+
printData(rows, [
|
|
5531
|
+
{ key: "invoiceNumber", header: "INVOICE#", width: 20 },
|
|
5532
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5533
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5534
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5535
|
+
{ key: "dueDate", header: "DUE_DATE", width: 16 }
|
|
5536
|
+
], opts.format);
|
|
5537
|
+
});
|
|
5538
|
+
addFormatOption(statements.command("preview").description("Preview unbilled statements for current period.")).action(async (opts) => {
|
|
5539
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=publisher` });
|
|
5540
|
+
const json = await resp.json();
|
|
5541
|
+
if (!resp.ok) {
|
|
5542
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5543
|
+
process.exit(1);
|
|
5544
|
+
}
|
|
5545
|
+
const data = json.data ?? json;
|
|
5546
|
+
if (data.summary) {
|
|
5547
|
+
console.log(`
|
|
5548
|
+
Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Net Payable: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
|
|
5549
|
+
`);
|
|
5550
|
+
}
|
|
5551
|
+
const rows = (data.previews ?? []).map((p) => ({
|
|
5552
|
+
tenant: p.tenantName,
|
|
5553
|
+
grossRevenue: p.grossRevenue,
|
|
5554
|
+
commission: p.platformCommission,
|
|
5555
|
+
netPayable: p.netPayable,
|
|
5556
|
+
days: p.daysWithData,
|
|
5557
|
+
impressions: p.impressions
|
|
5558
|
+
}));
|
|
5559
|
+
printData(rows, [
|
|
5560
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5561
|
+
{ key: "grossRevenue", header: "GROSS_REV", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5562
|
+
{ key: "commission", header: "COMMISSION", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5563
|
+
{ key: "netPayable", header: "NET_PAY", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5564
|
+
{ key: "days", header: "DAYS", width: 6 },
|
|
5565
|
+
{ key: "impressions", header: "IMPRESSIONS", width: 12 }
|
|
5566
|
+
], opts.format);
|
|
5567
|
+
});
|
|
5568
|
+
addFormatOption(statements.command("get").description("Get statement details.").argument("<id>", "Statement ID")).action(async (id, opts) => {
|
|
5569
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
|
|
5570
|
+
const json = await resp.json();
|
|
5571
|
+
if (!resp.ok) {
|
|
5572
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5573
|
+
process.exit(1);
|
|
5574
|
+
}
|
|
5575
|
+
printDetail(json.data ?? json, opts.format);
|
|
5576
|
+
});
|
|
5577
|
+
const payoutsAdmin = cmd.command("payouts").description("Payout operations");
|
|
5578
|
+
addFormatOption(payoutsAdmin.command("list").description("List payouts.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5579
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5580
|
+
if (opts.status)
|
|
5581
|
+
params.set("status", opts.status);
|
|
5582
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payout-ops/payouts?${params}` });
|
|
5583
|
+
const json = await resp.json();
|
|
5584
|
+
if (!resp.ok) {
|
|
5585
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5586
|
+
process.exit(1);
|
|
5587
|
+
}
|
|
5588
|
+
const rows = (json.data ?? json).map((p) => ({
|
|
5589
|
+
id: p.id,
|
|
5590
|
+
publisher: p.publisherName ?? p.publisher_name ?? p.tenantId ?? p.tenant_id,
|
|
5591
|
+
amount: p.amount,
|
|
5592
|
+
status: p.status,
|
|
5593
|
+
requestedAt: p.requestedAt ?? p.requested_at ?? p.createdAt ?? p.created_at
|
|
5594
|
+
}));
|
|
5595
|
+
printData(rows, [
|
|
5596
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5597
|
+
{ key: "publisher", header: "PUBLISHER", width: 25 },
|
|
5598
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5599
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5600
|
+
{ key: "requestedAt", header: "REQUESTED_AT", width: 22 }
|
|
5601
|
+
], opts.format);
|
|
5602
|
+
});
|
|
5603
|
+
payoutsAdmin.command("approve").description("Approve a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5604
|
+
await confirmAction(`Approve payout ${id}?`, opts.yes);
|
|
5605
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/approve` });
|
|
5606
|
+
if (!resp.ok) {
|
|
5607
|
+
const j = await resp.json();
|
|
5608
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5609
|
+
process.exit(1);
|
|
5610
|
+
}
|
|
5611
|
+
console.log(`Payout ${id} approved.`);
|
|
5612
|
+
});
|
|
5613
|
+
payoutsAdmin.command("reject").description("Reject a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
|
|
5614
|
+
await confirmAction(`Reject payout ${id}?`, opts.yes);
|
|
5615
|
+
const body = {};
|
|
5616
|
+
if (opts.reason)
|
|
5617
|
+
body.reason = opts.reason;
|
|
5618
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/reject`, body });
|
|
5619
|
+
if (!resp.ok) {
|
|
5620
|
+
const j = await resp.json();
|
|
5621
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5622
|
+
process.exit(1);
|
|
5623
|
+
}
|
|
5624
|
+
console.log(`Payout ${id} rejected.`);
|
|
5625
|
+
});
|
|
5626
|
+
payoutsAdmin.command("process").description("Process a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5627
|
+
await confirmAction(`Process payout ${id}?`, opts.yes);
|
|
5628
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/process` });
|
|
5629
|
+
if (!resp.ok) {
|
|
5630
|
+
const j = await resp.json();
|
|
5631
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5632
|
+
process.exit(1);
|
|
5633
|
+
}
|
|
5634
|
+
console.log(`Payout ${id} processed.`);
|
|
5635
|
+
});
|
|
5636
|
+
payoutsAdmin.command("manual-create").description("Manually create a payout.").option("--tenant-id <id>", "Publisher tenant ID (required)").option("--amount <amount>", "Payout amount (required)").option("--reason <reason>", "Reason for manual payout").option("--wallet <wallet>", "Wallet address").option("--network <network>", "Payment network").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5637
|
+
if (!opts.tenantId) {
|
|
5638
|
+
console.error("Error: --tenant-id is required.");
|
|
5639
|
+
process.exit(1);
|
|
5640
|
+
}
|
|
5641
|
+
if (!opts.amount) {
|
|
5642
|
+
console.error("Error: --amount is required.");
|
|
5643
|
+
process.exit(1);
|
|
5644
|
+
}
|
|
5645
|
+
await confirmAction(`Create manual payout of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
|
|
5646
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount };
|
|
5647
|
+
if (opts.reason)
|
|
5648
|
+
body.reason = opts.reason;
|
|
5649
|
+
if (opts.wallet)
|
|
5650
|
+
body.wallet = opts.wallet;
|
|
5651
|
+
if (opts.network)
|
|
5652
|
+
body.network = opts.network;
|
|
5653
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-payout`, body });
|
|
5654
|
+
const json = await resp.json();
|
|
5655
|
+
if (!resp.ok) {
|
|
5656
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5657
|
+
process.exit(1);
|
|
5658
|
+
}
|
|
5659
|
+
console.log(`Manual payout created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5660
|
+
});
|
|
5661
|
+
const deposits = cmd.command("deposits").description("Deposit operations");
|
|
5662
|
+
addFormatOption(deposits.command("list").description("List deposits.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5663
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5664
|
+
if (opts.status)
|
|
5665
|
+
params.set("status", opts.status);
|
|
5666
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/deposits?${params}` });
|
|
5667
|
+
const json = await resp.json();
|
|
5668
|
+
if (!resp.ok) {
|
|
5669
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5670
|
+
process.exit(1);
|
|
5671
|
+
}
|
|
5672
|
+
const transactions = json.data?.transactions ?? json.transactions ?? json.data ?? json;
|
|
5673
|
+
const items = Array.isArray(transactions) ? transactions : [];
|
|
5674
|
+
const rows = items.map((d) => ({
|
|
5675
|
+
id: d.id,
|
|
5676
|
+
advertiser: d.advertiser?.name ?? d.advertiserName ?? d.advertiser_name ?? d.tenantId ?? d.tenant_id,
|
|
5677
|
+
amount: d.amount,
|
|
5678
|
+
status: d.status,
|
|
5679
|
+
date: d.createdAt ?? d.created_at ?? d.date
|
|
5680
|
+
}));
|
|
5681
|
+
printData(rows, [
|
|
5682
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5683
|
+
{ key: "advertiser", header: "ADVERTISER", width: 25 },
|
|
5684
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5685
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5686
|
+
{ key: "date", header: "DATE", width: 22 }
|
|
5687
|
+
], opts.format);
|
|
5688
|
+
});
|
|
5689
|
+
deposits.command("confirm").description("Confirm a deposit.").argument("<id>", "Deposit ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5690
|
+
await confirmAction(`Confirm deposit ${id}?`, opts.yes);
|
|
5691
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/deposits/${id}/confirm` });
|
|
5692
|
+
if (!resp.ok) {
|
|
5693
|
+
const j = await resp.json();
|
|
5694
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5695
|
+
process.exit(1);
|
|
5696
|
+
}
|
|
5697
|
+
console.log(`Deposit ${id} confirmed.`);
|
|
5698
|
+
});
|
|
5699
|
+
deposits.command("manual-create").description("Manually create a deposit.").option("--tenant-id <id>", "Advertiser tenant ID (required)").option("--amount <amount>", "Deposit amount (required)").option("--reason <reason>", "Reason for manual deposit").option("--reference <ref>", "External reference").option("--notes <notes>", "Additional notes").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5700
|
+
if (!opts.tenantId) {
|
|
5701
|
+
console.error("Error: --tenant-id is required.");
|
|
5702
|
+
process.exit(1);
|
|
5703
|
+
}
|
|
5704
|
+
if (!opts.amount) {
|
|
5705
|
+
console.error("Error: --amount is required.");
|
|
5706
|
+
process.exit(1);
|
|
5707
|
+
}
|
|
5708
|
+
await confirmAction(`Create manual deposit of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
|
|
5709
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount };
|
|
5710
|
+
if (opts.reason)
|
|
5711
|
+
body.reason = opts.reason;
|
|
5712
|
+
if (opts.reference)
|
|
5713
|
+
body.reference = opts.reference;
|
|
5714
|
+
if (opts.notes)
|
|
5715
|
+
body.notes = opts.notes;
|
|
5716
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-deposit`, body });
|
|
5717
|
+
const json = await resp.json();
|
|
5718
|
+
if (!resp.ok) {
|
|
5719
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5720
|
+
process.exit(1);
|
|
5721
|
+
}
|
|
5722
|
+
console.log(`Manual deposit created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5723
|
+
});
|
|
5724
|
+
const refunds = cmd.command("refunds").description("Refund operations");
|
|
5725
|
+
addFormatOption(refunds.command("list").description("List refunds.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5726
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5727
|
+
if (opts.status)
|
|
5728
|
+
params.set("status", opts.status);
|
|
5729
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds?${params}` });
|
|
5730
|
+
const json = await resp.json();
|
|
5731
|
+
if (!resp.ok) {
|
|
5732
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5733
|
+
process.exit(1);
|
|
5734
|
+
}
|
|
5735
|
+
const rows = (json.data ?? json).map((r) => ({
|
|
5736
|
+
id: r.id,
|
|
5737
|
+
advertiser: r.advertiserName ?? r.advertiser_name ?? r.accountId ?? r.account_id,
|
|
5738
|
+
amount: r.amount,
|
|
5739
|
+
reason: r.reason,
|
|
5740
|
+
status: r.status
|
|
5741
|
+
}));
|
|
5742
|
+
printData(rows, [
|
|
5743
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5744
|
+
{ key: "advertiser", header: "ADVERTISER", width: 25 },
|
|
5745
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5746
|
+
{ key: "reason", header: "REASON", width: 20 },
|
|
5747
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
5748
|
+
], opts.format);
|
|
5749
|
+
});
|
|
5750
|
+
addFormatOption(refunds.command("get").description("Get refund details.").argument("<id>", "Refund ID")).action(async (id, opts) => {
|
|
5751
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds/${id}` });
|
|
5752
|
+
const json = await resp.json();
|
|
5753
|
+
if (!resp.ok) {
|
|
5754
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5755
|
+
process.exit(1);
|
|
5756
|
+
}
|
|
5757
|
+
printDetail(json.data ?? json, opts.format);
|
|
5758
|
+
});
|
|
5759
|
+
refunds.command("create").description("Create a refund.").option("--account-id <id>", "Account ID (required)").option("--amount <amount>", "Refund amount (required)").option("--reason <reason>", "Refund reason").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5760
|
+
if (!opts.accountId) {
|
|
5761
|
+
console.error("Error: --account-id is required.");
|
|
5762
|
+
process.exit(1);
|
|
5763
|
+
}
|
|
5764
|
+
if (!opts.amount) {
|
|
5765
|
+
console.error("Error: --amount is required.");
|
|
5766
|
+
process.exit(1);
|
|
5767
|
+
}
|
|
5768
|
+
await confirmAction(`Create refund of ${opts.amount} for account ${opts.accountId}?`, opts.yes);
|
|
5769
|
+
const body = { accountId: opts.accountId, amount: opts.amount };
|
|
5770
|
+
if (opts.reason)
|
|
5771
|
+
body.reason = opts.reason;
|
|
5772
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds`, body });
|
|
5773
|
+
const json = await resp.json();
|
|
5774
|
+
if (!resp.ok) {
|
|
5775
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5776
|
+
process.exit(1);
|
|
5777
|
+
}
|
|
5778
|
+
console.log(`Refund created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5779
|
+
});
|
|
5780
|
+
refunds.command("approve").description("Approve a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5781
|
+
await confirmAction(`Approve refund ${id}?`, opts.yes);
|
|
5782
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/approve` });
|
|
5783
|
+
if (!resp.ok) {
|
|
5784
|
+
const j = await resp.json();
|
|
5785
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5786
|
+
process.exit(1);
|
|
5787
|
+
}
|
|
5788
|
+
console.log(`Refund ${id} approved.`);
|
|
5789
|
+
});
|
|
5790
|
+
refunds.command("reject").description("Reject a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
|
|
5791
|
+
await confirmAction(`Reject refund ${id}?`, opts.yes);
|
|
5792
|
+
const body = {};
|
|
5793
|
+
if (opts.reason)
|
|
5794
|
+
body.reason = opts.reason;
|
|
5795
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/reject`, body });
|
|
5796
|
+
if (!resp.ok) {
|
|
5797
|
+
const j = await resp.json();
|
|
5798
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5799
|
+
process.exit(1);
|
|
5800
|
+
}
|
|
5801
|
+
console.log(`Refund ${id} rejected.`);
|
|
5802
|
+
});
|
|
5803
|
+
refunds.command("process").description("Process an approved refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5804
|
+
await confirmAction(`Process refund ${id}?`, opts.yes);
|
|
5805
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/process` });
|
|
5806
|
+
if (!resp.ok) {
|
|
5807
|
+
const j = await resp.json();
|
|
5808
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5809
|
+
process.exit(1);
|
|
5810
|
+
}
|
|
5811
|
+
console.log(`Refund ${id} processed.`);
|
|
5812
|
+
});
|
|
5813
|
+
const balances = cmd.command("balances").description("Account balance management");
|
|
5814
|
+
addFormatOption(balances.command("list").description("List account balances.")).action(async (opts) => {
|
|
5815
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/balances` });
|
|
5816
|
+
const json = await resp.json();
|
|
5817
|
+
if (!resp.ok) {
|
|
5818
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5819
|
+
process.exit(1);
|
|
5820
|
+
}
|
|
5821
|
+
printDetail(json.data ?? json, opts.format);
|
|
5822
|
+
});
|
|
5823
|
+
balances.command("adjust").description("Adjust an account balance.").option("--tenant-id <id>", "Tenant ID (required)").option("--amount <amount>", "Adjustment amount (required)").option("--reason <reason>", "Reason for adjustment").option("--account-type <type>", "Account type", "advertiser").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5824
|
+
if (!opts.tenantId) {
|
|
5825
|
+
console.error("Error: --tenant-id is required.");
|
|
5826
|
+
process.exit(1);
|
|
5827
|
+
}
|
|
5828
|
+
if (!opts.amount) {
|
|
5829
|
+
console.error("Error: --amount is required.");
|
|
5830
|
+
process.exit(1);
|
|
5831
|
+
}
|
|
5832
|
+
await confirmAction(`Adjust balance by ${opts.amount} for tenant ${opts.tenantId} (${opts.accountType})?`, opts.yes);
|
|
5833
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount, accountType: opts.accountType };
|
|
5834
|
+
if (opts.reason)
|
|
5835
|
+
body.reason = opts.reason;
|
|
5836
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/adjust`, body });
|
|
5837
|
+
const json = await resp.json();
|
|
5838
|
+
if (!resp.ok) {
|
|
5839
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5840
|
+
process.exit(1);
|
|
5841
|
+
}
|
|
5842
|
+
console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
|
|
5843
|
+
});
|
|
5844
|
+
return cmd;
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
// src/commands/external-ssp.ts
|
|
5848
|
+
var COLUMNS8 = [
|
|
5849
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5850
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
5851
|
+
{ key: "code", header: "CODE", width: 15 },
|
|
5852
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5853
|
+
{ key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
|
|
5854
|
+
];
|
|
5855
|
+
function createExternalSspCommand() {
|
|
5856
|
+
const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
|
|
5857
|
+
|
|
5858
|
+
Requires: platform_owner or platform_admin role.`).addHelpText("after", `
|
|
5859
|
+
Examples:
|
|
5860
|
+
$ a8techads external-ssp list
|
|
5861
|
+
$ a8techads external-ssp get <id>
|
|
5862
|
+
$ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
|
|
5863
|
+
$ a8techads external-ssp activate <id>
|
|
5864
|
+
$ a8techads external-ssp pause <id>`);
|
|
5865
|
+
addFormatOption(cmd.command("list").description("List all external SSP partners.").option("--status <status>", "Filter by status (TESTING, ACTIVE, SUSPENDED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
5866
|
+
const params = new URLSearchParams;
|
|
5867
|
+
if (opts.status)
|
|
5868
|
+
params.set("status", opts.status);
|
|
5869
|
+
params.set("limit", opts.limit);
|
|
5870
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners?${params}` });
|
|
5871
|
+
const json = await resp.json();
|
|
5872
|
+
if (!resp.ok) {
|
|
5873
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5874
|
+
process.exit(1);
|
|
5875
|
+
}
|
|
5876
|
+
const rows = (json.data ?? json).map((p) => ({
|
|
5877
|
+
id: p.id,
|
|
5878
|
+
name: p.name,
|
|
5879
|
+
code: p.code,
|
|
5880
|
+
status: p.status,
|
|
5881
|
+
formats: p.supportedFormats ?? p.supported_formats ?? []
|
|
5882
|
+
}));
|
|
5883
|
+
printData(rows, COLUMNS8, opts.format);
|
|
5884
|
+
});
|
|
5885
|
+
addFormatOption(cmd.command("get").description("Get external SSP partner details (includes API key).").argument("<id>", "Partner ID")).action(async (id, opts) => {
|
|
5886
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners/${id}` });
|
|
5887
|
+
const json = await resp.json();
|
|
5888
|
+
if (!resp.ok) {
|
|
5889
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5890
|
+
process.exit(1);
|
|
5891
|
+
}
|
|
5892
|
+
printDetail(json.data ?? json, opts.format);
|
|
5893
|
+
});
|
|
5894
|
+
cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").addHelpText("after", `
|
|
5895
|
+
Examples:
|
|
5896
|
+
$ a8techads external-ssp create --name "OpenX"
|
|
5897
|
+
$ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO`).action(async (opts) => {
|
|
5898
|
+
if (!opts.name) {
|
|
5899
|
+
console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
|
|
5900
|
+
process.exit(1);
|
|
5901
|
+
}
|
|
5902
|
+
const body = { name: opts.name };
|
|
5903
|
+
if (opts.code)
|
|
5904
|
+
body.code = opts.code;
|
|
5905
|
+
if (opts.formats)
|
|
5906
|
+
body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
|
|
5907
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
|
|
5908
|
+
const json = await resp.json();
|
|
5909
|
+
if (!resp.ok) {
|
|
5910
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5911
|
+
process.exit(1);
|
|
5912
|
+
}
|
|
5913
|
+
const data = json.data ?? json;
|
|
5914
|
+
console.log(`Partner created: ${data.id}`);
|
|
5915
|
+
if (data.code)
|
|
5916
|
+
console.log(` Code: ${data.code}`);
|
|
5917
|
+
if (data.apiKey)
|
|
5918
|
+
console.log(` API Key: ${data.apiKey}`);
|
|
5919
|
+
});
|
|
5920
|
+
cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").action(async (id, opts) => {
|
|
5921
|
+
const body = {};
|
|
5922
|
+
if (opts.name)
|
|
5923
|
+
body.name = opts.name;
|
|
5924
|
+
if (opts.formats)
|
|
5925
|
+
body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
|
|
5926
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
|
|
5927
|
+
if (!resp.ok) {
|
|
5928
|
+
const j = await resp.json();
|
|
5929
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5930
|
+
process.exit(1);
|
|
5931
|
+
}
|
|
5932
|
+
console.log(`Partner ${id} updated.`);
|
|
5933
|
+
});
|
|
5934
|
+
cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
5935
|
+
if (!opts.yes) {
|
|
5936
|
+
console.error("Add --yes to confirm deletion.");
|
|
5937
|
+
process.exit(1);
|
|
5938
|
+
}
|
|
5939
|
+
const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/ssp-partners/${id}` });
|
|
5940
|
+
if (!resp.ok && resp.status !== 204) {
|
|
5941
|
+
console.error(`Error: ${resp.statusText}`);
|
|
5942
|
+
process.exit(1);
|
|
5943
|
+
}
|
|
5944
|
+
console.log(`Partner ${id} deleted.`);
|
|
5945
|
+
});
|
|
5946
|
+
cmd.command("activate").description("Activate an external SSP partner (TESTING/SUSPENDED → ACTIVE).").argument("<id>", "Partner ID").action(async (id) => {
|
|
5947
|
+
const resp = await apiRequest({
|
|
5948
|
+
method: "PATCH",
|
|
5949
|
+
path: `${consolePrefix()}/ssp-partners/${id}/status`,
|
|
5950
|
+
body: { status: "ACTIVE" }
|
|
5951
|
+
});
|
|
5952
|
+
if (!resp.ok) {
|
|
5953
|
+
const j = await resp.json();
|
|
5954
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5955
|
+
process.exit(1);
|
|
5956
|
+
}
|
|
5957
|
+
console.log(`Partner ${id} activated.`);
|
|
5958
|
+
});
|
|
5959
|
+
cmd.command("pause").description("Pause an external SSP partner (ACTIVE → SUSPENDED).").argument("<id>", "Partner ID").action(async (id) => {
|
|
5960
|
+
const resp = await apiRequest({
|
|
5961
|
+
method: "PATCH",
|
|
5962
|
+
path: `${consolePrefix()}/ssp-partners/${id}/status`,
|
|
5963
|
+
body: { status: "SUSPENDED" }
|
|
5964
|
+
});
|
|
5965
|
+
if (!resp.ok) {
|
|
5966
|
+
const j = await resp.json();
|
|
5967
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5968
|
+
process.exit(1);
|
|
5969
|
+
}
|
|
5970
|
+
console.log(`Partner ${id} paused.`);
|
|
5971
|
+
});
|
|
5972
|
+
return cmd;
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
// src/commands/settings.ts
|
|
5976
|
+
function settingsPrefix() {
|
|
5977
|
+
const ctx = loadContext();
|
|
5978
|
+
const context = getCurrentContext(ctx);
|
|
5979
|
+
const app = context?.app ?? "dsp";
|
|
5980
|
+
if (app === "console") {
|
|
5981
|
+
console.error("Error: Settings commands are not available in Console mode.");
|
|
5982
|
+
console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
|
|
5983
|
+
process.exit(1);
|
|
5984
|
+
}
|
|
5985
|
+
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
5986
|
+
}
|
|
5987
|
+
function createSettingsCommand() {
|
|
5988
|
+
const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
|
|
5989
|
+
|
|
5990
|
+
Requires: ADVERTISER or PUBLISHER capability.
|
|
5991
|
+
Not available in Console mode.`).addHelpText("after", `
|
|
5992
|
+
Examples:
|
|
5993
|
+
$ a8techads settings show
|
|
5994
|
+
$ a8techads settings update --from-json settings.json
|
|
5995
|
+
$ a8techads settings profile
|
|
5996
|
+
$ a8techads settings profile-update --company-name "New Name"`);
|
|
5997
|
+
addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
|
|
5998
|
+
const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
|
|
5999
|
+
const json = await resp.json();
|
|
6000
|
+
if (!resp.ok) {
|
|
6001
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6002
|
+
process.exit(1);
|
|
6003
|
+
}
|
|
6004
|
+
printDetail(json.data ?? json, opts.format);
|
|
6005
|
+
});
|
|
6006
|
+
addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
|
|
6007
|
+
const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
|
|
6008
|
+
const json = await resp.json();
|
|
6009
|
+
if (!resp.ok) {
|
|
6010
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6011
|
+
process.exit(1);
|
|
6012
|
+
}
|
|
6013
|
+
printDetail(json.data ?? json, opts.format);
|
|
6014
|
+
});
|
|
6015
|
+
cmd.command("update").description(`Update tenant settings.
|
|
6016
|
+
|
|
6017
|
+
Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
6018
|
+
if (!opts.fromJson) {
|
|
6019
|
+
console.error("Error: --from-json is required for settings update.");
|
|
6020
|
+
process.exit(1);
|
|
6021
|
+
}
|
|
6022
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6023
|
+
const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6024
|
+
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
|
|
6025
|
+
if (!resp.ok) {
|
|
6026
|
+
const j = await resp.json();
|
|
6027
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6028
|
+
process.exit(1);
|
|
6029
|
+
}
|
|
6030
|
+
console.log("Settings updated.");
|
|
6031
|
+
});
|
|
6032
|
+
cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
6033
|
+
let body;
|
|
6034
|
+
if (opts.fromJson) {
|
|
6035
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6036
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6037
|
+
} else {
|
|
6038
|
+
body = {};
|
|
6039
|
+
if (opts.companyName)
|
|
6040
|
+
body.companyName = opts.companyName;
|
|
6041
|
+
}
|
|
6042
|
+
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
|
|
6043
|
+
if (!resp.ok) {
|
|
6044
|
+
const j = await resp.json();
|
|
6045
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6046
|
+
process.exit(1);
|
|
6047
|
+
}
|
|
6048
|
+
console.log("Profile updated.");
|
|
6049
|
+
});
|
|
6050
|
+
return cmd;
|
|
6051
|
+
}
|
|
6052
|
+
|
|
6053
|
+
// src/commands/invoices.ts
|
|
6054
|
+
function createInvoicesCommand() {
|
|
6055
|
+
const cmd = new Command("invoices").description(`Invoice management (DSP)
|
|
6056
|
+
|
|
6057
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6058
|
+
Examples:
|
|
6059
|
+
$ a8techads invoices list
|
|
6060
|
+
$ a8techads invoices get <id>
|
|
6061
|
+
$ a8techads invoices spending`);
|
|
6062
|
+
addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
|
|
6063
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
|
|
6064
|
+
const json = await resp.json();
|
|
6065
|
+
if (!resp.ok) {
|
|
6066
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6067
|
+
process.exit(1);
|
|
6068
|
+
}
|
|
6069
|
+
const columns = [
|
|
6070
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6071
|
+
{ key: "period", header: "PERIOD", width: 20 },
|
|
6072
|
+
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
6073
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
6074
|
+
];
|
|
6075
|
+
const rows = (json.data ?? json).map((i) => ({
|
|
6076
|
+
id: i.id,
|
|
6077
|
+
period: i.period ?? i.billingPeriod,
|
|
6078
|
+
amount: i.amount ?? i.totalAmount,
|
|
6079
|
+
status: i.status
|
|
6080
|
+
}));
|
|
6081
|
+
printData(rows, columns, opts.format);
|
|
6082
|
+
});
|
|
6083
|
+
addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
|
|
6084
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
|
|
6085
|
+
const json = await resp.json();
|
|
6086
|
+
if (!resp.ok) {
|
|
6087
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6088
|
+
process.exit(1);
|
|
6089
|
+
}
|
|
6090
|
+
printDetail(json.data ?? json, opts.format);
|
|
6091
|
+
});
|
|
6092
|
+
addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
|
|
6093
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
|
|
6094
|
+
const json = await resp.json();
|
|
6095
|
+
if (!resp.ok) {
|
|
6096
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6097
|
+
process.exit(1);
|
|
6098
|
+
}
|
|
6099
|
+
printDetail(json.data ?? json, opts.format);
|
|
6100
|
+
});
|
|
6101
|
+
return cmd;
|
|
6102
|
+
}
|
|
6103
|
+
|
|
6104
|
+
// src/commands/conversion-goals.ts
|
|
6105
|
+
var COLUMNS9 = [
|
|
6106
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6107
|
+
{ key: "name", header: "NAME", width: 24 },
|
|
6108
|
+
{ key: "goalOrder", header: "G#", width: 4 },
|
|
6109
|
+
{ key: "conversionType", header: "TYPE", width: 20 },
|
|
6110
|
+
{ key: "valueType", header: "VALUE", width: 10 },
|
|
6111
|
+
{ key: "status", header: "STATUS", width: 10 }
|
|
6112
|
+
];
|
|
6113
|
+
function createConversionGoalsCommand() {
|
|
6114
|
+
const cmd = new Command("conversion-goals").description(`Conversion goal management (DSP)
|
|
6115
|
+
|
|
6116
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6117
|
+
Examples:
|
|
6118
|
+
$ a8techads conversion-goals list
|
|
6119
|
+
$ a8techads conversion-goals get <id>
|
|
6120
|
+
$ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
|
|
6121
|
+
$ a8techads conversion-goals pause <id>`);
|
|
6122
|
+
addFormatOption(cmd.command("list").description("List conversion goals.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6123
|
+
const params = new URLSearchParams;
|
|
6124
|
+
if (opts.status)
|
|
6125
|
+
params.set("status", opts.status);
|
|
6126
|
+
params.set("limit", opts.limit);
|
|
6127
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals?${params}` });
|
|
6128
|
+
const json = await resp.json();
|
|
6129
|
+
if (!resp.ok) {
|
|
6130
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6131
|
+
process.exit(1);
|
|
6132
|
+
}
|
|
6133
|
+
const rows = (json.data ?? json).map((g) => ({
|
|
6134
|
+
id: g.id,
|
|
6135
|
+
name: g.name,
|
|
6136
|
+
goalOrder: `G${g.goalOrder ?? g.goal_order}`,
|
|
6137
|
+
conversionType: g.conversionType ?? g.conversion_type,
|
|
6138
|
+
valueType: g.valueType ?? g.value_type,
|
|
6139
|
+
status: g.status
|
|
6140
|
+
}));
|
|
6141
|
+
printData(rows, COLUMNS9, opts.format);
|
|
6142
|
+
});
|
|
6143
|
+
addFormatOption(cmd.command("get").description("Get conversion goal details.").argument("<id>", "Goal ID")).action(async (id, opts) => {
|
|
6144
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
|
|
6145
|
+
const json = await resp.json();
|
|
6146
|
+
if (!resp.ok) {
|
|
6147
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6148
|
+
process.exit(1);
|
|
6149
|
+
}
|
|
6150
|
+
printDetail(json.data ?? json, opts.format);
|
|
6151
|
+
});
|
|
6152
|
+
cmd.command("create").description(`Create a conversion goal.
|
|
6153
|
+
|
|
6154
|
+
Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
6155
|
+
PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6156
|
+
let body;
|
|
6157
|
+
if (opts.fromJson) {
|
|
6158
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6159
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6160
|
+
} else {
|
|
6161
|
+
if (!opts.name) {
|
|
6162
|
+
console.error("Error: --name is required");
|
|
6163
|
+
process.exit(1);
|
|
6164
|
+
}
|
|
6165
|
+
if (!opts.conversionType) {
|
|
6166
|
+
console.error("Error: --conversion-type is required");
|
|
6167
|
+
process.exit(1);
|
|
6168
|
+
}
|
|
6169
|
+
if (!opts.goalOrder) {
|
|
6170
|
+
console.error("Error: --goal-order is required (1-10)");
|
|
6171
|
+
process.exit(1);
|
|
6172
|
+
}
|
|
6173
|
+
body = {
|
|
6174
|
+
name: opts.name,
|
|
6175
|
+
conversionType: opts.conversionType,
|
|
6176
|
+
goalOrder: Number(opts.goalOrder),
|
|
6177
|
+
valueType: opts.valueType,
|
|
6178
|
+
countType: opts.countType,
|
|
6179
|
+
conversionWindowHours: Number(opts.window)
|
|
6180
|
+
};
|
|
6181
|
+
if (opts.fixedValue)
|
|
6182
|
+
body.fixedValue = Number(opts.fixedValue);
|
|
6183
|
+
if (opts.description)
|
|
6184
|
+
body.description = opts.description;
|
|
6185
|
+
}
|
|
6186
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals`, body });
|
|
6187
|
+
const json = await resp.json();
|
|
6188
|
+
if (!resp.ok) {
|
|
6189
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6190
|
+
process.exit(1);
|
|
6191
|
+
}
|
|
6192
|
+
const goal = json.data ?? json;
|
|
6193
|
+
console.log(`Conversion goal created: ${goal.id}`);
|
|
6194
|
+
console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
|
|
6195
|
+
console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
|
|
6196
|
+
});
|
|
6197
|
+
cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
6198
|
+
let body;
|
|
6199
|
+
if (opts.fromJson) {
|
|
6200
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6201
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6202
|
+
} else {
|
|
6203
|
+
body = {};
|
|
6204
|
+
if (opts.name)
|
|
6205
|
+
body.name = opts.name;
|
|
6206
|
+
if (opts.description)
|
|
6207
|
+
body.description = opts.description;
|
|
6208
|
+
if (opts.valueType)
|
|
6209
|
+
body.valueType = opts.valueType;
|
|
6210
|
+
if (opts.fixedValue)
|
|
6211
|
+
body.fixedValue = Number(opts.fixedValue);
|
|
6212
|
+
if (opts.countType)
|
|
6213
|
+
body.countType = opts.countType;
|
|
6214
|
+
if (opts.window)
|
|
6215
|
+
body.conversionWindowHours = Number(opts.window);
|
|
6216
|
+
}
|
|
6217
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}`, body });
|
|
6218
|
+
if (!resp.ok) {
|
|
6219
|
+
const j = await resp.json();
|
|
6220
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6221
|
+
process.exit(1);
|
|
6222
|
+
}
|
|
6223
|
+
console.log(`Conversion goal ${id} updated.`);
|
|
6224
|
+
});
|
|
6225
|
+
cmd.command("delete").description("Delete a conversion goal.").argument("<id>", "Goal ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
6226
|
+
if (!opts.yes) {
|
|
6227
|
+
console.error("Add --yes to confirm deletion.");
|
|
6228
|
+
process.exit(1);
|
|
6229
|
+
}
|
|
6230
|
+
const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/conversion-goals/${id}` });
|
|
6231
|
+
if (!resp.ok && resp.status !== 204) {
|
|
6232
|
+
console.error(`Error: ${resp.statusText}`);
|
|
6233
|
+
process.exit(1);
|
|
6234
|
+
}
|
|
6235
|
+
console.log(`Conversion goal ${id} deleted.`);
|
|
6236
|
+
});
|
|
6237
|
+
for (const action of ["pause", "activate", "archive"]) {
|
|
6238
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a conversion goal.`).argument("<id>", "Goal ID").action(async (id) => {
|
|
6239
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}/${action}` });
|
|
6240
|
+
if (!resp.ok) {
|
|
6241
|
+
const j = await resp.json();
|
|
6242
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6243
|
+
process.exit(1);
|
|
6244
|
+
}
|
|
6245
|
+
console.log(`Conversion goal ${id} ${action}d.`);
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
cmd.command("regenerate-secret").description("Regenerate secret key and postback URL.").argument("<id>", "Goal ID").action(async (id) => {
|
|
6249
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals/${id}/regenerate-secret` });
|
|
6250
|
+
const json = await resp.json();
|
|
6251
|
+
if (!resp.ok) {
|
|
6252
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6253
|
+
process.exit(1);
|
|
6254
|
+
}
|
|
6255
|
+
const goal = json.data ?? json;
|
|
6256
|
+
console.log(`Secret regenerated for goal ${id}`);
|
|
6257
|
+
console.log(` New postback URL: ${goal.postbackUrl ?? goal.postback_url}`);
|
|
6258
|
+
});
|
|
6259
|
+
return cmd;
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
// src/commands/algorithms.ts
|
|
6263
|
+
var COLUMNS10 = [
|
|
6264
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6265
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
6266
|
+
{ key: "goal", header: "GOAL", width: 8 },
|
|
6267
|
+
{ key: "conversionGoalId", header: "CONV_GOAL", width: 36 },
|
|
6268
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
6269
|
+
{ key: "campaignCount", header: "CAMPAIGNS", width: 10 }
|
|
6270
|
+
];
|
|
6271
|
+
function createAlgorithmsCommand() {
|
|
6272
|
+
const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
|
|
6273
|
+
|
|
6274
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6275
|
+
Examples:
|
|
6276
|
+
$ a8techads algorithms list
|
|
6277
|
+
$ a8techads algorithms get <id>
|
|
6278
|
+
$ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
|
|
6279
|
+
$ a8techads algorithms update <id> --conversion-goal-id <goal-id>
|
|
6280
|
+
$ a8techads algorithms pause <id>`);
|
|
6281
|
+
addFormatOption(cmd.command("list").description("List bidder algorithms.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6282
|
+
const params = new URLSearchParams;
|
|
6283
|
+
if (opts.status)
|
|
6284
|
+
params.set("status", opts.status);
|
|
6285
|
+
params.set("limit", opts.limit);
|
|
6286
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
|
|
6287
|
+
const json = await resp.json();
|
|
6288
|
+
if (!resp.ok) {
|
|
6289
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6290
|
+
process.exit(1);
|
|
6291
|
+
}
|
|
6292
|
+
const rows = (json.data ?? json).map((algo) => ({
|
|
6293
|
+
id: algo.id,
|
|
6294
|
+
name: algo.name,
|
|
6295
|
+
goal: algo.optimizationGoal,
|
|
6296
|
+
conversionGoalId: algo.conversionGoalId,
|
|
6297
|
+
status: algo.status,
|
|
6298
|
+
campaignCount: algo.campaignCount ?? 0
|
|
6299
|
+
}));
|
|
6300
|
+
printData(rows, COLUMNS10, opts.format);
|
|
6301
|
+
});
|
|
6302
|
+
addFormatOption(cmd.command("get").description("Get bidder algorithm details.").argument("<id>", "Algorithm ID")).action(async (id, opts) => {
|
|
6303
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
|
|
6304
|
+
const json = await resp.json();
|
|
6305
|
+
if (!resp.ok) {
|
|
6306
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6307
|
+
process.exit(1);
|
|
6308
|
+
}
|
|
6309
|
+
printDetail(json.data ?? json, opts.format);
|
|
6310
|
+
});
|
|
6311
|
+
cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6312
|
+
const body = await buildAlgorithmBody(opts, true);
|
|
6313
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
|
|
6314
|
+
const json = await resp.json();
|
|
6315
|
+
if (!resp.ok) {
|
|
6316
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6317
|
+
process.exit(1);
|
|
6318
|
+
}
|
|
6319
|
+
console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
|
|
6320
|
+
});
|
|
6321
|
+
cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
6322
|
+
const body = await buildAlgorithmBody(opts, false);
|
|
6323
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
|
|
6324
|
+
const json = await resp.json().catch(() => ({}));
|
|
6325
|
+
if (!resp.ok) {
|
|
6326
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6327
|
+
process.exit(1);
|
|
6328
|
+
}
|
|
6329
|
+
console.log(`Algorithm ${id} updated.`);
|
|
6330
|
+
});
|
|
6331
|
+
cmd.command("archive").description("Archive a bidder algorithm by setting status to ARCHIVED.").argument("<id>", "Algorithm ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
6332
|
+
if (!opts.yes) {
|
|
6333
|
+
console.error("Add --yes to confirm archiving.");
|
|
6334
|
+
process.exit(1);
|
|
6335
|
+
}
|
|
6336
|
+
const resp = await apiRequest({
|
|
6337
|
+
method: "PATCH",
|
|
6338
|
+
path: `${dspPrefix()}/bidder-algorithms/${id}`,
|
|
6339
|
+
body: { status: "ARCHIVED" }
|
|
6340
|
+
});
|
|
6341
|
+
const json = await resp.json().catch(() => ({}));
|
|
6342
|
+
if (!resp.ok) {
|
|
6343
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6344
|
+
process.exit(1);
|
|
6345
|
+
}
|
|
6346
|
+
console.log(`Algorithm ${id} archived.`);
|
|
6347
|
+
});
|
|
6348
|
+
for (const action of ["pause", "activate"]) {
|
|
6349
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a bidder algorithm.`).argument("<id>", "Algorithm ID").action(async (id) => {
|
|
6350
|
+
const resp = await apiRequest({
|
|
6351
|
+
method: "PATCH",
|
|
6352
|
+
path: `${dspPrefix()}/bidder-algorithms/${id}/${action}`
|
|
6353
|
+
});
|
|
6354
|
+
const json = await resp.json().catch(() => ({}));
|
|
6355
|
+
if (!resp.ok) {
|
|
6356
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6357
|
+
process.exit(1);
|
|
6358
|
+
}
|
|
6359
|
+
console.log(`Algorithm ${id} ${action}d.`);
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6362
|
+
return cmd;
|
|
6363
|
+
}
|
|
6364
|
+
async function buildAlgorithmBody(opts, creating) {
|
|
6365
|
+
if (opts.fromJson) {
|
|
6366
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6367
|
+
return JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6368
|
+
}
|
|
6369
|
+
const body = {};
|
|
6370
|
+
if (opts.name)
|
|
6371
|
+
body.name = opts.name;
|
|
6372
|
+
if (opts.description)
|
|
6373
|
+
body.description = opts.description;
|
|
6374
|
+
if (opts.optimizationGoal)
|
|
6375
|
+
body.optimizationGoal = String(opts.optimizationGoal).toUpperCase();
|
|
6376
|
+
if (opts.targetValue !== undefined)
|
|
6377
|
+
body.targetValue = Number(opts.targetValue);
|
|
6378
|
+
if (opts.conversionGoalId)
|
|
6379
|
+
body.conversionGoalId = opts.conversionGoalId;
|
|
6380
|
+
if (creating) {
|
|
6381
|
+
if (!body.name) {
|
|
6382
|
+
console.error("Error: --name is required.");
|
|
6383
|
+
process.exit(1);
|
|
6384
|
+
}
|
|
6385
|
+
if (!body.optimizationGoal) {
|
|
6386
|
+
console.error("Error: --optimization-goal is required.");
|
|
6387
|
+
process.exit(1);
|
|
6388
|
+
}
|
|
6389
|
+
if (body.targetValue === undefined) {
|
|
6390
|
+
console.error("Error: --target-value is required.");
|
|
6391
|
+
process.exit(1);
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
return body;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
// src/commands/payouts.ts
|
|
6398
|
+
function createPayoutsCommand() {
|
|
6399
|
+
const cmd = new Command("payouts").description(`Payout management (SSP)
|
|
6400
|
+
|
|
6401
|
+
Requires: PUBLISHER capability.`).addHelpText("after", `
|
|
6402
|
+
Examples:
|
|
6403
|
+
$ a8techads payouts balance
|
|
6404
|
+
$ a8techads payouts account
|
|
6405
|
+
$ a8techads payouts list --limit 10
|
|
6406
|
+
$ a8techads payouts request --amount 50 --method usdt --yes`);
|
|
6407
|
+
addFormatOption(cmd.command("balance").description("Show current payout balance.")).action(async (opts) => {
|
|
6408
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/balance` });
|
|
6409
|
+
const json = await resp.json();
|
|
6410
|
+
if (!resp.ok) {
|
|
6411
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6412
|
+
process.exit(1);
|
|
6413
|
+
}
|
|
6414
|
+
printDetail(json.data ?? json, opts.format);
|
|
6415
|
+
});
|
|
6416
|
+
addFormatOption(cmd.command("account").description("Show payout account details.")).action(async (opts) => {
|
|
6417
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/account` });
|
|
6418
|
+
const json = await resp.json();
|
|
6419
|
+
if (!resp.ok) {
|
|
6420
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6421
|
+
process.exit(1);
|
|
6422
|
+
}
|
|
6423
|
+
printDetail(json.data ?? json, opts.format);
|
|
6424
|
+
});
|
|
6425
|
+
addFormatOption(cmd.command("list").description("List payout history.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
6426
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
6427
|
+
if (opts.status)
|
|
6428
|
+
params.set("status", opts.status);
|
|
6429
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/list?${params}` });
|
|
6430
|
+
const json = await resp.json();
|
|
6431
|
+
if (!resp.ok) {
|
|
6432
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4583
6433
|
process.exit(1);
|
|
4584
6434
|
}
|
|
4585
6435
|
const columns = [
|
|
4586
6436
|
{ key: "id", header: "ID", width: 36 },
|
|
4587
|
-
{ key: "
|
|
6437
|
+
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
6438
|
+
{ key: "status", header: "STATUS", width: 14 },
|
|
6439
|
+
{ key: "method", header: "METHOD", width: 10 },
|
|
6440
|
+
{ key: "requestedAt", header: "REQUESTED_AT", width: 20 }
|
|
6441
|
+
];
|
|
6442
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
6443
|
+
id: t.id,
|
|
6444
|
+
amount: t.amount,
|
|
6445
|
+
status: t.status,
|
|
6446
|
+
method: t.method,
|
|
6447
|
+
requestedAt: t.requestedAt ?? t.requested_at
|
|
6448
|
+
}));
|
|
6449
|
+
printData(rows, columns, opts.format);
|
|
6450
|
+
});
|
|
6451
|
+
cmd.command("request").description("Request a payout.").option("--amount <dollars>", "Payout amount in dollars (required)").option("--method <method>", "Payout method", "usdt").option("--wallet <address>", "Wallet address").option("--network <network>", "Network for crypto payouts", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6452
|
+
if (!opts.amount) {
|
|
6453
|
+
console.error("Error: --amount is required.");
|
|
6454
|
+
process.exit(1);
|
|
6455
|
+
}
|
|
6456
|
+
const amount = Number(opts.amount);
|
|
6457
|
+
if (isNaN(amount) || amount <= 0) {
|
|
6458
|
+
console.error("Error: Amount must be a positive number.");
|
|
6459
|
+
process.exit(1);
|
|
6460
|
+
}
|
|
6461
|
+
if (!opts.yes) {
|
|
6462
|
+
const rl = await import("readline");
|
|
6463
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
6464
|
+
const answer = await new Promise((resolve) => iface.question(`Request payout of $${amount.toFixed(2)}? (y/N) `, resolve));
|
|
6465
|
+
iface.close();
|
|
6466
|
+
if (answer.toLowerCase() !== "y") {
|
|
6467
|
+
console.log("Cancelled.");
|
|
6468
|
+
process.exit(0);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
let resp;
|
|
6472
|
+
if (opts.method === "usdt") {
|
|
6473
|
+
const body = { amount, walletAddress: opts.wallet, network: opts.network };
|
|
6474
|
+
resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payments/usdt/payout`, body });
|
|
6475
|
+
} else {
|
|
6476
|
+
resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payouts/request`, body: { amount } });
|
|
6477
|
+
}
|
|
6478
|
+
const json = await resp.json();
|
|
6479
|
+
if (!resp.ok) {
|
|
6480
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6481
|
+
process.exit(1);
|
|
6482
|
+
}
|
|
6483
|
+
console.log(`Payout of $${amount.toFixed(2)} requested successfully.`);
|
|
6484
|
+
});
|
|
6485
|
+
return cmd;
|
|
6486
|
+
}
|
|
6487
|
+
|
|
6488
|
+
// src/commands/statements.ts
|
|
6489
|
+
function createStatementsCommand() {
|
|
6490
|
+
const cmd = new Command("statements").description(`Statement management (SSP)
|
|
6491
|
+
|
|
6492
|
+
Requires: PUBLISHER capability.`).addHelpText("after", `
|
|
6493
|
+
Examples:
|
|
6494
|
+
$ a8techads statements list
|
|
6495
|
+
$ a8techads statements get <id>`);
|
|
6496
|
+
addFormatOption(cmd.command("list").description("List statements.").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6497
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
6498
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/statements?${params}` });
|
|
6499
|
+
const json = await resp.json();
|
|
6500
|
+
if (!resp.ok) {
|
|
6501
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6502
|
+
process.exit(1);
|
|
6503
|
+
}
|
|
6504
|
+
const columns = [
|
|
6505
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6506
|
+
{ key: "number", header: "NUMBER", width: 14 },
|
|
6507
|
+
{ key: "period", header: "PERIOD", width: 16 },
|
|
4588
6508
|
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
4589
6509
|
{ key: "status", header: "STATUS", width: 12 }
|
|
4590
6510
|
];
|
|
4591
|
-
const rows = (json.data ?? json).map((
|
|
4592
|
-
id:
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
6511
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
6512
|
+
id: t.id,
|
|
6513
|
+
number: t.number,
|
|
6514
|
+
period: t.period,
|
|
6515
|
+
amount: t.amount,
|
|
6516
|
+
status: t.status
|
|
4596
6517
|
}));
|
|
4597
6518
|
printData(rows, columns, opts.format);
|
|
4598
6519
|
});
|
|
4599
|
-
addFormatOption(cmd.command("get").description("
|
|
4600
|
-
const resp = await apiRequest({ path: `${
|
|
6520
|
+
addFormatOption(cmd.command("get <id>").description("Show statement details.")).action(async (id, opts) => {
|
|
6521
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/statements/${id}` });
|
|
4601
6522
|
const json = await resp.json();
|
|
4602
6523
|
if (!resp.ok) {
|
|
4603
6524
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
@@ -4605,21 +6526,122 @@ Examples:
|
|
|
4605
6526
|
}
|
|
4606
6527
|
printDetail(json.data ?? json, opts.format);
|
|
4607
6528
|
});
|
|
4608
|
-
|
|
4609
|
-
|
|
6529
|
+
return cmd;
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
// src/commands/simulator.ts
|
|
6533
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
6534
|
+
async function confirmAction2(message, yes) {
|
|
6535
|
+
if (yes)
|
|
6536
|
+
return;
|
|
6537
|
+
const rl = await import("readline");
|
|
6538
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
6539
|
+
const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
|
|
6540
|
+
iface.close();
|
|
6541
|
+
if (answer.toLowerCase() !== "y") {
|
|
6542
|
+
console.log("Cancelled.");
|
|
6543
|
+
process.exit(0);
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
function createSimulatorCommand() {
|
|
6547
|
+
const cmd = new Command("simulator").description(`Simulator control and inspection
|
|
6548
|
+
|
|
6549
|
+
Used for replay and traffic verification workflows.`).addHelpText("after", `
|
|
6550
|
+
Examples:
|
|
6551
|
+
$ a8techads simulator status
|
|
6552
|
+
$ a8techads simulator start
|
|
6553
|
+
$ a8techads simulator stop --yes
|
|
6554
|
+
$ a8techads simulator reset --yes
|
|
6555
|
+
$ a8techads simulator config show
|
|
6556
|
+
$ a8techads simulator config update --from-json ./simulator-config.json`);
|
|
6557
|
+
addFormatOption(cmd.command("status").description("Show simulator runtime status and counters.")).action(async (opts) => {
|
|
6558
|
+
const resp = await simulatorRequest("GET", "/api/v1/simulator/status");
|
|
4610
6559
|
const json = await resp.json();
|
|
4611
6560
|
if (!resp.ok) {
|
|
4612
6561
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4613
6562
|
process.exit(1);
|
|
4614
6563
|
}
|
|
4615
|
-
printDetail(json
|
|
6564
|
+
printDetail(json, opts.format);
|
|
6565
|
+
});
|
|
6566
|
+
cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
|
|
6567
|
+
let body = undefined;
|
|
6568
|
+
if (opts.fromJson) {
|
|
6569
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6570
|
+
}
|
|
6571
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
|
|
6572
|
+
const json = await resp.json();
|
|
6573
|
+
if (!resp.ok) {
|
|
6574
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6575
|
+
process.exit(1);
|
|
6576
|
+
}
|
|
6577
|
+
console.log(json.message ?? "Simulator started.");
|
|
6578
|
+
});
|
|
6579
|
+
cmd.command("stop").description("Stop the simulator.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6580
|
+
await confirmAction2("Stop simulator?", opts.yes);
|
|
6581
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/stop");
|
|
6582
|
+
const json = await resp.json();
|
|
6583
|
+
if (!resp.ok) {
|
|
6584
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6585
|
+
process.exit(1);
|
|
6586
|
+
}
|
|
6587
|
+
console.log(json.message ?? "Simulator stopped.");
|
|
6588
|
+
});
|
|
6589
|
+
cmd.command("reset").description("Reset simulator counters and stats.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6590
|
+
await confirmAction2("Reset simulator stats?", opts.yes);
|
|
6591
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/reset");
|
|
6592
|
+
const json = await resp.json();
|
|
6593
|
+
if (!resp.ok) {
|
|
6594
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6595
|
+
process.exit(1);
|
|
6596
|
+
}
|
|
6597
|
+
console.log(json.message ?? "Simulator stats reset.");
|
|
6598
|
+
});
|
|
6599
|
+
const config = cmd.command("config").description("Simulator config management");
|
|
6600
|
+
addFormatOption(config.command("show").description("Show current simulator config.")).action(async (opts) => {
|
|
6601
|
+
const resp = await simulatorRequest("GET", "/api/v1/simulator/config");
|
|
6602
|
+
const json = await resp.json();
|
|
6603
|
+
if (!resp.ok) {
|
|
6604
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6605
|
+
process.exit(1);
|
|
6606
|
+
}
|
|
6607
|
+
printDetail(json, opts.format);
|
|
6608
|
+
});
|
|
6609
|
+
config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
|
|
6610
|
+
const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6611
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
|
|
6612
|
+
const json = await resp.json();
|
|
6613
|
+
if (!resp.ok) {
|
|
6614
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6615
|
+
process.exit(1);
|
|
6616
|
+
}
|
|
6617
|
+
console.log("Simulator config updated.");
|
|
6618
|
+
if (json.config) {
|
|
6619
|
+
printDetail(json.config, "table");
|
|
6620
|
+
}
|
|
4616
6621
|
});
|
|
4617
6622
|
return cmd;
|
|
4618
6623
|
}
|
|
6624
|
+
async function simulatorRequest(method, path, body) {
|
|
6625
|
+
const creds = loadCredentials();
|
|
6626
|
+
const profile = getCurrentProfile(creds);
|
|
6627
|
+
if (!profile) {
|
|
6628
|
+
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
6629
|
+
}
|
|
6630
|
+
const baseUrl = toConsoleApiBase(profile.api_url);
|
|
6631
|
+
const request = await buildAuthenticatedRequest({ method, path, body, baseUrl });
|
|
6632
|
+
return fetch(request.url, request.init);
|
|
6633
|
+
}
|
|
6634
|
+
function toConsoleApiBase(apiUrl) {
|
|
6635
|
+
const url = new URL(apiUrl);
|
|
6636
|
+
if (url.hostname.startsWith("api.")) {
|
|
6637
|
+
url.hostname = url.hostname.replace(/^api\./, "console.");
|
|
6638
|
+
}
|
|
6639
|
+
return url.origin;
|
|
6640
|
+
}
|
|
4619
6641
|
|
|
4620
6642
|
// src/index.ts
|
|
4621
6643
|
function createProgram() {
|
|
4622
|
-
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.
|
|
6644
|
+
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
|
|
4623
6645
|
Command Groups:
|
|
4624
6646
|
auth Authentication (login, logout, token, status)
|
|
4625
6647
|
profile Multi-profile management
|
|
@@ -4627,13 +6649,21 @@ Command Groups:
|
|
|
4627
6649
|
audiences Audience management (DSP)
|
|
4628
6650
|
campaigns Campaign management (DSP)
|
|
4629
6651
|
variations Ad variation management (DSP)
|
|
6652
|
+
conversion-goals Conversion goal management (DSP)
|
|
6653
|
+
media-assets Media library management (DSP)
|
|
6654
|
+
algorithms Bidder algorithm management (DSP)
|
|
4630
6655
|
sites Site management (SSP)
|
|
4631
6656
|
zones Ad zone management (SSP)
|
|
4632
6657
|
reports Analytics and reporting
|
|
4633
6658
|
billing Billing and payments (DSP)
|
|
4634
6659
|
invoices Invoice management (DSP)
|
|
6660
|
+
payouts Payout management (SSP)
|
|
6661
|
+
statements Statement management (SSP)
|
|
6662
|
+
simulator Simulator control and replay support
|
|
4635
6663
|
users Team member management
|
|
4636
6664
|
settings Tenant settings
|
|
6665
|
+
admin Platform administration (Console)
|
|
6666
|
+
external-ssp External SSP partner management (Console)
|
|
4637
6667
|
|
|
4638
6668
|
Getting Started:
|
|
4639
6669
|
$ a8techads auth login # Authenticate
|
|
@@ -4647,13 +6677,21 @@ Getting Started:
|
|
|
4647
6677
|
program2.addCommand(createAudiencesCommand());
|
|
4648
6678
|
program2.addCommand(createCampaignsCommand());
|
|
4649
6679
|
program2.addCommand(createVariationsCommand());
|
|
6680
|
+
program2.addCommand(createMediaAssetsCommand());
|
|
6681
|
+
program2.addCommand(createConversionGoalsCommand());
|
|
6682
|
+
program2.addCommand(createAlgorithmsCommand());
|
|
4650
6683
|
program2.addCommand(createSitesCommand());
|
|
4651
6684
|
program2.addCommand(createZonesCommand());
|
|
4652
6685
|
program2.addCommand(createReportsCommand());
|
|
4653
6686
|
program2.addCommand(createBillingCommand());
|
|
6687
|
+
program2.addCommand(createPayoutsCommand());
|
|
6688
|
+
program2.addCommand(createStatementsCommand());
|
|
6689
|
+
program2.addCommand(createSimulatorCommand());
|
|
4654
6690
|
program2.addCommand(createUsersCommand());
|
|
4655
6691
|
program2.addCommand(createSettingsCommand());
|
|
4656
6692
|
program2.addCommand(createInvoicesCommand());
|
|
6693
|
+
program2.addCommand(createAdminCommand());
|
|
6694
|
+
program2.addCommand(createExternalSspCommand());
|
|
4657
6695
|
return program2;
|
|
4658
6696
|
}
|
|
4659
6697
|
|