adapty 0.8.4-beta.1 → 0.8.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.
Files changed (107) hide show
  1. package/README.md +133 -3
  2. package/bin/dev.js +2 -2
  3. package/bin/run.js +2 -2
  4. package/dist/commands/access-levels/create.js +1 -1
  5. package/dist/commands/access-levels/list.d.ts +1 -1
  6. package/dist/commands/apps/create.js +9 -6
  7. package/dist/commands/apps/list.d.ts +1 -1
  8. package/dist/commands/apps/update.js +7 -4
  9. package/dist/commands/asa/ad-groups/create.js +9 -7
  10. package/dist/commands/asa/ad-groups/get.js +3 -2
  11. package/dist/commands/asa/ad-groups/list.d.ts +1 -1
  12. package/dist/commands/asa/ad-groups/list.js +1 -1
  13. package/dist/commands/asa/ad-groups/update.js +22 -12
  14. package/dist/commands/asa/ads/create.js +7 -5
  15. package/dist/commands/asa/ads/get.js +3 -2
  16. package/dist/commands/asa/ads/list.d.ts +1 -1
  17. package/dist/commands/asa/ads/list.js +1 -1
  18. package/dist/commands/asa/ads/update.js +9 -5
  19. package/dist/commands/asa/apps/list.d.ts +1 -1
  20. package/dist/commands/asa/apps/list.js +1 -1
  21. package/dist/commands/asa/automations/create.d.ts +9 -0
  22. package/dist/commands/asa/automations/create.js +26 -7
  23. package/dist/commands/asa/automations/get.js +3 -2
  24. package/dist/commands/asa/automations/list.d.ts +1 -1
  25. package/dist/commands/asa/automations/list.js +1 -1
  26. package/dist/commands/asa/automations/run.js +5 -3
  27. package/dist/commands/asa/automations/runs.d.ts +1 -1
  28. package/dist/commands/asa/automations/runs.js +4 -4
  29. package/dist/commands/asa/automations/update.d.ts +9 -0
  30. package/dist/commands/asa/automations/update.js +36 -12
  31. package/dist/commands/asa/campaigns/bulk-create.js +31 -18
  32. package/dist/commands/asa/campaigns/bulk-list.js +7 -5
  33. package/dist/commands/asa/campaigns/bulk-status.js +3 -2
  34. package/dist/commands/asa/campaigns/create.js +10 -8
  35. package/dist/commands/asa/campaigns/get.js +3 -2
  36. package/dist/commands/asa/campaigns/list.d.ts +1 -1
  37. package/dist/commands/asa/campaigns/list.js +1 -1
  38. package/dist/commands/asa/campaigns/update.js +25 -15
  39. package/dist/commands/asa/competitors/summary.js +5 -5
  40. package/dist/commands/asa/connect.js +5 -3
  41. package/dist/commands/asa/creatives/list.d.ts +1 -1
  42. package/dist/commands/asa/creatives/list.js +1 -1
  43. package/dist/commands/asa/keywords/add.js +13 -10
  44. package/dist/commands/asa/keywords/list.d.ts +1 -1
  45. package/dist/commands/asa/keywords/list.js +1 -1
  46. package/dist/commands/asa/keywords/update.js +15 -10
  47. package/dist/commands/asa/metrics/index.d.ts +6 -3
  48. package/dist/commands/asa/metrics/index.js +38 -14
  49. package/dist/commands/asa/metrics/overview.js +3 -3
  50. package/dist/commands/asa/negative-keywords/add.js +10 -7
  51. package/dist/commands/asa/negative-keywords/list.d.ts +1 -1
  52. package/dist/commands/asa/negative-keywords/list.js +1 -1
  53. package/dist/commands/asa/orgs/list.d.ts +1 -1
  54. package/dist/commands/asa/orgs/list.js +1 -1
  55. package/dist/commands/asa/product-pages/list.d.ts +1 -1
  56. package/dist/commands/asa/product-pages/list.js +1 -1
  57. package/dist/commands/asa/product-pages/sync.js +1 -1
  58. package/dist/commands/asa/search-terms/list.d.ts +1 -1
  59. package/dist/commands/asa/search-terms/list.js +1 -1
  60. package/dist/commands/asa/whoami.js +7 -2
  61. package/dist/commands/auth/login.js +3 -1
  62. package/dist/commands/flows/config/get.js +2 -2
  63. package/dist/commands/flows/config/preview.d.ts +2 -2
  64. package/dist/commands/flows/config/preview.js +6 -4
  65. package/dist/commands/flows/config/update.js +5 -3
  66. package/dist/commands/flows/config/validate.js +5 -3
  67. package/dist/commands/flows/list.d.ts +1 -1
  68. package/dist/commands/flows/media/upload.js +1 -1
  69. package/dist/commands/flows/publish.js +5 -5
  70. package/dist/commands/paywalls/create.js +1 -1
  71. package/dist/commands/paywalls/list.d.ts +1 -1
  72. package/dist/commands/paywalls/placements.d.ts +2 -2
  73. package/dist/commands/paywalls/update.js +1 -1
  74. package/dist/commands/placements/create.js +17 -12
  75. package/dist/commands/placements/list.d.ts +1 -1
  76. package/dist/commands/placements/update.js +20 -15
  77. package/dist/commands/products/create.js +20 -13
  78. package/dist/commands/products/list.d.ts +1 -1
  79. package/dist/commands/products/update.js +1 -1
  80. package/dist/commands/segments/list.d.ts +1 -1
  81. package/dist/lib/api-client.d.ts +10 -6
  82. package/dist/lib/api-client.js +56 -35
  83. package/dist/lib/api-schemas.d.ts +56 -56
  84. package/dist/lib/app-url.js +2 -1
  85. package/dist/lib/asa-client.d.ts +13 -8
  86. package/dist/lib/asa-client.js +9 -4
  87. package/dist/lib/asa-flags.d.ts +37 -13
  88. package/dist/lib/asa-flags.js +87 -20
  89. package/dist/lib/asa-keyword-action.d.ts +18 -0
  90. package/dist/lib/asa-keyword-action.js +175 -0
  91. package/dist/lib/asa-schemas.d.ts +100 -80
  92. package/dist/lib/auth.js +2 -1
  93. package/dist/lib/client-from-config.d.ts +1 -1
  94. package/dist/lib/client-from-config.js +2 -1
  95. package/dist/lib/config.d.ts +2 -2
  96. package/dist/lib/confirm.d.ts +4 -4
  97. package/dist/lib/confirm.js +12 -7
  98. package/dist/lib/errors.d.ts +18 -18
  99. package/dist/lib/errors.js +14 -10
  100. package/dist/lib/flags.d.ts +3 -3
  101. package/dist/lib/flags.js +2 -1
  102. package/dist/lib/flow-help.js +11 -6
  103. package/dist/lib/output.js +23 -12
  104. package/dist/lib/preview.d.ts +5 -5
  105. package/dist/lib/preview.js +2 -1
  106. package/oclif.manifest.json +372 -200
  107. package/package.json +10 -8
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  npm install -g adapty
13
13
  ```
14
14
 
15
- Requires Node.js >= 18.
15
+ Requires Node.js 22 or 24. Node 18 and 20 are past end-of-life and are not supported.
16
16
 
17
17
  ## Authentication
18
18
 
@@ -199,12 +199,142 @@ adapty asa product-pages sync [--adam-id 123456]
199
199
 
200
200
  adapty asa automations list
201
201
  adapty asa automations get AUTOMATION_ID
202
- adapty asa automations create --file rule.json [--run-now]
203
- adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json]
202
+ adapty asa automations create --file rule.json [--run-now] [--target-ad-group UUID ...]
203
+ adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json] [--target-ad-group UUID ...]
204
204
  adapty asa automations run AUTOMATION_ID [--dry-run]
205
205
  adapty asa automations runs AUTOMATION_ID
206
206
  ```
