adapty 0.3.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 (84) hide show
  1. package/README.md +134 -5
  2. package/dist/commands/asa/ad-groups/create.d.ts +22 -0
  3. package/dist/commands/asa/ad-groups/create.js +54 -0
  4. package/dist/commands/asa/ad-groups/get.d.ts +11 -0
  5. package/dist/commands/asa/ad-groups/get.js +21 -0
  6. package/dist/commands/asa/ad-groups/list.d.ts +18 -0
  7. package/dist/commands/asa/ad-groups/list.js +24 -0
  8. package/dist/commands/asa/ad-groups/update.d.ts +23 -0
  9. package/dist/commands/asa/ad-groups/update.js +62 -0
  10. package/dist/commands/asa/ads/create.d.ts +16 -0
  11. package/dist/commands/asa/ads/create.js +41 -0
  12. package/dist/commands/asa/ads/get.d.ts +11 -0
  13. package/dist/commands/asa/ads/get.js +21 -0
  14. package/dist/commands/asa/ads/list.d.ts +18 -0
  15. package/dist/commands/asa/ads/list.js +24 -0
  16. package/dist/commands/asa/ads/update.d.ts +17 -0
  17. package/dist/commands/asa/ads/update.js +44 -0
  18. package/dist/commands/asa/apps/list.d.ts +13 -0
  19. package/dist/commands/asa/apps/list.js +18 -0
  20. package/dist/commands/asa/automations/create.d.ts +16 -0
  21. package/dist/commands/asa/automations/create.js +60 -0
  22. package/dist/commands/asa/automations/get.d.ts +11 -0
  23. package/dist/commands/asa/automations/get.js +21 -0
  24. package/dist/commands/asa/automations/list.d.ts +13 -0
  25. package/dist/commands/asa/automations/list.js +18 -0
  26. package/dist/commands/asa/automations/run.d.ts +16 -0
  27. package/dist/commands/asa/automations/run.js +41 -0
  28. package/dist/commands/asa/automations/runs.d.ts +16 -0
  29. package/dist/commands/asa/automations/runs.js +24 -0
  30. package/dist/commands/asa/automations/update.d.ts +21 -0
  31. package/dist/commands/asa/automations/update.js +73 -0
  32. package/dist/commands/asa/campaigns/create.d.ts +25 -0
  33. package/dist/commands/asa/campaigns/create.js +67 -0
  34. package/dist/commands/asa/campaigns/get.d.ts +11 -0
  35. package/dist/commands/asa/campaigns/get.js +21 -0
  36. package/dist/commands/asa/campaigns/list.d.ts +17 -0
  37. package/dist/commands/asa/campaigns/list.js +24 -0
  38. package/dist/commands/asa/campaigns/update.d.ts +23 -0
  39. package/dist/commands/asa/campaigns/update.js +66 -0
  40. package/dist/commands/asa/competitors/summary.d.ts +11 -0
  41. package/dist/commands/asa/competitors/summary.js +56 -0
  42. package/dist/commands/asa/connect.d.ts +12 -0
  43. package/dist/commands/asa/connect.js +39 -0
  44. package/dist/commands/asa/creatives/list.d.ts +15 -0
  45. package/dist/commands/asa/creatives/list.js +24 -0
  46. package/dist/commands/asa/keywords/add.d.ts +20 -0
  47. package/dist/commands/asa/keywords/add.js +73 -0
  48. package/dist/commands/asa/keywords/list.d.ts +19 -0
  49. package/dist/commands/asa/keywords/list.js +24 -0
  50. package/dist/commands/asa/keywords/update.d.ts +21 -0
  51. package/dist/commands/asa/keywords/update.js +64 -0
  52. package/dist/commands/asa/metrics/index.d.ts +21 -0
  53. package/dist/commands/asa/metrics/index.js +68 -0
  54. package/dist/commands/asa/metrics/overview.d.ts +15 -0
  55. package/dist/commands/asa/metrics/overview.js +51 -0
  56. package/dist/commands/asa/negative-keywords/add.d.ts +18 -0
  57. package/dist/commands/asa/negative-keywords/add.js +70 -0
  58. package/dist/commands/asa/negative-keywords/list.d.ts +19 -0
  59. package/dist/commands/asa/negative-keywords/list.js +29 -0
  60. package/dist/commands/asa/orgs/list.d.ts +13 -0
  61. package/dist/commands/asa/orgs/list.js +18 -0
  62. package/dist/commands/asa/product-pages/list.d.ts +15 -0
  63. package/dist/commands/asa/product-pages/list.js +24 -0
  64. package/dist/commands/asa/product-pages/sync.d.ts +13 -0
  65. package/dist/commands/asa/product-pages/sync.js +28 -0
  66. package/dist/commands/asa/search-terms/list.d.ts +20 -0
  67. package/dist/commands/asa/search-terms/list.js +25 -0
  68. package/dist/commands/asa/whoami.d.ts +8 -0
  69. package/dist/commands/asa/whoami.js +22 -0
  70. package/dist/lib/api-client.d.ts +15 -3
  71. package/dist/lib/api-client.js +49 -17
  72. package/dist/lib/asa-client.d.ts +16 -0
  73. package/dist/lib/asa-client.js +46 -0
  74. package/dist/lib/asa-confirm.d.ts +18 -0
  75. package/dist/lib/asa-confirm.js +41 -0
  76. package/dist/lib/asa-flags.d.ts +88 -0
  77. package/dist/lib/asa-flags.js +137 -0
  78. package/dist/lib/asa-schemas.d.ts +287 -0
  79. package/dist/lib/asa-schemas.js +1 -0
  80. package/dist/lib/errors.d.ts +25 -2
  81. package/dist/lib/errors.js +48 -7
  82. package/dist/lib/output.js +45 -15
  83. package/oclif.manifest.json +2802 -157
  84. package/package.json +40 -1
