adapty 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +147 -2
  2. package/dist/cli/base/adapty/adapty-command.d.ts +3 -2
  3. package/dist/cli/base/adapty/adapty-command.js +8 -5
  4. package/dist/cli/base/adapty/build.d.ts +6 -1
  5. package/dist/cli/base/adapty/build.js +1 -0
  6. package/dist/cli/base/adapty/index.d.ts +1 -0
  7. package/dist/cli/base/adapty/index.js +1 -0
  8. package/dist/cli/base/adapty/migration-command.d.ts +11 -0
  9. package/dist/cli/base/adapty/migration-command.js +17 -0
  10. package/dist/cli/base/base-command.d.ts +6 -0
  11. package/dist/cli/base/base-command.js +14 -0
  12. package/dist/cli/commands/apps/create.d.ts +1 -1
  13. package/dist/cli/commands/apps/get.js +1 -1
  14. package/dist/cli/commands/apps/list.js +1 -1
  15. package/dist/cli/commands/apps/update.js +1 -1
  16. package/dist/cli/commands/attribution/report/command.d.ts +18 -0
  17. package/dist/cli/commands/attribution/report/command.js +93 -0
  18. package/dist/cli/commands/attribution/report/index.d.ts +1 -18
  19. package/dist/cli/commands/attribution/report/index.js +1 -93
  20. package/dist/cli/commands/attribution/values.js +1 -1
  21. package/dist/cli/commands/auth/login.js +1 -0
  22. package/dist/cli/commands/auth/{logout.d.ts → logout/command.d.ts} +1 -1
  23. package/dist/cli/commands/auth/logout/command.js +33 -0
  24. package/dist/cli/commands/auth/logout/index.d.ts +1 -0
  25. package/dist/cli/commands/auth/logout/index.js +1 -0
  26. package/dist/cli/commands/auth/logout/lib/cleanup.d.ts +3 -0
  27. package/dist/cli/commands/auth/logout/lib/cleanup.js +21 -0
  28. package/dist/cli/commands/auth/{revoke.d.ts → revoke/command.d.ts} +1 -1
  29. package/dist/cli/commands/auth/{revoke.js → revoke/command.js} +7 -7
  30. package/dist/cli/commands/auth/revoke/index.d.ts +1 -0
  31. package/dist/cli/commands/auth/revoke/index.js +1 -0
  32. package/dist/cli/commands/auth/revoke/lib/cleanup.d.ts +4 -0
  33. package/dist/cli/commands/auth/revoke/lib/cleanup.js +29 -0
  34. package/dist/cli/commands/auth/status/command.d.ts +7 -0
  35. package/dist/cli/commands/auth/status/command.js +22 -0
  36. package/dist/cli/commands/auth/status/index.d.ts +1 -7
  37. package/dist/cli/commands/auth/status/index.js +1 -22
  38. package/dist/cli/commands/migrations/close/command.d.ts +16 -0
  39. package/dist/cli/commands/migrations/close/command.js +58 -0
  40. package/dist/cli/commands/migrations/close/index.d.ts +1 -0
  41. package/dist/cli/commands/migrations/close/index.js +1 -0
  42. package/dist/cli/commands/migrations/create/command.d.ts +20 -0
  43. package/dist/cli/commands/migrations/create/command.js +86 -0
  44. package/dist/cli/commands/migrations/create/index.d.ts +1 -0
  45. package/dist/cli/commands/migrations/create/index.js +1 -0
  46. package/dist/cli/commands/migrations/current/command.d.ts +12 -0
  47. package/dist/cli/commands/migrations/current/command.js +25 -0
  48. package/dist/cli/commands/migrations/current/index.d.ts +1 -0
  49. package/dist/cli/commands/migrations/current/index.js +1 -0
  50. package/dist/cli/commands/migrations/list/command.d.ts +11 -0
  51. package/dist/cli/commands/migrations/list/command.js +27 -0
  52. package/dist/cli/commands/migrations/list/index.d.ts +1 -0
  53. package/dist/cli/commands/migrations/list/index.js +1 -0
  54. package/dist/cli/commands/migrations/list/lib/render.d.ts +3 -0
  55. package/dist/cli/commands/migrations/list/lib/render.js +77 -0
  56. package/dist/cli/commands/migrations/run/command.d.ts +25 -0
  57. package/dist/cli/commands/migrations/run/command.js +141 -0
  58. package/dist/cli/commands/migrations/run/index.d.ts +1 -0
  59. package/dist/cli/commands/migrations/run/index.js +1 -0
  60. package/dist/cli/commands/migrations/run/lib/action-view.d.ts +7 -0
  61. package/dist/cli/commands/migrations/run/lib/action-view.js +2 -0
  62. package/dist/cli/commands/migrations/run/lib/actions.d.ts +4 -0
  63. package/dist/cli/commands/migrations/run/lib/actions.js +20 -0
  64. package/dist/cli/commands/migrations/run/lib/https.d.ts +1 -0
  65. package/dist/cli/commands/migrations/run/lib/https.js +8 -0
  66. package/dist/cli/commands/migrations/run/lib/input.d.ts +10 -0
  67. package/dist/cli/commands/migrations/run/lib/input.js +51 -0
  68. package/dist/cli/commands/migrations/run/lib/open-link.d.ts +7 -0
  69. package/dist/cli/commands/migrations/run/lib/open-link.js +11 -0
  70. package/dist/cli/commands/migrations/show/command.d.ts +17 -0
  71. package/dist/cli/commands/migrations/show/command.js +47 -0
  72. package/dist/cli/commands/migrations/show/index.d.ts +1 -0
  73. package/dist/cli/commands/migrations/show/index.js +1 -0
  74. package/dist/cli/commands/migrations/show/lib/render.d.ts +4 -0
  75. package/dist/cli/commands/migrations/show/lib/render.js +27 -0
  76. package/dist/cli/commands/migrations/status/command.d.ts +18 -0
  77. package/dist/cli/commands/migrations/status/command.js +54 -0
  78. package/dist/cli/commands/migrations/status/index.d.ts +1 -0
  79. package/dist/cli/commands/migrations/status/index.js +1 -0
  80. package/dist/cli/commands/migrations/status/lib/flags.d.ts +8 -0
  81. package/dist/cli/commands/migrations/status/lib/flags.js +29 -0
  82. package/dist/cli/commands/migrations/status/lib/notice.d.ts +3 -0
  83. package/dist/cli/commands/migrations/status/lib/notice.js +10 -0
  84. package/dist/cli/commands/migrations/steps/command.d.ts +14 -0
  85. package/dist/cli/commands/migrations/steps/command.js +34 -0
  86. package/dist/cli/commands/migrations/steps/index.d.ts +1 -0
  87. package/dist/cli/commands/migrations/steps/index.js +1 -0
  88. package/dist/cli/commands/migrations/steps/lib/render.d.ts +3 -0
  89. package/dist/cli/commands/migrations/steps/lib/render.js +28 -0
  90. package/dist/cli/commands/migrations/unuse/command.d.ts +11 -0
  91. package/dist/cli/commands/migrations/unuse/command.js +23 -0
  92. package/dist/cli/commands/migrations/unuse/index.d.ts +1 -0
  93. package/dist/cli/commands/migrations/unuse/index.js +1 -0
  94. package/dist/cli/commands/migrations/use/command.d.ts +14 -0
  95. package/dist/cli/commands/migrations/use/command.js +24 -0
  96. package/dist/cli/commands/migrations/use/index.d.ts +1 -0
  97. package/dist/cli/commands/migrations/use/index.js +1 -0
  98. package/dist/cli/context/migration/current.d.ts +32 -0
  99. package/dist/cli/context/migration/current.js +30 -0
  100. package/dist/cli/context/migration/index.d.ts +4 -0
  101. package/dist/cli/context/migration/index.js +2 -0
  102. package/dist/cli/context/migration/model.d.ts +14 -0
  103. package/dist/cli/context/migration/model.js +30 -0
  104. package/dist/cli/context/migration/resolve.d.ts +16 -0
  105. package/dist/cli/context/migration/resolve.js +26 -0
  106. package/dist/cli/context/migration/store.d.ts +9 -0
  107. package/dist/cli/context/migration/store.js +99 -0
  108. package/dist/cli/errors/wizard.d.ts +5 -0
  109. package/dist/cli/errors/wizard.js +54 -0
  110. package/dist/cli/errors.d.ts +37 -4
  111. package/dist/cli/errors.js +58 -4
  112. package/dist/cli/input/app.d.ts +8 -0
  113. package/dist/cli/{flags.js → input/app.js} +2 -9
  114. package/dist/cli/input/migration.d.ts +7 -0
  115. package/dist/cli/input/migration.js +17 -0
  116. package/dist/cli/input/pagination.d.ts +11 -0
  117. package/dist/cli/input/pagination.js +8 -0
  118. package/dist/cli/views/migrations/envelope/action-block.d.ts +2 -0
  119. package/dist/cli/views/migrations/envelope/action-block.js +32 -0
  120. package/dist/cli/views/migrations/envelope/envelope.d.ts +3 -0
  121. package/dist/cli/views/migrations/envelope/envelope.js +40 -0
  122. package/dist/cli/views/migrations/envelope/issue-block.d.ts +2 -0
  123. package/dist/cli/views/migrations/envelope/issue-block.js +13 -0
  124. package/dist/cli/views/migrations/notices.d.ts +7 -0
  125. package/dist/cli/views/migrations/notices.js +7 -0
  126. package/dist/commands/access-levels/create.js +1 -1
  127. package/dist/commands/access-levels/get.js +1 -1
  128. package/dist/commands/access-levels/list.js +1 -1
  129. package/dist/commands/access-levels/update.js +1 -1
  130. package/dist/commands/auth/logout.d.ts +1 -1
  131. package/dist/commands/auth/logout.js +1 -1
  132. package/dist/commands/auth/revoke.d.ts +1 -1
  133. package/dist/commands/auth/revoke.js +1 -1
  134. package/dist/commands/flows/config/get.js +1 -1
  135. package/dist/commands/flows/config/update.js +1 -1
  136. package/dist/commands/flows/config/validate.js +1 -1
  137. package/dist/commands/flows/create.js +1 -1
  138. package/dist/commands/flows/get.js +1 -1
  139. package/dist/commands/flows/list.js +1 -1
  140. package/dist/commands/flows/media/upload.js +1 -1
  141. package/dist/commands/flows/publish.js +1 -1
  142. package/dist/commands/flows/update.js +1 -1
  143. package/dist/commands/migrations/close.d.ts +1 -0
  144. package/dist/commands/migrations/close.js +2 -0
  145. package/dist/commands/migrations/create.d.ts +1 -0
  146. package/dist/commands/migrations/create.js +2 -0
  147. package/dist/commands/migrations/current.d.ts +1 -0
  148. package/dist/commands/migrations/current.js +1 -0
  149. package/dist/commands/migrations/list.d.ts +1 -0
  150. package/dist/commands/migrations/list.js +2 -0
  151. package/dist/commands/migrations/run.d.ts +1 -0
  152. package/dist/commands/migrations/run.js +2 -0
  153. package/dist/commands/migrations/show.d.ts +1 -0
  154. package/dist/commands/migrations/show.js +2 -0
  155. package/dist/commands/migrations/status.d.ts +1 -0
  156. package/dist/commands/migrations/status.js +2 -0
  157. package/dist/commands/migrations/steps.d.ts +1 -0
  158. package/dist/commands/migrations/steps.js +2 -0
  159. package/dist/commands/migrations/unuse.d.ts +1 -0
  160. package/dist/commands/migrations/unuse.js +1 -0
  161. package/dist/commands/migrations/use.d.ts +1 -0
  162. package/dist/commands/migrations/use.js +1 -0
  163. package/dist/commands/paywalls/create.js +1 -1
  164. package/dist/commands/paywalls/get.js +1 -1
  165. package/dist/commands/paywalls/list.js +1 -1
  166. package/dist/commands/paywalls/placements.js +1 -1
  167. package/dist/commands/paywalls/update.js +1 -1
  168. package/dist/commands/placements/create.js +1 -1
  169. package/dist/commands/placements/get.js +1 -1
  170. package/dist/commands/placements/list.js +1 -1
  171. package/dist/commands/placements/update.js +1 -1
  172. package/dist/commands/products/create.js +1 -1
  173. package/dist/commands/products/get.js +1 -1
  174. package/dist/commands/products/list.js +1 -1
  175. package/dist/commands/products/update.js +1 -1
  176. package/dist/commands/segments/get.js +1 -1
  177. package/dist/commands/segments/list.js +1 -1
  178. package/dist/sdk/adapty/errors.d.ts +4 -0
  179. package/dist/sdk/adapty/errors.js +25 -0
  180. package/dist/sdk/adapty/index.d.ts +5 -0
  181. package/dist/sdk/adapty/index.js +29 -3
  182. package/dist/sdk/adapty/migrations/action.d.ts +15 -0
  183. package/dist/sdk/adapty/migrations/action.js +17 -0
  184. package/dist/sdk/adapty/migrations/close.d.ts +16 -0
  185. package/dist/sdk/adapty/migrations/close.js +7 -0
  186. package/dist/sdk/adapty/migrations/create.d.ts +16 -0
  187. package/dist/sdk/adapty/migrations/create.js +43 -0
  188. package/dist/sdk/adapty/migrations/index.d.ts +9 -0
  189. package/dist/sdk/adapty/migrations/index.js +4 -0
  190. package/dist/sdk/adapty/migrations/model.d.ts +100 -0
  191. package/dist/sdk/adapty/migrations/model.js +1 -0
  192. package/dist/sdk/adapty/migrations/resource.d.ts +27 -0
  193. package/dist/sdk/adapty/migrations/resource.js +52 -0
  194. package/dist/sdk/adapty/migrations/wait.d.ts +6 -0
  195. package/dist/sdk/adapty/migrations/wait.js +9 -0
  196. package/oclif.manifest.json +1722 -1117
  197. package/package.json +4 -1
  198. package/dist/cli/commands/auth/logout.js +0 -28
  199. package/dist/cli/flags.d.ts +0 -20