207
207
 
208
+ A rule file carries the whole rule: `name`, `status` (1 active, 0 stopped), `operate_with` (what the
209
+ rule iterates over), `apply_to` (where it looks), exactly one condition, exactly one action and a
210
+ `run_frequency`. The API stores one action and one condition per rule and rejects anything else.
211
+
212
+ | Field | Shape |
213
+ | --------------- | ------------------------------------------------------------------------------------------- |
214
+ | `operate_with` | `search-term`, `targeting-keyword`, `campaign`, `ad-group` |
215
+ | `apply_to[]` | `{"internal_id": UUID, "type": "campaign-group" \| "app" \| "campaign" \| "ad-group" \| "targeting-keywords"}` |
216
+ | `conditions[0]` | `{"operator": ..., "args": number, "operand": {...}}`, or `{"operator": "and" \| "or", "args": [condition, ...]}` |
217
+ | `operator` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` for a leaf; `and`, `or` to nest |
218
+ | `operand.field` | a metric name in dashboard nomenclature — the same vocabulary `asa metrics --metric` takes |
219
+ | `date_range_type` | `today`, `yesterday`, `last_1_d`, `last_3_d`, `last_7_d`, `last_14_d`, `last_28_d`, `last_30_d`, `last_60_d`, `last_90_d`, `custom` |
220
+ | `run_frequency` | `{"type": "daily", "hour": 8}`, `{"type": "hour", "value": 24, "start_time": 8}`, `{"type": "weekly", "weekdays": ["monday"], "hour": 8}`, `{"type": "monthly", "days": [1], "hour": 8}`, `{"type": "once", "date_time": "2026-10-01T09:00:00Z"}` — `hour` and `start_time` are UTC |
221
+
222
+ The `add-as-keyword-to` action promotes what the rule found into a keyword in one or more target ad
223
+ groups. Its `params` differ by `operate_with`, and the API picks the variant by shape without a
224
+ discriminator: a key that belongs to another variant makes it silently choose that variant and drop
225
+ the rest, which is how a rule ends up as "Add as keyword to 0 ad groups". `negate` and
226
+ `skip_enable_duplicate_keywords` exist only on a `search-term` rule, `pause_in_original_ad_group`
227
+ only on a `targeting-keyword` one, and `targets` holds `internal_ids` — never `ids`.
228
+
229
+ A full `search-term` rule — every term with 10+ taps over the last week becomes an exact keyword in
230
+ the target ad group at the term's own CPT, and is negated in the ad group it came from:
231
+
232
+ ```json
233
+ {
234
+ "name": "Search term harvester",
235
+ "status": 1,
236
+ "operate_with": "search-term",
237
+ "apply_to": [{"internal_id": "CAMPAIGN_UUID", "type": "campaign"}],
238
+ "conditions": [
239
+ {
240
+ "operator": "gte",
241
+ "args": 10,
242
+ "operand": {
243
+ "field": "taps",
244
+ "field_type": "base_field",
245
+ "date_range_type": "last_7_d",
246
+ "date_range_size": 0,
247
+ "date_range_offset": 0,
248
+ "by_days": null
249
+ }
250
+ }
251
+ ],
252
+ "actions": [
253
+ {
254
+ "type": "add-as-keyword-to",
255
+ "params": {
256
+ "targets": {"type": "ad-group", "internal_ids": ["AD_GROUP_UUID"]},
257
+ "cpt_bid": {"type": "search_term_current_cpt", "value": null},
258
+ "match_type": "EXACT",
259
+ "negate": {"enabled": true, "type": "ad-group"},
260
+ "skip_enable_duplicate_keywords": false
261
+ }
262
+ }
263
+ ],
264
+ "run_frequency": {"type": "daily", "hour": 8}
265
+ }
266
+ ```
267
+
268
+ The same action on a `targeting-keyword` rule — a broad keyword that has earned installs graduates
269
+ into the exact ad group at its current bid and is paused where it was:
270
+
271
+ ```json
272
+ {
273
+ "name": "Graduate broad keywords to exact",
274
+ "status": 1,
275
+ "operate_with": "targeting-keyword",
276
+ "apply_to": [{"internal_id": "SOURCE_AD_GROUP_UUID", "type": "ad-group"}],
277
+ "conditions": [
278
+ {
279
+ "operator": "gte",
280
+ "args": 3,
281
+ "operand": {
282
+ "field": "total_installs",
283
+ "field_type": "base_field",
284
+ "date_range_type": "last_14_d",
285
+ "date_range_size": 0,
286
+ "date_range_offset": 0,
287
+ "by_days": null
288
+ }
289
+ }
290
+ ],
291
+ "actions": [
292
+ {
293
+ "type": "add-as-keyword-to",
294
+ "params": {
295
+ "targets": {"type": "ad-group", "internal_ids": ["EXACT_AD_GROUP_UUID"]},
296
+ "cpt_bid": {"type": "keyword_current_bid", "value": null},
297
+ "match_type": "EXACT",
298
+ "pause_in_original_ad_group": true
299
+ }
300
+ }
301
+ ],
302
+ "run_frequency": {"type": "daily", "hour": 8}
303
+ }
304
+ ```
305
+
306
+ Rather than hand-writing that `params` block, pass the action flags — they fill in or override
307
+ `actions[0].params` in the file, and the CLI refuses a rule the API would have quietly accepted and
308
+ broken:
309
+
310
+ ```sh
311
+ adapty asa automations create --file rule.json --target-ad-group AD_GROUP_UUID \
312
+ --match-type EXACT --cpt-bid-type search_term_current_cpt --negate ad-group
313
+ adapty asa automations create --file rule.json --target-ad-group AD_GROUP_UUID \
314
+ --match-type BROAD --cpt-bid-type set_to --cpt-bid 1.50 --no-negate --skip-enable-duplicates
315
+ adapty asa automations update AUTOMATION_ID --target-ad-group AD_GROUP_UUID \
316
+ --match-type EXACT --cpt-bid-type search_term_current_cpt
317
+ ```
318
+
319
+ | Flag | Where it lands |
320
+ | -------------------------- | --------------------------------------------------------------------- |
321
+ | `--target-ad-group` | `targets.internal_ids`, repeatable; a rule without one does nothing |
322
+ | `--match-type` | `match_type`: `BROAD` or `EXACT` |
323
+ | `--cpt-bid-type` | `cpt_bid.type`: `ad_group_default_bid`, `set_to`, `search_term_current_cpt`, `keyword_current_bid` |
324
+ | `--cpt-bid` | `cpt_bid.value`: the bid itself with `set_to` (required), a percent markup on the entity's own bid with `search_term_current_cpt` / `keyword_current_bid`, rejected with `ad_group_default_bid` |
325
+ | `--negate` / `--no-negate` | `negate` (search-term rules): `ad-group` or `campaign`, or off |
326
+ | `--skip-enable-duplicates` | `skip_enable_duplicate_keywords` (search-term rules) |
327
+ | `--pause-original` | `pause_in_original_ad_group` (targeting-keyword rules) |
328
+
329
+ `--cpt-bid-type` and `--match-type` have no defaults, here or in the API: a bid and a match type are
330
+ a spend decision and a reach decision, so when `params` are built from scratch the CLI asks for them
331
+ instead of guessing. On `update`, an action flag turns the call into a read-modify-write — the rule
332
+ is read, `actions[0].params` is rebuilt and the whole `actions` list is written back, since the API
333
+ replaces it wholesale. That also repairs a rule whose stored `params` carry the wrong shape: only
334
+ the keys that fit are kept, the strays are dropped, and anything missing has to come from a flag. A
335
+ dashboard edit made between the read and the write is overwritten.
336
+
337
+
208
338
  Metrics take an entity level, a period and an optional metric selection. Rows come back one per entity,
209
339
  aggregated and sorted server-side, so a top-N or a breakdown is a single call — use `--order-by` with a small
210
340
  `--page-size` for rankings, `metrics overview` for account totals and time series, and one big page (up to
package/bin/dev.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
2
 
3
- import {execute} from '@oclif/core'
3
+ import { execute } from '@oclif/core';
4
4
 
5
- await execute({development: true, dir: import.meta.url})
5
+ await execute({ development: true, dir: import.meta.url });
package/bin/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import {execute} from '@oclif/core'
3
+ import { execute } from '@oclif/core';
4
4
 
5
- await execute({dir: import.meta.url})
5
+ await execute({ dir: import.meta.url });
@@ -11,7 +11,7 @@ export default class AccessLevelsCreate extends Command {
11
11
  static flags = {
12
12
  ...appFlag,
13
13
  'sdk-id': Flags.string({ description: 'Access level SDK identifier', required: true }),
14
- title: Flags.string({ description: 'Access level title', required: true }),
14
+ 'title': Flags.string({ description: 'Access level title', required: true }),
15
15
  };
16
16
  async run() {
17
17
  const { flags } = await this.parse(AccessLevelsCreate);
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { AccessLevelDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class AccessLevelsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -11,13 +11,13 @@ export default class AppsCreate extends Command {
11
11
  static flags = {
12
12
  'apple-bundle-id': Flags.string({ description: 'Apple bundle ID (required with --platform ios)' }),
13
13
  'google-bundle-id': Flags.string({ description: 'Google bundle ID (required with --platform android)' }),
14
- platform: Flags.string({
14
+ 'platform': Flags.string({
15
15
  description: 'Platform (ios, android). Repeat for multiple.',
16
16
  multiple: true,
17
17
  options: ['ios', 'android'],
18
18
  required: true,
19
19
  }),
20
- title: Flags.string({ description: 'App title', required: true }),
20
+ 'title': Flags.string({ description: 'App title', required: true }),
21
21
  };
22
22
  async run() {
23
23
  const { flags } = await this.parse(AppsCreate);
@@ -32,18 +32,21 @@ export default class AppsCreate extends Command {
32
32
  platforms: flags.platform,
33
33
  title: flags.title,
34
34
  };
35
- if (flags['apple-bundle-id'])
35
+ if (flags['apple-bundle-id']) {
36
36
  body.apple_bundle_id = flags['apple-bundle-id'];
37
- if (flags['google-bundle-id'])
37
+ }
38
+ if (flags['google-bundle-id']) {
38
39
  body.google_bundle_id = flags['google-bundle-id'];
40
+ }
39
41
  const result = await client.post('/apps', body);
40
42
  this.log('App created!');
41
43
  printResponse(result, this.log.bind(this));
42
44
  try {
43
45
  const accessLevels = await client.get(`/apps/${result.id}/access-levels`);
44
- if (accessLevels.items?.length) {
46
+ const [defaultAccessLevel] = accessLevels.items ?? [];
47
+ if (defaultAccessLevel) {
45
48
  this.log('\nDefault access level:');
46
- printResponse(accessLevels.items[0], this.log.bind(this));
49
+ printResponse(defaultAccessLevel, this.log.bind(this));
47
50
  }
48
51
  }
49
52
  catch {
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { AppSummaryDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class AppsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -12,7 +12,7 @@ export default class AppsUpdate extends Command {
12
12
  static flags = {
13
13
  'apple-bundle-id': Flags.string({ description: 'Apple bundle ID' }),
14
14
  'google-bundle-id': Flags.string({ description: 'Google bundle ID' }),
15
- title: Flags.string({ description: 'App title' }),
15
+ 'title': Flags.string({ description: 'App title' }),
16
16
  };
17
17
  async run() {
18
18
  const { args, flags } = await this.parse(AppsUpdate);
@@ -24,12 +24,15 @@ export default class AppsUpdate extends Command {
24
24
  }
25
25
  const client = await createAuthenticatedClient(this.config);
26
26
  const body = {};
27
- if (flags.title)
27
+ if (flags.title) {
28
28
  body.title = flags.title;
29
- if (flags['apple-bundle-id'])
29
+ }
30
+ if (flags['apple-bundle-id']) {
30
31
  body.apple_bundle_id = flags['apple-bundle-id'];
31
- if (flags['google-bundle-id'])
32
+ }
33
+ if (flags['google-bundle-id']) {
32
34
  body.google_bundle_id = flags['google-bundle-id'];
35
+ }
33
36
  const result = await client.put(`/apps/${args.app_id}`, body);
34
37
  this.log('App updated!');
35
38
  printResponse(result, this.log.bind(this));
@@ -18,21 +18,22 @@ export default class AsaAdGroupsCreate extends Command {
18
18
  ...pricingModelFlag,
19
19
  ...confirmFlags,
20
20
  ...idempotencyFlags,
21
- automated: Flags.boolean({
21
+ 'automated': Flags.boolean({
22
22
  description: 'Create the automated ad group a Max Conversions campaign needs (no bid, no schedule, always ENABLED)',
23
23
  exclusive: ['automated-keywords'],
24
24
  }),
25
25
  'automated-keywords': Flags.boolean({ allowNo: true, description: 'Let Apple add keywords automatically' }),
26
- campaign: Flags.string({ description: 'Campaign ID (UUID)', required: true }),
26
+ 'campaign': Flags.string({ description: 'Campaign ID (UUID)', required: true }),
27
27
  'cpa-goal': moneyFlag('CPA goal'),
28
28
  'default-bid': moneyFlag('Default bid; required unless --automated is set'),
29
- name: Flags.string({ description: 'Ad group name', required: true }),
30
- status: Flags.string({ description: 'Initial status', options: ['ENABLED', 'PAUSED'] }),
29
+ 'name': Flags.string({ description: 'Ad group name', required: true }),
30
+ 'status': Flags.string({ description: 'Initial status', options: ['ENABLED', 'PAUSED'] }),
31
31
  };
32
32
  async run() {
33
33
  const { flags } = await this.parse(AsaAdGroupsCreate);
34
- if (!isValidUuid(flags.campaign))
34
+ if (!isValidUuid(flags.campaign)) {
35
35
  this.error('Invalid campaign ID format.', { exit: 2 });
36
+ }
36
37
  if (!flags.automated && flags['default-bid'] === undefined) {
37
38
  this.error('--default-bid is required unless --automated is set.', { exit: 2 });
38
39
  }
@@ -55,14 +56,15 @@ export default class AsaAdGroupsCreate extends Command {
55
56
  status: flags.status,
56
57
  };
57
58
  await confirmMutation(this, { body, method: 'POST', path: '/ad-groups/', summary: `Create ad group ${flags.name}` }, flags.yes);
58
- const client = await createAsaClient(this.config);
59
+ const client = await createAsaClient(this);
59
60
  const { replayed, result } = await asaWrite(client, 'post', '/ad-groups', {
60
61
  body,
61
62
  idempotencyKey: flags['idempotency-key'],
62
63
  });
63
64
  noteReplay(replayed, this.log.bind(this));
64
- if (result.ad_group && !replayed)
65
+ if (result.ad_group && !replayed) {
65
66
  this.log('Ad group created!');
67
+ }
66
68
  printResponse(result, this.log.bind(this));
67
69
  return result;
68
70
  }
@@ -11,9 +11,10 @@ export default class AsaAdGroupsGet extends Command {
11
11
  static examples = ['<%= config.bin %> asa ad-groups get 550e8400-e29b-41d4-a716-446655440000'];
12
12
  async run() {
13
13
  const { args } = await this.parse(AsaAdGroupsGet);
14
- if (!isValidUuid(args.ad_group_id))
14
+ if (!isValidUuid(args.ad_group_id)) {
15
15
  this.error('Invalid ad group ID format.', { exit: 2 });
16
- const client = await createAsaClient(this.config);
16
+ }
17
+ const client = await createAsaClient(this);
17
18
  const result = await client.get(`/ad-groups/${args.ad_group_id}`);
18
19
  printResponse(result, this.log.bind(this));
19
20
  return result;
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { AsaAdGroupDTO } from '../../../lib/asa-schemas.js';
3
- import { type PaginatedResponse } from '../../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../../lib/flags.js';
4
4
  export default class AsaAdGroupsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -13,7 +13,7 @@ export default class AsaAdGroupsList extends Command {
13
13
  static flags = { ...asaPaginationFlags, ...campaignScopeFlags, ...statusFilter(['ENABLED', 'PAUSED']) };
14
14
  async run() {
15
15
  const { flags } = await this.parse(AsaAdGroupsList);
16
- const client = await createAsaClient(this.config);
16
+ const client = await createAsaClient(this);
17
17
  const result = await client.get('/ad-groups', {
18
18
  ...paginationParams(flags),
19
19
  ...scopeParams(flags),
@@ -22,40 +22,50 @@ export default class AsaAdGroupsUpdate extends Command {
22
22
  'automated-keywords': Flags.boolean({ allowNo: true, description: 'Let Apple add keywords automatically' }),
23
23
  'cpa-goal': moneyFlag('CPA goal'),
24
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'] }),
25
+ 'name': Flags.string({ description: 'Ad group name' }),
26
+ 'status': Flags.string({ description: 'Ad group status', options: ['ENABLED', 'PAUSED'] }),
27
27
  };
28
28
  async run() {
29
29
  const { args, flags } = await this.parse(AsaAdGroupsUpdate);
30
- if (!isValidUuid(args.ad_group_id))
30
+ if (!isValidUuid(args.ad_group_id)) {
31
31
  this.error('Invalid ad group ID format.', { exit: 2 });
32
+ }
32
33
  const body = {};
33
- if (flags.name !== undefined)
34
+ if (flags.name !== undefined) {
34
35
  body.name = flags.name;
35
- if (flags.status !== undefined)
36
+ }
37
+ if (flags.status !== undefined) {
36
38
  body.status = flags.status;
37
- if (flags['default-bid'] !== undefined)
39
+ }
40
+ if (flags['default-bid'] !== undefined) {
38
41
  body.default_bid_amount = money(flags['default-bid'], flags.currency);
39
- if (flags['cpa-goal'] !== undefined)
42
+ }
43
+ if (flags['cpa-goal'] !== undefined) {
40
44
  body.cpa_goal = money(flags['cpa-goal'], flags.currency);
41
- if (flags['automated-keywords'] !== undefined)
45
+ }
46
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- FIXME if you see this
47
+ if (flags['automated-keywords'] !== undefined) {
42
48
  body.automated_keywords_opt_in = flags['automated-keywords'];
43
- if (flags['start-time'] !== undefined)
49
+ }
50
+ if (flags['start-time'] !== undefined) {
44
51
  body.start_time = startOfDayUtc(flags['start-time']);
45
- if (flags['end-time'] !== undefined)
52
+ }
53
+ if (flags['end-time'] !== undefined) {
46
54
  body.end_time = startOfDayUtc(flags['end-time']);
55
+ }
47
56
  if (Object.keys(body).length === 0) {
48
57
  this.error('Nothing to change. Pass at least one field, e.g. --status PAUSED.', { exit: 2 });
49
58
  }
50
59
  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);
60
+ const client = await createAsaClient(this);
52
61
  const { replayed, result } = await asaWrite(client, 'put', `/ad-groups/${args.ad_group_id}`, {
53
62
  body,
54
63
  idempotencyKey: flags['idempotency-key'],
55
64
  });
56
65
  noteReplay(replayed, this.log.bind(this));
57
- if (result.ad_group && !replayed)
66
+ if (result.ad_group && !replayed) {
58
67
  this.log('Ad group updated!');
68
+ }
59
69
  printResponse(result, this.log.bind(this));
60
70
  return result;
61
71
  }
@@ -13,13 +13,14 @@ export default class AsaAdsCreate extends Command {
13
13
  ...idempotencyFlags,
14
14
  'ad-group': Flags.string({ description: 'Ad group ID (UUID) — the campaign is resolved from it', required: true }),
15
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'] }),
16
+ 'name': Flags.string({ description: 'Ad name', required: true }),
17
+ 'status': Flags.string({ description: 'Initial status', options: ['ENABLED', 'PAUSED'] }),
18
18
  };
19
19
  async run() {
20
20
  const { flags } = await this.parse(AsaAdsCreate);
21
- if (!isValidUuid(flags['ad-group']))
21
+ if (!isValidUuid(flags['ad-group'])) {
22
22
  this.error('Invalid ad group ID format.', { exit: 2 });
23
+ }
23
24
  const body = {
24
25
  ad_group_id: flags['ad-group'],
25
26
  creative_id: flags['creative-id'],
@@ -27,14 +28,15 @@ export default class AsaAdsCreate extends Command {
27
28
  ...(flags.status === undefined ? {} : { status: flags.status }),
28
29
  };
29
30
  await confirmMutation(this, { body, method: 'POST', path: '/ads/', summary: `Create ad ${flags.name}` }, flags.yes);
30
- const client = await createAsaClient(this.config);
31
+ const client = await createAsaClient(this);
31
32
  const { replayed, result } = await asaWrite(client, 'post', '/ads', {
32
33
  body,
33
34
  idempotencyKey: flags['idempotency-key'],
34
35
  });
35
36
  noteReplay(replayed, this.log.bind(this));
36
- if (result.ad && !replayed)
37
+ if (result.ad && !replayed) {
37
38
  this.log('Ad created!');
39
+ }
38
40
  printResponse(result, this.log.bind(this));
39
41
  return result;
40
42
  }
@@ -11,9 +11,10 @@ export default class AsaAdsGet extends Command {
11
11
  static examples = ['<%= config.bin %> asa ads get 550e8400-e29b-41d4-a716-446655440000'];
12
12
  async run() {
13
13
  const { args } = await this.parse(AsaAdsGet);
14
- if (!isValidUuid(args.ad_id))
14
+ if (!isValidUuid(args.ad_id)) {
15
15
  this.error('Invalid ad ID format.', { exit: 2 });
16
- const client = await createAsaClient(this.config);
16
+ }
17
+ const client = await createAsaClient(this);
17
18
  const result = await client.get(`/ads/${args.ad_id}`);
18
19
  printResponse(result, this.log.bind(this));
19
20
  return result;
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { AsaAdDTO } from '../../../lib/asa-schemas.js';
3
- import { type PaginatedResponse } from '../../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../../lib/flags.js';
4
4
  export default class AsaAdsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -13,7 +13,7 @@ export default class AsaAdsList extends Command {
13
13
  static flags = { ...asaPaginationFlags, ...adScopeFlags, ...statusFilter(['ENABLED', 'PAUSED']) };
14
14
  async run() {
15
15
  const { flags } = await this.parse(AsaAdsList);
16
- const client = await createAsaClient(this.config);
16
+ const client = await createAsaClient(this);
17
17
  const result = await client.get('/ads', {
18
18
  ...paginationParams(flags),
19
19
  ...scopeParams(flags),
@@ -19,25 +19,29 @@ export default class AsaAdsUpdate extends Command {
19
19
  };
20
20
  async run() {
21
21
  const { args, flags } = await this.parse(AsaAdsUpdate);
22
- if (!isValidUuid(args.ad_id))
22
+ if (!isValidUuid(args.ad_id)) {
23
23
  this.error('Invalid ad ID format.', { exit: 2 });
24
+ }
24
25
  const body = {};
25
- if (flags.name !== undefined)
26
+ if (flags.name !== undefined) {
26
27
  body.name = flags.name;
27
- if (flags.status !== undefined)
28
+ }
29
+ if (flags.status !== undefined) {
28
30
  body.status = flags.status;
31
+ }
29
32
  if (Object.keys(body).length === 0) {
30
33
  this.error('Nothing to change. Pass --name or --status.', { exit: 2 });
31
34
  }
32
35
  await confirmMutation(this, { body, method: 'PUT', path: `/ads/${args.ad_id}/`, summary: 'Update ad' }, flags.yes);
33
- const client = await createAsaClient(this.config);
36
+ const client = await createAsaClient(this);
34
37
  const { replayed, result } = await asaWrite(client, 'put', `/ads/${args.ad_id}`, {
35
38
  body,
36
39
  idempotencyKey: flags['idempotency-key'],
37
40
  });
38
41
  noteReplay(replayed, this.log.bind(this));
39
- if (result.ad && !replayed)
42
+ if (result.ad && !replayed) {
40
43
  this.log('Ad updated!');
44
+ }
41
45
  printResponse(result, this.log.bind(this));
42
46
  return result;
43
47
  }
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { AsaAppDTO } from '../../../lib/asa-schemas.js';
3
- import { type PaginatedResponse } from '../../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../../lib/flags.js';
4
4
  export default class AsaAppsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -10,7 +10,7 @@ export default class AsaAppsList extends Command {
10
10
  static flags = { ...asaPaginationFlags };
11
11
  async run() {
12
12
  const { flags } = await this.parse(AsaAppsList);
13
- const client = await createAsaClient(this.config);
13
+ const client = await createAsaClient(this);
14
14
  const result = await client.get('/apps', paginationParams(flags));
15
15
  printList(result.data, this.log.bind(this), result.meta.pagination);
16
16
  return result;
@@ -9,8 +9,17 @@ export default class AsaAutomationsCreate extends Command {
9
9
  'run-now': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ 'cpt-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'cpt-bid-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ negate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'no-negate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ 'pause-original': import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ 'skip-enable-duplicates': import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ 'target-ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
20
  };
13
21
  run(): Promise<AsaAutomationMutationDTO>;
22
+ private applyActionFlags;
14
23
  private readRule;
15
24
  private readStdin;
16
25
  }
@@ -1,30 +1,36 @@
1
- import { Command, Flags } from '@oclif/core';
2
1
  import { readFile } from 'node:fs/promises';
2
+ import { Command, Flags } from '@oclif/core';
3
3
  import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
4
- import { idempotencyFlags } from '../../../lib/asa-flags.js';
4
+ import { addKeywordActionFlags, idempotencyFlags } from '../../../lib/asa-flags.js';
5
+ import { hasAddKeywordActionFlags, rebuildAddKeywordAction, requireSingleAction, } from '../../../lib/asa-keyword-action.js';
5
6
  import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
6
7
  import { printResponse } from '../../../lib/output.js';
7
8
  export default class AsaAutomationsCreate extends Command {
8
- static description = 'Create an automation rule from a JSON rule file';
9
+ static description = 'Create an automation rule from a JSON rule file. The add-as-keyword action flags fill in or override actions[0].params, so the bid, the match type and the target ad groups need no hand-written JSON.';
9
10
  static enableJsonFlag = true;
10
11
  static examples = [
11
12
  '<%= config.bin %> asa automations create --file rule.json',
12
13
  '<%= config.bin %> asa automations create --file rule.json --run-now',
14
+ '<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type search_term_current_cpt',
15
+ '<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type set_to --cpt-bid 1.50 --negate ad-group',
13
16
  ];
14
17
  static flags = {
18
+ ...addKeywordActionFlags,
15
19
  ...confirmFlags,
16
20
  ...idempotencyFlags,
17
- file: Flags.string({ description: 'JSON file with the rule body, or - to read stdin', required: true }),
21
+ 'file': Flags.string({ description: 'JSON file with the rule body, or - to read stdin', required: true }),
18
22
  'run-now': Flags.boolean({ description: 'Queue the first run right after the rule is stored' }),
19
23
  };
20
24
  async run() {
21
25
  const { flags } = await this.parse(AsaAutomationsCreate);
22
26
  const body = await this.readRule(flags.file);
23
- if (flags['run-now'])
27
+ if (flags['run-now']) {
24
28
  body.run_immediately = true;
29
+ }
30
+ this.applyActionFlags(body, flags);
25
31
  const summary = flags['run-now'] ? 'Create automation rule and run it immediately' : 'Create automation rule';
26
32
  await confirmMutation(this, { body, method: 'POST', path: '/automations/', summary }, flags.yes);
27
- const client = await createAsaClient(this.config);
33
+ const client = await createAsaClient(this);
28
34
  const { replayed, result } = await asaWrite(client, 'post', '/automations', {
29
35
  body,
30
36
  idempotencyKey: flags['idempotency-key'],
@@ -36,6 +42,18 @@ export default class AsaAutomationsCreate extends Command {
36
42
  printResponse(result, this.log.bind(this));
37
43
  return result;
38
44
  }
45
+ applyActionFlags(body, flags) {
46
+ try {
47
+ requireSingleAction(body.actions);
48
+ if (!hasAddKeywordActionFlags(flags)) {
49
+ return;
50
+ }
51
+ body.actions = rebuildAddKeywordAction(body.actions, body.operate_with, flags);
52
+ }
53
+ catch (error) {
54
+ this.error(error instanceof Error ? error.message : String(error), { exit: 2 });
55
+ }
56
+ }
39
57
  async readRule(path) {
40
58
  let raw;
41
59
  try {
@@ -53,8 +71,9 @@ export default class AsaAutomationsCreate extends Command {
53
71
  }
54
72
  async readStdin() {
55
73
  const chunks = [];
56
- for await (const chunk of process.stdin)
74
+ for await (const chunk of process.stdin) {
57
75
  chunks.push(chunk);
76
+ }
58
77
  return Buffer.concat(chunks).toString('utf8');
59
78
  }
60
79
  }