package/README.md CHANGED
@@ -86,6 +86,131 @@ adapty access-levels create --app UUID [flags]
86
86
  adapty access-levels update --app UUID ACCESS_LEVEL_ID [flags]
87
87
  ```
88
88
 
89
+ ### Apple Search Ads
90
+
91
+ Apple Search Ads commands live under `adapty asa` and talk to the ASA service rather than the Developer
92
+ API. They take **no `--app`**: the scope is the company behind your token. A connected Apple Ads account and
93
+ an active Ads Manager subscription are required — `adapty asa whoami` tells you where you stand.
94
+
95
+ ```sh
96
+ adapty asa whoami # company, how access was granted, Apple connection state
97
+ adapty asa connect [--no-wait] # link an Apple Search Ads account
98
+ adapty asa apps list # apps promoted in Apple Search Ads
99
+ adapty asa orgs list # Apple Search Ads organizations
100
+ ```
101
+
102
+ Every list takes scope filters, and they narrow the query rather than the printed page — `asa keywords list`
103
+ unfiltered pages through the whole account, while one ad group is a handful of rows, so scope the read:
104
+
105
+ ```sh
106
+ adapty asa campaigns list --app APP_UUID --status PAUSED
107
+ adapty asa ad-groups list --campaign CAMPAIGN_UUID
108
+ adapty asa keywords list --ad-group AD_GROUP_UUID --status ACTIVE
109
+ adapty asa keywords list --ad-group AD_GROUP_UUID --ad-group OTHER_UUID # repeatable
110
+ adapty asa creatives list --app APP_UUID
111
+ ```
112
+
113
+ `--campaign-group`, `--app`, `--campaign`, `--ad-group` are repeatable and take the UUIDs printed by the
114
+ matching list command; `--search` matches names case-insensitively. Each list accepts only the filters that
115
+ make sense for it: `--ad-group` starts at keywords, negative keywords, search terms and ads, `--status` is
116
+ `ENABLED`/`PAUSED` everywhere except keywords, which are `ACTIVE`/`PAUSED`, and `asa ads list` has no `--app`
117
+ because ads hang off ad groups. An id belonging to another company simply matches nothing.
118
+
119
+ Campaign structure. These lists return metadata only — numbers come from `asa metrics`, and only
120
+ `asa search-terms list` takes `--date-from` / `--date-to` (default: today):
121
+
122
+ ```sh
123
+ adapty asa campaigns list
124
+ adapty asa campaigns get CAMPAIGN_ID
125
+ adapty asa campaigns create --org UUID --name "Winter push" --adam-id 123456 --country US --daily-budget 50
126
+ adapty asa campaigns update CAMPAIGN_ID [--status PAUSED] [--daily-budget 80] [--country US]
127
+
128
+ adapty asa ad-groups list
129
+ adapty asa ad-groups get AD_GROUP_ID
130
+ adapty asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20
131
+ adapty asa ad-groups update AD_GROUP_ID [--default-bid 1.50] [--status PAUSED]
132
+
133
+ adapty asa ads list
134
+ adapty asa ads get AD_ID
135
+ adapty asa ads create --ad-group UUID --creative-id 4321 --name "Summer ad"
136
+ adapty asa ads update AD_ID [--name "..."] [--status PAUSED]
137
+ ```
138
+
139
+ Keywords are always applied as a batch, at most 100 per call, and a partial rejection is reported per item:
140
+
141
+ ```sh
142
+ adapty asa keywords list
143
+ adapty asa keywords add --ad-group UUID --text "running shoes" --text "trail shoes" [--bid 1.20] [--match-type EXACT]
144
+ adapty asa keywords add --ad-group UUID --from-file keywords.txt
145
+ adapty asa keywords update KEYWORD_ID [KEYWORD_ID...] [--bid 2.00] [--status PAUSED]
146
+
147
+ adapty asa negative-keywords list
148
+ adapty asa negative-keywords add --ad-group UUID --text free
149
+ adapty asa negative-keywords add --campaign UUID [--all-ad-groups] --text free
150
+
151
+ adapty asa search-terms list [--date-from ... --date-to ...]
152
+ ```
153
+
154
+ Product pages and rule-based automations:
155
+
156
+ ```sh
157
+ adapty asa product-pages list
158
+ adapty asa product-pages sync [--adam-id 123456]
159
+
160
+ adapty asa automations list
161
+ adapty asa automations get AUTOMATION_ID
162
+ adapty asa automations create --file rule.json [--run-now]
163
+ adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json]
164
+ adapty asa automations run AUTOMATION_ID [--dry-run]
165
+ adapty asa automations runs AUTOMATION_ID
166
+ ```
167
+
168
+ Metrics take an entity level, a period and an optional metric selection. Rows come back one per entity,
169
+ aggregated and sorted server-side, so a top-N or a breakdown is a single call — use `--order-by` with a small
170
+ `--page-size` for rankings, `metrics overview` for account totals and time series, and one big page (up to
171
+ 1000 rows) when you genuinely need every row; never sum pages client-side:
172
+
173
+ ```sh
174
+ adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31
175
+ adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5
176
+ adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000
177
+ adapty asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas
178
+ adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90
179
+ adapty asa metrics overview --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 [--period-unit week]
180
+ ```
181
+
182
+ There is no `ltv` metric: lifetime value is a cohort metric read at a renewal window, so `--by-days` is how you
183
+ ask for day-7 or day-90 values — on either route, up to 16 windows per call. `--order-by-day` ranks the rows by
184
+ one of those windows, which is how you get the top campaigns by day-90 ROAS in a single call.
185
+
186
+ Competitor summary takes 1–5 Apple App Store IDs and covers the last full month across every country — there
187
+ are no period or country flags on purpose. The first call on a cold cache can take tens of seconds:
188
+
189
+ ```sh
190
+ adapty asa competitors summary --app-ids 111111111,2222222
191
+ ```
192
+
193
+ Writes go straight to Apple and take seconds, so every writing command first prints the exact request body and
194
+ asks for a yes. `--yes` skips the question for scripts; in a pipe or under `--json` the command refuses instead
195
+ of waiting for input that will never come. There is no undo — the CLI has no delete.
196
+
197
+ Every write also carries an idempotency key. The CLI generates one per invocation and retries once on a
198
+ network error, so a request that died on the wire is never applied twice. Pass `--idempotency-key` to pin the
199
+ key yourself: re-running a script with the same key within 24 hours replays the stored result — the CLI prints
200
+ "Already applied earlier — showing the stored result." — instead of creating a second entity. The same key
201
+ with a different body is rejected (`422 cli_idempotency_key_reuse`), and a concurrent duplicate answers
202
+ `409 cli_idempotency_in_progress`.
203
+
204
+ Analytics is rate limited per company: the metrics routes get 5 calls a minute (at most 2 in any 10 seconds)
205
+ and share a pool of two concurrent queries with the search-terms list — a busy pool answers
206
+ `429 cli_analytics_busy`, an exhausted window `429 cli_rate_limit_exceeded`, both with the exact wait in
207
+ `Retry-After`. The CLI absorbs a single 429 on its own — it waits the announced `Retry-After` (up to 60
208
+ seconds; cool-downs are never waited out) and retries once — so a 429 that reaches you means the retry failed
209
+ too. A burst of 429s puts the token into an escalating cool-down (`cli_cooldown_active`, 5 minutes →
210
+ 30 minutes → 3 hours); retries during the pause don't extend it, but the cure is fixing the failing request,
211
+ not waiting out the pause in a loop. An automation run is queued rather than awaited: `run` prints a run ID
212
+ and the outcome shows up in `adapty asa automations runs`.
213
+
89
214
  ### Global Flags
90
215
 
91
216
  | Flag | Description |
@@ -93,14 +218,18 @@ adapty access-levels update --app UUID ACCESS_LEVEL_ID [flags]
93
218
  | `--json` | Output as JSON |
94
219
  | `--help` | Show help |
95
220
  | `--page` | Page number (default: 1) |
96
- | `--page-size` | Items per page (default: 20, max: 100) |
221
+ | `--page-size` | Items per page (default: 20, max: 100; `asa` commands: default 100, max 1000) |
97
222
 
98
223
  ## Environment Variables
99
224
 
100
- | Variable | Description |
101
- | ---------------- | ------------------------------------------------------------------------------- |
102
- | `ADAPTY_TOKEN` | Override stored auth token |
103
- | `ADAPTY_API_URL` | Override API base URL (default: `https://api-admin.adapty.io/api/v1/developer`) |
225
+ | Variable | Description |
226
+ | -------------------- | --------------------------------------------------------------------------------------- |
227
+ | `ADAPTY_TOKEN` | Override stored auth token |
228
+ | `ADAPTY_API_URL` | Override Developer API base URL (default: `https://api-admin.adapty.io/api/v1/developer`) |
229
+ | `ADAPTY_ASA_API_URL` | Override Apple Search Ads base URL (default: `https://api-asa-admin.adapty.io/api/v1/cli`) |
230
+
231
+ The two API URLs are independent: pointing `ADAPTY_API_URL` at a staging host leaves `adapty asa` on the ASA
232
+ default, and the other way round.
104
233
 
