adapty 0.8.5 → 0.8.7
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 +7 -1
- package/dist/cli/base/adapty/adapty-command.d.ts +21 -0
- package/dist/cli/base/adapty/adapty-command.js +47 -0
- package/dist/cli/base/adapty/build.d.ts +11 -0
- package/dist/cli/base/adapty/build.js +13 -0
- package/dist/cli/base/adapty/index.d.ts +5 -0
- package/dist/cli/base/adapty/index.js +3 -0
- package/dist/cli/base/adapty/openSession.d.ts +22 -0
- package/dist/cli/base/adapty/openSession.js +25 -0
- package/dist/cli/base/base-command.d.ts +30 -0
- package/dist/cli/base/base-command.js +64 -0
- package/dist/cli/commands/apps/create.d.ts +19 -0
- package/dist/cli/commands/apps/create.js +68 -0
- package/dist/cli/commands/apps/get.d.ts +10 -0
- package/dist/cli/commands/apps/get.js +14 -0
- package/dist/cli/commands/apps/list.d.ts +11 -0
- package/dist/cli/commands/apps/list.js +14 -0
- package/dist/cli/commands/apps/update.d.ts +15 -0
- package/dist/cli/commands/apps/update.js +30 -0
- package/dist/cli/commands/auth/login.d.ts +11 -0
- package/dist/cli/commands/auth/login.js +68 -0
- package/dist/cli/commands/auth/logout.d.ts +12 -0
- package/dist/cli/commands/auth/logout.js +28 -0
- package/dist/cli/commands/auth/revoke.d.ts +13 -0
- package/dist/cli/commands/auth/revoke.js +32 -0
- package/dist/cli/commands/auth/status/index.d.ts +7 -0
- package/dist/cli/commands/auth/status/index.js +22 -0
- package/dist/cli/commands/auth/status/lib/render.d.ts +3 -0
- package/dist/cli/commands/auth/status/lib/render.js +14 -0
- package/dist/cli/commands/auth/status/lib/result.d.ts +15 -0
- package/dist/cli/commands/auth/status/lib/result.js +1 -0
- package/dist/cli/commands/auth/whoami.d.ts +10 -0
- package/dist/cli/commands/auth/whoami.js +16 -0
- package/dist/cli/errors.d.ts +39 -0
- package/dist/cli/errors.js +92 -0
- package/dist/cli/flags.d.ts +16 -0
- package/dist/cli/flags.js +27 -0
- package/dist/cli/views/list.d.ts +6 -0
- package/dist/cli/views/list.js +10 -0
- package/dist/cli/views/record.d.ts +9 -0
- package/dist/cli/views/record.js +14 -0
- package/dist/commands/apps/create.d.ts +1 -14
- package/dist/commands/apps/create.js +2 -57
- package/dist/commands/apps/get.d.ts +1 -11
- package/dist/commands/apps/get.js +2 -22
- package/dist/commands/apps/list.d.ts +1 -13
- package/dist/commands/apps/list.js +2 -19
- package/dist/commands/apps/update.d.ts +1 -16
- package/dist/commands/apps/update.js +2 -41
- package/dist/commands/asa/keywords/recommend.d.ts +20 -0
- package/dist/commands/asa/keywords/recommend.js +102 -0
- package/dist/commands/asa/whoami.js +6 -2
- package/dist/commands/auth/login.d.ts +1 -8
- package/dist/commands/auth/login.js +2 -122
- package/dist/commands/auth/logout.d.ts +1 -9
- package/dist/commands/auth/logout.js +2 -18
- package/dist/commands/auth/revoke.d.ts +1 -9
- package/dist/commands/auth/revoke.js +2 -21
- package/dist/commands/auth/status.d.ts +1 -7
- package/dist/commands/auth/status.js +2 -26
- package/dist/commands/auth/whoami.d.ts +1 -7
- package/dist/commands/auth/whoami.js +2 -15
- package/dist/lib/api-schemas.d.ts +7 -3
- package/dist/lib/asa-schemas.d.ts +41 -1
- package/dist/sdk/adapty/access-levels.d.ts +15 -0
- package/dist/sdk/adapty/access-levels.js +4 -0
- package/dist/sdk/adapty/apps/create.d.ts +26 -0
- package/dist/sdk/adapty/apps/create.js +36 -0
- package/dist/sdk/adapty/apps/index.d.ts +13 -0
- package/dist/sdk/adapty/apps/index.js +9 -0
- package/dist/sdk/adapty/apps/model.d.ts +24 -0
- package/dist/sdk/adapty/apps/model.js +10 -0
- package/dist/sdk/adapty/apps/resource.d.ts +19 -0
- package/dist/sdk/adapty/apps/resource.js +23 -0
- package/dist/sdk/adapty/apps/update.d.ts +16 -0
- package/dist/sdk/adapty/apps/update.js +25 -0
- package/dist/sdk/adapty/auth/device-code.d.ts +8 -0
- package/dist/sdk/adapty/auth/device-code.js +19 -0
- package/dist/sdk/adapty/auth/index.d.ts +7 -0
- package/dist/sdk/adapty/auth/index.js +5 -0
- package/dist/sdk/adapty/auth/model.d.ts +15 -0
- package/dist/sdk/adapty/auth/model.js +6 -0
- package/dist/sdk/adapty/auth/poll-token.d.ts +8 -0
- package/dist/sdk/adapty/auth/poll-token.js +58 -0
- package/dist/sdk/adapty/auth/resource.d.ts +17 -0
- package/dist/sdk/adapty/auth/resource.js +21 -0
- package/dist/sdk/adapty/errors.d.ts +10 -0
- package/dist/sdk/adapty/errors.js +38 -0
- package/dist/sdk/adapty/index.d.ts +36 -0
- package/dist/sdk/adapty/index.js +27 -0
- package/dist/sdk/adapty/pagination.d.ts +23 -0
- package/dist/sdk/adapty/pagination.js +1 -0
- package/dist/sdk/core/auth/device-flow.d.ts +46 -0
- package/dist/sdk/core/auth/device-flow.js +62 -0
- package/dist/sdk/core/clock.d.ts +7 -0
- package/dist/sdk/core/clock.js +21 -0
- package/dist/sdk/core/errors.d.ts +81 -0
- package/dist/sdk/core/errors.js +84 -0
- package/dist/sdk/core/http/client.d.ts +46 -0
- package/dist/sdk/core/http/client.js +127 -0
- package/dist/sdk/core/http/index.d.ts +11 -0
- package/dist/sdk/core/http/index.js +7 -0
- package/dist/sdk/core/http/policies.d.ts +14 -0
- package/dist/sdk/core/http/policies.js +29 -0
- package/dist/sdk/core/http/retry.d.ts +27 -0
- package/dist/sdk/core/http/retry.js +22 -0
- package/dist/sdk/core/http/url.d.ts +3 -0
- package/dist/sdk/core/http/url.js +19 -0
- package/dist/sdk/core/session.d.ts +20 -0
- package/dist/sdk/core/session.js +69 -0
- package/dist/sdk/core/testing.d.ts +25 -0
- package/dist/sdk/core/testing.js +46 -0
- package/dist/sdk/core/validation.d.ts +10 -0
- package/dist/sdk/core/validation.js +14 -0
- package/oclif.manifest.json +211 -148
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -34,9 +34,14 @@ Other auth commands:
|
|
|
34
34
|
adapty auth whoami # verify token, show user info
|
|
35
35
|
adapty auth status # show local auth state
|
|
36
36
|
adapty auth logout # clear stored token (local only)
|
|
37
|
-
adapty auth revoke # revoke token
|
|
37
|
+
adapty auth revoke # revoke active token and clear any matching stored session
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
`auth revoke` uses `ADAPTY_TOKEN` when set, otherwise the stored token. A different token in the
|
|
41
|
+
session file is preserved. After revoking an environment token, unset `ADAPTY_TOKEN`; a preserved
|
|
42
|
+
stored session will then become active again. With no token, revoke succeeds without a request
|
|
43
|
+
and returns `{"status":"not_authenticated"}` under `--json`.
|
|
44
|
+
|
|
40
45
|
## Commands
|
|
41
46
|
|
|
42
47
|
All resource commands require `--app APP_ID` (UUID). Use `adapty apps list` to find your app ID.
|
|
@@ -180,6 +185,7 @@ Keywords are always applied as a batch, at most 100 per call, and a partial reje
|
|
|
180
185
|
|
|
181
186
|
```sh
|
|
182
187
|
adapty asa keywords list
|
|
188
|
+
adapty asa keywords recommend --adam-id 1668337467 --type brand|generic|competitor [--country US...]
|
|
183
189
|
adapty asa keywords add --ad-group UUID --text "running shoes" --text "trail shoes" [--bid 1.20] [--match-type EXACT]
|
|
184
190
|
adapty asa keywords add --ad-group UUID --from-file keywords.txt
|
|
185
191
|
adapty asa keywords update KEYWORD_ID [KEYWORD_ID...] [--bid 2.00] [--status PAUSED]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { ResolvedSession } from './openSession.js';
|
|
3
|
+
import type { Adapty } from '../../../sdk/adapty/index.js';
|
|
4
|
+
export type AuthenticatedSession = ResolvedSession & {
|
|
5
|
+
token: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Commands that require a token; auth commands other than `whoami` also work without one.
|
|
9
|
+
*
|
|
10
|
+
* "Needs authorization" becomes a fact of the type system, written in what the command extends
|
|
11
|
+
* instead of checked inside run(). One lazy class would put a "what if we are not logged in"
|
|
12
|
+
* branch into all 75 commands, and one of them would forget it.
|
|
13
|
+
*/
|
|
14
|
+
export declare abstract class AdaptyCommand extends BaseCommand {
|
|
15
|
+
#private;
|
|
16
|
+
static enableJsonFlag: boolean;
|
|
17
|
+
protected get adapty(): Adapty;
|
|
18
|
+
/** Narrowed once here, so nothing downstream re-checks the token. */
|
|
19
|
+
protected get session(): AuthenticatedSession;
|
|
20
|
+
protected init(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AuthRequiredError } from '../../../sdk/core/errors.js';
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { build } from './build.js';
|
|
4
|
+
import { openSession } from './openSession.js';
|
|
5
|
+
/**
|
|
6
|
+
* Commands that require a token; auth commands other than `whoami` also work without one.
|
|
7
|
+
*
|
|
8
|
+
* "Needs authorization" becomes a fact of the type system, written in what the command extends
|
|
9
|
+
* instead of checked inside run(). One lazy class would put a "what if we are not logged in"
|
|
10
|
+
* branch into all 75 commands, and one of them would forget it.
|
|
11
|
+
*/
|
|
12
|
+
export class AdaptyCommand extends BaseCommand {
|
|
13
|
+
// Keep one own static so oclif's manifest cache walks through this intermediate class and
|
|
14
|
+
// includes statics inherited from BaseCommand.
|
|
15
|
+
static enableJsonFlag = true;
|
|
16
|
+
#adapty;
|
|
17
|
+
#resolved;
|
|
18
|
+
get adapty() {
|
|
19
|
+
this.#adapty ??= build(this.session, {
|
|
20
|
+
config: this.config,
|
|
21
|
+
signal: this.signal,
|
|
22
|
+
warn: message => this.warn(message),
|
|
23
|
+
});
|
|
24
|
+
return this.#adapty;
|
|
25
|
+
}
|
|
26
|
+
/** Narrowed once here, so nothing downstream re-checks the token. */
|
|
27
|
+
get session() {
|
|
28
|
+
const resolved = this.#resolved;
|
|
29
|
+
if (resolved === undefined) {
|
|
30
|
+
// A bug, not a user error, and deliberately not an SdkError: it lands as exit 1 with a
|
|
31
|
+
// stack instead of passing for a normal scenario.
|
|
32
|
+
throw new Error('session is available only after init()');
|
|
33
|
+
}
|
|
34
|
+
if (resolved.token === undefined) {
|
|
35
|
+
throw new AuthRequiredError('missing');
|
|
36
|
+
}
|
|
37
|
+
return { ...resolved, token: resolved.token };
|
|
38
|
+
}
|
|
39
|
+
async init() {
|
|
40
|
+
await super.init();
|
|
41
|
+
// The session is read here, the sdk built lazily from run(), because this.parse() runs
|
|
42
|
+
// after init(). Demanding the token here would answer `apps list --pge 2` with "Not
|
|
43
|
+
// authenticated" instead of "Nonexistent flag": input errors come before state errors.
|
|
44
|
+
// Commands also validate business rules before accessing this.adapty or this.session.
|
|
45
|
+
this.#resolved = await openSession(this.config);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ResolvedSession } from './openSession.js';
|
|
2
|
+
import type { Adapty } from '../../../sdk/adapty/index.js';
|
|
3
|
+
import type { Config } from '@oclif/core';
|
|
4
|
+
type CommandContext = {
|
|
5
|
+
config: Config;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
warn: (message: string) => void;
|
|
8
|
+
};
|
|
9
|
+
/** Shared by authenticated commands and login/revoke, which can run without a token. */
|
|
10
|
+
export declare const build: (session: ResolvedSession, context: CommandContext) => Adapty;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildUserAgent } from '../../../lib/client-from-config.js';
|
|
2
|
+
import { createAdapty } from '../../../sdk/adapty/index.js';
|
|
3
|
+
/** Shared by authenticated commands and login/revoke, which can run without a token. */
|
|
4
|
+
export const build = (session, context) => createAdapty({
|
|
5
|
+
baseUrl: session.apiUrl,
|
|
6
|
+
onRetry: ({ attempt, delayMs }) => {
|
|
7
|
+
context.warn(`Request failed, retrying in ${delayMs / 1000}s (attempt ${attempt + 1})`);
|
|
8
|
+
},
|
|
9
|
+
signal: context.signal,
|
|
10
|
+
token: session.token,
|
|
11
|
+
// Both CLI stacks use the same User-Agent while migration is in progress.
|
|
12
|
+
userAgent: buildUserAgent(context.config),
|
|
13
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SessionStore, SessionUser } from '../../../sdk/core/session.js';
|
|
2
|
+
import type { Config } from '@oclif/core';
|
|
3
|
+
/**
|
|
4
|
+
* The CLI's world resolved into what the sdk asks for: where to talk, as whom, and where the
|
|
5
|
+
* session lives. Env vars are read here and only here — the sdk never touches process.env, which
|
|
6
|
+
* is what lets the same product code run under an MCP server.
|
|
7
|
+
*/
|
|
8
|
+
export type ResolvedSession = {
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Where the token came from. With ADAPTY_TOKEN in play, "no token in the file" and "not
|
|
12
|
+
* authenticated" are different answers, and `auth status` has to say which one it means.
|
|
13
|
+
*/
|
|
14
|
+
source: 'env' | 'file' | 'none';
|
|
15
|
+
/** The store itself, not its path: `auth login`/`logout` write through it. */
|
|
16
|
+
store: SessionStore;
|
|
17
|
+
token?: string | undefined;
|
|
18
|
+
user?: SessionUser | undefined;
|
|
19
|
+
};
|
|
20
|
+
/** ADAPTY_TOKEN wins over the stored session, as the published CLI already does. */
|
|
21
|
+
export declare const resolveSession: (config: Config) => Promise<ResolvedSession>;
|
|
22
|
+
export declare const openSession: (config: Config) => Promise<ResolvedSession>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_ADAPTY_API_URL } from '../../../sdk/adapty/index.js';
|
|
2
|
+
import { createFileSessionStore } from '../../../sdk/core/session.js';
|
|
3
|
+
/** ADAPTY_TOKEN wins over the stored session, as the published CLI already does. */
|
|
4
|
+
export const resolveSession = async (config) => {
|
|
5
|
+
const apiUrl = process.env.ADAPTY_API_URL ?? DEFAULT_ADAPTY_API_URL;
|
|
6
|
+
// oclif's configDir already knows XDG on unix and LOCALAPPDATA on Windows
|
|
7
|
+
const store = createFileSessionStore(config.configDir);
|
|
8
|
+
const envToken = process.env.ADAPTY_TOKEN;
|
|
9
|
+
if (envToken !== undefined && envToken !== '') {
|
|
10
|
+
return { apiUrl, source: 'env', store, token: envToken };
|
|
11
|
+
}
|
|
12
|
+
const stored = await store.load();
|
|
13
|
+
if (stored === undefined) {
|
|
14
|
+
return { apiUrl, source: 'none', store };
|
|
15
|
+
}
|
|
16
|
+
return { apiUrl, source: 'file', store, token: stored.token, user: stored.user };
|
|
17
|
+
};
|
|
18
|
+
export const openSession = async (config) => {
|
|
19
|
+
const session = await resolveSession(config);
|
|
20
|
+
if (session.apiUrl !== DEFAULT_ADAPTY_API_URL) {
|
|
21
|
+
// oclif's warn() is silent under --json; the destination warning must remain visible.
|
|
22
|
+
process.stderr.write(`Warning: Using non-default API URL: ${session.apiUrl}\n`);
|
|
23
|
+
}
|
|
24
|
+
return session;
|
|
25
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { ErrorJson } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* What every command gets and nothing more: the output channel, cancellation, the single place
|
|
5
|
+
* where sdk errors become CLI errors. Product SDKs and sessions belong to their adapters.
|
|
6
|
+
*
|
|
7
|
+
* Whatever lives here is paid for by 75 commands, so what stays out matters as much: flags stay
|
|
8
|
+
* composable objects, views stay plain functions, and one command's needs stay in that command.
|
|
9
|
+
*/
|
|
10
|
+
export declare abstract class BaseCommand extends Command {
|
|
11
|
+
#private;
|
|
12
|
+
/** --json is a property of the CLI, not a per-command opt-in; `auth login` opts back out. */
|
|
13
|
+
static enableJsonFlag: boolean;
|
|
14
|
+
protected get signal(): AbortSignal;
|
|
15
|
+
protected catch(error: Error): Promise<unknown>;
|
|
16
|
+
protected toErrorJson(error: unknown): {
|
|
17
|
+
error: ErrorJson;
|
|
18
|
+
};
|
|
19
|
+
protected finally(error: Error | undefined): Promise<unknown>;
|
|
20
|
+
protected init(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* run() returns the data and render() prints it, so run()'s return type *is* the --json
|
|
23
|
+
* contract, checked by the compiler: change a response shape in the sdk and the command stops
|
|
24
|
+
* compiling instead of quietly changing what users parse.
|
|
25
|
+
*
|
|
26
|
+
* The guard is not what keeps --json clean — oclif's `log` is already silent then. It keeps the
|
|
27
|
+
* view from being built at all, so a human view may cost whatever it needs.
|
|
28
|
+
*/
|
|
29
|
+
protected render<T>(value: T, view: (value: T) => string): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { CliError, toCliError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* What every command gets and nothing more: the output channel, cancellation, the single place
|
|
5
|
+
* where sdk errors become CLI errors. Product SDKs and sessions belong to their adapters.
|
|
6
|
+
*
|
|
7
|
+
* Whatever lives here is paid for by 75 commands, so what stays out matters as much: flags stay
|
|
8
|
+
* composable objects, views stay plain functions, and one command's needs stay in that command.
|
|
9
|
+
*/
|
|
10
|
+
export class BaseCommand extends Command {
|
|
11
|
+
/** --json is a property of the CLI, not a per-command opt-in; `auth login` opts back out. */
|
|
12
|
+
static enableJsonFlag = true;
|
|
13
|
+
#abort = new AbortController();
|
|
14
|
+
// A field, not an inline closure: removeListener needs this very reference, or listeners pile
|
|
15
|
+
// up whenever several commands run in one process — which tests do.
|
|
16
|
+
#onSigint = () => {
|
|
17
|
+
this.#abort.abort();
|
|
18
|
+
};
|
|
19
|
+
get signal() {
|
|
20
|
+
return this.#abort.signal;
|
|
21
|
+
}
|
|
22
|
+
async catch(error) {
|
|
23
|
+
return super.catch(toCliError(error));
|
|
24
|
+
}
|
|
25
|
+
toErrorJson(error) {
|
|
26
|
+
if (error instanceof CliError) {
|
|
27
|
+
return { error: error.json };
|
|
28
|
+
}
|
|
29
|
+
// Error.message is non-enumerable; serializing the Error itself drops the explanation.
|
|
30
|
+
// Only expose public fields, not oclif's parser context or a command's internal state.
|
|
31
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
return {
|
|
33
|
+
error: {
|
|
34
|
+
code: 'code' in cause && typeof cause.code === 'string' ? cause.code : undefined,
|
|
35
|
+
message: cause.message,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async finally(error) {
|
|
40
|
+
process.removeListener('SIGINT', this.#onSigint);
|
|
41
|
+
return super.finally(error);
|
|
42
|
+
}
|
|
43
|
+
async init() {
|
|
44
|
+
await super.init();
|
|
45
|
+
// Abort instead of exit: the command unwinds, the sdk raises CancelledError and
|
|
46
|
+
// cli/errors.ts turns it into exit 130 — exiting from the handler is how the published
|
|
47
|
+
// `auth login` ends a cancelled login with exit 0. `once` leaves a second Ctrl+C to the
|
|
48
|
+
// runtime, so an unresponsive command can still be killed the usual way.
|
|
49
|
+
process.once('SIGINT', this.#onSigint);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* run() returns the data and render() prints it, so run()'s return type *is* the --json
|
|
53
|
+
* contract, checked by the compiler: change a response shape in the sdk and the command stops
|
|
54
|
+
* compiling instead of quietly changing what users parse.
|
|
55
|
+
*
|
|
56
|
+
* The guard is not what keeps --json clean — oclif's `log` is already silent then. It keeps the
|
|
57
|
+
* view from being built at all, so a human view may cost whatever it needs.
|
|
58
|
+
*/
|
|
59
|
+
render(value, view) {
|
|
60
|
+
if (!this.jsonEnabled()) {
|
|
61
|
+
this.log(view(value));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import type { AppSummary } from '../../../sdk/adapty/index.js';
|
|
3
|
+
export default class AppsCreate extends AdaptyCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
platform: import("@oclif/core/interfaces").OptionFlag<("ios" | "android")[], import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'apple-bundle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'google-bundle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<AppSummary>;
|
|
13
|
+
/**
|
|
14
|
+
* A courtesy for a human reader: the server creates a default access level with the app. Under
|
|
15
|
+
* --json the result is the app itself, so the request is skipped — and a failure to read it
|
|
16
|
+
* never fails the create, which has already happened.
|
|
17
|
+
*/
|
|
18
|
+
private showDefaultAccessLevel;
|
|
19
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { validateCreateApp } from '../../../sdk/adapty/apps/index.js';
|
|
3
|
+
import { platforms } from '../../../sdk/adapty/index.js';
|
|
4
|
+
import { CancelledError } from '../../../sdk/core/errors.js';
|
|
5
|
+
import { assertValid } from '../../../sdk/core/validation.js';
|
|
6
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
7
|
+
import { renderRecord } from '../../views/record.js';
|
|
8
|
+
export default class AppsCreate extends AdaptyCommand {
|
|
9
|
+
static description = 'Create a new Adapty app';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> apps create --title "My App" --platform ios --apple-bundle-id com.example.app',
|
|
12
|
+
'<%= config.bin %> apps create --title "My App" --platform ios --platform android --apple-bundle-id com.example.app --google-bundle-id com.example.app',
|
|
13
|
+
];
|
|
14
|
+
// Input shape here, the rule (a bundle id per platform) in sdk/adapty/apps/create.ts — where a
|
|
15
|
+
// future MCP server obeys it too.
|
|
16
|
+
static flags = {
|
|
17
|
+
'title': Flags.string({ description: 'App title', required: true }),
|
|
18
|
+
'platform': Flags.option({
|
|
19
|
+
description: 'Platform (ios, android). Repeat for multiple.',
|
|
20
|
+
multiple: true,
|
|
21
|
+
options: platforms,
|
|
22
|
+
required: true,
|
|
23
|
+
})(),
|
|
24
|
+
'apple-bundle-id': Flags.string({ description: 'Apple bundle ID (required with --platform ios)' }),
|
|
25
|
+
'google-bundle-id': Flags.string({ description: 'Google bundle ID (required with --platform android)' }),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags } = await this.parse(AppsCreate);
|
|
29
|
+
const input = {
|
|
30
|
+
appleBundleId: flags['apple-bundle-id'],
|
|
31
|
+
googleBundleId: flags['google-bundle-id'],
|
|
32
|
+
platforms: flags.platform,
|
|
33
|
+
title: flags.title,
|
|
34
|
+
};
|
|
35
|
+
assertValid(validateCreateApp(input));
|
|
36
|
+
const app = await this.adapty.apps.create(input);
|
|
37
|
+
this.log('App created!');
|
|
38
|
+
this.render(app, renderRecord);
|
|
39
|
+
await this.showDefaultAccessLevel(app.id);
|
|
40
|
+
return app;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A courtesy for a human reader: the server creates a default access level with the app. Under
|
|
44
|
+
* --json the result is the app itself, so the request is skipped — and a failure to read it
|
|
45
|
+
* never fails the create, which has already happened.
|
|
46
|
+
*/
|
|
47
|
+
async showDefaultAccessLevel(appId) {
|
|
48
|
+
if (this.jsonEnabled()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { items } = await this.adapty.accessLevels.list(appId);
|
|
53
|
+
const [first] = items ?? [];
|
|
54
|
+
if (first) {
|
|
55
|
+
this.log('\nDefault access level:');
|
|
56
|
+
this.log(renderRecord(first));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Ctrl+C is not a failed request: swallowing it would warn and carry on as if the
|
|
61
|
+
// user had not asked to stop.
|
|
62
|
+
if (error instanceof CancelledError) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
this.warn('Could not fetch access levels for new app');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import type { AppDetail } from '../../../sdk/adapty/index.js';
|
|
3
|
+
export default class AppsGet extends AdaptyCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
app_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
run(): Promise<AppDetail>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import { appIdArg } from '../../flags.js';
|
|
3
|
+
import { renderRecord } from '../../views/record.js';
|
|
4
|
+
export default class AppsGet extends AdaptyCommand {
|
|
5
|
+
static args = { ...appIdArg };
|
|
6
|
+
static description = 'Get app details';
|
|
7
|
+
static examples = ['<%= config.bin %> apps get 550e8400-e29b-41d4-a716-446655440000'];
|
|
8
|
+
async run() {
|
|
9
|
+
const { args } = await this.parse(AppsGet);
|
|
10
|
+
const app = await this.adapty.apps.get(args.app_id);
|
|
11
|
+
this.render(app, renderRecord);
|
|
12
|
+
return app;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import type { AppSummary, Paginated } from '../../../sdk/adapty/index.js';
|
|
3
|
+
export default class AppsList extends AdaptyCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<Paginated<AppSummary>>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import { pageParams, paginationFlags } from '../../flags.js';
|
|
3
|
+
import { renderPage } from '../../views/list.js';
|
|
4
|
+
export default class AppsList extends AdaptyCommand {
|
|
5
|
+
static description = 'List Adapty apps';
|
|
6
|
+
static examples = ['<%= config.bin %> apps list', '<%= config.bin %> apps list --page 2 --page-size 10'];
|
|
7
|
+
static flags = { ...paginationFlags };
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(AppsList);
|
|
10
|
+
const page = await this.adapty.apps.list(pageParams(flags));
|
|
11
|
+
this.render(page, renderPage);
|
|
12
|
+
return page;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
2
|
+
import type { AppDetail } from '../../../sdk/adapty/index.js';
|
|
3
|
+
export default class AppsUpdate extends AdaptyCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
app_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'apple-bundle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'google-bundle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<AppDetail>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { validateUpdateApp } from '../../../sdk/adapty/apps/index.js';
|
|
3
|
+
import { assertValid } from '../../../sdk/core/validation.js';
|
|
4
|
+
import { AdaptyCommand } from '../../base/adapty/index.js';
|
|
5
|
+
import { appIdArg } from '../../flags.js';
|
|
6
|
+
import { renderRecord } from '../../views/record.js';
|
|
7
|
+
export default class AppsUpdate extends AdaptyCommand {
|
|
8
|
+
static args = { ...appIdArg };
|
|
9
|
+
static description = 'Update an app';
|
|
10
|
+
static examples = ['<%= config.bin %> apps update 550e8400-... --title "My App"'];
|
|
11
|
+
// The "at least one field" rule lives in the sdk and is checked before requiring a token.
|
|
12
|
+
static flags = {
|
|
13
|
+
'title': Flags.string({ description: 'App title' }),
|
|
14
|
+
'apple-bundle-id': Flags.string({ description: 'Apple bundle ID' }),
|
|
15
|
+
'google-bundle-id': Flags.string({ description: 'Google bundle ID' }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args, flags } = await this.parse(AppsUpdate);
|
|
19
|
+
const input = {
|
|
20
|
+
appleBundleId: flags['apple-bundle-id'],
|
|
21
|
+
googleBundleId: flags['google-bundle-id'],
|
|
22
|
+
title: flags.title,
|
|
23
|
+
};
|
|
24
|
+
assertValid(validateUpdateApp(input));
|
|
25
|
+
const app = await this.adapty.apps.update(args.app_id, input);
|
|
26
|
+
this.log('App updated!');
|
|
27
|
+
this.render(app, renderRecord);
|
|
28
|
+
return app;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
2
|
+
export default class AuthLogin extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
/** The one command that opts out: an interactive flow has no result to serialize. */
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
/** ADAPTY_APP_URL is the CLI's business: the sdk knows nothing about browsers or dashboards. */
|
|
9
|
+
private dashboardLink;
|
|
10
|
+
private openBrowser;
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import { onAppHost } from '../../../lib/app-url.js';
|
|
3
|
+
import { runDeviceFlow } from '../../../sdk/core/auth/device-flow.js';
|
|
4
|
+
import { systemClock } from '../../../sdk/core/clock.js';
|
|
5
|
+
import { build, openSession } from '../../base/adapty/index.js';
|
|
6
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
7
|
+
import { exitCode } from '../../errors.js';
|
|
8
|
+
export default class AuthLogin extends BaseCommand {
|
|
9
|
+
static description = 'Authenticate with Adapty via browser';
|
|
10
|
+
/** The one command that opts out: an interactive flow has no result to serialize. */
|
|
11
|
+
static enableJsonFlag = false;
|
|
12
|
+
static examples = ['<%= config.bin %> auth login'];
|
|
13
|
+
async run() {
|
|
14
|
+
await this.parse(AuthLogin);
|
|
15
|
+
const session = await openSession(this.config);
|
|
16
|
+
if (session.user !== undefined) {
|
|
17
|
+
this.log(`Already authenticated as ${session.user.email}. Re-authenticating...`);
|
|
18
|
+
}
|
|
19
|
+
// No token yet: the same factory without one, which leaves only auth reachable
|
|
20
|
+
const adapty = build({ ...session, token: undefined }, {
|
|
21
|
+
config: this.config,
|
|
22
|
+
signal: this.signal,
|
|
23
|
+
warn: message => this.warn(message),
|
|
24
|
+
});
|
|
25
|
+
const issued = await runDeviceFlow(adapty.auth, {
|
|
26
|
+
clock: systemClock,
|
|
27
|
+
onCode: (code) => {
|
|
28
|
+
// The fallback comes before the host rewrite, so the printed link and the browser
|
|
29
|
+
// always agree
|
|
30
|
+
const link = this.dashboardLink(code.verificationUriComplete ?? code.verificationUri);
|
|
31
|
+
this.log(`\nYour code: ${code.userCode}\n`);
|
|
32
|
+
this.log(`If the browser doesn't open, visit: ${link}\n`);
|
|
33
|
+
this.openBrowser(link);
|
|
34
|
+
this.log('Waiting for authorization... (Ctrl+C to cancel)');
|
|
35
|
+
},
|
|
36
|
+
onTransientError: (error, consecutive) => {
|
|
37
|
+
// One failed poll is normal and stays quiet; three in a row is worth saying,
|
|
38
|
+
// because the screen otherwise looks stuck
|
|
39
|
+
if (consecutive >= 3) {
|
|
40
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
41
|
+
this.warn(`${consecutive} consecutive errors while polling: ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
signal: this.signal,
|
|
45
|
+
});
|
|
46
|
+
await session.store.save({ token: issued.accessToken, user: issued.user });
|
|
47
|
+
this.log(`\nAuthenticated as ${issued.user.email}`);
|
|
48
|
+
this.log(`Session saved to ${session.store.path}`);
|
|
49
|
+
}
|
|
50
|
+
/** ADAPTY_APP_URL is the CLI's business: the sdk knows nothing about browsers or dashboards. */
|
|
51
|
+
dashboardLink(issuedUrl) {
|
|
52
|
+
try {
|
|
53
|
+
return onAppHost(issuedUrl);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// A malformed ADAPTY_APP_URL is bad input, not a failed login
|
|
57
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: exitCode.usage });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
openBrowser(url) {
|
|
61
|
+
if (!process.stdin.isTTY) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// onCode is synchronous, so the launch is fired and forgotten: the link is already on
|
|
65
|
+
// screen, and a browser that refuses to start must not fail the login
|
|
66
|
+
void open(url).catch(() => undefined);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
2
|
+
type Result = {
|
|
3
|
+
/** The env var outlives the file, so "Logged out" alone would be a lie. */
|
|
4
|
+
env_token_set: boolean;
|
|
5
|
+
status: 'logged_out' | 'not_authenticated';
|
|
6
|
+
};
|
|
7
|
+
export default class AuthLogout extends BaseCommand {
|
|
8
|
+
static description: string;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
run(): Promise<Result>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { openSession } from '../../base/adapty/index.js';
|
|
2
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
3
|
+
/** A record rather than an if: a new status cannot be added without a text for it. */
|
|
4
|
+
const messages = {
|
|
5
|
+
logged_out: 'Logged out. Note: the token stays valid server-side until it expires — see `adapty auth revoke`.',
|
|
6
|
+
not_authenticated: 'Not currently authenticated.',
|
|
7
|
+
};
|
|
8
|
+
const ENV_STILL_SET = 'ADAPTY_TOKEN is still set in the environment, so commands stay authenticated. Unset it to finish logging out.';
|
|
9
|
+
export default class AuthLogout extends BaseCommand {
|
|
10
|
+
static description = 'Remove the stored session';
|
|
11
|
+
static examples = ['<%= config.bin %> auth logout'];
|
|
12
|
+
async run() {
|
|
13
|
+
await this.parse(AuthLogout);
|
|
14
|
+
const session = await openSession(this.config);
|
|
15
|
+
const stored = await session.store.load();
|
|
16
|
+
if (stored !== undefined) {
|
|
17
|
+
await session.store.clear();
|
|
18
|
+
}
|
|
19
|
+
const result = {
|
|
20
|
+
env_token_set: session.source === 'env',
|
|
21
|
+
status: stored === undefined ? 'not_authenticated' : 'logged_out',
|
|
22
|
+
};
|
|
23
|
+
this.render(result, status => (status.env_token_set
|
|
24
|
+
? `${messages[status.status]}\n${ENV_STILL_SET}`
|
|
25
|
+
: messages[status.status]));
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
2
|
+
type Result = {
|
|
3
|
+
status: 'not_authenticated';
|
|
4
|
+
} | {
|
|
5
|
+
env_token_set: boolean;
|
|
6
|
+
status: 'revoked';
|
|
7
|
+
};
|
|
8
|
+
export default class AuthRevoke extends BaseCommand {
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
run(): Promise<Result>;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { build, openSession } from '../../base/adapty/index.js';
|
|
2
|
+
import { BaseCommand } from '../../base/base-command.js';
|
|
3
|
+
export default class AuthRevoke extends BaseCommand {
|
|
4
|
+
static description = 'Revoke the current token on the server and remove any matching stored session';
|
|
5
|
+
static examples = ['<%= config.bin %> auth revoke'];
|
|
6
|
+
async run() {
|
|
7
|
+
await this.parse(AuthRevoke);
|
|
8
|
+
const session = await openSession(this.config);
|
|
9
|
+
if (session.token === undefined) {
|
|
10
|
+
this.log('Not currently authenticated.');
|
|
11
|
+
return { status: 'not_authenticated' };
|
|
12
|
+
}
|
|
13
|
+
// Server first, file second. The other order, on a failed request, leaves a live token on
|
|
14
|
+
// the server and no local copy to revoke it with — only the dashboard could undo that.
|
|
15
|
+
const adapty = build(session, {
|
|
16
|
+
config: this.config,
|
|
17
|
+
signal: this.signal,
|
|
18
|
+
warn: message => this.warn(message),
|
|
19
|
+
});
|
|
20
|
+
await adapty.auth.revokeToken(session.token);
|
|
21
|
+
// ADAPTY_TOKEN may override a different, still-valid session in the file.
|
|
22
|
+
const stored = await session.store.load();
|
|
23
|
+
if (stored?.token === session.token) {
|
|
24
|
+
await session.store.clear();
|
|
25
|
+
}
|
|
26
|
+
const result = { env_token_set: session.source === 'env', status: 'revoked' };
|
|
27
|
+
this.render(result, status => (status.env_token_set
|
|
28
|
+
? 'Token revoked. ADAPTY_TOKEN is still set and now holds a dead token — unset it.'
|
|
29
|
+
: 'Token revoked and logged out.'));
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}
|