adapty 0.9.4 → 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
@@ -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,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
+ }
@@ -0,0 +1,35 @@
1
+ import { Args, 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 { isValidUuid } from '../../lib/flags.js';
6
+ import { printResponse } from '../../lib/output.js';
7
+ export default class SegmentsUpdate extends Command {
8
+ static args = {
9
+ segment_id: Args.string({ description: 'Segment ID (UUID)', required: true }),
10
+ };
11
+ static description = `Replace a segment's title, description and filters
12
+
13
+ The server takes the whole segment, as the dashboard sends it: pass every filter you want to keep, not only
14
+ the changed one. Read the current ones with \`segments get\`.`;
15
+ static enableJsonFlag = true;
16
+ static examples = [
17
+ '<%= config.bin %> segments update --app UUID 550e8400-e29b-41d4-a716-446655440000 --title "[ASA] Campaign: Brand" --filter campaign:IN:2144520245',
18
+ ];
19
+ static flags = { ...appIdFlag, ...segmentWriteFlags };
20
+ async run() {
21
+ const { args, flags } = await this.parse(SegmentsUpdate);
22
+ if (!isValidUuid(args.segment_id)) {
23
+ this.error('Invalid segment ID format.', { exit: 2 });
24
+ }
25
+ const body = {
26
+ description: flags.description,
27
+ filters: segmentFilters(flags),
28
+ title: flags.title,
29
+ };
30
+ const client = await createAuthenticatedClient(this.config);
31
+ const result = await client.put(`/apps/${flags.app}/segments/${args.segment_id}`, body);
32
+ printResponse(result, this.log.bind(this));
33
+ return result;
34
+ }
35
+ }
@@ -136,6 +136,30 @@ export type SegmentDTO = {
136
136
  id: string;
137
137
  title: string;
138
138
  };
139
+ export type SegmentFilterDTO = {
140
+ field_name: string;
141
+ operator: string;
142
+ segment_filter_id: null | string;
143
+ value_list: unknown[];
144
+ };
145
+ /** What `segments get`, `create` and `update` return; the list stays `SegmentDTO`. */
146
+ export type SegmentDetailDTO = SegmentDTO & {
147
+ created_at: string;
148
+ filters: SegmentFilterDTO[];
149
+ profile_count: number;
150
+ refresh_in_progress: boolean;
151
+ updated_at: string;
152
+ };
153
+ export type SegmentFilterInput = {
154
+ field_name: string;
155
+ operator: string;
156
+ value_list: unknown[];
157
+ };
158
+ export type SegmentWriteRequestDTO = {
159
+ description?: string | undefined;
160
+ filters: SegmentFilterInput[];
161
+ title: string;
162
+ };
139
163
  export type PlacementPaywallAudienceEntryDTO = {
140
164
  content_type: 'paywall';
141
165
  paywall_id: string;
@@ -1,5 +1,6 @@
1
1
  import { ApiClient } from './api-client.js';
2
2
  import type { QueryParams } from './api-client.js';
3
+ import type { AsaSegmentEntity, AsaSegmentSelection } from './asa-flags.js';
3
4
  import type { Config } from '@oclif/core';
4
5
  export declare const ASA_API_URL = "https://api-asa-admin.adapty.io/api/v1/cli";
5
6
  export declare const ASA_API_URL_ENV_VAR = "ADAPTY_ASA_API_URL";
@@ -19,3 +20,5 @@ export type AsaWriteOutcome<T> = {
19
20
  };
20
21
  export declare function asaWrite<T>(client: ApiClient, method: 'post' | 'put', path: string, opts?: AsaWriteOptions): Promise<AsaWriteOutcome<T>>;
21
22
  export declare function noteReplay(replayed: boolean, log: (msg: string) => void): void;
23
+ /** The ASA entities behind a segment source, with the Apple ids the portal filters on. */
24
+ export declare function fetchAsaSegmentEntities(client: ApiClient, selection: AsaSegmentSelection): Promise<AsaSegmentEntity[]>;
@@ -49,3 +49,29 @@ export function noteReplay(replayed, log) {
49
49
  log('Already applied earlier — showing the stored result.');
50
50
  }
51
51
  }
52
+ /** The ASA entities behind a segment source, with the Apple ids the portal filters on. */
53
+ export async function fetchAsaSegmentEntities(client, selection) {
54
+ if (selection.source === 'campaign') {
55
+ const campaigns = await Promise.all(selection.ids.map(id => client.get(`/campaigns/${id}`)));
56
+ return campaigns.map(campaign => ({ id: String(campaign.campaign_id), name: campaign.name }));
57
+ }
58
+ if (selection.source === 'ad-group') {
59
+ const adGroups = await Promise.all(selection.ids.map(id => client.get(`/ad-groups/${id}`)));
60
+ return adGroups.map(adGroup => ({ id: String(adGroup.ad_group_id), name: adGroup.name }));
61
+ }
62
+ // No single-keyword read exists; the ad group is small, so one scoped page covers the lookup
63
+ const page = await client.get('/keywords', {
64
+ 'ad_group_id': selection.scopeAdGroup,
65
+ 'page[size]': '1000',
66
+ });
67
+ const byId = new Map(page.data.map(keyword => [keyword.internal_id, keyword]));
68
+ const missing = selection.ids.filter(id => !byId.has(id));
69
+ if (missing.length > 0) {
70
+ throw new Error(`Keyword(s) not found in ad group ${selection.scopeAdGroup}: ${missing.join(', ')}`);
71
+ }
72
+ return selection.ids.map((id) => {
73
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- membership checked above
74
+ const keyword = byId.get(id);
75
+ return { id: String(keyword.keyword_id), name: keyword.text };
76
+ });
77
+ }
@@ -131,4 +131,35 @@ type BulkOutcome = {
131
131
  kind: string;
132
132
  };
133
133
  export declare function reportBulkOutcome({ applied, errors, isValidationFailure, kind }: BulkOutcome, log: (msg: string) => void): void;
134
+ export type AsaSegmentSource = 'ad-group' | 'campaign' | 'keyword';
135
+ export declare const asaSegmentSourceFlags: {
136
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
137
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
138
+ keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
139
+ };
140
+ export type AsaSegmentSourceFlags = {
141
+ 'ad-group'?: string[] | undefined;
142
+ 'campaign'?: string[] | undefined;
143
+ 'keyword'?: string[] | undefined;
144
+ };
145
+ /**
146
+ * One source kind per segment, as in the Ads Manager modal. Keywords are looked up inside one ad group, so
147
+ * `--keyword` borrows `--ad-group` as its scope instead of competing with it.
148
+ */
149
+ export type AsaSegmentSelection = {
150
+ ids: string[];
151
+ scopeAdGroup: string | undefined;
152
+ source: AsaSegmentSource;
153
+ };
154
+ export declare function resolveAsaSegmentSource(flags: AsaSegmentSourceFlags): AsaSegmentSelection;
155
+ export declare const ASA_SEGMENT_FIELD: Record<AsaSegmentSource, string>;
156
+ export type AsaSegmentEntity = {
157
+ id: string;
158
+ name: string;
159
+ };
160
+ /** The dashboard's own title and description for an Ads Manager segment, so it recognises the segment as its own. */
161
+ export declare function buildAsaSegmentNaming(source: AsaSegmentSource, entities: AsaSegmentEntity[]): {
162
+ description: string;
163
+ title: string;
164
+ };
134
165
  export {};