105
234
  ## Claude Code Skill
106
235
 
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdGroupMutationDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdGroupsCreate extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ 'automated-keywords': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ campaign: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'cpa-goal': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'default-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ 'pricing-model': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ 'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
20
+ };
21
+ run(): Promise<AsaAdGroupMutationDTO>;
22
+ }
@@ -0,0 +1,54 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
3
+ import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
4
+ import { currencyFlag, idempotencyFlags, money, moneyFlag, pricingModelFlag, scheduleFlags, startOfDayUtc, todayUtc, } from '../../../lib/asa-flags.js';
5
+ import { isValidUuid } from '../../../lib/flags.js';
6
+ import { printResponse } from '../../../lib/output.js';
7
+ export default class AsaAdGroupsCreate extends Command {
8
+ static description = 'Create an ad group inside a campaign';
9
+ static enableJsonFlag = true;
10
+ static examples = [
11
+ '<%= config.bin %> asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20',
12
+ '<%= config.bin %> asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20 --start-time 2026-09-01 --pricing-model CPM',
13
+ ];
14
+ static flags = {
15
+ ...currencyFlag,
16
+ ...scheduleFlags,
17
+ ...pricingModelFlag,
18
+ ...confirmFlags,
19
+ ...idempotencyFlags,
20
+ 'automated-keywords': Flags.boolean({ allowNo: true, description: 'Let Apple add keywords automatically' }),
21
+ campaign: Flags.string({ description: 'Campaign ID (UUID)', required: true }),
22
+ 'cpa-goal': moneyFlag('CPA goal'),
23
+ 'default-bid': moneyFlag('Default bid', { required: true }),
24
+ name: Flags.string({ description: 'Ad group name', required: true }),
25
+ status: Flags.string({ description: 'Initial status', options: ['ENABLED', 'PAUSED'] }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(AsaAdGroupsCreate);
29
+ if (!isValidUuid(flags.campaign))
30
+ this.error('Invalid campaign ID format.', { exit: 2 });
31
+ const body = {
32
+ automated_keywords_opt_in: flags['automated-keywords'],
33
+ campaign_id: flags.campaign,
34
+ cpa_goal: money(flags['cpa-goal'], flags.currency),
35
+ default_bid_amount: money(flags['default-bid'], flags.currency),
36
+ end_time: startOfDayUtc(flags['end-time']),
37
+ name: flags.name,
38
+ pricing_model: flags['pricing-model'],
39
+ start_time: startOfDayUtc(flags['start-time'] ?? todayUtc()),
40
+ status: flags.status,
41
+ };
42
+ await confirmMutation(this, { body, method: 'POST', path: '/ad-groups/', summary: `Create ad group ${flags.name}` }, flags.yes);
43
+ const client = await createAsaClient(this.config);
44
+ const { replayed, result } = await asaWrite(client, 'post', '/ad-groups', {
45
+ body,
46
+ idempotencyKey: flags['idempotency-key'],
47
+ });
48
+ noteReplay(replayed, this.log.bind(this));
49
+ if (result.ad_group && !replayed)
50
+ this.log('Ad group created!');
51
+ printResponse(result, this.log.bind(this));
52
+ return result;
53
+ }
54
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdGroupDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdGroupsGet extends Command {
4
+ static args: {
5
+ ad_group_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ run(): Promise<AsaAdGroupDTO>;
11
+ }
@@ -0,0 +1,21 @@
1
+ import { Args, Command } 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
+ export default class AsaAdGroupsGet extends Command {
6
+ static args = {
7
+ ad_group_id: Args.string({ description: 'Ad group ID (UUID)', required: true }),
8
+ };
9
+ static description = 'Show one ad group; read numbers with asa metrics';
10
+ static enableJsonFlag = true;
11
+ static examples = ['<%= config.bin %> asa ad-groups get 550e8400-e29b-41d4-a716-446655440000'];
12
+ async run() {
13
+ const { args } = await this.parse(AsaAdGroupsGet);
14
+ if (!isValidUuid(args.ad_group_id))
15
+ this.error('Invalid ad group ID format.', { exit: 2 });
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get(`/ad-groups/${args.ad_group_id}`);
18
+ printResponse(result, this.log.bind(this));
19
+ return result;
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdGroupDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaAdGroupsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<PaginatedResponse<AsaAdGroupDTO>>;
18
+ }
@@ -0,0 +1,24 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { asaPaginationFlags, campaignScopeFlags, scopeParams, statusFilter } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaAdGroupsList extends Command {
7
+ static description = 'List ad groups; read numbers with asa metrics';
8
+ static enableJsonFlag = true;
9
+ static examples = [
10
+ '<%= config.bin %> asa ad-groups list',
11
+ '<%= config.bin %> asa ad-groups list --campaign CAMPAIGN_UUID',
12
+ ];
13
+ static flags = { ...asaPaginationFlags, ...campaignScopeFlags, ...statusFilter(['ENABLED', 'PAUSED']) };
14
+ async run() {
15
+ const { flags } = await this.parse(AsaAdGroupsList);
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get('/ad-groups', {
18
+ ...paginationParams(flags),
19
+ ...scopeParams(flags),
20
+ });
21
+ printList(result.data, this.log.bind(this), result.meta.pagination);
22
+ return result;
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdGroupMutationDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdGroupsUpdate extends Command {
4
+ static args: {
5
+ ad_group_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
+ 'automated-keywords': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ 'cpa-goal': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'default-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ 'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
21
+ };
22
+ run(): Promise<AsaAdGroupMutationDTO>;
23
+ }
@@ -0,0 +1,62 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
3
+ import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
4
+ import { currencyFlag, idempotencyFlags, money, moneyFlag, scheduleFlags, startOfDayUtc } from '../../../lib/asa-flags.js';
5
+ import { isValidUuid } from '../../../lib/flags.js';
6
+ import { printResponse } from '../../../lib/output.js';
7
+ export default class AsaAdGroupsUpdate extends Command {
8
+ static args = {
9
+ ad_group_id: Args.string({ description: 'Ad group ID (UUID)', required: true }),
10
+ };
11
+ static description = 'Change an ad group: bid, CPA goal, status or schedule';
12
+ static enableJsonFlag = true;
13
+ static examples = [
14
+ '<%= config.bin %> asa ad-groups update UUID --default-bid 1.50',
15
+ '<%= config.bin %> asa ad-groups update UUID --status PAUSED',
16
+ ];
17
+ static flags = {
18
+ ...currencyFlag,
19
+ ...scheduleFlags,
20
+ ...confirmFlags,
21
+ ...idempotencyFlags,
22
+ 'automated-keywords': Flags.boolean({ allowNo: true, description: 'Let Apple add keywords automatically' }),
23
+ 'cpa-goal': moneyFlag('CPA goal'),
24
+ 'default-bid': moneyFlag('Default bid'),
25
+ name: Flags.string({ description: 'Ad group name' }),
26
+ status: Flags.string({ description: 'Ad group status', options: ['ENABLED', 'PAUSED'] }),
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(AsaAdGroupsUpdate);
30
+ if (!isValidUuid(args.ad_group_id))
31
+ this.error('Invalid ad group ID format.', { exit: 2 });
32
+ const body = {};
33
+ if (flags.name !== undefined)
34
+ body.name = flags.name;
35
+ if (flags.status !== undefined)
36
+ body.status = flags.status;
37
+ if (flags['default-bid'] !== undefined)
38
+ body.default_bid_amount = money(flags['default-bid'], flags.currency);
39
+ if (flags['cpa-goal'] !== undefined)
40
+ body.cpa_goal = money(flags['cpa-goal'], flags.currency);
41
+ if (flags['automated-keywords'] !== undefined)
42
+ body.automated_keywords_opt_in = flags['automated-keywords'];
43
+ if (flags['start-time'] !== undefined)
44
+ body.start_time = startOfDayUtc(flags['start-time']);
45
+ if (flags['end-time'] !== undefined)
46
+ body.end_time = startOfDayUtc(flags['end-time']);
47
+ if (Object.keys(body).length === 0) {
48
+ this.error('Nothing to change. Pass at least one field, e.g. --status PAUSED.', { exit: 2 });
49
+ }
50
+ await confirmMutation(this, { body, method: 'PUT', path: `/ad-groups/${args.ad_group_id}/`, summary: 'Update ad group' }, flags.yes);
51
+ const client = await createAsaClient(this.config);
52
+ const { replayed, result } = await asaWrite(client, 'put', `/ad-groups/${args.ad_group_id}`, {
53
+ body,
54
+ idempotencyKey: flags['idempotency-key'],
55
+ });
56
+ noteReplay(replayed, this.log.bind(this));
57
+ if (result.ad_group && !replayed)
58
+ this.log('Ad group updated!');
59
+ printResponse(result, this.log.bind(this));
60
+ return result;
61
+ }
62
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdMutationDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdsCreate 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, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'creative-id': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<AsaAdMutationDTO>;
16
+ }
@@ -0,0 +1,41 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
3
+ import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
4
+ import { idempotencyFlags } from '../../../lib/asa-flags.js';
5
+ import { isValidUuid } from '../../../lib/flags.js';
6
+ import { printResponse } from '../../../lib/output.js';
7
+ export default class AsaAdsCreate extends Command {
8
+ static description = 'Create an ad from a creative in an ad group';
9
+ static enableJsonFlag = true;
10
+ static examples = ['<%= config.bin %> asa ads create --ad-group UUID --creative-id 4321 --name "Summer ad"'];
11
+ static flags = {
12
+ ...confirmFlags,
13
+ ...idempotencyFlags,
14
+ 'ad-group': Flags.string({ description: 'Ad group ID (UUID) — the campaign is resolved from it', required: true }),
15
+ 'creative-id': Flags.integer({ description: 'Apple creative ID from a product page or the default set', required: true }),
16
+ name: Flags.string({ description: 'Ad name', required: true }),
17
+ status: Flags.string({ description: 'Initial status', options: ['ENABLED', 'PAUSED'] }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(AsaAdsCreate);
21
+ if (!isValidUuid(flags['ad-group']))
22
+ this.error('Invalid ad group ID format.', { exit: 2 });
23
+ const body = {
24
+ ad_group_id: flags['ad-group'],
25
+ creative_id: flags['creative-id'],
26
+ name: flags.name,
27
+ ...(flags.status === undefined ? {} : { status: flags.status }),
28
+ };
29
+ await confirmMutation(this, { body, method: 'POST', path: '/ads/', summary: `Create ad ${flags.name}` }, flags.yes);
30
+ const client = await createAsaClient(this.config);
31
+ const { replayed, result } = await asaWrite(client, 'post', '/ads', {
32
+ body,
33
+ idempotencyKey: flags['idempotency-key'],
34
+ });
35
+ noteReplay(replayed, this.log.bind(this));
36
+ if (result.ad && !replayed)
37
+ this.log('Ad created!');
38
+ printResponse(result, this.log.bind(this));
39
+ return result;
40
+ }
41
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdsGet extends Command {
4
+ static args: {
5
+ ad_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static enableJsonFlag: boolean;
9
+ static examples: string[];
10
+ run(): Promise<AsaAdDTO>;
11
+ }
@@ -0,0 +1,21 @@
1
+ import { Args, Command } 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
+ export default class AsaAdsGet extends Command {
6
+ static args = {
7
+ ad_id: Args.string({ description: 'Ad ID (UUID)', required: true }),
8
+ };
9
+ static description = 'Show one ad and its serving state';
10
+ static enableJsonFlag = true;
11
+ static examples = ['<%= config.bin %> asa ads get 550e8400-e29b-41d4-a716-446655440000'];
12
+ async run() {
13
+ const { args } = await this.parse(AsaAdsGet);
14
+ if (!isValidUuid(args.ad_id))
15
+ this.error('Invalid ad ID format.', { exit: 2 });
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get(`/ads/${args.ad_id}`);
18
+ printResponse(result, this.log.bind(this));
19
+ return result;
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaAdsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
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
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<PaginatedResponse<AsaAdDTO>>;
18
+ }
@@ -0,0 +1,24 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { adScopeFlags, asaPaginationFlags, scopeParams, statusFilter } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaAdsList extends Command {
7
+ static description = 'List ads; serving_state_reasons explains why an enabled ad is not running';
8
+ static enableJsonFlag = true;
9
+ static examples = [
10
+ '<%= config.bin %> asa ads list',
11
+ '<%= config.bin %> asa ads list --ad-group AD_GROUP_UUID --status ENABLED',
12
+ ];
13
+ static flags = { ...asaPaginationFlags, ...adScopeFlags, ...statusFilter(['ENABLED', 'PAUSED']) };
14
+ async run() {
15
+ const { flags } = await this.parse(AsaAdsList);
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get('/ads', {
18
+ ...paginationParams(flags),
19
+ ...scopeParams(flags),
20
+ });
21
+ printList(result.data, this.log.bind(this), result.meta.pagination);
22
+ return result;
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAdMutationDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaAdsUpdate extends Command {
4
+ static args: {
5
+ ad_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
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<AsaAdMutationDTO>;
17
+ }
@@ -0,0 +1,44 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
3
+ import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
4
+ import { idempotencyFlags } from '../../../lib/asa-flags.js';
5
+ import { isValidUuid } from '../../../lib/flags.js';
6
+ import { printResponse } from '../../../lib/output.js';
7
+ export default class AsaAdsUpdate extends Command {
8
+ static args = {
9
+ ad_id: Args.string({ description: 'Ad ID (UUID)', required: true }),
10
+ };
11
+ static description = 'Rename an ad or pause it; the creative and ad group are fixed at creation';
12
+ static enableJsonFlag = true;
13
+ static examples = ['<%= config.bin %> asa ads update UUID --status PAUSED'];
14
+ static flags = {
15
+ ...confirmFlags,
16
+ ...idempotencyFlags,
17
+ name: Flags.string({ description: 'Ad name' }),
18
+ status: Flags.string({ description: 'Ad status', options: ['ENABLED', 'PAUSED'] }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(AsaAdsUpdate);
22
+ if (!isValidUuid(args.ad_id))
23
+ this.error('Invalid ad ID format.', { exit: 2 });
24
+ const body = {};
25
+ if (flags.name !== undefined)
26
+ body.name = flags.name;
27
+ if (flags.status !== undefined)
28
+ body.status = flags.status;
29
+ if (Object.keys(body).length === 0) {
30
+ this.error('Nothing to change. Pass --name or --status.', { exit: 2 });
31
+ }
32
+ await confirmMutation(this, { body, method: 'PUT', path: `/ads/${args.ad_id}/`, summary: 'Update ad' }, flags.yes);
33
+ const client = await createAsaClient(this.config);
34
+ const { replayed, result } = await asaWrite(client, 'put', `/ads/${args.ad_id}`, {
35
+ body,
36
+ idempotencyKey: flags['idempotency-key'],
37
+ });
38
+ noteReplay(replayed, this.log.bind(this));
39
+ if (result.ad && !replayed)
40
+ this.log('Ad updated!');
41
+ printResponse(result, this.log.bind(this));
42
+ return result;
43
+ }
44
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaAppDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaAppsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<PaginatedResponse<AsaAppDTO>>;
13
+ }
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { asaPaginationFlags } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaAppsList extends Command {
7
+ static description = 'List the apps promoted by this company in Apple Search Ads';
8
+ static enableJsonFlag = true;
9
+ static examples = ['<%= config.bin %> asa apps list', '<%= config.bin %> asa apps list --page 2 --page-size 50'];
10
+ static flags = { ...asaPaginationFlags };
11
+ async run() {
12
+ const { flags } = await this.parse(AsaAppsList);
13
+ const client = await createAsaClient(this.config);
14
+ const result = await client.get('/apps', paginationParams(flags));
15
+ printList(result.data, this.log.bind(this), result.meta.pagination);
16
+ return result;
17
+ }
18
+ }