@@ -0,0 +1,23 @@
1
+ import { BaseCommand } from '../../../base/base-command.js';
2
+ import { openCurrentMigration } from '../../../context/migration/index.js';
3
+ import { envSuppliesMigration } from '../../../views/migrations/notices.js';
4
+ export default class Unuse extends BaseCommand {
5
+ static summary = 'Clear the saved migration selection (no network)';
6
+ static description = [
7
+ 'Works without authentication, including when the saved context is malformed.',
8
+ 'ADAPTY_MIGRATION remains active until you unset it in your shell.',
9
+ ].join('\n');
10
+ static examples = ['<%= config.bin %> migrations unuse'];
11
+ async run() {
12
+ await this.parse(Unuse);
13
+ // No session: removing a record does not depend on whose it is, or on being logged in.
14
+ const current = openCurrentMigration({ configDir: this.config.configDir });
15
+ await current.clear();
16
+ if (current.overridden) {
17
+ process.stderr.write(envSuppliesMigration);
18
+ }
19
+ const result = { currentMigrationId: null };
20
+ this.render(result, () => 'Saved migration selection cleared.');
21
+ return result;
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ export { default } from './command.js';
@@ -0,0 +1 @@
1
+ export { default } from './command.js';
@@ -0,0 +1,14 @@
1
+ import { MigrationCommand } from '../../../base/adapty/index.js';
2
+ type Result = {
3
+ currentMigrationId: string;
4
+ };
5
+ export default class Use extends MigrationCommand {
6
+ static summary: string;
7
+ static description: string;
8
+ static examples: string[];
9
+ static args: {
10
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ };
12
+ run(): Promise<Result>;
13
+ }
14
+ export {};
@@ -0,0 +1,24 @@
1
+ import { MigrationCommand } from '../../../base/adapty/index.js';
2
+ import { migrationArgs } from '../../../input/migration.js';
3
+ import { envOverridesSelection } from '../../../views/migrations/notices.js';
4
+ export default class Use extends MigrationCommand {
5
+ static summary = 'Save the current migration after verifying access';
6
+ static description = [
7
+ 'Select a migration for the current token. This does not start or modify the migration.',
8
+ 'Completed, canceled and failed migrations can also be selected for inspection.',
9
+ 'ADAPTY_MIGRATION overrides the saved selection until you unset it in your shell.',
10
+ ].join('\n');
11
+ static examples = ['<%= config.bin %> migrations use mig_7x2'];
12
+ static args = { ...migrationArgs };
13
+ async run() {
14
+ const { args } = await this.parse(Use);
15
+ const { migration } = await this.adapty.migrations.get(args.id);
16
+ await this.currentMigration.set(migration.id);
17
+ if (this.currentMigration.overridden) {
18
+ process.stderr.write(envOverridesSelection);
19
+ }
20
+ const result = { currentMigrationId: migration.id };
21
+ this.render(result, value => `Current migration: ${value.currentMigrationId}`);
22
+ return result;
23
+ }
24
+ }
@@ -0,0 +1 @@
1
+ export { default } from './command.js';
@@ -0,0 +1 @@
1
+ export { default } from './command.js';
@@ -0,0 +1,32 @@
1
+ import type { MigrationSession } from './model.js';
2
+ import type { MigrationSelection } from './resolve.js';
3
+ /**
4
+ * Everything a command may ask about "which migration am I on", behind verbs. The file, the token
5
+ * that scopes it and ADAPTY_MIGRATION are put together here, once, and not in every `run()`.
6
+ */
7
+ export type CurrentMigration = {
8
+ /** Unconditional: `unuse` and `logout` remove a record they may not even be able to read. */
9
+ clear(): Promise<void>;
10
+ /**
11
+ * Only if the record belongs to this token: another token's selection stays usable — with
12
+ * ADAPTY_TOKEN set, the file may hold a different, still-valid one. The check is not atomic,
13
+ * and nothing here can make it so: a concurrent `login` landing between the read and the
14
+ * remove loses its fresh record. That costs one more `adapty auth login`, never a selection
15
+ * left behind for a dead token, so the file keeps the same last-writer-wins rule as `save`.
16
+ */
17
+ clearFor(token: string): Promise<void>;
18
+ get(explicit?: string): Promise<MigrationSelection | undefined>;
19
+ /** ADAPTY_MIGRATION is set, so neither a save nor a clear changes what the next command uses. */
20
+ readonly overridden: boolean;
21
+ readonly path: string;
22
+ require(explicit?: string): Promise<MigrationSelection>;
23
+ set(id: string): Promise<void>;
24
+ };
25
+ export type CurrentMigrationOptions = {
26
+ configDir: string;
27
+ /** The one place ADAPTY_MIGRATION is read; a parameter so a test needs no global. */
28
+ env?: NodeJS.ProcessEnv | undefined;
29
+ /** Absent for `unuse`, tokenless for `current`: neither needs credentials to answer. */
30
+ session?: MigrationSession | undefined;
31
+ };
32
+ export declare const openCurrentMigration: (options: CurrentMigrationOptions) => CurrentMigration;
@@ -0,0 +1,30 @@
1
+ import { AuthRequiredError } from '../../../sdk/core/errors.js';
2
+ import { createMigrationContext, migrationTokenFingerprint } from './model.js';
3
+ import { requireMigrationSelection, resolveMigrationSelection } from './resolve.js';
4
+ import { createMigrationContextStore } from './store.js';
5
+ export const openCurrentMigration = (options) => {
6
+ const store = createMigrationContextStore(options.configDir);
7
+ const session = options.session ?? {};
8
+ const envMigration = (options.env ?? process.env).ADAPTY_MIGRATION;
9
+ const sources = { envMigration, session, store };
10
+ return {
11
+ overridden: envMigration !== undefined && envMigration !== '',
12
+ path: store.path,
13
+ clear: () => store.clear(),
14
+ clearFor: async (token) => {
15
+ const stored = await store.load();
16
+ if (stored?.tokenFingerprint === migrationTokenFingerprint(token)) {
17
+ await store.clear();
18
+ }
19
+ },
20
+ get: explicit => resolveMigrationSelection({ ...sources, migration: explicit }),
21
+ require: explicit => requireMigrationSelection({ ...sources, migration: explicit }),
22
+ set: async (id) => {
23
+ const { token } = session;
24
+ if (token === undefined || token === '') {
25
+ throw new AuthRequiredError('missing');
26
+ }
27
+ await store.save(createMigrationContext({ ...session, token }, id));
28
+ },
29
+ };
30
+ };
@@ -0,0 +1,4 @@
1
+ export { openCurrentMigration } from './current.js';
2
+ export { validateMigrationId } from './model.js';
3
+ export type { CurrentMigration } from './current.js';
4
+ export type { MigrationSelection } from './resolve.js';
@@ -0,0 +1,2 @@
1
+ export { openCurrentMigration } from './current.js';
2
+ export { validateMigrationId } from './model.js';
@@ -0,0 +1,14 @@
1
+ export type MigrationContext = {
2
+ version: 1;
3
+ tokenFingerprint: string;
4
+ currentMigrationId: string;
5
+ };
6
+ export type MigrationSession = {
7
+ token?: string | undefined;
8
+ };
9
+ export declare const migrationTokenFingerprint: (token: string) => string;
10
+ export declare const validateMigrationId: (id: string) => string;
11
+ export declare const createMigrationContext: (session: MigrationSession & {
12
+ token: string;
13
+ }, currentMigrationId: string) => MigrationContext;
14
+ export declare const isMigrationContext: (value: unknown) => value is MigrationContext;
@@ -0,0 +1,30 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { CliError, exitCode } from '../../errors.js';
3
+ export const migrationTokenFingerprint = (token) => {
4
+ return createHash('sha256').update(token).digest('hex');
5
+ };
6
+ export const validateMigrationId = (id) => {
7
+ if (id.trim() === '') {
8
+ throw new CliError('Migration ID must not be empty or whitespace.', exitCode.usage, 'migration_required');
9
+ }
10
+ return id;
11
+ };
12
+ export const createMigrationContext = (session, currentMigrationId) => ({
13
+ version: 1,
14
+ tokenFingerprint: migrationTokenFingerprint(session.token),
15
+ currentMigrationId: validateMigrationId(currentMigrationId),
16
+ });
17
+ const isNonBlankString = (value) => {
18
+ return typeof value === 'string' && value.trim() !== '';
19
+ };
20
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
21
+ export const isMigrationContext = (value) => {
22
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
23
+ return false;
24
+ }
25
+ const record = value;
26
+ return record.version === 1
27
+ && typeof record.tokenFingerprint === 'string'
28
+ && SHA256_HEX.test(record.tokenFingerprint)
29
+ && isNonBlankString(record.currentMigrationId);
30
+ };
@@ -0,0 +1,16 @@
1
+ import type { MigrationSession } from './model.js';
2
+ import type { MigrationContextStore } from './store.js';
3
+ export type MigrationSelection = {
4
+ currentMigrationId: string;
5
+ source: 'flag' | 'env' | 'context';
6
+ };
7
+ type ResolutionOptions = {
8
+ migration?: string | undefined;
9
+ envMigration?: string | undefined;
10
+ session: MigrationSession;
11
+ store: Pick<MigrationContextStore, 'load'>;
12
+ };
13
+ /** Local-only and lazy: an explicit ID never reads context, even if it is broken. */
14
+ export declare const resolveMigrationSelection: (options: ResolutionOptions) => Promise<MigrationSelection | undefined>;
15
+ export declare const requireMigrationSelection: (options: ResolutionOptions) => Promise<MigrationSelection>;
16
+ export {};
@@ -0,0 +1,26 @@
1
+ import { CliError, exitCode } from '../../errors.js';
2
+ import { migrationTokenFingerprint, validateMigrationId } from './model.js';
3
+ /** Local-only and lazy: an explicit ID never reads context, even if it is broken. */
4
+ export const resolveMigrationSelection = async (options) => {
5
+ if (options.migration !== undefined) {
6
+ return { currentMigrationId: validateMigrationId(options.migration), source: 'flag' };
7
+ }
8
+ if (options.envMigration !== undefined && options.envMigration !== '') {
9
+ return { currentMigrationId: validateMigrationId(options.envMigration), source: 'env' };
10
+ }
11
+ if (options.session.token === undefined || options.session.token === '') {
12
+ return undefined;
13
+ }
14
+ const context = await options.store.load();
15
+ if (context?.tokenFingerprint !== migrationTokenFingerprint(options.session.token)) {
16
+ return undefined;
17
+ }
18
+ return { currentMigrationId: context.currentMigrationId, source: 'context' };
19
+ };
20
+ export const requireMigrationSelection = async (options) => {
21
+ const selection = await resolveMigrationSelection(options);
22
+ if (selection === undefined) {
23
+ throw new CliError('No migration selected. Run `adapty migrations list`, then `adapty migrations use <id>`, or supply `-m <id>`.', exitCode.usage, 'migration_required');
24
+ }
25
+ return selection;
26
+ };
@@ -0,0 +1,9 @@
1
+ import type { MigrationContext } from './model.js';
2
+ export type MigrationContextStore = {
3
+ readonly path: string;
4
+ load(): Promise<MigrationContext | undefined>;
5
+ save(context: MigrationContext): Promise<void>;
6
+ clear(): Promise<void>;
7
+ };
8
+ /** CLI-owned state, separate from credentials. Concurrent mutations are applied in call order. */
9
+ export declare const createMigrationContextStore: (dir: string) => MigrationContextStore;
@@ -0,0 +1,99 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { CliError, errorCode } from '../../errors.js';
5
+ import { isMigrationContext } from './model.js';
6
+ const REPLACE = 'Run `adapty migrations unuse` to remove it or `adapty migrations use <id>` to replace it.';
7
+ /**
8
+ * Two stable codes, but not one text: a malformed record, a denied read and a full disk are fixed
9
+ * in three different places, and the errno is what says which. The original travels as `cause`
10
+ * rather than in the message, because that message may quote the record — and the record carries a
11
+ * token fingerprint.
12
+ */
13
+ const contextError = (path, failure, cause) => {
14
+ const errno = errorCode(cause);
15
+ const file = errno === undefined ? path : `${path} (${errno})`;
16
+ const messages = {
17
+ invalid: `Invalid migration context: ${path}. ${REPLACE}`,
18
+ read: `Could not read migration context: ${file}. Check its permissions and ownership. ${REPLACE}`,
19
+ remove: `Could not remove migration context: ${file}. Check its permissions and ownership, or remove the file yourself.`,
20
+ write: `Could not save migration context: ${file}. Check the permissions and free space of its directory; the previous selection is unchanged.`,
21
+ };
22
+ return new CliError(messages[failure], 1, failure === 'invalid' ? 'migration_context_invalid' : 'migration_context_io', { cause });
23
+ };
24
+ const isMissing = (error) => {
25
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
26
+ };
27
+ /** CLI-owned state, separate from credentials. Concurrent mutations are applied in call order. */
28
+ export const createMigrationContextStore = (dir) => {
29
+ const path = join(dir, 'context.json');
30
+ let mutations = Promise.resolve();
31
+ const mutate = (operation) => {
32
+ const result = mutations.then(operation);
33
+ // A failed mutation must not prevent later calls from running.
34
+ mutations = result.then(() => undefined, () => undefined);
35
+ return result;
36
+ };
37
+ const store = {
38
+ path,
39
+ async load() {
40
+ let raw;
41
+ try {
42
+ raw = await fs.readFile(path, 'utf8');
43
+ }
44
+ catch (error) {
45
+ if (isMissing(error)) {
46
+ return undefined;
47
+ }
48
+ throw contextError(path, 'read', error);
49
+ }
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(raw);
53
+ }
54
+ catch (error) {
55
+ throw contextError(path, 'invalid', error);
56
+ }
57
+ if (!isMigrationContext(parsed)) {
58
+ throw contextError(path, 'invalid');
59
+ }
60
+ return parsed;
61
+ },
62
+ async save(context) {
63
+ if (!isMigrationContext(context)) {
64
+ throw contextError(path, 'invalid');
65
+ }
66
+ return mutate(async () => {
67
+ const temporary = join(dir, `.context-${randomUUID()}.tmp`);
68
+ try {
69
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
70
+ await fs.writeFile(temporary, `${JSON.stringify(context, null, 2)}\n`, {
71
+ encoding: 'utf8', flag: 'wx', mode: 0o600,
72
+ });
73
+ await fs.rename(temporary, path);
74
+ }
75
+ catch (error) {
76
+ // Never remove the destination on failure: it may hold a previous selection.
77
+ try {
78
+ await fs.rm(temporary, { force: true });
79
+ }
80
+ catch {
81
+ // Preserve the context-specific error if cleanup also fails.
82
+ }
83
+ throw contextError(path, 'write', error);
84
+ }
85
+ });
86
+ },
87
+ async clear() {
88
+ return mutate(async () => {
89
+ try {
90
+ await fs.rm(path, { force: true });
91
+ }
92
+ catch (error) {
93
+ throw contextError(path, 'remove', error);
94
+ }
95
+ });
96
+ },
97
+ };
98
+ return store;
99
+ };
@@ -0,0 +1,5 @@
1
+ import type { WizardError } from '../../sdk/adapty/migrations/index.js';
2
+ export type WizardDiagnostics = Partial<Omit<WizardError['error'], 'code' | 'message'>>;
3
+ /** Copy only known, correctly typed diagnostics from the untrusted response body. */
4
+ export declare const wizardDiagnostics: (body: unknown) => WizardDiagnostics;
5
+ export declare const wizardErrorMessage: (message: string, diagnostics: WizardDiagnostics) => string;
@@ -0,0 +1,54 @@
1
+ const isRecord = (value) => {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ };
4
+ /** Copy only known, correctly typed diagnostics from the untrusted response body. */
5
+ export const wizardDiagnostics = (body) => {
6
+ if (!isRecord(body) || (typeof body.error_code === 'string' && body.error_code !== '')) {
7
+ return {};
8
+ }
9
+ const error = body.error;
10
+ if (!isRecord(error) || typeof error.code !== 'string' || error.code === '') {
11
+ return {};
12
+ }
13
+ const diagnostics = {};
14
+ for (const key of ['detail', 'next_step']) {
15
+ if (typeof error[key] === 'string' || error[key] === null) {
16
+ diagnostics[key] = error[key];
17
+ }
18
+ }
19
+ if (typeof error.retryable === 'boolean') {
20
+ diagnostics.retryable = error.retryable;
21
+ }
22
+ const delay = error.retry_after_seconds;
23
+ if (delay === null || (typeof delay === 'number' && Number.isFinite(delay) && delay >= 0)) {
24
+ diagnostics.retry_after_seconds = delay;
25
+ }
26
+ if (typeof error.request_id === 'string') {
27
+ diagnostics.request_id = error.request_id;
28
+ }
29
+ if (Array.isArray(error.fields)) {
30
+ diagnostics.fields = [];
31
+ for (const field of error.fields) {
32
+ if (isRecord(field) && typeof field.path === 'string' && typeof field.message === 'string') {
33
+ diagnostics.fields.push({ path: field.path, message: field.message });
34
+ }
35
+ }
36
+ }
37
+ return diagnostics;
38
+ };
39
+ export const wizardErrorMessage = (message, diagnostics) => {
40
+ const lines = [message];
41
+ if (diagnostics.detail && diagnostics.detail !== message) {
42
+ lines.push(diagnostics.detail);
43
+ }
44
+ if (diagnostics.fields?.length) {
45
+ lines.push('', ...diagnostics.fields.map(field => ` ${field.path}: ${field.message}`));
46
+ }
47
+ if (diagnostics.next_step) {
48
+ lines.push('', `Next step: ${diagnostics.next_step}`);
49
+ }
50
+ if (diagnostics.request_id) {
51
+ lines.push(`Request ID: ${diagnostics.request_id}`);
52
+ }
53
+ return lines.join('\n');
54
+ };
@@ -1,30 +1,46 @@
1
1
  import { Errors } from '@oclif/core';
