adapty 0.8.5 → 0.8.6

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 (114) hide show
  1. package/README.md +6 -1
  2. package/dist/cli/base/adapty/adapty-command.d.ts +21 -0
  3. package/dist/cli/base/adapty/adapty-command.js +47 -0
  4. package/dist/cli/base/adapty/build.d.ts +11 -0
  5. package/dist/cli/base/adapty/build.js +13 -0
  6. package/dist/cli/base/adapty/index.d.ts +5 -0
  7. package/dist/cli/base/adapty/index.js +3 -0
  8. package/dist/cli/base/adapty/openSession.d.ts +22 -0
  9. package/dist/cli/base/adapty/openSession.js +25 -0
  10. package/dist/cli/base/base-command.d.ts +30 -0
  11. package/dist/cli/base/base-command.js +64 -0
  12. package/dist/cli/commands/apps/create.d.ts +19 -0
  13. package/dist/cli/commands/apps/create.js +68 -0
  14. package/dist/cli/commands/apps/get.d.ts +10 -0
  15. package/dist/cli/commands/apps/get.js +14 -0
  16. package/dist/cli/commands/apps/list.d.ts +11 -0
  17. package/dist/cli/commands/apps/list.js +14 -0
  18. package/dist/cli/commands/apps/update.d.ts +15 -0
  19. package/dist/cli/commands/apps/update.js +30 -0
  20. package/dist/cli/commands/auth/login.d.ts +11 -0
  21. package/dist/cli/commands/auth/login.js +68 -0
  22. package/dist/cli/commands/auth/logout.d.ts +12 -0
  23. package/dist/cli/commands/auth/logout.js +28 -0
  24. package/dist/cli/commands/auth/revoke.d.ts +13 -0
  25. package/dist/cli/commands/auth/revoke.js +32 -0
  26. package/dist/cli/commands/auth/status/index.d.ts +7 -0
  27. package/dist/cli/commands/auth/status/index.js +22 -0
  28. package/dist/cli/commands/auth/status/lib/render.d.ts +3 -0
  29. package/dist/cli/commands/auth/status/lib/render.js +14 -0
  30. package/dist/cli/commands/auth/status/lib/result.d.ts +15 -0
  31. package/dist/cli/commands/auth/status/lib/result.js +1 -0
  32. package/dist/cli/commands/auth/whoami.d.ts +10 -0
  33. package/dist/cli/commands/auth/whoami.js +16 -0
  34. package/dist/cli/errors.d.ts +39 -0
  35. package/dist/cli/errors.js +92 -0
  36. package/dist/cli/flags.d.ts +16 -0
  37. package/dist/cli/flags.js +27 -0
  38. package/dist/cli/views/list.d.ts +6 -0
  39. package/dist/cli/views/list.js +10 -0
  40. package/dist/cli/views/record.d.ts +9 -0
  41. package/dist/cli/views/record.js +14 -0
  42. package/dist/commands/apps/create.d.ts +1 -14
  43. package/dist/commands/apps/create.js +2 -57
  44. package/dist/commands/apps/get.d.ts +1 -11
  45. package/dist/commands/apps/get.js +2 -22
  46. package/dist/commands/apps/list.d.ts +1 -13
  47. package/dist/commands/apps/list.js +2 -19
  48. package/dist/commands/apps/update.d.ts +1 -16
  49. package/dist/commands/apps/update.js +2 -41
  50. package/dist/commands/asa/whoami.js +6 -2
  51. package/dist/commands/auth/login.d.ts +1 -8
  52. package/dist/commands/auth/login.js +2 -122
  53. package/dist/commands/auth/logout.d.ts +1 -9
  54. package/dist/commands/auth/logout.js +2 -18
  55. package/dist/commands/auth/revoke.d.ts +1 -9
  56. package/dist/commands/auth/revoke.js +2 -21
  57. package/dist/commands/auth/status.d.ts +1 -7
  58. package/dist/commands/auth/status.js +2 -26
  59. package/dist/commands/auth/whoami.d.ts +1 -7
  60. package/dist/commands/auth/whoami.js +2 -15
  61. package/dist/lib/api-schemas.d.ts +7 -3
  62. package/dist/lib/asa-schemas.d.ts +1 -1
  63. package/dist/sdk/adapty/access-levels.d.ts +15 -0
  64. package/dist/sdk/adapty/access-levels.js +4 -0
  65. package/dist/sdk/adapty/apps/create.d.ts +26 -0
  66. package/dist/sdk/adapty/apps/create.js +36 -0
  67. package/dist/sdk/adapty/apps/index.d.ts +13 -0
  68. package/dist/sdk/adapty/apps/index.js +9 -0
  69. package/dist/sdk/adapty/apps/model.d.ts +24 -0
  70. package/dist/sdk/adapty/apps/model.js +10 -0
  71. package/dist/sdk/adapty/apps/resource.d.ts +19 -0
  72. package/dist/sdk/adapty/apps/resource.js +23 -0
  73. package/dist/sdk/adapty/apps/update.d.ts +16 -0
  74. package/dist/sdk/adapty/apps/update.js +25 -0
  75. package/dist/sdk/adapty/auth/device-code.d.ts +8 -0
  76. package/dist/sdk/adapty/auth/device-code.js +19 -0
  77. package/dist/sdk/adapty/auth/index.d.ts +7 -0
  78. package/dist/sdk/adapty/auth/index.js +5 -0
  79. package/dist/sdk/adapty/auth/model.d.ts +15 -0
  80. package/dist/sdk/adapty/auth/model.js +6 -0
  81. package/dist/sdk/adapty/auth/poll-token.d.ts +8 -0
  82. package/dist/sdk/adapty/auth/poll-token.js +58 -0
  83. package/dist/sdk/adapty/auth/resource.d.ts +17 -0
  84. package/dist/sdk/adapty/auth/resource.js +21 -0
  85. package/dist/sdk/adapty/errors.d.ts +10 -0
  86. package/dist/sdk/adapty/errors.js +38 -0
  87. package/dist/sdk/adapty/index.d.ts +36 -0
  88. package/dist/sdk/adapty/index.js +27 -0
  89. package/dist/sdk/adapty/pagination.d.ts +23 -0
  90. package/dist/sdk/adapty/pagination.js +1 -0
  91. package/dist/sdk/core/auth/device-flow.d.ts +46 -0
  92. package/dist/sdk/core/auth/device-flow.js +62 -0
  93. package/dist/sdk/core/clock.d.ts +7 -0
  94. package/dist/sdk/core/clock.js +21 -0
  95. package/dist/sdk/core/errors.d.ts +81 -0
  96. package/dist/sdk/core/errors.js +84 -0
  97. package/dist/sdk/core/http/client.d.ts +46 -0
  98. package/dist/sdk/core/http/client.js +127 -0
  99. package/dist/sdk/core/http/index.d.ts +11 -0
  100. package/dist/sdk/core/http/index.js +7 -0
  101. package/dist/sdk/core/http/policies.d.ts +14 -0
  102. package/dist/sdk/core/http/policies.js +29 -0
  103. package/dist/sdk/core/http/retry.d.ts +27 -0
  104. package/dist/sdk/core/http/retry.js +22 -0
  105. package/dist/sdk/core/http/url.d.ts +3 -0
  106. package/dist/sdk/core/http/url.js +19 -0
  107. package/dist/sdk/core/session.d.ts +20 -0
  108. package/dist/sdk/core/session.js +69 -0
  109. package/dist/sdk/core/testing.d.ts +25 -0
  110. package/dist/sdk/core/testing.js +46 -0
  111. package/dist/sdk/core/validation.d.ts +10 -0
  112. package/dist/sdk/core/validation.js +14 -0
  113. package/oclif.manifest.json +813 -813
  114. 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 server-side and clear local
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.
@@ -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,5 @@
1
+ export { AdaptyCommand } from './adapty-command.js';
2
+ export { build } from './build.js';
3
+ export { openSession } from './openSession.js';
4
+ export type { AuthenticatedSession } from './adapty-command.js';
5
+ export type { ResolvedSession } from './openSession.js';
@@ -0,0 +1,3 @@
1
+ export { AdaptyCommand } from './adapty-command.js';
2
+ export { build } from './build.js';
3
+ export { openSession } from './openSession.js';
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { BaseCommand } from '../../../base/base-command.js';
2
+ import type { Result } from './lib/result.js';
3
+ export default class AuthStatus extends BaseCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<Result>;
7
+ }