@a8techads/cli 0.2.0 → 0.4.0
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 +452 -53
- package/package.json +1 -1
package/dist/a8techads.js
CHANGED
|
@@ -2646,6 +2646,9 @@ async function login(opts = {}) {
|
|
|
2646
2646
|
if (opts.tenantId) {
|
|
2647
2647
|
authorizeUrl.searchParams.set("tenant_id", opts.tenantId);
|
|
2648
2648
|
}
|
|
2649
|
+
if (opts.forceLogin) {
|
|
2650
|
+
authorizeUrl.searchParams.set("prompt", "login");
|
|
2651
|
+
}
|
|
2649
2652
|
const code = await waitForAuthorizationCode(authorizeUrl.toString(), state);
|
|
2650
2653
|
console.log("Exchanging authorization code for tokens...");
|
|
2651
2654
|
const tokenResponse = await fetch(tokenEndpoint, {
|
|
@@ -3083,11 +3086,12 @@ Examples:
|
|
|
3083
3086
|
Browser mode (default): Opens browser for OAuth 2.1 Authorization Code + PKCE.
|
|
3084
3087
|
Client credentials mode: Non-interactive auth using --client-id and --client-secret.
|
|
3085
3088
|
|
|
3086
|
-
Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser mode only)", "default").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").option("--client-id <id>", "OAuth client ID (enables client_credentials flow)").option("--client-secret <secret>", "OAuth client secret (requires --client-id)").addHelpText("after", `
|
|
3089
|
+
Requires: network access to auth server.`).option("-p, --profile <name>", "Profile name (browser mode only)", "default").option("--api-url <url>", "API base URL (default: https://api.a8.tech)").option("--auth-url <url>", "Auth server URL (default: https://auth.a8.tech)").option("--client-id <id>", "OAuth client ID (enables client_credentials flow)").option("--client-secret <secret>", "OAuth client secret (requires --client-id)").option("--force-login", "Force login prompt even if browser session exists (use to switch users)").addHelpText("after", `
|
|
3087
3090
|
Examples:
|
|
3088
3091
|
$ a8techads auth login # Interactive browser login
|
|
3089
3092
|
$ a8techads auth login -p staging --api-url https://api.staging.a8.tech
|
|
3090
3093
|
$ a8techads auth login --client-id svc-001 --client-secret s3cret
|
|
3094
|
+
$ a8techads auth login -p owner --force-login # Switch to different user
|
|
3091
3095
|
|
|
3092
3096
|
Note: Client credentials tokens cannot be refreshed. The CLI will prompt
|
|
3093
3097
|
for re-authentication when the token expires.`).action(async (opts) => {
|
|
@@ -3107,7 +3111,8 @@ Note: Client credentials tokens cannot be refreshed. The CLI will prompt
|
|
|
3107
3111
|
await login({
|
|
3108
3112
|
profile: opts.profile,
|
|
3109
3113
|
apiUrl: opts.apiUrl,
|
|
3110
|
-
authUrl: opts.authUrl
|
|
3114
|
+
authUrl: opts.authUrl,
|
|
3115
|
+
forceLogin: opts.forceLogin
|
|
3111
3116
|
});
|
|
3112
3117
|
}
|
|
3113
3118
|
} catch (err) {
|
|
@@ -3195,6 +3200,52 @@ function createProfileCommand() {
|
|
|
3195
3200
|
return profile;
|
|
3196
3201
|
}
|
|
3197
3202
|
|
|
3203
|
+
// src/utils/http.ts
|
|
3204
|
+
async function apiRequest(opts) {
|
|
3205
|
+
await refreshTokenIfNeeded();
|
|
3206
|
+
const creds = loadCredentials();
|
|
3207
|
+
const profile = getCurrentProfile(creds);
|
|
3208
|
+
if (!profile) {
|
|
3209
|
+
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
3210
|
+
}
|
|
3211
|
+
const ctx = loadContext();
|
|
3212
|
+
const context = getCurrentContext(ctx);
|
|
3213
|
+
validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
|
|
3214
|
+
const headers = {
|
|
3215
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
3216
|
+
"Content-Type": "application/json",
|
|
3217
|
+
...opts.headers
|
|
3218
|
+
};
|
|
3219
|
+
if (context?.current_capability) {
|
|
3220
|
+
headers["X-Effective-Capability"] = context.current_capability;
|
|
3221
|
+
}
|
|
3222
|
+
if (context?.impersonation) {
|
|
3223
|
+
headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
|
|
3224
|
+
if (context.impersonation.effective_capability) {
|
|
3225
|
+
headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
const url = `${profile.api_url}${opts.path}`;
|
|
3229
|
+
return fetch(url, {
|
|
3230
|
+
method: opts.method ?? "GET",
|
|
3231
|
+
headers,
|
|
3232
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
function validateTenantMatch(accessToken, contextTenantId) {
|
|
3236
|
+
const claims = decodeJwt(accessToken);
|
|
3237
|
+
if (!claims)
|
|
3238
|
+
return;
|
|
3239
|
+
const authClaims = getAuthClaims(claims);
|
|
3240
|
+
const tokenTenantId = authClaims?.tenant_id ?? null;
|
|
3241
|
+
if (!contextTenantId || !tokenTenantId)
|
|
3242
|
+
return;
|
|
3243
|
+
if (contextTenantId !== tokenTenantId) {
|
|
3244
|
+
throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
|
|
3245
|
+
` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3198
3249
|
// src/commands/context.ts
|
|
3199
3250
|
function createContextCommand() {
|
|
3200
3251
|
const context = new Command("context").description("Workspace context — tenant, capability, and app selection").addHelpText("after", `
|
|
@@ -3237,6 +3288,16 @@ Examples:
|
|
|
3237
3288
|
}
|
|
3238
3289
|
console.log(`App: ${profileCtx?.app ?? "none"}`);
|
|
3239
3290
|
console.log(`Capability: ${profileCtx?.current_capability ?? "null (platform)"}`);
|
|
3291
|
+
if (profileCtx?.impersonation) {
|
|
3292
|
+
const imp = profileCtx.impersonation;
|
|
3293
|
+
console.log(`
|
|
3294
|
+
--- Managed Access Session ---`);
|
|
3295
|
+
console.log(`Target: ${imp.target_tenant_name} (${imp.target_tenant_id})`);
|
|
3296
|
+
console.log(`Capability: ${imp.effective_capability}`);
|
|
3297
|
+
console.log(`Role: ${imp.effective_role}`);
|
|
3298
|
+
console.log(`Grant: ${imp.managed_grant_id ?? "none"}`);
|
|
3299
|
+
console.log(`Started: ${imp.started_at}`);
|
|
3300
|
+
}
|
|
3240
3301
|
});
|
|
3241
3302
|
context.command("set-tenant").description(`Switch to a different tenant. Triggers re-authentication if the
|
|
3242
3303
|
tenant differs from the current token tenant.
|
|
@@ -3371,6 +3432,74 @@ Shortcuts:
|
|
|
3371
3432
|
console.log("Capability: PUBLISHER");
|
|
3372
3433
|
console.log("App: ssp");
|
|
3373
3434
|
});
|
|
3435
|
+
context.command("assume-role").description(`Start a managed access session as a platform operator.
|
|
3436
|
+
|
|
3437
|
+
Requires: platform_owner or platform_admin role.`).option("--tenant <id>", "Target tenant ID (required)").option("--capability <cap>", "Effective capability: ADVERTISER or PUBLISHER").addHelpText("after", `
|
|
3438
|
+
Examples:
|
|
3439
|
+
$ a8techads context assume-role --tenant 00000000-... --capability ADVERTISER
|
|
3440
|
+
$ a8techads context assume-role --tenant <id>`).action(async (opts) => {
|
|
3441
|
+
if (!opts.tenant) {
|
|
3442
|
+
console.error("Error: --tenant is required.");
|
|
3443
|
+
console.error('Run "a8techads context assume-role --help" for usage.');
|
|
3444
|
+
process.exit(1);
|
|
3445
|
+
}
|
|
3446
|
+
try {
|
|
3447
|
+
const body = { target_tenant_id: opts.tenant };
|
|
3448
|
+
if (opts.capability)
|
|
3449
|
+
body.target_capability = opts.capability;
|
|
3450
|
+
const resp = await apiRequest({
|
|
3451
|
+
method: "POST",
|
|
3452
|
+
path: "/api/v1/console/impersonation/start",
|
|
3453
|
+
body
|
|
3454
|
+
});
|
|
3455
|
+
if (!resp.ok) {
|
|
3456
|
+
const err = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
3457
|
+
console.error(`Error: ${err.error || resp.statusText}`);
|
|
3458
|
+
process.exit(1);
|
|
3459
|
+
}
|
|
3460
|
+
const data = await resp.json();
|
|
3461
|
+
const targetTenant = data.target_tenant || {};
|
|
3462
|
+
const ctx = loadContext();
|
|
3463
|
+
const impersonation = {
|
|
3464
|
+
target_tenant_id: opts.tenant,
|
|
3465
|
+
target_tenant_name: targetTenant.company_name || "Unknown",
|
|
3466
|
+
effective_capability: data.effective_capability || opts.capability || "",
|
|
3467
|
+
effective_role: data.effective_role || "",
|
|
3468
|
+
managed_grant_id: data.managed_grant_id || null,
|
|
3469
|
+
started_at: new Date().toISOString()
|
|
3470
|
+
};
|
|
3471
|
+
setCurrentContext(ctx, { impersonation });
|
|
3472
|
+
saveContext(ctx);
|
|
3473
|
+
console.log("Managed access session started.");
|
|
3474
|
+
console.log(`Target: ${impersonation.target_tenant_name} (${opts.tenant})`);
|
|
3475
|
+
console.log(`Capability: ${impersonation.effective_capability}`);
|
|
3476
|
+
console.log(`Grant: ${impersonation.managed_grant_id || "auto-created"}`);
|
|
3477
|
+
} catch (err) {
|
|
3478
|
+
console.error(`Failed: ${err.message}`);
|
|
3479
|
+
process.exit(1);
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
context.command("end-session").description(`End the current managed access session.
|
|
3483
|
+
|
|
3484
|
+
Requires: an active managed access session.`).addHelpText("after", `
|
|
3485
|
+
Examples:
|
|
3486
|
+
$ a8techads context end-session`).action(async () => {
|
|
3487
|
+
const ctx = loadContext();
|
|
3488
|
+
const profileCtx = getCurrentContext(ctx);
|
|
3489
|
+
if (!profileCtx?.impersonation) {
|
|
3490
|
+
console.log("No active managed access session.");
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
try {
|
|
3494
|
+
await apiRequest({
|
|
3495
|
+
method: "POST",
|
|
3496
|
+
path: "/api/v1/console/impersonation/end"
|
|
3497
|
+
});
|
|
3498
|
+
} catch {}
|
|
3499
|
+
setCurrentContext(ctx, { impersonation: null });
|
|
3500
|
+
saveContext(ctx);
|
|
3501
|
+
console.log("Managed access session ended.");
|
|
3502
|
+
});
|
|
3374
3503
|
return context;
|
|
3375
3504
|
}
|
|
3376
3505
|
function deriveAppFromTenant(tenant) {
|
|
@@ -3387,46 +3516,6 @@ function deriveAuthUrl(tokenEndpoint) {
|
|
|
3387
3516
|
return url.origin;
|
|
3388
3517
|
}
|
|
3389
3518
|
|
|
3390
|
-
// src/utils/http.ts
|
|
3391
|
-
async function apiRequest(opts) {
|
|
3392
|
-
await refreshTokenIfNeeded();
|
|
3393
|
-
const creds = loadCredentials();
|
|
3394
|
-
const profile = getCurrentProfile(creds);
|
|
3395
|
-
if (!profile) {
|
|
3396
|
-
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
3397
|
-
}
|
|
3398
|
-
const ctx = loadContext();
|
|
3399
|
-
const context = getCurrentContext(ctx);
|
|
3400
|
-
validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
|
|
3401
|
-
const headers = {
|
|
3402
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
3403
|
-
"Content-Type": "application/json",
|
|
3404
|
-
...opts.headers
|
|
3405
|
-
};
|
|
3406
|
-
if (context?.current_capability) {
|
|
3407
|
-
headers["X-Effective-Capability"] = context.current_capability;
|
|
3408
|
-
}
|
|
3409
|
-
const url = `${profile.api_url}${opts.path}`;
|
|
3410
|
-
return fetch(url, {
|
|
3411
|
-
method: opts.method ?? "GET",
|
|
3412
|
-
headers,
|
|
3413
|
-
body: opts.body ? JSON.stringify(opts.body) : undefined
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
function validateTenantMatch(accessToken, contextTenantId) {
|
|
3417
|
-
const claims = decodeJwt(accessToken);
|
|
3418
|
-
if (!claims)
|
|
3419
|
-
return;
|
|
3420
|
-
const authClaims = getAuthClaims(claims);
|
|
3421
|
-
const tokenTenantId = authClaims?.tenant_id ?? null;
|
|
3422
|
-
if (!contextTenantId || !tokenTenantId)
|
|
3423
|
-
return;
|
|
3424
|
-
if (contextTenantId !== tokenTenantId) {
|
|
3425
|
-
throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
|
|
3426
|
-
` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
3519
|
// src/utils/api-prefix.ts
|
|
3431
3520
|
function getApiPrefix() {
|
|
3432
3521
|
const ctx = loadContext();
|
|
@@ -3514,8 +3603,204 @@ function addFormatOption(cmd) {
|
|
|
3514
3603
|
return cmd.option("-f, --format <format>", "Output format: table, json, csv (default: table)", "table");
|
|
3515
3604
|
}
|
|
3516
3605
|
|
|
3517
|
-
// src/commands/
|
|
3606
|
+
// src/commands/audiences.ts
|
|
3518
3607
|
var COLUMNS = [
|
|
3608
|
+
{ key: "id", header: "ID", width: 36 },
|
|
3609
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
3610
|
+
{ key: "type", header: "TYPE", width: 15 },
|
|
3611
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
3612
|
+
{ key: "estimatedSize", header: "SIZE", width: 10, format: (v) => v != null ? Number(v).toLocaleString() : "-" }
|
|
3613
|
+
];
|
|
3614
|
+
function createAudiencesCommand() {
|
|
3615
|
+
const cmd = new Command("audiences").description(`Audience management (DSP)
|
|
3616
|
+
|
|
3617
|
+
Phase 1: UPLOADED_LIST only.
|
|
3618
|
+
|
|
3619
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
3620
|
+
Examples:
|
|
3621
|
+
$ a8techads audiences list
|
|
3622
|
+
$ a8techads audiences get <id>
|
|
3623
|
+
$ a8techads audiences create --name "High-Value Customers" --type UPLOADED_LIST
|
|
3624
|
+
$ a8techads audiences upload <id> --file users.csv --identifier-type EMAIL_HASH
|
|
3625
|
+
$ a8techads audiences activate <id>
|
|
3626
|
+
$ a8techads audiences pause <id>`);
|
|
3627
|
+
addFormatOption(cmd.command("list").description("List audiences.").option("--type <type>", "Filter by type (UPLOADED_LIST)").option("--status <status>", "Filter by status (DRAFT, READY, ACTIVE, PAUSED, etc.)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
3628
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
3629
|
+
if (opts.type)
|
|
3630
|
+
params.set("type", opts.type);
|
|
3631
|
+
if (opts.status)
|
|
3632
|
+
params.set("status", opts.status);
|
|
3633
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/audiences?${params}` });
|
|
3634
|
+
const json = await resp.json();
|
|
3635
|
+
if (!resp.ok) {
|
|
3636
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3637
|
+
process.exit(1);
|
|
3638
|
+
}
|
|
3639
|
+
const rows = (json.data ?? json).map((a) => ({
|
|
3640
|
+
id: a.id,
|
|
3641
|
+
name: a.name,
|
|
3642
|
+
type: a.type,
|
|
3643
|
+
status: a.status,
|
|
3644
|
+
estimatedSize: a.estimatedSize ?? a.estimated_size
|
|
3645
|
+
}));
|
|
3646
|
+
printData(rows, COLUMNS, opts.format);
|
|
3647
|
+
});
|
|
3648
|
+
addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
|
|
3649
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
|
|
3650
|
+
const json = await resp.json();
|
|
3651
|
+
if (!resp.ok) {
|
|
3652
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3653
|
+
process.exit(1);
|
|
3654
|
+
}
|
|
3655
|
+
printDetail(json.data ?? json, opts.format);
|
|
3656
|
+
});
|
|
3657
|
+
cmd.command("create").description(`Create a new audience.
|
|
3658
|
+
|
|
3659
|
+
Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST or RETARGETING", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
|
|
3660
|
+
Examples:
|
|
3661
|
+
$ a8techads audiences create --name "High-Value Customers"
|
|
3662
|
+
$ a8techads audiences create --name "Retarget Pool" --ttl 30
|
|
3663
|
+
$ a8techads audiences create --from-json audience.json`).action(async (opts) => {
|
|
3664
|
+
let body;
|
|
3665
|
+
if (opts.fromJson) {
|
|
3666
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
3667
|
+
body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
3668
|
+
} else {
|
|
3669
|
+
if (!opts.name) {
|
|
3670
|
+
console.error('Error: --name is required. Run "a8techads audiences create --help".');
|
|
3671
|
+
process.exit(1);
|
|
3672
|
+
}
|
|
3673
|
+
if (opts.type === "RETARGETING" && !opts.goalId) {
|
|
3674
|
+
console.error("Error: --goal-id is required for RETARGETING type.");
|
|
3675
|
+
process.exit(1);
|
|
3676
|
+
}
|
|
3677
|
+
body = {
|
|
3678
|
+
name: opts.name,
|
|
3679
|
+
type: opts.type,
|
|
3680
|
+
membershipTtlDays: Number(opts.ttl)
|
|
3681
|
+
};
|
|
3682
|
+
if (opts.description)
|
|
3683
|
+
body.description = opts.description;
|
|
3684
|
+
if (opts.goalId) {
|
|
3685
|
+
body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
|
|
3689
|
+
const json = await resp.json();
|
|
3690
|
+
if (!resp.ok) {
|
|
3691
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
3692
|
+
process.exit(1);
|
|
3693
|
+
}
|
|
3694
|
+
console.log(`Audience created: ${json.data?.id ?? json.id}`);
|
|
3695
|
+
});
|
|
3696
|
+
cmd.command("update").description("Update an audience.").argument("<id>", "Audience ID").option("--name <name>", "New name").option("--description <desc>", "New description").option("--ttl <days>", "New membership TTL in days").action(async (id, opts) => {
|
|
3697
|
+
const body = {};
|
|
3698
|
+
if (opts.name)
|
|
3699
|
+
body.name = opts.name;
|
|
3700
|
+
if (opts.description)
|
|
3701
|
+
body.description = opts.description;
|
|
3702
|
+
if (opts.ttl)
|
|
3703
|
+
body.membershipTtlDays = Number(opts.ttl);
|
|
3704
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/audiences/${id}`, body });
|
|
3705
|
+
if (!resp.ok) {
|
|
3706
|
+
const j = await resp.json();
|
|
3707
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
3708
|
+
process.exit(1);
|
|
3709
|
+
}
|
|
3710
|
+
console.log(`Audience ${id} updated.`);
|
|
3711
|
+
});
|
|
3712
|
+
cmd.command("delete").description("Delete an audience.").argument("<id>", "Audience ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
3713
|
+
if (!opts.yes) {
|
|
3714
|
+
console.error("Add --yes to confirm deletion.");
|
|
3715
|
+
process.exit(1);
|
|
3716
|
+
}
|
|
3717
|
+
const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/audiences/${id}` });
|
|
3718
|
+
if (!resp.ok && resp.status !== 204) {
|
|
3719
|
+
console.error(`Error: ${resp.statusText}`);
|
|
3720
|
+
process.exit(1);
|
|
3721
|
+
}
|
|
3722
|
+
console.log(`Audience ${id} deleted.`);
|
|
3723
|
+
});
|
|
3724
|
+
cmd.command("activate").description("Activate an audience (must be READY or PAUSED).").argument("<id>", "Audience ID").action(async (id) => {
|
|
3725
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/audiences/${id}/activate` });
|
|
3726
|
+
if (!resp.ok) {
|
|
3727
|
+
const j = await resp.json();
|
|
3728
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
3729
|
+
process.exit(1);
|
|
3730
|
+
}
|
|
3731
|
+
console.log(`Audience ${id} activated.`);
|
|
3732
|
+
});
|
|
3733
|
+
cmd.command("pause").description("Pause an active audience.").argument("<id>", "Audience ID").action(async (id) => {
|
|
3734
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/audiences/${id}/pause` });
|
|
3735
|
+
if (!resp.ok) {
|
|
3736
|
+
const j = await resp.json();
|
|
3737
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
3738
|
+
process.exit(1);
|
|
3739
|
+
}
|
|
3740
|
+
console.log(`Audience ${id} paused.`);
|
|
3741
|
+
});
|
|
3742
|
+
cmd.command("archive").description("Archive an audience.").argument("<id>", "Audience ID").action(async (id) => {
|
|
3743
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/audiences/${id}/archive` });
|
|
3744
|
+
if (!resp.ok) {
|
|
3745
|
+
const j = await resp.json();
|
|
3746
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
3747
|
+
process.exit(1);
|
|
3748
|
+
}
|
|
3749
|
+
console.log(`Audience ${id} archived.`);
|
|
3750
|
+
});
|
|
3751
|
+
cmd.command("upload").description(`Upload user identifiers to populate audience membership.
|
|
3752
|
+
|
|
3753
|
+
Reads a file with one identifier per line (email hash, device ID, etc.)
|
|
3754
|
+
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", `
|
|
3755
|
+
Examples:
|
|
3756
|
+
$ a8techads audiences upload <id> --file users.txt --identifier-type EMAIL_HASH
|
|
3757
|
+
$ a8techads audiences upload <id> --file devices.txt --identifier-type DEVICE_ID
|
|
3758
|
+
|
|
3759
|
+
File format (one per line):
|
|
3760
|
+
a1b2c3d4e5f6...
|
|
3761
|
+
f6e5d4c3b2a1...`).action(async (id, opts) => {
|
|
3762
|
+
if (!opts.file) {
|
|
3763
|
+
console.error("Error: --file is required.");
|
|
3764
|
+
process.exit(1);
|
|
3765
|
+
}
|
|
3766
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
3767
|
+
let identifiers;
|
|
3768
|
+
try {
|
|
3769
|
+
const content = readFileSync3(opts.file, "utf-8").trim();
|
|
3770
|
+
try {
|
|
3771
|
+
identifiers = JSON.parse(content);
|
|
3772
|
+
if (!Array.isArray(identifiers))
|
|
3773
|
+
throw new Error("not array");
|
|
3774
|
+
} catch {
|
|
3775
|
+
identifiers = content.split(`
|
|
3776
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3777
|
+
}
|
|
3778
|
+
} catch (err) {
|
|
3779
|
+
console.error(`Error reading file: ${err.message}`);
|
|
3780
|
+
process.exit(1);
|
|
3781
|
+
}
|
|
3782
|
+
if (identifiers.length === 0) {
|
|
3783
|
+
console.error("Error: file contains no identifiers.");
|
|
3784
|
+
process.exit(1);
|
|
3785
|
+
}
|
|
3786
|
+
console.log(`Uploading ${identifiers.length} identifiers...`);
|
|
3787
|
+
const body = {
|
|
3788
|
+
identifiers,
|
|
3789
|
+
identifierType: opts.identifierType
|
|
3790
|
+
};
|
|
3791
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences/${id}/upload`, body });
|
|
3792
|
+
const json = await resp.json();
|
|
3793
|
+
if (!resp.ok) {
|
|
3794
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3795
|
+
process.exit(1);
|
|
3796
|
+
}
|
|
3797
|
+
console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
|
|
3798
|
+
});
|
|
3799
|
+
return cmd;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/commands/campaigns.ts
|
|
3803
|
+
var COLUMNS2 = [
|
|
3519
3804
|
{ key: "id", header: "ID", width: 36 },
|
|
3520
3805
|
{ key: "name", header: "NAME", width: 30 },
|
|
3521
3806
|
{ key: "status", header: "STATUS", width: 12 },
|
|
@@ -3549,7 +3834,7 @@ Examples:
|
|
|
3549
3834
|
budget: c.budget ?? c.dailyBudget,
|
|
3550
3835
|
spent: c.stats?.spend ?? c.spent
|
|
3551
3836
|
}));
|
|
3552
|
-
printData(rows,
|
|
3837
|
+
printData(rows, COLUMNS2, opts.format);
|
|
3553
3838
|
});
|
|
3554
3839
|
addFormatOption(cmd.command("get").description("Get campaign details by ID.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
|
|
3555
3840
|
const resp = await apiRequest({ path: `${dspPrefix()}/campaigns/${id}` });
|
|
@@ -3661,7 +3946,7 @@ Examples:
|
|
|
3661
3946
|
}
|
|
3662
3947
|
|
|
3663
3948
|
// src/commands/variations.ts
|
|
3664
|
-
var
|
|
3949
|
+
var COLUMNS3 = [
|
|
3665
3950
|
{ key: "id", header: "ID", width: 36 },
|
|
3666
3951
|
{ key: "name", header: "NAME", width: 25 },
|
|
3667
3952
|
{ key: "type", header: "TYPE", width: 10 },
|
|
@@ -3692,7 +3977,7 @@ Examples:
|
|
|
3692
3977
|
status: v.status,
|
|
3693
3978
|
campaignId: v.campaignId ?? v.campaign_id
|
|
3694
3979
|
}));
|
|
3695
|
-
printData(rows,
|
|
3980
|
+
printData(rows, COLUMNS3, opts.format);
|
|
3696
3981
|
});
|
|
3697
3982
|
addFormatOption(cmd.command("get").description("Get variation details.").argument("<id>", "Variation ID")).action(async (id, opts) => {
|
|
3698
3983
|
const resp = await apiRequest({ path: `${dspPrefix()}/variations/${id}` });
|
|
@@ -3757,7 +4042,7 @@ Examples:
|
|
|
3757
4042
|
}
|
|
3758
4043
|
|
|
3759
4044
|
// src/commands/sites.ts
|
|
3760
|
-
var
|
|
4045
|
+
var COLUMNS4 = [
|
|
3761
4046
|
{ key: "id", header: "ID", width: 36 },
|
|
3762
4047
|
{ key: "name", header: "NAME", width: 25 },
|
|
3763
4048
|
{ key: "domain", header: "DOMAIN", width: 25 },
|
|
@@ -3790,7 +4075,7 @@ Examples:
|
|
|
3790
4075
|
status: s.status,
|
|
3791
4076
|
zoneCount: s.zoneCount ?? s.zone_count ?? "-"
|
|
3792
4077
|
}));
|
|
3793
|
-
printData(rows,
|
|
4078
|
+
printData(rows, COLUMNS4, opts.format);
|
|
3794
4079
|
});
|
|
3795
4080
|
addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
|
|
3796
4081
|
const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
|
|
@@ -3876,7 +4161,7 @@ Examples:
|
|
|
3876
4161
|
}
|
|
3877
4162
|
|
|
3878
4163
|
// src/commands/zones.ts
|
|
3879
|
-
var
|
|
4164
|
+
var COLUMNS5 = [
|
|
3880
4165
|
{ key: "id", header: "ID", width: 36 },
|
|
3881
4166
|
{ key: "name", header: "NAME", width: 25 },
|
|
3882
4167
|
{ key: "format", header: "FORMAT", width: 18 },
|
|
@@ -3908,7 +4193,7 @@ Examples:
|
|
|
3908
4193
|
format: z.adFormat ?? z.ad_format ?? "-",
|
|
3909
4194
|
status: z.status
|
|
3910
4195
|
}));
|
|
3911
|
-
printData(rows,
|
|
4196
|
+
printData(rows, COLUMNS5, opts.format);
|
|
3912
4197
|
});
|
|
3913
4198
|
addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
|
|
3914
4199
|
const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
|
|
@@ -4209,7 +4494,7 @@ function usersPrefix() {
|
|
|
4209
4494
|
}
|
|
4210
4495
|
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
4211
4496
|
}
|
|
4212
|
-
var
|
|
4497
|
+
var COLUMNS6 = [
|
|
4213
4498
|
{ key: "id", header: "ID", width: 36 },
|
|
4214
4499
|
{ key: "email", header: "EMAIL", width: 30 },
|
|
4215
4500
|
{ key: "name", header: "NAME", width: 20 },
|
|
@@ -4240,7 +4525,7 @@ Examples:
|
|
|
4240
4525
|
role: u.role,
|
|
4241
4526
|
status: u.status
|
|
4242
4527
|
}));
|
|
4243
|
-
printData(rows,
|
|
4528
|
+
printData(rows, COLUMNS6, opts.format);
|
|
4244
4529
|
});
|
|
4245
4530
|
addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
|
|
4246
4531
|
const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
|
|
@@ -4319,6 +4604,117 @@ Examples:
|
|
|
4319
4604
|
return cmd;
|
|
4320
4605
|
}
|
|
4321
4606
|
|
|
4607
|
+
// src/commands/admin.ts
|
|
4608
|
+
function createAdminCommand() {
|
|
4609
|
+
const cmd = new Command("admin").description(`Platform administration (Console)
|
|
4610
|
+
|
|
4611
|
+
Requires: platform_owner or platform_admin role.`).addHelpText("after", `
|
|
4612
|
+
Examples:
|
|
4613
|
+
$ a8techads admin tenants list
|
|
4614
|
+
$ a8techads admin tenants get <id>
|
|
4615
|
+
$ a8techads admin audit-logs
|
|
4616
|
+
$ a8techads admin system health`);
|
|
4617
|
+
const tenants = cmd.command("tenants").description("Tenant management");
|
|
4618
|
+
addFormatOption(tenants.command("list").description("List all tenants.")).action(async (opts) => {
|
|
4619
|
+
const resp = await apiRequest({ path: "/api/v1/console/tenants" });
|
|
4620
|
+
const json = await resp.json();
|
|
4621
|
+
if (!resp.ok) {
|
|
4622
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4623
|
+
process.exit(1);
|
|
4624
|
+
}
|
|
4625
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
4626
|
+
id: t.id,
|
|
4627
|
+
name: t.companyName ?? t.company_name,
|
|
4628
|
+
type: t.tenantType ?? t.tenant_type,
|
|
4629
|
+
status: t.status
|
|
4630
|
+
}));
|
|
4631
|
+
const columns = [
|
|
4632
|
+
{ key: "id", header: "ID", width: 36 },
|
|
4633
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
4634
|
+
{ key: "type", header: "TYPE", width: 12 },
|
|
4635
|
+
{ key: "status", header: "STATUS", width: 10 }
|
|
4636
|
+
];
|
|
4637
|
+
printData(rows, columns, opts.format);
|
|
4638
|
+
});
|
|
4639
|
+
addFormatOption(tenants.command("get").description("Get tenant details.").argument("<id>", "Tenant ID")).action(async (id, opts) => {
|
|
4640
|
+
const resp = await apiRequest({ path: `/api/v1/console/tenants/${id}` });
|
|
4641
|
+
const json = await resp.json();
|
|
4642
|
+
if (!resp.ok) {
|
|
4643
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4644
|
+
process.exit(1);
|
|
4645
|
+
}
|
|
4646
|
+
printDetail(json.data ?? json, opts.format);
|
|
4647
|
+
});
|
|
4648
|
+
const members = tenants.command("members").description("Tenant member management");
|
|
4649
|
+
addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
|
|
4650
|
+
if (!opts.tenant) {
|
|
4651
|
+
console.error("Error: --tenant is required.");
|
|
4652
|
+
process.exit(1);
|
|
4653
|
+
}
|
|
4654
|
+
const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
|
|
4655
|
+
const json = await resp.json();
|
|
4656
|
+
if (!resp.ok) {
|
|
4657
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4658
|
+
process.exit(1);
|
|
4659
|
+
}
|
|
4660
|
+
const rows = (json.data ?? json).map((m) => ({
|
|
4661
|
+
id: m.userId ?? m.user_id ?? m.id,
|
|
4662
|
+
email: m.email,
|
|
4663
|
+
role: m.role,
|
|
4664
|
+
status: m.status
|
|
4665
|
+
}));
|
|
4666
|
+
printData(rows, [
|
|
4667
|
+
{ key: "id", header: "USER ID", width: 36 },
|
|
4668
|
+
{ key: "email", header: "EMAIL", width: 30 },
|
|
4669
|
+
{ key: "role", header: "ROLE", width: 20 }
|
|
4670
|
+
], opts.format);
|
|
4671
|
+
});
|
|
4672
|
+
addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
4673
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
4674
|
+
if (opts.tenant)
|
|
4675
|
+
params.set("tenant_id", opts.tenant);
|
|
4676
|
+
const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
|
|
4677
|
+
const json = await resp.json();
|
|
4678
|
+
if (!resp.ok) {
|
|
4679
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4680
|
+
process.exit(1);
|
|
4681
|
+
}
|
|
4682
|
+
const rows = (json.data ?? json).map((l) => ({
|
|
4683
|
+
action: l.action,
|
|
4684
|
+
target: l.targetType ?? l.target_type,
|
|
4685
|
+
targetId: l.targetId ?? l.target_id,
|
|
4686
|
+
user: l.adminUserId ?? l.admin_user_id,
|
|
4687
|
+
time: l.createdAt ?? l.created_at
|
|
4688
|
+
}));
|
|
4689
|
+
printData(rows, [
|
|
4690
|
+
{ key: "action", header: "ACTION", width: 25 },
|
|
4691
|
+
{ key: "target", header: "TARGET", width: 12 },
|
|
4692
|
+
{ key: "targetId", header: "TARGET ID", width: 36 },
|
|
4693
|
+
{ key: "time", header: "TIME", width: 22 }
|
|
4694
|
+
], opts.format);
|
|
4695
|
+
});
|
|
4696
|
+
const system = cmd.command("system").description("System operations");
|
|
4697
|
+
addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
|
|
4698
|
+
const resp = await apiRequest({ path: "/api/v1/console/system/health" });
|
|
4699
|
+
const json = await resp.json();
|
|
4700
|
+
if (!resp.ok) {
|
|
4701
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4702
|
+
process.exit(1);
|
|
4703
|
+
}
|
|
4704
|
+
printDetail(json.data ?? json, opts.format);
|
|
4705
|
+
});
|
|
4706
|
+
system.command("cache-clear").description("Clear system cache.").action(async () => {
|
|
4707
|
+
const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
|
|
4708
|
+
if (!resp.ok) {
|
|
4709
|
+
const j = await resp.json();
|
|
4710
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4711
|
+
process.exit(1);
|
|
4712
|
+
}
|
|
4713
|
+
console.log("Cache cleared.");
|
|
4714
|
+
});
|
|
4715
|
+
return cmd;
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4322
4718
|
// src/commands/settings.ts
|
|
4323
4719
|
function settingsPrefix() {
|
|
4324
4720
|
const ctx = loadContext();
|
|
@@ -4450,11 +4846,12 @@ Examples:
|
|
|
4450
4846
|
|
|
4451
4847
|
// src/index.ts
|
|
4452
4848
|
function createProgram() {
|
|
4453
|
-
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.
|
|
4849
|
+
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.0").addHelpText("after", `
|
|
4454
4850
|
Command Groups:
|
|
4455
4851
|
auth Authentication (login, logout, token, status)
|
|
4456
4852
|
profile Multi-profile management
|
|
4457
4853
|
context Workspace context (tenant, capability, app)
|
|
4854
|
+
audiences Audience management (DSP)
|
|
4458
4855
|
campaigns Campaign management (DSP)
|
|
4459
4856
|
variations Ad variation management (DSP)
|
|
4460
4857
|
sites Site management (SSP)
|
|
@@ -4474,6 +4871,7 @@ Getting Started:
|
|
|
4474
4871
|
program2.addCommand(createAuthCommand());
|
|
4475
4872
|
program2.addCommand(createProfileCommand());
|
|
4476
4873
|
program2.addCommand(createContextCommand());
|
|
4874
|
+
program2.addCommand(createAudiencesCommand());
|
|
4477
4875
|
program2.addCommand(createCampaignsCommand());
|
|
4478
4876
|
program2.addCommand(createVariationsCommand());
|
|
4479
4877
|
program2.addCommand(createSitesCommand());
|
|
@@ -4483,6 +4881,7 @@ Getting Started:
|
|
|
4483
4881
|
program2.addCommand(createUsersCommand());
|
|
4484
4882
|
program2.addCommand(createSettingsCommand());
|
|
4485
4883
|
program2.addCommand(createInvoicesCommand());
|
|
4884
|
+
program2.addCommand(createAdminCommand());
|
|
4486
4885
|
return program2;
|
|
4487
4886
|
}
|
|
4488
4887
|
|