2
+ import type { WizardDiagnostics } from './errors/wizard.js';
2
3
  /**
3
4
  * Exit codes are the adapter's business: sdk errors carry none.
4
5
  * 2 usage — the input is wrong (oclif's own parse errors exit 2 as well)
5
6
  * 3 auth — no token, an expired one, or a refused authorization
6
7
  * 4 api — the server rejected a well-formed request
7
8
  * 5 network — the server was never reached
9
+ * 6 confirm — nothing was done: the action changes production data and wants --yes
8
10
  * 130 cancelled — Ctrl+C, by the shell convention 128 + SIGINT
9
11
  */
10
12
  export declare const exitCode: {
11
13
  readonly api: 4;
12
14
  readonly auth: 3;
13
15
  readonly cancelled: 130;
16
+ readonly confirmRequired: 6;
14
17
  readonly network: 5;
15
18
  readonly usage: 2;
16
19
  };
17
20
  /** HTTP status is separate from the process exit code. Snake_case fields preserve the old JSON contract. */
18
- export type ErrorJson = {
21
+ export type ErrorJson = WizardDiagnostics & {
19
22
  code?: string | undefined;
20
23
  error_code?: string | undefined;
21
24
  errors?: unknown;
22
25
  message: string;
23
- /** The server's Retry-After in whole seconds, rounded up; absent when the server asked for no wait. */
24
- retry_after_seconds?: number | undefined;
26
+ /**
27
+ * The server's wait in whole seconds: Retry-After rounded up, else the Wizard envelope's own field,
28
+ * which says null for no wait. Absent when the server said nothing.
29
+ */
30
+ retry_after_seconds?: number | null | undefined;
25
31
  status?: number | undefined;
26
32
  status_code?: number | undefined;
27
33
  };
34
+ export type CliErrorOptions = {
35
+ /**
36
+ * The failure this error explains. It is not printed and not serialized: a foreign message may
37
+ * quote whatever was handed to the syscall, and these paths handle credentials. It travels so
38
+ * that a stack, a debugger or a test can still reach the original.
39
+ */
40
+ cause?: unknown;
41
+ /** Fields for --json beyond `message` and `code`. */
42
+ json?: Partial<ErrorJson> | undefined;
43
+ };
28
44
  /**
29
45
  * oclif takes the exit code from two places: `handle()` reads `oclif.exit`, Command.catch under
30
46
  * --json reads `exitCode` and never rethrows. Set one and the other mode exits 1.
@@ -32,7 +48,7 @@ export type ErrorJson = {
32
48
  export declare class CliError extends Errors.CLIError {
33
49
  readonly exitCode: number;
34
50
  readonly json: ErrorJson;
35
- constructor(message: string, exit: number, code?: string, data?: Partial<ErrorJson>);
51
+ constructor(message: string, exit: number, code?: string, options?: CliErrorOptions);
36
52
  /**
37
53
  * What a command still on oclif's own Command prints under --json, where the error object is
38
54
  * serialized as it is: the same `{ message, ... }` a migrated command prints, not its internals.
@@ -44,6 +60,23 @@ export declare class CliError extends Errors.CLIError {
44
60
  * json keeps the text as written; oclif prefixes only the human message with the flag's name.
45
61
  */
46
62
  export declare const usageError: (message: string) => CliError;
63
+ /**
64
+ * An errno — ENOSPC, EACCES, EROFS — classifies a failure without repeating it. It is the part of
65
+ * a foreign error that is safe to show: a full disk and a denied write are fixed differently, and
66
+ * a message that only says "could not access" sends whoever reads it looking in the wrong place.
67
+ */
68
+ export declare const errorCode: (error: unknown) => string | undefined;
69
+ export type CleanupFailure = {
70
+ cause: unknown;
71
+ file: string;
72
+ reason: string;
73
+ };
74
+ /**
75
+ * Which files a best-effort cleanup could not finish, and why. Files and settled results are
76
+ * positional siblings, one per attempt: "could not remove these two" alone leaves the next person
77
+ * guessing between a permission, a read-only mount and a directory sitting where a file belongs.
78
+ */
79
+ export declare const describeCleanupFailures: (files: readonly string[], results: readonly PromiseSettledResult<unknown>[]) => CleanupFailure[];
47
80
  /**
48
81
  * The one place where an sdk error gets a human text and an exit code. No default in the switch:
49
82
  * a new kind fails to compile until it is mapped here.
@@ -1,17 +1,20 @@
1
1
  import { Errors } from '@oclif/core';
2
2
  import { isSdkError } from '../sdk/core/errors.js';
3
+ import { wizardDiagnostics, wizardErrorMessage } from './errors/wizard.js';
3
4
  /**
4
5
  * Exit codes are the adapter's business: sdk errors carry none.
5
6
  * 2 usage — the input is wrong (oclif's own parse errors exit 2 as well)
6
7
  * 3 auth — no token, an expired one, or a refused authorization
7
8
  * 4 api — the server rejected a well-formed request
8
9
  * 5 network — the server was never reached
10
+ * 6 confirm — nothing was done: the action changes production data and wants --yes
9
11
  * 130 cancelled — Ctrl+C, by the shell convention 128 + SIGINT
10
12
  */
11
13
  export const exitCode = {
12
14
  api: 4,
13
15
  auth: 3,
14
16
  cancelled: 130,
17
+ confirmRequired: 6,
15
18
  network: 5,
16
19
  usage: 2,
17
20
  };
@@ -22,11 +25,16 @@ export const exitCode = {
22
25
  export class CliError extends Errors.CLIError {
23
26
  exitCode;
24
27
  json;
25
- constructor(message, exit, code, data = {}) {
28
+ constructor(message, exit, code, options = {}) {
26
29
  super(message, { exit });
27
30
  this.exitCode = exit;
28
31
  this.code = code;
29
- this.json = { message, code, ...data };
32
+ this.json = { message, code, ...options.json };
33
+ // Assigned only when there is one: an own `cause: undefined` would read as "none known"
34
+ // where none was ever offered.
35
+ if (options.cause !== undefined) {
36
+ this.cause = options.cause;
37
+ }
30
38
  }
31
39
  /**
32
40
  * What a command still on oclif's own Command prints under --json, where the error object is
@@ -41,7 +49,43 @@ export class CliError extends Errors.CLIError {
41
49
  * json keeps the text as written; oclif prefixes only the human message with the flag's name.
42
50
  */
43
51
  export const usageError = (message) => new CliError(message, exitCode.usage);
44
- const cliError = (message, exit, code, data) => new CliError(message, exit, code, data);
52
+ const cliError = (message, exit, code, json) => {
53
+ return new CliError(message, exit, code, { json });
54
+ };
55
+ /**
56
+ * An errno — ENOSPC, EACCES, EROFS — classifies a failure without repeating it. It is the part of
57
+ * a foreign error that is safe to show: a full disk and a denied write are fixed differently, and
58
+ * a message that only says "could not access" sends whoever reads it looking in the wrong place.
59
+ */
60
+ export const errorCode = (error) => {
61
+ return error instanceof Error && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
62
+ };
63
+ /** What a nested failure adds to an error of ours: our own text, or a stranger's errno alone. */
64
+ const describeFailure = (error) => {
65
+ if (error instanceof CliError || isSdkError(error)) {
66
+ return error.message;
67
+ }
68
+ return errorCode(error) ?? 'unknown error';
69
+ };
70
+ /** `reason` is `any` on the settled result; naming it `unknown` is what keeps it from spreading. */
71
+ const rejectionOf = (result) => {
72
+ return result?.status === 'rejected' ? result : undefined;
73
+ };
74
+ /**
75
+ * Which files a best-effort cleanup could not finish, and why. Files and settled results are
76
+ * positional siblings, one per attempt: "could not remove these two" alone leaves the next person
77
+ * guessing between a permission, a read-only mount and a directory sitting where a file belongs.
78
+ */
79
+ export const describeCleanupFailures = (files, results) => {
80
+ const failures = [];
81
+ for (const [index, file] of files.entries()) {
82
+ const rejection = rejectionOf(results[index]);
83
+ if (rejection) {
84
+ failures.push({ cause: rejection.reason, file, reason: describeFailure(rejection.reason) });
85
+ }
86
+ }
87
+ return failures;
88
+ };
45
89
  /** Issue.path is a camelCase sdk field; the user typed a kebab-case flag. */
46
90
  const flagName = (path) => `--${path.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)}`;
47
91
  const describeIssue = (issue) => (issue.path === undefined ? issue.message : `${flagName(issue.path)}: ${issue.message}`);
@@ -51,6 +95,11 @@ const describeIssue = (issue) => (issue.path === undefined ? issue.message : `${
51
95
  */
52
96
  export const toCliError = (error) => {
53
97
  if (!isSdkError(error)) {
98
+ // Parser errors carry only oclif.exit; JSON handling reads exitCode instead.
99
+ // Preserve the error instance and its diagnostics, including any explicit non-usage exit.
100
+ if (error instanceof Errors.CLIError && typeof error.oclif.exit === 'number' && !('exitCode' in error)) {
101
+ return Object.assign(error, { exitCode: error.oclif.exit });
102
+ }
54
103
  return error instanceof Error ? error : new Error(String(error));
55
104
  }
56
105
  switch (error.kind) {
@@ -61,10 +110,15 @@ export const toCliError = (error) => {
61
110
  const fields = typeof error.details === 'object' && error.details !== null && 'errors' in error.details
62
111
  ? error.details.errors
63
112
  : undefined;
64
- return cliError(error.message, exitCode.api, code, {
113
+ const diagnostics = wizardDiagnostics(error.details);
114
+ // A permission denial is an auth failure; retain the server's explanation and diagnostics.
115
+ const exit = error.status === 403 ? exitCode.auth : exitCode.api;
116
+ return cliError(wizardErrorMessage(error.message, diagnostics), exit, code, {
117
+ ...diagnostics,
65
118
  code: jsonCode,
66
119
  error_code: jsonCode,
67
120
  errors: fields,
121
+ message: error.message,
68
122
  status: error.status,
69
123
  status_code: error.status,
70
124
  // Only when the server asked for a wait: an agent reads it instead of guessing one
@@ -0,0 +1,8 @@
1
+ /** Spread into a command: `static args = { ...appIdArg }`. Texts kept as published. */
2
+ export declare const appIdArg: {
3
+ app_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
4
+ };
5
+ /** `appIdArg` as a flag: `static flags = { ...appIdFlag }`. Texts kept as the published `--app`. */
6
+ export declare const appIdFlag: {
7
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
@@ -1,9 +1,9 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
- import { usageError } from './errors.js';
2
+ import { usageError } from '../errors.js';
3
3
  const UUID_PATTERN = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
4
4
  /** The published wording of both the `app_id` arg and the `--app` flag. */
5
5
  const APP_ID_HINT = 'Invalid app ID format. Run `adapty apps list` to find your app ID.';
6
- export const isUuid = (value) => UUID_PATTERN.test(value);
6
+ const isUuid = (value) => UUID_PATTERN.test(value);
7
7
  /**
8
8
  * Checking the shape of a value is the parser's job, so a run() never starts with one. oclif turns
9
9
  * a *flag* parser's failure into exit 2 itself but passes an *arg* parser's error through, hence
@@ -28,10 +28,3 @@ export const appIdFlag = {
28
28
  required: true,
29
29
  }),
30
30
  };
31
- /** The published defaults, so a migrated `list` asks for the same page as an untouched one. */
32
- export const paginationFlags = {
33
- 'page': Flags.integer({ default: 1, description: 'Page number', min: 1 }),
34
- 'page-size': Flags.integer({ default: 20, description: 'Items per page (max 100)', max: 100, min: 1 }),
35
- };
36
- /** The one place where flag names meet sdk field names. */
37
- export const pageParams = (flags) => ({ page: flags.page, pageSize: flags['page-size'] });
@@ -0,0 +1,7 @@
1
+ export declare const migrationArgs: {
2
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
3
+ };
4
+ /** Explicit ID wins over the environment and the saved selection, resolved after parsing. */
5
+ export declare const migrationFlags: {
6
+ migration: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };