adapty 0.9.3 → 0.9.5

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/README.md CHANGED
@@ -90,6 +90,24 @@ adapty placements create --app UUID [flags]
90
90
  adapty placements update --app UUID PLACEMENT_ID [flags]
91
91
  ```
92
92
 
93
+ ### Segments
94
+
95
+ ```sh
96
+ adapty segments list --app UUID [--page N] [--page-size N]
97
+ adapty segments get --app UUID SEGMENT_ID # with filters, profile_count, refresh_in_progress
98
+ adapty segments create --app UUID --title "US installs" --filter ip_country:IN:US --filter install_date:>=:2026-01-01
99
+ adapty segments update --app UUID SEGMENT_ID --title "..." --filter campaign:IN:2144520245
100
+ ```
101
+
102
+ A filter is `field:OPERATOR:value[,value...]`, or `field:IS NULL` / `field:IS NOT NULL` with no value. Operators:
103
+ `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `>`, `>=`, `<`, `<=`, `BETWEEN`. Numeric fields (`age`,
104
+ `total_revenue_usd`, `@float` custom attributes) take the plain number — `age:>:18` — the server converts it. For
105
+ anything the short form cannot express pass one JSON object: `--filter '{"field_name":"age","operator":">","value_list":[18]}'`.
106
+ The server validates field names, operators and value types and reports one error per field.
107
+ `update` replaces the whole segment — pass every filter you want to keep. Segments
108
+ built from Apple Search Ads entities are easier to create with `adapty asa segments create`, which resolves the
109
+ Apple ids and names the segment the way the dashboard does (see [Apple Search Ads](#apple-search-ads)).
110
+
93
111
  ### Flows
94
112
 
95
113
  ```sh
@@ -299,6 +317,28 @@ adapty asa keywords list --ad-group AD_GROUP_UUID --ad-group OTHER_UUID # repe
299
317
  adapty asa creatives list --app APP_UUID
300
318
  ```
301
319
 
320
+ #### Segments from Apple Search Ads entities
321
+
322
+ The Ads Manager "create segment" button, as commands. Each one builds an Adapty segment with a single
323
+ attribution filter over the Apple ids of the chosen entities and names it `[ASA] Campaign: <name>` (or the ad
324
+ group / keyword wording), so the dashboard recognises it as its own. These commands write through the Developer
325
+ API, which is why they are the one place in the `asa` topic that takes `--app`: the Adapty app the segment
326
+ belongs to, from `adapty apps list`.
327
+
328
+ ```sh
329
+ adapty asa segments create --app APP_UUID --campaign CAMPAIGN_UUID
330
+ adapty asa segments create --app APP_UUID --campaign UUID_1 --campaign UUID_2 --title "[ASA] Brand campaigns"
331
+ adapty asa segments create --app APP_UUID --ad-group AD_GROUP_UUID
332
+ adapty asa segments create --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID --keyword OTHER_UUID
333
+ adapty asa segments update SEGMENT_UUID --app APP_UUID --campaign CAMPAIGN_UUID # keeps title and description
334
+ adapty asa segments list --app APP_UUID [--campaign CAMPAIGN_UUID | --ad-group AD_GROUP_UUID]
335
+ ```
336
+
337
+ One kind of entity per call. `--keyword` needs exactly one `--ad-group` to look the keywords up in. `list` shows
338
+ the `[ASA]`-titled segments; with `--campaign` or `--ad-group` it keeps only those whose filter carries that
339
+ entity, so a script can check for an existing segment before creating one. Any other filter shape goes through
340
+ `adapty segments create`.
341
+
302
342
  `--campaign-group`, `--app`, `--campaign`, `--ad-group` are repeatable and take the UUIDs printed by the
303
343
  matching list command; `--search` matches names case-insensitively. Each list accepts only the filters that
304
344
  make sense for it: `--ad-group` starts at keywords, negative keywords, search terms and ads, `--status` is
@@ -365,8 +405,14 @@ adapty asa negative-keywords add --ad-group UUID --text free
365
405
  adapty asa negative-keywords add --campaign UUID [--all-ad-groups] --text free
366
406
 
367
407
  adapty asa search-terms list [--date-from ... --date-to ...]
408
+
409
+ adapty asa change-history list --campaign-group UUID [--date-from ... --date-to ...] [--entity-type Campaign --event-type UPDATE]
410
+ adapty asa change-history list --campaign-group UUID --fields # flat rows: field, old values, new values (page size 10)
411
+ adapty asa change-history get Campaign.444555666.TXN_ID --campaign-group UUID # detail_id comes from the list
368
412
  ```
369
413
 
414
+ Change history is read live from Apple (proxied from the Apple Ads Platform API; Apple keeps 6 months). Its id filters (`--campaign`, `--ad-group`, `--entity-id`, `--user`, `--txn`) take Apple's own ids, not UUIDs; `modified_by` is an Apple user id, not an email.
415
+
370
416
  Product pages and rule-based automations:
371
417
 
372
418
  ```sh
@@ -0,0 +1,20 @@
1
+ /** One filter of a segment body, as the developer API takes it. */
2
+ export type SegmentFilterInput = {
3
+ field_name: string;
4
+ operator: string;
5
+ value_list: unknown[];
6
+ };
7
+ /**
8
+ * Values stay strings on the short form: the server turns them into numbers for numeric fields. The JSON form is
9
+ * for typed or nested values the short form cannot express.
10
+ */
11
+ export declare function parseSegmentFilter(input: string): SegmentFilterInput;
12
+ /** Spread into a write command: `static flags = { ...appIdFlag, ...segmentWriteFlags }`. */
13
+ export declare const segmentWriteFlags: {
14
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
16
+ title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ export declare function segmentFilters(flags: {
19
+ filter?: string[] | undefined;
20
+ }): SegmentFilterInput[];
@@ -0,0 +1,61 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { usageError } from '../errors.js';
3
+ const FILTER_USAGE = 'Filters are written as field:OPERATOR:value[,value...] (e.g. campaign:IN:2144520245), as field:IS NULL, '
4
+ + 'or as one JSON object {"field_name": ..., "operator": ..., "value_list": [...]}.';
5
+ const EXISTENCE_OPERATORS = new Set(['IS NOT NULL', 'IS NULL']);
6
+ function parseJsonSegmentFilter(raw) {
7
+ let parsed;
8
+ try {
9
+ parsed = JSON.parse(raw);
10
+ }
11
+ catch {
12
+ throw usageError(`The filter is not valid JSON. ${FILTER_USAGE}`);
13
+ }
14
+ const { field_name: fieldName, operator, value_list: valueList } = (parsed ?? {});
15
+ if (typeof fieldName !== 'string' || typeof operator !== 'string' || !Array.isArray(valueList)) {
16
+ throw usageError(`A JSON filter needs field_name, operator and value_list. ${FILTER_USAGE}`);
17
+ }
18
+ return { field_name: fieldName, operator, value_list: valueList };
19
+ }
20
+ /**
21
+ * Values stay strings on the short form: the server turns them into numbers for numeric fields. The JSON form is
22
+ * for typed or nested values the short form cannot express.
23
+ */
24
+ export function parseSegmentFilter(input) {
25
+ const trimmed = input.trim();
26
+ if (trimmed.startsWith('{')) {
27
+ return parseJsonSegmentFilter(trimmed);
28
+ }
29
+ const [fieldName, rawOperator, ...rest] = trimmed.split(':');
30
+ const operator = rawOperator?.trim() ?? '';
31
+ if (!fieldName || !operator) {
32
+ throw usageError(FILTER_USAGE);
33
+ }
34
+ const valueList = rest.join(':').split(',').map(value => value.trim()).filter(value => value !== '');
35
+ if (EXISTENCE_OPERATORS.has(operator)) {
36
+ if (valueList.length > 0) {
37
+ throw usageError(`${operator} takes no values. ${FILTER_USAGE}`);
38
+ }
39
+ return { field_name: fieldName, operator, value_list: [] };
40
+ }
41
+ if (valueList.length === 0) {
42
+ throw usageError(`A filter needs at least one value. ${FILTER_USAGE}`);
43
+ }
44
+ return { field_name: fieldName, operator, value_list: valueList };
45
+ }
46
+ /** Spread into a write command: `static flags = { ...appIdFlag, ...segmentWriteFlags }`. */
47
+ export const segmentWriteFlags = {
48
+ description: Flags.string({ description: 'Segment description' }),
49
+ filter: Flags.string({
50
+ description: 'Filter as field:OPERATOR:value[,value...] (e.g. campaign:IN:2144520245), field:IS NULL, or a JSON '
51
+ + 'object {"field_name","operator","value_list"}; repeatable',
52
+ multiple: true,
53
+ // oclif keeps a string flag a string: the parsed filter travels as JSON and segmentFilters unpacks it
54
+ parse: (input) => Promise.resolve(JSON.stringify(parseSegmentFilter(input))),
55
+ required: true,
56
+ }),
57
+ title: Flags.string({ description: 'Segment title', required: true }),
58
+ };
59
+ export function segmentFilters(flags) {
60
+ return (flags.filter ?? []).map(raw => JSON.parse(raw));
61
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaChangeHistoryDetailDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaChangeHistoryGet extends Command {
4
+ static args: {
5
+ detail_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ static flags: {
11
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<AsaChangeHistoryDetailDTO>;
14
+ }
@@ -0,0 +1,39 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { isValidUuid } from '../../../lib/flags.js';
4
+ import { printResponse } from '../../../lib/output.js';
5
+ const DETAIL_ID_REGEX = /^[A-Za-z]+\.[\w-]+\.[\w-]+$/;
6
+ export default class AsaChangeHistoryGet extends Command {
7
+ static args = {
8
+ detail_id: Args.string({
9
+ description: 'Detail ID from asa change-history list (EntityType.entityId.txnId)',
10
+ required: true,
11
+ }),
12
+ };
13
+ static description = 'Field-level before/after of one change (proxied from the Apple Ads Platform API)';
14
+ static enableJsonFlag = true;
15
+ static examples = ['<%= config.bin %> asa change-history get Campaign.444555666.TXN_ID --campaign-group <uuid>'];
16
+ static flags = {
17
+ 'campaign-group': Flags.string({
18
+ description: 'Organization the change belongs to (UUID; take campaign_group_id from the list row)',
19
+ required: true,
20
+ }),
21
+ };
22
+ async run() {
23
+ const { args, flags } = await this.parse(AsaChangeHistoryGet);
24
+ if (!DETAIL_ID_REGEX.test(args.detail_id)) {
25
+ this.error('Invalid detail ID format. Take it from `adapty asa change-history list` (EntityType.entityId.txnId).', {
26
+ exit: 2,
27
+ });
28
+ }
29
+ if (!isValidUuid(flags['campaign-group'])) {
30
+ this.error('Invalid campaign group ID format. Run `adapty asa orgs list` to find it.', { exit: 2 });
31
+ }
32
+ const client = await createAsaClient(this);
33
+ const result = await client.get(`/change-history/${args.detail_id}`, {
34
+ campaign_group_id: flags['campaign-group'],
35
+ });
36
+ printResponse(result, this.log.bind(this));
37
+ return result;
38
+ }
39
+ }
@@ -0,0 +1,27 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaChangeHistoryEventDTO, AsaChangeHistoryFieldEventDTO } from '../../../lib/asa-schemas.js';
3
+ import type { PaginatedResponse } from '../../../lib/flags.js';
4
+ type ChangeHistoryPage = PaginatedResponse<AsaChangeHistoryEventDTO> | PaginatedResponse<AsaChangeHistoryFieldEventDTO>;
5
+ export default class AsaChangeHistoryList extends Command {
6
+ static description: string;
7
+ static enableJsonFlag: boolean;
8
+ static examples: string[];
9
+ static flags: {
10
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'date-to': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'entity-id': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'entity-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'event-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ fields: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ txn: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ user: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'user-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
23
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
24
+ };
25
+ run(): Promise<ChangeHistoryPage>;
26
+ }
27
+ export {};
@@ -0,0 +1,102 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { asaPaginationFlags, parseDate, periodParams } from '../../../lib/asa-flags.js';
4
+ import { isValidUuid, paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ const FIELDS_MAX_PAGE_SIZE = 10;
7
+ const MAX_SPAN_DAYS = 14;
8
+ const MS_PER_DAY = 86_400_000;
9
+ export default class AsaChangeHistoryList extends Command {
10
+ static description = 'Apple Ads change history of one organization, read live from Apple (proxied from the Apple Ads Platform API)';
11
+ static enableJsonFlag = true;
12
+ static examples = [
13
+ '<%= config.bin %> asa change-history list --campaign-group <uuid>',
14
+ '<%= config.bin %> asa change-history list --campaign-group <uuid> --date-from 2026-09-01 --entity-type Campaign --event-type UPDATE',
15
+ '<%= config.bin %> asa change-history list --campaign-group <uuid> --fields',
16
+ ];
17
+ static flags = {
18
+ ...asaPaginationFlags,
19
+ 'ad-group': Flags.string({
20
+ description: 'Keep only changes inside these Apple ad group ids (not UUIDs); repeatable',
21
+ multiple: true,
22
+ }),
23
+ 'campaign': Flags.string({
24
+ description: 'Keep only changes inside these Apple campaign ids (not UUIDs); repeatable',
25
+ multiple: true,
26
+ }),
27
+ 'campaign-group': Flags.string({
28
+ description: 'Organization to read (UUID from asa orgs list); one organization per call',
29
+ required: true,
30
+ }),
31
+ 'date-from': Flags.string({
32
+ description: 'Start of the period (YYYY-MM-DD), defaults to 7 days ago; at most 14 days per call, Apple keeps 6 months',
33
+ parse: parseDate,
34
+ }),
35
+ 'date-to': Flags.string({
36
+ description: 'End of the period (YYYY-MM-DD), defaults to today',
37
+ parse: parseDate,
38
+ }),
39
+ 'entity-id': Flags.string({
40
+ description: 'Keep only changes of these Apple entity ids (not UUIDs); repeatable',
41
+ multiple: true,
42
+ }),
43
+ 'entity-type': Flags.string({
44
+ description: 'Keep only changes of this entity type; repeatable. Default is the campaign domain (Campaign, AdGroup, Keyword, NegativeKeyword, Ad, Creative, LocationGroup); Org and AdAccount cannot be mixed with it',
45
+ multiple: true,
46
+ options: ['Ad', 'AdAccount', 'AdGroup', 'Campaign', 'Creative', 'Keyword', 'LocationGroup', 'NegativeKeyword', 'Org'],
47
+ }),
48
+ 'event-type': Flags.string({
49
+ description: 'Keep only this operation; repeatable',
50
+ multiple: true,
51
+ options: ['CREATE', 'DELETE', 'UPDATE'],
52
+ }),
53
+ 'fields': Flags.boolean({
54
+ description: `Expand into flat field-level rows (field, old values, new values); page size defaults to ${FIELDS_MAX_PAGE_SIZE} and may not exceed it`,
55
+ }),
56
+ 'txn': Flags.string({ description: 'Keep only these Apple transaction ids; repeatable', multiple: true }),
57
+ 'user': Flags.string({
58
+ description: 'Keep only changes made by these Apple user ids (not emails); repeatable',
59
+ multiple: true,
60
+ }),
61
+ 'user-type': Flags.string({
62
+ description: 'Keep only changes by this actor category; repeatable',
63
+ multiple: true,
64
+ options: ['APPLE_SUPPORT', 'CUSTOMER', 'CUSTOMER_API'],
65
+ }),
66
+ };
67
+ async run() {
68
+ const { flags } = await this.parse(AsaChangeHistoryList);
69
+ const isPageSizeGiven = this.argv.some(arg => arg === '--page-size' || arg.startsWith('--page-size='));
70
+ if (flags.fields && isPageSizeGiven && flags['page-size'] > FIELDS_MAX_PAGE_SIZE) {
71
+ this.error(`--fields allows --page-size up to ${FIELDS_MAX_PAGE_SIZE}.`, { exit: 2 });
72
+ }
73
+ if (!isValidUuid(flags['campaign-group'])) {
74
+ this.error('Invalid campaign group ID format. Run `adapty asa orgs list` to find it.', { exit: 2 });
75
+ }
76
+ const dateFrom = flags['date-from'];
77
+ const dateTo = flags['date-to'];
78
+ if (dateFrom !== undefined
79
+ && dateTo !== undefined
80
+ && (Date.parse(dateTo) - Date.parse(dateFrom)) / MS_PER_DAY >= MAX_SPAN_DAYS) {
81
+ this.error(`--date-from/--date-to may span at most ${MAX_SPAN_DAYS} days.`, { exit: 2 });
82
+ }
83
+ const client = await createAsaClient(this);
84
+ const result = await client.get('/change-history', {
85
+ ...paginationParams(flags),
86
+ ...(flags.fields && !isPageSizeGiven ? { 'page[size]': String(FIELDS_MAX_PAGE_SIZE) } : {}),
87
+ ...periodParams(flags),
88
+ ad_group_id: flags['ad-group'],
89
+ campaign_group_id: flags['campaign-group'],
90
+ campaign_id: flags.campaign,
91
+ entity_id: flags['entity-id'],
92
+ entity_type: flags['entity-type'],
93
+ event_type: flags['event-type'],
94
+ fields: flags.fields ? 'true' : undefined,
95
+ txn_id: flags.txn,
96
+ user_id: flags.user,
97
+ user_type: flags['user-type'],
98
+ });
99
+ printList(result.data, this.log.bind(this), result.meta.pagination);
100
+ return result;
101
+ }
102
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { SegmentDetailDTO } from '../../../lib/api-schemas.js';
3
+ export default class AsaSegmentsCreate extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<SegmentDetailDTO>;
17
+ }
@@ -0,0 +1,53 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { appIdFlag } from '../../../cli/input/app.js';
3
+ import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
4
+ import { ASA_SEGMENT_FIELD, asaSegmentSourceFlags, buildAsaSegmentNaming, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
5
+ import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
6
+ import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
7
+ import { printResponse } from '../../../lib/output.js';
8
+ export default class AsaSegmentsCreate extends Command {
9
+ static description = `Create an Adapty segment from Apple Search Ads campaigns, ad groups or keywords
10
+
11
+ Builds the same segment the Ads Manager "create segment" button does: one attribution filter over the Apple ids
12
+ of the chosen entities, named "[ASA] Campaign: <name>" unless --title is passed. Pass one kind of entity per call;
13
+ --keyword needs exactly one --ad-group to look the keywords up in. --app is the Adapty app the segment belongs to
14
+ (see \`adapty apps list\`).`;
15
+ static enableJsonFlag = true;
16
+ static examples = [
17
+ '<%= config.bin %> asa segments create --app APP_UUID --campaign CAMPAIGN_UUID',
18
+ '<%= config.bin %> asa segments create --app APP_UUID --campaign UUID_1 --campaign UUID_2 --title "[ASA] Brand campaigns"',
19
+ '<%= config.bin %> asa segments create --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID',
20
+ ];
21
+ static flags = {
22
+ ...appIdFlag,
23
+ ...asaSegmentSourceFlags,
24
+ ...confirmFlags,
25
+ description: Flags.string({ description: 'Segment description; defaults to the dashboard wording' }),
26
+ title: Flags.string({ description: 'Segment title; defaults to the dashboard wording, e.g. "[ASA] Campaign: <name>"' }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(AsaSegmentsCreate);
30
+ let selection;
31
+ try {
32
+ selection = resolveAsaSegmentSource(flags);
33
+ }
34
+ catch (error) {
35
+ this.error(error.message, { exit: 2 });
36
+ }
37
+ const asaClient = await createAsaClient(this);
38
+ const entities = await fetchAsaSegmentEntities(asaClient, selection);
39
+ const naming = buildAsaSegmentNaming(selection.source, entities);
40
+ const body = {
41
+ description: flags.description ?? naming.description,
42
+ filters: [{ field_name: ASA_SEGMENT_FIELD[selection.source], operator: 'IN', value_list: entities.map(entity => entity.id) }],
43
+ title: flags.title ?? naming.title,
44
+ };
45
+ const path = `/apps/${flags.app}/segments`;
46
+ await confirmMutation(this, { body, method: 'POST', path: `${path}/`, summary: `Create segment ${body.title}` }, flags.yes);
47
+ const client = await createAuthenticatedClient(this.config);
48
+ const result = await client.post(path, body);
49
+ this.log('Segment created!');
50
+ printResponse(result, this.log.bind(this));
51
+ return result;
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { SegmentDTO } from '../../../lib/api-schemas.js';
3
+ export default class AsaSegmentsList extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ campaign: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<SegmentDTO[]>;
13
+ /** The Apple id the segments must filter on, resolved from the ASA entity the user named. */
14
+ private wantedFilterValue;
15
+ private keepMatching;
16
+ }
@@ -0,0 +1,59 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { appIdFlag } from '../../../cli/input/app.js';
3
+ import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
4
+ import { ASA_SEGMENT_FIELD, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
5
+ import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
6
+ import { isValidUuid } from '../../../lib/flags.js';
7
+ import { printList } from '../../../lib/output.js';
8
+ const ASA_TITLE_PREFIX = '[ASA]';
9
+ const parseId = (input) => (isValidUuid(input)
10
+ ? Promise.resolve(input)
11
+ : Promise.reject(new Error('Ids are the UUIDs printed by the matching list command.')));
12
+ export default class AsaSegmentsList extends Command {
13
+ static description = `List the segments created from Apple Search Ads entities
14
+
15
+ Segments whose title starts with "[ASA]", the dashboard's own marker. With --campaign or --ad-group only the
16
+ segments whose filter carries that entity's Apple id are kept, so a script can tell whether a campaign already
17
+ has its segment before creating one.`;
18
+ static enableJsonFlag = true;
19
+ static examples = [
20
+ '<%= config.bin %> asa segments list --app APP_UUID',
21
+ '<%= config.bin %> asa segments list --app APP_UUID --campaign CAMPAIGN_UUID',
22
+ ];
23
+ static flags = {
24
+ ...appIdFlag,
25
+ 'ad-group': Flags.string({ description: 'Keep only segments built from this ad group (UUID)', parse: parseId }),
26
+ 'campaign': Flags.string({ description: 'Keep only segments built from this campaign (UUID)', parse: parseId }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(AsaSegmentsList);
30
+ const wanted = await this.wantedFilterValue(flags);
31
+ const client = await createAuthenticatedClient(this.config);
32
+ const page = await client.get(`/apps/${flags.app}/segments`, { 'page[size]': '100' });
33
+ const asaSegments = page.data.filter(segment => segment.title.startsWith(ASA_TITLE_PREFIX));
34
+ const result = wanted === undefined
35
+ ? asaSegments
36
+ : await this.keepMatching(client, flags.app, asaSegments, wanted);
37
+ printList(result, this.log.bind(this));
38
+ return result;
39
+ }
40
+ /** The Apple id the segments must filter on, resolved from the ASA entity the user named. */
41
+ async wantedFilterValue(flags) {
42
+ if (flags.campaign === undefined && flags['ad-group'] === undefined) {
43
+ return undefined;
44
+ }
45
+ if (flags.campaign !== undefined && flags['ad-group'] !== undefined) {
46
+ this.error('Pass either --campaign or --ad-group, not both.', { exit: 2 });
47
+ }
48
+ const selection = resolveAsaSegmentSource({
49
+ 'ad-group': flags['ad-group'] === undefined ? undefined : [flags['ad-group']],
50
+ 'campaign': flags.campaign === undefined ? undefined : [flags.campaign],
51
+ });
52
+ const [entity] = await fetchAsaSegmentEntities(await createAsaClient(this), selection);
53
+ return { field: ASA_SEGMENT_FIELD[selection.source], value: entity?.id };
54
+ }
55
+ async keepMatching(client, app, segments, wanted) {
56
+ const details = await Promise.all(segments.map(segment => client.get(`/apps/${app}/segments/${segment.id}`)));
57
+ return details.filter(detail => detail.filters.some(filter => filter.field_name === wanted.field && filter.value_list.map(String).includes(wanted.value ?? '')));
58
+ }
59
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { SegmentDetailDTO } from '../../../lib/api-schemas.js';
3
+ export default class AsaSegmentsUpdate extends Command {
4
+ static args: {
5
+ segment_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ static flags: {
11
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
+ };
19
+ run(): Promise<SegmentDetailDTO>;
20
+ }
@@ -0,0 +1,60 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { appIdFlag } from '../../../cli/input/app.js';
3
+ import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
4
+ import { ASA_SEGMENT_FIELD, asaSegmentSourceFlags, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
5
+ import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
6
+ import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
7
+ import { isValidUuid } from '../../../lib/flags.js';
8
+ import { printResponse } from '../../../lib/output.js';
9
+ export default class AsaSegmentsUpdate extends Command {
10
+ static args = {
11
+ segment_id: Args.string({ description: 'Segment ID (UUID)', required: true }),
12
+ };
13
+ static description = `Point an existing segment at other Apple Search Ads campaigns, ad groups or keywords
14
+
15
+ Replaces the segment's filter with one attribution filter over the chosen entities, as the dashboard does.
16
+ Title and description stay as stored unless --title / --description are passed.`;
17
+ static enableJsonFlag = true;
18
+ static examples = [
19
+ '<%= config.bin %> asa segments update SEGMENT_UUID --app APP_UUID --campaign CAMPAIGN_UUID --campaign OTHER_UUID',
20
+ '<%= config.bin %> asa segments update SEGMENT_UUID --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID --title "Top keywords"',
21
+ ];
22
+ static flags = {
23
+ ...appIdFlag,
24
+ ...asaSegmentSourceFlags,
25
+ ...confirmFlags,
26
+ description: Flags.string({ description: 'New description; omit to keep the stored one' }),
27
+ title: Flags.string({ description: 'New title; omit to keep the stored one' }),
28
+ };
29
+ async run() {
30
+ const { args, flags } = await this.parse(AsaSegmentsUpdate);
31
+ if (!isValidUuid(args.segment_id)) {
32
+ this.error('Invalid segment ID format.', { exit: 2 });
33
+ }
34
+ let selection;
35
+ try {
36
+ selection = resolveAsaSegmentSource(flags);
37
+ }
38
+ catch (error) {
39
+ this.error(error.message, { exit: 2 });
40
+ }
41
+ const asaClient = await createAsaClient(this);
42
+ const entities = await fetchAsaSegmentEntities(asaClient, selection);
43
+ const client = await createAuthenticatedClient(this.config);
44
+ const path = `/apps/${flags.app}/segments/${args.segment_id}`;
45
+ // PUT replaces the whole segment, so the stored title and description are read back unless overridden
46
+ const overridesBoth = flags.title !== undefined && flags.description !== undefined;
47
+ const stored = overridesBoth ? undefined : await client.get(path);
48
+ const body = {
49
+ description: flags.description ?? stored?.description ?? undefined,
50
+ filters: [{ field_name: ASA_SEGMENT_FIELD[selection.source], operator: 'IN', value_list: entities.map(entity => entity.id) }],
51
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- stored is read whenever title is missing
52
+ title: flags.title ?? stored.title,
53
+ };
54
+ await confirmMutation(this, { body, method: 'PUT', path: `${path}/`, summary: `Replace filters of segment ${args.segment_id}` }, flags.yes);
55
+ const result = await client.put(path, body);
56
+ this.log('Segment updated!');
57
+ printResponse(result, this.log.bind(this));
58
+ return result;
59
+ }
60
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { SegmentDetailDTO } from '../../lib/api-schemas.js';
3
+ export default class SegmentsCreate extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
10
+ title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<SegmentDetailDTO>;
14
+ }
@@ -0,0 +1,30 @@
1
+ import { Command } from '@oclif/core';
2
+ import { appIdFlag } from '../../cli/input/app.js';
3
+ import { segmentFilters, segmentWriteFlags } from '../../cli/input/segment.js';
4
+ import { createAuthenticatedClient } from '../../lib/client-from-config.js';
5
+ import { printResponse } from '../../lib/output.js';
6
+ export default class SegmentsCreate extends Command {
7
+ static description = `Create a segment from filters over profile fields
8
+
9
+ Filters are written as field:OPERATOR:value[,value...] and validated by the server. Apple Search Ads
10
+ attribution uses campaign, ad_group and creative (a keyword) with Apple ids as values; build those from
11
+ ASA entities with \`asa segments create\` instead of copying ids by hand.`;
12
+ static enableJsonFlag = true;
13
+ static examples = [
14
+ '<%= config.bin %> segments create --app UUID --title "[ASA] Campaign: Brand" --filter campaign:IN:2144520245',
15
+ '<%= config.bin %> segments create --app UUID --title "US installs" --filter ip_country:IN:US --filter install_date:>=:2026-01-01',
16
+ ];
17
+ static flags = { ...appIdFlag, ...segmentWriteFlags };
18
+ async run() {
19
+ const { flags } = await this.parse(SegmentsCreate);
20
+ const body = {
21
+ description: flags.description,
22
+ filters: segmentFilters(flags),
23
+ title: flags.title,
24
+ };
25
+ const client = await createAuthenticatedClient(this.config);
26
+ const result = await client.post(`/apps/${flags.app}/segments`, body);
27
+ printResponse(result, this.log.bind(this));
28
+ return result;
29
+ }
30
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { SegmentDetailDTO } from '../../lib/api-schemas.js';
3
+ export default class SegmentsUpdate extends Command {
4
+ static args: {
5
+ segment_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ static flags: {
11
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
13
+ title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<SegmentDetailDTO>;
17
+ }