adapty 0.4.0 → 0.6.0-beta.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.
- package/README.md +37 -1
- package/dist/commands/asa/campaigns/bulk-create.d.ts +25 -0
- package/dist/commands/asa/campaigns/bulk-create.js +177 -0
- package/dist/commands/asa/campaigns/bulk-status.d.ts +15 -0
- package/dist/commands/asa/campaigns/bulk-status.js +26 -0
- package/dist/commands/asa/connect.js +2 -1
- package/dist/commands/auth/login.js +7 -5
- package/dist/commands/flows/config/get.d.ts +14 -0
- package/dist/commands/flows/config/get.js +25 -0
- package/dist/commands/flows/config/update.d.ts +21 -0
- package/dist/commands/flows/config/update.js +84 -0
- package/dist/commands/flows/create.d.ts +12 -0
- package/dist/commands/flows/create.js +22 -0
- package/dist/commands/flows/get.d.ts +14 -0
- package/dist/commands/flows/get.js +25 -0
- package/dist/commands/flows/list.d.ts +14 -0
- package/dist/commands/flows/list.js +20 -0
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/api-client.js +4 -1
- package/dist/lib/api-schemas.d.ts +28 -0
- package/dist/lib/asa-schemas.d.ts +40 -0
- package/oclif.manifest.json +1020 -592
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Adapty CLI
|
|
6
6
|
|
|
7
|
-
[Adapty Developer CLI](https://adapty.io/docs/developer-cli). Manage apps, products, paywalls, placements, and access levels from your terminal.
|
|
7
|
+
[Adapty Developer CLI](https://adapty.io/docs/developer-cli). Manage apps, products, paywalls, placements, flows, and access levels from your terminal.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -77,6 +77,20 @@ adapty placements create --app UUID [flags]
|
|
|
77
77
|
adapty placements update --app UUID PLACEMENT_ID [flags]
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### Flows
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
adapty flows list --app UUID [--page N] [--page-size N]
|
|
84
|
+
adapty flows get --app UUID FLOW_ID
|
|
85
|
+
adapty flows create --app UUID --name "Name"
|
|
86
|
+
adapty flows config get --app UUID FLOW_ID
|
|
87
|
+
adapty flows config update --app UUID FLOW_ID (--config JSON | --config-file PATH|-) [--remote-configs JSON] [--expected-updated-at MS]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A freshly created flow has no config until the first `flows config update`; `flows config get` returns 404
|
|
91
|
+
until then. Pass `--expected-updated-at` (the `updated_at` from a prior `config get`) to fail instead of
|
|
92
|
+
overwriting a concurrent dashboard edit.
|
|
93
|
+
|
|
80
94
|
### Access Levels
|
|
81
95
|
|
|
82
96
|
```sh
|
|
@@ -136,6 +150,28 @@ adapty asa ads create --ad-group UUID --creative-id 4321 --name "Summer ad"
|
|
|
136
150
|
adapty asa ads update AD_ID [--name "..."] [--status PAUSED]
|
|
137
151
|
```
|
|
138
152
|
|
|
153
|
+
Bulk structure creation submits a whole tree — campaigns with nested ad groups, keywords, negative keywords
|
|
154
|
+
and ads — in one call. The input is a JSON structure (the natural path for an AI agent: generate it, pipe it
|
|
155
|
+
in), or a native Apple Ads template converted server-side. The whole payload is validated before anything is
|
|
156
|
+
created — a rejection lists every bad node; on acceptance the command polls progress and prints the final
|
|
157
|
+
report (`success` / `partial` / `failed`, with per-object failures):
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
adapty asa campaigns bulk-create --file structure.json # JSON structure, waits and reports
|
|
161
|
+
cat structure.json | adapty asa campaigns bulk-create --file - # same, from stdin
|
|
162
|
+
adapty asa campaigns bulk-create --file structure.json --no-wait
|
|
163
|
+
adapty asa campaigns bulk-create --from-file Campaign_And_Adgroup_Template.xlsx --org-id 1234567
|
|
164
|
+
adapty asa campaigns bulk-create --from-file keywords_template.csv --org-id 1234567 --preview
|
|
165
|
+
adapty asa campaigns bulk-status OPERATION_ID
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The structure is `{campaign_group_id | campaign_group_internal_id, campaigns: [...]}` where each campaign
|
|
169
|
+
node either creates (`payload`) or addresses an existing campaign by id (an *anchor*, optionally carrying
|
|
170
|
+
`update_payload`), and nests `ad_groups` with `keywords`, `negative_keywords` and `ads` the same way.
|
|
171
|
+
`--from-file` takes the native Apple Ads templates and needs `--org-id` (the Apple org id from
|
|
172
|
+
`asa orgs list`); `--preview` prints the converted request without creating anything. Conversion issues are
|
|
173
|
+
reported with their sheet, row and column.
|
|
174
|
+
|
|
139
175
|
Keywords are always applied as a batch, at most 100 per call, and a partial rejection is reported per item:
|
|
140
176
|
|
|
141
177
|
```sh
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaBulkOperationStateDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaCampaignsBulkCreate extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'from-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'org-id': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'poll-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
preview: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<AsaBulkOperationStateDTO | Record<string, unknown>>;
|
|
19
|
+
private convertTemplate;
|
|
20
|
+
private countStructure;
|
|
21
|
+
private formatIssue;
|
|
22
|
+
private readStructure;
|
|
23
|
+
private report;
|
|
24
|
+
private waitForCompletion;
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
5
|
+
import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
|
|
6
|
+
import { idempotencyFlags } from '../../../lib/asa-flags.js';
|
|
7
|
+
import { printResponse } from '../../../lib/output.js';
|
|
8
|
+
const TERMINAL_STATUSES = new Set(['failed', 'partial', 'success']);
|
|
9
|
+
export default class AsaCampaignsBulkCreate extends Command {
|
|
10
|
+
static description = 'Create a whole campaign structure (campaigns → ad groups → keywords/negative keywords/ads) in one bulk operation';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> asa campaigns bulk-create --file structure.json',
|
|
14
|
+
'cat structure.json | <%= config.bin %> asa campaigns bulk-create --file -',
|
|
15
|
+
'<%= config.bin %> asa campaigns bulk-create --from-file Campaign_And_Adgroup_Template.xlsx --org-id 1234567',
|
|
16
|
+
'<%= config.bin %> asa campaigns bulk-create --from-file keywords_template.csv --org-id 1234567 --preview',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...confirmFlags,
|
|
20
|
+
...idempotencyFlags,
|
|
21
|
+
file: Flags.string({
|
|
22
|
+
description: 'JSON file with the campaign structure (the POST /bulk-operations/ body); use "-" for stdin',
|
|
23
|
+
exactlyOne: ['file', 'from-file'],
|
|
24
|
+
}),
|
|
25
|
+
'from-file': Flags.string({
|
|
26
|
+
description: 'Apple Ads bulk template (Campaign_And_Adgroup_Template.xlsx or keywords .csv) to convert and submit',
|
|
27
|
+
}),
|
|
28
|
+
'org-id': Flags.integer({
|
|
29
|
+
description: 'Apple org id (campaign_group_id) the converted template will target; required with --from-file',
|
|
30
|
+
}),
|
|
31
|
+
'poll-interval': Flags.integer({ default: 5, description: 'Seconds between progress polls' }),
|
|
32
|
+
preview: Flags.boolean({
|
|
33
|
+
description: 'With --from-file: print the converted request and stop, nothing is created',
|
|
34
|
+
}),
|
|
35
|
+
timeout: Flags.integer({ default: 900, description: 'Max seconds to wait for the operation to finish' }),
|
|
36
|
+
wait: Flags.boolean({
|
|
37
|
+
allowNo: true,
|
|
38
|
+
default: true,
|
|
39
|
+
description: 'Poll the operation until it finishes; --no-wait returns the operation id immediately',
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { flags } = await this.parse(AsaCampaignsBulkCreate);
|
|
44
|
+
const client = await createAsaClient(this.config);
|
|
45
|
+
const body = flags['from-file'] ? await this.convertTemplate(client, flags) : await this.readStructure(flags.file);
|
|
46
|
+
if (flags.preview) {
|
|
47
|
+
this.log(JSON.stringify(body, null, 2));
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
const counts = this.countStructure(body);
|
|
51
|
+
const summary = `Bulk-create: ${counts.campaigns} campaign node(s), ${counts.adGroups} ad group(s), ` +
|
|
52
|
+
`${counts.keywords} keyword(s), ${counts.negativeKeywords} negative keyword(s), ${counts.ads} ad(s)`;
|
|
53
|
+
await confirmMutation(this, { body, method: 'POST', path: '/bulk-operations/', summary }, flags.yes);
|
|
54
|
+
const { replayed, result } = await asaWrite(client, 'post', '/bulk-operations', {
|
|
55
|
+
body,
|
|
56
|
+
idempotencyKey: flags['idempotency-key'],
|
|
57
|
+
});
|
|
58
|
+
noteReplay(replayed, this.log.bind(this));
|
|
59
|
+
this.log(`Operation accepted: ${result.operation_id}`);
|
|
60
|
+
if (!flags.wait) {
|
|
61
|
+
this.log(`Check progress with: adapty asa campaigns bulk-status ${result.operation_id}`);
|
|
62
|
+
return { operation_id: result.operation_id };
|
|
63
|
+
}
|
|
64
|
+
const state = await this.waitForCompletion(client, result.operation_id, flags['poll-interval'], flags.timeout);
|
|
65
|
+
this.report(state);
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
async convertTemplate(client, flags) {
|
|
69
|
+
if (flags['org-id'] === undefined)
|
|
70
|
+
this.error('--org-id is required with --from-file.', { exit: 2 });
|
|
71
|
+
const path = flags['from-file'];
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = await readFile(path);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
this.error(`Cannot read file: ${path}`, { exit: 2 });
|
|
78
|
+
}
|
|
79
|
+
const form = new FormData();
|
|
80
|
+
form.append('file', new Blob([new Uint8Array(content)]), basename(path));
|
|
81
|
+
form.append('campaign_group_id', String(flags['org-id']));
|
|
82
|
+
const result = await client.postForm('/bulk-operations/convert', form);
|
|
83
|
+
for (const warning of result.warnings)
|
|
84
|
+
this.warn(this.formatIssue(warning));
|
|
85
|
+
if (result.errors.length > 0 || result.request === null) {
|
|
86
|
+
for (const issue of result.errors)
|
|
87
|
+
this.log(this.formatIssue(issue));
|
|
88
|
+
this.error(`Template conversion failed with ${result.errors.length} error(s); nothing was created.`, { exit: 2 });
|
|
89
|
+
}
|
|
90
|
+
return result.request;
|
|
91
|
+
}
|
|
92
|
+
countStructure(body) {
|
|
93
|
+
const counts = { adGroups: 0, ads: 0, campaigns: 0, keywords: 0, negativeKeywords: 0 };
|
|
94
|
+
const campaigns = Array.isArray(body.campaigns) ? body.campaigns : [];
|
|
95
|
+
counts.campaigns = campaigns.length;
|
|
96
|
+
for (const campaign of campaigns) {
|
|
97
|
+
if (typeof campaign !== 'object' || campaign === null)
|
|
98
|
+
continue;
|
|
99
|
+
const campaignNode = campaign;
|
|
100
|
+
counts.negativeKeywords += Array.isArray(campaignNode.negative_keywords) ? campaignNode.negative_keywords.length : 0;
|
|
101
|
+
const adGroups = Array.isArray(campaignNode.ad_groups) ? campaignNode.ad_groups : [];
|
|
102
|
+
counts.adGroups += adGroups.length;
|
|
103
|
+
for (const adGroup of adGroups) {
|
|
104
|
+
if (typeof adGroup !== 'object' || adGroup === null)
|
|
105
|
+
continue;
|
|
106
|
+
const adGroupNode = adGroup;
|
|
107
|
+
counts.keywords += Array.isArray(adGroupNode.keywords) ? adGroupNode.keywords.length : 0;
|
|
108
|
+
counts.negativeKeywords += Array.isArray(adGroupNode.negative_keywords) ? adGroupNode.negative_keywords.length : 0;
|
|
109
|
+
counts.ads += Array.isArray(adGroupNode.ads) ? adGroupNode.ads.length : 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return counts;
|
|
113
|
+
}
|
|
114
|
+
formatIssue(issue) {
|
|
115
|
+
const place = [issue.sheet, issue.row === null ? undefined : `row ${issue.row}`, issue.column ?? undefined]
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join(', ');
|
|
118
|
+
return place ? `${place}: ${issue.message}` : issue.message;
|
|
119
|
+
}
|
|
120
|
+
async readStructure(source) {
|
|
121
|
+
let raw;
|
|
122
|
+
if (source === '-') {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
for await (const chunk of process.stdin)
|
|
125
|
+
chunks.push(chunk);
|
|
126
|
+
raw = Buffer.concat(chunks).toString('utf8');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
try {
|
|
130
|
+
raw = await readFile(source, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
this.error(`Cannot read file: ${source}`, { exit: 2 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(raw);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
this.error('The structure is not valid JSON.', { exit: 2 });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
report(state) {
|
|
144
|
+
const { counts } = state;
|
|
145
|
+
this.log(`Finished: ${state.status} — ${counts.applied}/${counts.total} applied, ${counts.failed} failed`);
|
|
146
|
+
const failedObjects = state.objects.filter((object) => object.status === 'failed');
|
|
147
|
+
for (const object of failedObjects) {
|
|
148
|
+
printResponse(object, this.log.bind(this));
|
|
149
|
+
this.log('---');
|
|
150
|
+
}
|
|
151
|
+
if (state.status === 'failed')
|
|
152
|
+
this.error('Bulk operation failed.', { exit: 1 });
|
|
153
|
+
if (state.status === 'partial')
|
|
154
|
+
this.warn('Some objects were not created — see the failures above.');
|
|
155
|
+
}
|
|
156
|
+
async waitForCompletion(client, operationId, pollIntervalSeconds, timeoutSeconds) {
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
let lastProgress = '';
|
|
159
|
+
for (;;) {
|
|
160
|
+
const state = await client.get(`/bulk-operations/${operationId}`);
|
|
161
|
+
const progress = `${state.status}: ${state.counts.applied}/${state.counts.total} applied, ${state.counts.failed} failed`;
|
|
162
|
+
if (progress !== lastProgress) {
|
|
163
|
+
this.log(progress);
|
|
164
|
+
lastProgress = progress;
|
|
165
|
+
}
|
|
166
|
+
if (TERMINAL_STATUSES.has(state.status))
|
|
167
|
+
return state;
|
|
168
|
+
if ((Date.now() - startedAt) / 1000 >= timeoutSeconds) {
|
|
169
|
+
this.warn(`Still ${state.status} after ${timeoutSeconds}s — check later with: adapty asa campaigns bulk-status ${operationId}`);
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, pollIntervalSeconds * 1000);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { AsaBulkOperationStateDTO } from '../../../lib/asa-schemas.js';
|
|
3
|
+
export default class AsaCampaignsBulkStatus extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
'operation-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
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<AsaBulkOperationStateDTO>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
+
import { asaPaginationFlags } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { isValidUuid, paginationParams } from '../../../lib/flags.js';
|
|
5
|
+
import { printResponse } from '../../../lib/output.js';
|
|
6
|
+
export default class AsaCampaignsBulkStatus extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
'operation-id': Args.string({ description: 'Bulk operation ID returned by bulk-create', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Progress board of one bulk operation: counts, pipelines and the per-object log';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = ['<%= config.bin %> asa campaigns bulk-status 660e8400-e29b-41d4-a716-446655440001'];
|
|
13
|
+
static flags = {
|
|
14
|
+
...asaPaginationFlags,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args, flags } = await this.parse(AsaCampaignsBulkStatus);
|
|
18
|
+
const operationId = args['operation-id'];
|
|
19
|
+
if (!isValidUuid(operationId))
|
|
20
|
+
this.error('Invalid operation ID format.', { exit: 2 });
|
|
21
|
+
const client = await createAsaClient(this.config);
|
|
22
|
+
const state = await client.get(`/bulk-operations/${operationId}`, paginationParams(flags));
|
|
23
|
+
printResponse(state, this.log.bind(this));
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -17,7 +17,8 @@ export default class AsaConnect extends Command {
|
|
|
17
17
|
this.log(`If the browser doesn't open, visit: ${authUrl}\n`);
|
|
18
18
|
this.log('The link is valid for one hour. Sign in to the Adapty dashboard in that browser first — the last');
|
|
19
19
|
this.log('step is authorized by the dashboard session, not by this CLI.');
|
|
20
|
-
|
|
20
|
+
if (process.stdin.isTTY === true)
|
|
21
|
+
await open(authUrl).catch(() => false);
|
|
21
22
|
if (!flags.wait)
|
|
22
23
|
return { auth_url: authUrl };
|
|
23
24
|
const deadline = Date.now() + flags.timeout * 1000;
|
|
@@ -29,11 +29,13 @@ export default class AuthLogin extends Command {
|
|
|
29
29
|
}
|
|
30
30
|
this.log(`\nYour code: ${device.user_code}\n`);
|
|
31
31
|
this.log(`If browser doesn't open, visit: ${device.verification_uri_complete}\n`);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
if (process.stdin.isTTY === true) {
|
|
33
|
+
try {
|
|
34
|
+
await open(device.verification_uri_complete);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// browser open failed silently — URL already printed
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
this.log('Waiting for authorization...');
|
|
39
41
|
let interval = Math.max((device.interval_seconds || 5) * 1000, 5000);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowConfigDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class FlowsConfigGet extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
flow_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
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<FlowConfigDTO>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
3
|
+
import { appFlag, isValidUuid } from '../../../lib/flags.js';
|
|
4
|
+
import { printResponse } from '../../../lib/output.js';
|
|
5
|
+
export default class FlowsConfigGet extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Read the flow builder config (404 until the config has been written at least once)';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['<%= config.bin %> flows config get --app UUID 550e8400-e29b-41d4-a716-446655440000'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...appFlag,
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(FlowsConfigGet);
|
|
17
|
+
if (!isValidUuid(args.flow_id)) {
|
|
18
|
+
this.error('Invalid flow ID format.', { exit: 2 });
|
|
19
|
+
}
|
|
20
|
+
const client = await createAuthenticatedClient(this.config);
|
|
21
|
+
const result = await client.get(`/apps/${flags.app}/flows/${args.flow_id}/config`);
|
|
22
|
+
printResponse(result, this.log.bind(this));
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowConfigDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class FlowsConfigUpdate extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
flow_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
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'config-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'expected-updated-at': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'remote-configs': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<FlowConfigDTO>;
|
|
18
|
+
private parseJson;
|
|
19
|
+
private readConfig;
|
|
20
|
+
private readStdin;
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
4
|
+
import { appFlag, isValidUuid } from '../../../lib/flags.js';
|
|
5
|
+
import { printResponse } from '../../../lib/output.js';
|
|
6
|
+
export default class FlowsConfigUpdate extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Write the flow builder config (creates the first version, or edits/forks the current one)';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> flows config update --app UUID FLOW_UUID --config-file config.json',
|
|
14
|
+
'cat config.json | <%= config.bin %> flows config update --app UUID FLOW_UUID --config-file -',
|
|
15
|
+
'<%= config.bin %> flows config update --app UUID FLOW_UUID --config \'{"screens":[],"locales":[]}\' --expected-updated-at 1755001800000',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...appFlag,
|
|
19
|
+
config: Flags.string({
|
|
20
|
+
description: 'Builder config as a JSON string',
|
|
21
|
+
exactlyOne: ['config', 'config-file'],
|
|
22
|
+
}),
|
|
23
|
+
'config-file': Flags.string({
|
|
24
|
+
description: 'JSON file with the builder config, or - to read stdin',
|
|
25
|
+
exactlyOne: ['config', 'config-file'],
|
|
26
|
+
}),
|
|
27
|
+
'expected-updated-at': Flags.integer({
|
|
28
|
+
description: 'Optimistic lock: the `updated_at` from a prior config read. Omit for last-write-wins.',
|
|
29
|
+
}),
|
|
30
|
+
'remote-configs': Flags.string({
|
|
31
|
+
description: 'JSON array of remote config entries: [{locale, data}]',
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(FlowsConfigUpdate);
|
|
36
|
+
if (!isValidUuid(args.flow_id)) {
|
|
37
|
+
this.error('Invalid flow ID format.', { exit: 2 });
|
|
38
|
+
}
|
|
39
|
+
const config = await this.readConfig(flags);
|
|
40
|
+
const body = { config };
|
|
41
|
+
if (flags['remote-configs'] !== undefined) {
|
|
42
|
+
body.remote_configs = this.parseJson(flags['remote-configs'], '--remote-configs');
|
|
43
|
+
}
|
|
44
|
+
if (flags['expected-updated-at'] !== undefined) {
|
|
45
|
+
body.expected_updated_at = flags['expected-updated-at'];
|
|
46
|
+
}
|
|
47
|
+
const client = await createAuthenticatedClient(this.config);
|
|
48
|
+
const result = await client.put(`/apps/${flags.app}/flows/${args.flow_id}/config`, body);
|
|
49
|
+
this.log('Flow config saved!');
|
|
50
|
+
printResponse(result, this.log.bind(this));
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
parseJson(raw, label) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.error(`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`, { exit: 2 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async readConfig(flags) {
|
|
62
|
+
if (flags.config !== undefined) {
|
|
63
|
+
return this.parseJson(flags.config, '--config');
|
|
64
|
+
}
|
|
65
|
+
const path = flags['config-file'];
|
|
66
|
+
if (path === undefined) {
|
|
67
|
+
this.error('Provide --config or --config-file.', { exit: 2 });
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = path === '-' ? await this.readStdin() : await readFile(path, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.error(`Could not read ${path}.`, { exit: 2 });
|
|
75
|
+
}
|
|
76
|
+
return this.parseJson(raw, path);
|
|
77
|
+
}
|
|
78
|
+
async readStdin() {
|
|
79
|
+
const chunks = [];
|
|
80
|
+
for await (const chunk of process.stdin)
|
|
81
|
+
chunks.push(chunk);
|
|
82
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowDTO } from '../../lib/api-schemas.js';
|
|
3
|
+
export default class FlowsCreate extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<FlowDTO>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
|
+
import { appFlag } from '../../lib/flags.js';
|
|
4
|
+
import { printResponse } from '../../lib/output.js';
|
|
5
|
+
export default class FlowsCreate extends Command {
|
|
6
|
+
static description = 'Create a flow (row only — write its config with `flows config update`)';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = ['<%= config.bin %> flows create --app UUID --name "Onboarding"'];
|
|
9
|
+
static flags = {
|
|
10
|
+
...appFlag,
|
|
11
|
+
name: Flags.string({ description: 'Flow name', required: true }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(FlowsCreate);
|
|
15
|
+
const client = await createAuthenticatedClient(this.config);
|
|
16
|
+
const body = { name: flags.name };
|
|
17
|
+
const result = await client.post(`/apps/${flags.app}/flows`, body);
|
|
18
|
+
this.log('Flow created!');
|
|
19
|
+
printResponse(result, this.log.bind(this));
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowDTO } from '../../lib/api-schemas.js';
|
|
3
|
+
export default class FlowsGet extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
flow_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
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<FlowDTO>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
|
+
import { appFlag, isValidUuid } from '../../lib/flags.js';
|
|
4
|
+
import { printResponse } from '../../lib/output.js';
|
|
5
|
+
export default class FlowsGet extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Get flow details';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['<%= config.bin %> flows get --app UUID 550e8400-e29b-41d4-a716-446655440000'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...appFlag,
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(FlowsGet);
|
|
17
|
+
if (!isValidUuid(args.flow_id)) {
|
|
18
|
+
this.error('Invalid flow ID format.', { exit: 2 });
|
|
19
|
+
}
|
|
20
|
+
const client = await createAuthenticatedClient(this.config);
|
|
21
|
+
const result = await client.get(`/apps/${flags.app}/flows/${args.flow_id}`);
|
|
22
|
+
printResponse(result, this.log.bind(this));
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowDTO } from '../../lib/api-schemas.js';
|
|
3
|
+
import { type PaginatedResponse } from '../../lib/flags.js';
|
|
4
|
+
export default class FlowsList 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
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<PaginatedResponse<FlowDTO>>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
3
|
+
import { appFlag, paginationFlags, paginationParams } from '../../lib/flags.js';
|
|
4
|
+
import { printList } from '../../lib/output.js';
|
|
5
|
+
export default class FlowsList extends Command {
|
|
6
|
+
static description = 'List flows for an app';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = ['<%= config.bin %> flows list --app 550e8400-...'];
|
|
9
|
+
static flags = {
|
|
10
|
+
...appFlag,
|
|
11
|
+
...paginationFlags,
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(FlowsList);
|
|
15
|
+
const client = await createAuthenticatedClient(this.config);
|
|
16
|
+
const result = await client.get(`/apps/${flags.app}/flows`, paginationParams(flags));
|
|
17
|
+
printList(result.data, this.log.bind(this), result.meta.pagination);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare class ApiClient {
|
|
|
20
20
|
constructor(opts?: ApiClientOptions);
|
|
21
21
|
get<T = unknown>(path: string, params?: QueryParams): Promise<T>;
|
|
22
22
|
post<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
|
|
23
|
+
postForm<T = unknown>(path: string, form: FormData, params?: QueryParams, opts?: RequestOptions): Promise<T>;
|
|
23
24
|
put<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
|
|
24
25
|
private buildUrl;
|
|
25
26
|
private isRetryableRateLimit;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -29,6 +29,9 @@ export class ApiClient {
|
|
|
29
29
|
method: 'POST',
|
|
30
30
|
}, opts);
|
|
31
31
|
}
|
|
32
|
+
async postForm(path, form, params, opts) {
|
|
33
|
+
return this.request(this.buildUrl(path, params), { body: form, method: 'POST' }, opts);
|
|
34
|
+
}
|
|
32
35
|
async put(path, body, params, opts) {
|
|
33
36
|
return this.request(this.buildUrl(path, params), {
|
|
34
37
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -60,7 +63,7 @@ export class ApiClient {
|
|
|
60
63
|
const headers = {
|
|
61
64
|
'User-Agent': this.userAgent,
|
|
62
65
|
};
|
|
63
|
-
if (init.body) {
|
|
66
|
+
if (init.body && !(init.body instanceof FormData)) {
|
|
64
67
|
headers['Content-Type'] = 'application/json';
|
|
65
68
|
}
|
|
66
69
|
if (this.token) {
|