@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.
Files changed (2) hide show
  1. package/dist/a8techads.js +452 -53
  2. 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/campaigns.ts
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, COLUMNS, opts.format);
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 COLUMNS2 = [
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, COLUMNS2, opts.format);
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 COLUMNS3 = [
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, COLUMNS3, opts.format);
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 COLUMNS4 = [
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, COLUMNS4, opts.format);
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 COLUMNS5 = [
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, COLUMNS5, opts.format);
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.2.0").addHelpText("after", `
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a8techads/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A8TechAds CLI — programmatic ad platform management",
5
5
  "type": "module",
6
6
  "bin": {