hotcodepush 0.0.1 → 0.1.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 (161) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/app/create.js +49 -0
  3. package/dist/commands/app/delete.js +34 -0
  4. package/dist/commands/app/get.js +33 -0
  5. package/dist/commands/app/list.js +38 -0
  6. package/dist/commands/app/transfer.js +58 -0
  7. package/dist/commands/app/update.js +34 -0
  8. package/dist/commands/audience/get.js +69 -0
  9. package/dist/commands/audit-log/list.js +82 -0
  10. package/dist/commands/binary/create.js +189 -0
  11. package/dist/commands/binary/get.js +56 -0
  12. package/dist/commands/binary/list.js +58 -0
  13. package/dist/commands/bundle/delete.js +35 -0
  14. package/dist/commands/bundle/get.js +63 -0
  15. package/dist/commands/bundle/list.js +66 -0
  16. package/dist/commands/bundle/upload.js +176 -0
  17. package/dist/commands/channel/create.js +59 -0
  18. package/dist/commands/channel/delete.js +68 -0
  19. package/dist/commands/channel/get.js +45 -0
  20. package/dist/commands/channel/list.js +39 -0
  21. package/dist/commands/channel/pause.js +32 -0
  22. package/dist/commands/channel/resume.js +32 -0
  23. package/dist/commands/channel/update.js +40 -0
  24. package/dist/commands/checkout/create.js +25 -0
  25. package/dist/commands/completion.js +36 -0
  26. package/dist/commands/customer-portal-session/create.js +25 -0
  27. package/dist/commands/device/delete.js +29 -0
  28. package/dist/commands/device/get.js +60 -0
  29. package/dist/commands/device/list.js +97 -0
  30. package/dist/commands/device/probe.js +253 -0
  31. package/dist/commands/doctor.js +363 -0
  32. package/dist/commands/fingerprint/diff.js +133 -0
  33. package/dist/commands/fingerprint.js +41 -0
  34. package/dist/commands/init.js +492 -0
  35. package/dist/commands/invitation/accept.js +53 -0
  36. package/dist/commands/invitation/create.js +52 -0
  37. package/dist/commands/invitation/delete.js +60 -0
  38. package/dist/commands/invitation/list.js +40 -0
  39. package/dist/commands/limit/list.js +34 -0
  40. package/dist/commands/login.js +185 -0
  41. package/dist/commands/logout.js +79 -0
  42. package/dist/commands/member/delete.js +35 -0
  43. package/dist/commands/member/get.js +37 -0
  44. package/dist/commands/member/list.js +41 -0
  45. package/dist/commands/member/update.js +53 -0
  46. package/dist/commands/open.js +37 -0
  47. package/dist/commands/organization/create.js +30 -0
  48. package/dist/commands/organization/delete.js +39 -0
  49. package/dist/commands/organization/get.js +38 -0
  50. package/dist/commands/organization/list.js +41 -0
  51. package/dist/commands/organization/update.js +64 -0
  52. package/dist/commands/release/create.js +237 -0
  53. package/dist/commands/release/get.js +81 -0
  54. package/dist/commands/release/list.js +69 -0
  55. package/dist/commands/release/pause.js +36 -0
  56. package/dist/commands/release/resume.js +36 -0
  57. package/dist/commands/release/revoke.js +253 -0
  58. package/dist/commands/release/rollback.js +73 -0
  59. package/dist/commands/release/rollout.js +63 -0
  60. package/dist/commands/release/update.js +82 -0
  61. package/dist/commands/resource-file/write.js +76 -0
  62. package/dist/commands/signing-key/add.js +45 -0
  63. package/dist/commands/signing-key/create.js +84 -0
  64. package/dist/commands/signing-key/delete.js +67 -0
  65. package/dist/commands/signing-key/list.js +38 -0
  66. package/dist/commands/sso-provider/delete.js +32 -0
  67. package/dist/commands/sso-provider/get.js +63 -0
  68. package/dist/commands/sso-provider/set.js +161 -0
  69. package/dist/commands/sso-provider/verify.js +31 -0
  70. package/dist/commands/statistics/get.js +189 -0
  71. package/dist/commands/usage/get.js +31 -0
  72. package/dist/commands/usage/list.js +41 -0
  73. package/dist/commands/webhook/create.js +72 -0
  74. package/dist/commands/webhook/delete.js +32 -0
  75. package/dist/commands/webhook/get.js +34 -0
  76. package/dist/commands/webhook/list.js +40 -0
  77. package/dist/commands/webhook/test.js +32 -0
  78. package/dist/commands/webhook/update.js +56 -0
  79. package/dist/commands/webhook-delivery/list.js +63 -0
  80. package/dist/commands/webhook-delivery/redeliver.js +48 -0
  81. package/dist/commands/whoami.js +24 -0
  82. package/dist/completion/hotcodepush.bash +536 -0
  83. package/dist/completion/hotcodepush.fish +979 -0
  84. package/dist/completion/hotcodepush.zsh +1372 -0
  85. package/dist/config/consts.js +32 -0
  86. package/dist/index.js +5 -0
  87. package/dist/registry.js +83 -0
  88. package/dist/utils/api-client.js +17 -0
  89. package/dist/utils/audience.js +9 -0
  90. package/dist/utils/auth-client.js +35 -0
  91. package/dist/utils/boolean-flag.js +3 -0
  92. package/dist/utils/browser.js +6 -0
  93. package/dist/utils/build-step.js +104 -0
  94. package/dist/utils/bundle-files.js +80 -0
  95. package/dist/utils/bundle-resolution.js +46 -0
  96. package/dist/utils/channel-fields.js +130 -0
  97. package/dist/utils/cli.js +75 -0
  98. package/dist/utils/command-registry.js +79 -0
  99. package/dist/utils/completion/bash.js +48 -0
  100. package/dist/utils/completion/command-tree.js +72 -0
  101. package/dist/utils/completion/fish.js +44 -0
  102. package/dist/utils/completion/posix-case-arms.js +9 -0
  103. package/dist/utils/completion/posix-quote.js +4 -0
  104. package/dist/utils/completion/script-files.js +26 -0
  105. package/dist/utils/completion/zsh.js +72 -0
  106. package/dist/utils/compressed-files.js +31 -0
  107. package/dist/utils/credential.js +15 -0
  108. package/dist/utils/device-resolution.js +17 -0
  109. package/dist/utils/duration.js +16 -0
  110. package/dist/utils/environment.js +22 -0
  111. package/dist/utils/error-mapping.js +79 -0
  112. package/dist/utils/errors.js +266 -0
  113. package/dist/utils/fingerprint.js +76 -0
  114. package/dist/utils/flag-name.js +4 -0
  115. package/dist/utils/fleet.js +9 -0
  116. package/dist/utils/framework.js +74 -0
  117. package/dist/utils/frameworks/binary-create-step.js +70 -0
  118. package/dist/utils/frameworks/capacitor.js +150 -0
  119. package/dist/utils/frameworks/cordova.js +110 -0
  120. package/dist/utils/frameworks/expo.js +152 -0
  121. package/dist/utils/frameworks/index.js +13 -0
  122. package/dist/utils/frameworks/native-glue.js +10 -0
  123. package/dist/utils/frameworks/native-projects.js +27 -0
  124. package/dist/utils/frameworks/react-native-build.js +182 -0
  125. package/dist/utils/frameworks/react-native.js +127 -0
  126. package/dist/utils/frameworks/sdk-package.js +63 -0
  127. package/dist/utils/git-provenance.js +71 -0
  128. package/dist/utils/global-options.js +34 -0
  129. package/dist/utils/hosts.js +53 -0
  130. package/dist/utils/init-steps.js +51 -0
  131. package/dist/utils/json-file.js +28 -0
  132. package/dist/utils/member-resolution.js +49 -0
  133. package/dist/utils/native-project-edit.js +41 -0
  134. package/dist/utils/nullable-field.js +10 -0
  135. package/dist/utils/outcome.js +24 -0
  136. package/dist/utils/output.js +37 -0
  137. package/dist/utils/pack.js +20 -0
  138. package/dist/utils/package-json.js +5 -0
  139. package/dist/utils/package-manager.js +50 -0
  140. package/dist/utils/pagination.js +29 -0
  141. package/dist/utils/progress.js +23 -0
  142. package/dist/utils/progression.js +119 -0
  143. package/dist/utils/project-config.js +61 -0
  144. package/dist/utils/prompts.js +53 -0
  145. package/dist/utils/react-native-project.js +119 -0
  146. package/dist/utils/release-conditions.js +80 -0
  147. package/dist/utils/release-output.js +13 -0
  148. package/dist/utils/release-resolution.js +83 -0
  149. package/dist/utils/resource-file.js +81 -0
  150. package/dist/utils/resource-resolution.js +107 -0
  151. package/dist/utils/signing-private-key.js +105 -0
  152. package/dist/utils/spending-cap.js +16 -0
  153. package/dist/utils/telemetry.js +171 -0
  154. package/dist/utils/token-store.js +63 -0
  155. package/dist/utils/update-notice.js +67 -0
  156. package/dist/utils/upload.js +148 -0
  157. package/dist/utils/usage-month.js +14 -0
  158. package/dist/utils/user-config.js +46 -0
  159. package/dist/utils/webhook-resolution.js +47 -0
  160. package/dist/utils/xcode-project.js +181 -0
  161. package/package.json +11 -9
@@ -0,0 +1,40 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson, printTable, resolveDate } from '../../utils/output.js';
5
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
6
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
7
+ export default defineCommand({
8
+ description: "List the organization's pending invitations, the ones a link can still accept, newest first.",
9
+ examples: [
10
+ 'hotcodepush invitation list --organization Acme',
11
+ 'hotcodepush invitation list --limit 10 --offset 10 --json',
12
+ ],
13
+ options: defineCommandOptions(paginationShape),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const listedInvitations = await hotCodePush.organizations.invitations.list({
17
+ limit: options.limit,
18
+ offset: options.offset,
19
+ organizationId: await fetchOrganizationId(hotCodePush, options),
20
+ status: 'pending',
21
+ });
22
+ const nextOffset = resolveNextOffset(listedInvitations.length, options);
23
+ if (options.json) {
24
+ printJson({ invitations: listedInvitations, nextOffset });
25
+ return;
26
+ }
27
+ printTable({
28
+ emptyText: 'No pending invitations.',
29
+ headers: ['ID', 'EMAIL', 'ROLE', 'EXPIRES', 'CREATED'],
30
+ nextOffset,
31
+ rows: listedInvitations.map(({ createdAt, email, expiresAt, id, role }) => [
32
+ id,
33
+ email,
34
+ role,
35
+ resolveDate(expiresAt),
36
+ resolveDate(createdAt),
37
+ ]),
38
+ });
39
+ },
40
+ });
@@ -0,0 +1,34 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson, printTable } from '../../utils/output.js';
5
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
6
+ export default defineCommand({
7
+ description: "List the limits in effect for an organization: each value, the plan's default and whether it is overridden.",
8
+ examples: [
9
+ 'hotcodepush limit list',
10
+ 'hotcodepush limit list --organization Acme --json',
11
+ ],
12
+ options: defineCommandOptions({}),
13
+ action: async (options) => {
14
+ const hotCodePush = createApiClient();
15
+ const fetchedLimits = await hotCodePush.organizations.limits.get({
16
+ organizationId: await fetchOrganizationId(hotCodePush, options),
17
+ });
18
+ if (options.json) {
19
+ printJson(fetchedLimits);
20
+ return;
21
+ }
22
+ printTable({
23
+ emptyText: 'No limits.',
24
+ headers: ['LIMIT', 'VALUE', 'DEFAULT', 'OVERRIDDEN'],
25
+ nextOffset: null,
26
+ rows: Object.entries(fetchedLimits).map(([key, limit]) => [
27
+ key,
28
+ String(limit.value),
29
+ String(limit.default),
30
+ limit.isOverridden ? 'yes' : 'no',
31
+ ]),
32
+ });
33
+ },
34
+ });
@@ -0,0 +1,185 @@
1
+ import { setTimeout } from 'node:timers/promises';
2
+ import { z } from 'zod';
3
+ import { defineCommand } from 'zodline';
4
+ import { CLI_CLIENT_ID } from '../config/consts.js';
5
+ import { createApiAuthClient, fetchSession, resolveResponseData, } from '../utils/auth-client.js';
6
+ import { openBrowser } from '../utils/browser.js';
7
+ import { isInteractive } from '../utils/environment.js';
8
+ import { resolveApiError } from '../utils/error-mapping.js';
9
+ import { LoginDeniedError, LoginExpiredError, NotLoggedInError, } from '../utils/errors.js';
10
+ import { defineCommandOptions } from '../utils/global-options.js';
11
+ import { printJson } from '../utils/output.js';
12
+ import { writeToken } from '../utils/token-store.js';
13
+ import { readUserConfig, writeUserConfig } from '../utils/user-config.js';
14
+ // RFC 8628, section 3.2: without an interval from the server, a client polls every five seconds.
15
+ const DEFAULT_INTERVAL_SECONDS = 5;
16
+ const DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
17
+ // RFC 8628, section 3.5: each slow_down adds five seconds to this and every later interval.
18
+ const SLOW_DOWN_INCREMENT_SECONDS = 5;
19
+ export default defineCommand({
20
+ description: 'Log in by approving a one-time code in the browser.',
21
+ examples: [
22
+ 'hotcodepush login',
23
+ 'hotcodepush login --api-url https://api.example.com --json',
24
+ ],
25
+ options: defineCommandOptions({
26
+ apiUrl: z
27
+ .url({
28
+ error: 'Expected an absolute http or https URL such as https://api.example.com',
29
+ protocol: z.regexes.httpProtocol,
30
+ })
31
+ .transform(apiUrl => apiUrl.replace(/\/+$/, ''))
32
+ .optional()
33
+ .describe('The API to log in to, kept in config.json for every later command; the one kept there otherwise.'),
34
+ }),
35
+ action: async (options) => {
36
+ if (options.apiUrl !== undefined) {
37
+ writeUserConfig({ ...readUserConfig(), apiUrl: options.apiUrl });
38
+ }
39
+ const user = await logIn(options);
40
+ if (options.json) {
41
+ printJson({ user });
42
+ }
43
+ else {
44
+ console.log(`Logged in as ${user.name} (${user.email}).`);
45
+ }
46
+ },
47
+ });
48
+ // The page and the code go to stderr, so the stdout of the calling command stays its own.
49
+ export async function logIn(options) {
50
+ const authClient = createApiAuthClient();
51
+ const sessionToken = await fetchSessionToken(authClient, isInteractive(options));
52
+ const { session, user } = await fetchSession(createApiAuthClient(sessionToken));
53
+ writeToken(sessionToken);
54
+ deletePendingDeviceAuthorization();
55
+ writeUserConfig({ ...readUserConfig(), sessionId: session.id });
56
+ return { email: user.email, id: user.id, name: user.name };
57
+ }
58
+ // A request without an answer or with a transient status is polled again, since the person may be approving meanwhile.
59
+ async function fetchApprovedSessionToken(authClient, deviceAuthorization) {
60
+ let currentIntervalSeconds = deviceAuthorization.intervalSeconds;
61
+ for (;;) {
62
+ await setTimeout(currentIntervalSeconds * 1000);
63
+ if (isDeviceAuthorizationExpired(deviceAuthorization)) {
64
+ throw new LoginExpiredError();
65
+ }
66
+ const response = await fetchDeviceToken(authClient, deviceAuthorization.deviceCode).catch(() => undefined);
67
+ if (response === undefined) {
68
+ continue;
69
+ }
70
+ const { data, error } = response;
71
+ if (error === null) {
72
+ return data.access_token;
73
+ }
74
+ switch (error.error) {
75
+ case 'access_denied':
76
+ throw new LoginDeniedError();
77
+ case 'authorization_pending':
78
+ break;
79
+ case 'expired_token':
80
+ throw new LoginExpiredError();
81
+ case 'slow_down':
82
+ currentIntervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;
83
+ break;
84
+ default:
85
+ if (!isTransientStatus(error.status)) {
86
+ throw resolveApiError(error);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ async function fetchDeviceAuthorization(authClient) {
92
+ const { device_code, expires_in, interval, user_code, verification_uri_complete, } = resolveResponseData(await authClient.device.code({ client_id: CLI_CLIENT_ID }));
93
+ return {
94
+ deviceCode: device_code,
95
+ expiresAt: new Date(Date.now() + expires_in * 1000).toISOString(),
96
+ intervalSeconds: interval,
97
+ userCode: user_code,
98
+ verificationUrl: verification_uri_complete,
99
+ };
100
+ }
101
+ function fetchDeviceToken(authClient, deviceCode) {
102
+ return authClient.device.token({
103
+ client_id: CLI_CLIENT_ID,
104
+ device_code: deviceCode,
105
+ grant_type: DEVICE_CODE_GRANT_TYPE,
106
+ });
107
+ }
108
+ // A code that a non-interactive login kept is redeemed first, with one token request.
109
+ // While pending it is shown again. Denied, expired or unknown, it gives way to a new one.
110
+ async function fetchSessionToken(authClient, isLoginInteractive) {
111
+ const pendingDeviceAuthorization = readPendingDeviceAuthorization();
112
+ if (pendingDeviceAuthorization !== undefined &&
113
+ !isDeviceAuthorizationExpired(pendingDeviceAuthorization)) {
114
+ const { data, error } = await fetchDeviceToken(authClient, pendingDeviceAuthorization.deviceCode);
115
+ if (error === null) {
116
+ return data.access_token;
117
+ }
118
+ switch (error.error) {
119
+ case 'access_denied':
120
+ case 'expired_token':
121
+ case 'invalid_grant':
122
+ break;
123
+ case 'authorization_pending':
124
+ case 'slow_down':
125
+ return fetchSessionTokenOnApproval(authClient, pendingDeviceAuthorization, isLoginInteractive);
126
+ default:
127
+ throw resolveApiError(error);
128
+ }
129
+ }
130
+ deletePendingDeviceAuthorization();
131
+ return fetchSessionTokenOnApproval(authClient, await fetchDeviceAuthorization(authClient), isLoginInteractive);
132
+ }
133
+ // The code is kept, so the next login redeems one that ended before the approval.
134
+ async function fetchSessionTokenOnApproval(authClient, deviceAuthorization, isLoginInteractive) {
135
+ writePendingDeviceAuthorization(deviceAuthorization);
136
+ if (!isLoginInteractive) {
137
+ throw new NotLoggedInError(deviceAuthorization);
138
+ }
139
+ process.stderr.write(`Open ${deviceAuthorization.verificationUrl} and approve the code ${deviceAuthorization.userCode}.\nWaiting for the approval…\n`);
140
+ await openBrowser(deviceAuthorization.verificationUrl);
141
+ return fetchApprovedSessionToken(authClient, deviceAuthorization);
142
+ }
143
+ function deletePendingDeviceAuthorization() {
144
+ const userConfig = readUserConfig();
145
+ if (userConfig.pendingDeviceCode === undefined) {
146
+ return;
147
+ }
148
+ delete userConfig.pendingDeviceCode;
149
+ delete userConfig.pendingDeviceCodeExpiresAt;
150
+ delete userConfig.pendingUserCode;
151
+ delete userConfig.pendingVerificationUrl;
152
+ writeUserConfig(userConfig);
153
+ }
154
+ function isDeviceAuthorizationExpired({ expiresAt, }) {
155
+ return new Date(expiresAt) <= new Date();
156
+ }
157
+ // The statuses the Node client also retries: a timeout, the rate limit or a server error.
158
+ function isTransientStatus(status) {
159
+ return status === 408 || status === 429 || status >= 500;
160
+ }
161
+ function readPendingDeviceAuthorization() {
162
+ const { pendingDeviceCode, pendingDeviceCodeExpiresAt, pendingUserCode, pendingVerificationUrl, } = readUserConfig();
163
+ if (pendingDeviceCode === undefined ||
164
+ pendingDeviceCodeExpiresAt === undefined ||
165
+ pendingUserCode === undefined ||
166
+ pendingVerificationUrl === undefined) {
167
+ return undefined;
168
+ }
169
+ return {
170
+ deviceCode: pendingDeviceCode,
171
+ expiresAt: pendingDeviceCodeExpiresAt,
172
+ intervalSeconds: DEFAULT_INTERVAL_SECONDS,
173
+ userCode: pendingUserCode,
174
+ verificationUrl: pendingVerificationUrl,
175
+ };
176
+ }
177
+ function writePendingDeviceAuthorization({ deviceCode, expiresAt, userCode, verificationUrl, }) {
178
+ writeUserConfig({
179
+ ...readUserConfig(),
180
+ pendingDeviceCode: deviceCode,
181
+ pendingDeviceCodeExpiresAt: expiresAt,
182
+ pendingUserCode: userCode,
183
+ pendingVerificationUrl: verificationUrl,
184
+ });
185
+ }
@@ -0,0 +1,79 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiAuthClient } from '../utils/auth-client.js';
3
+ import { resolveApiError } from '../utils/error-mapping.js';
4
+ import { NotLoggedInError } from '../utils/errors.js';
5
+ import { defineCommandOptions } from '../utils/global-options.js';
6
+ import { printJson } from '../utils/output.js';
7
+ import { deleteToken, readStoredToken } from '../utils/token-store.js';
8
+ import { readUserConfig, writeUserConfig } from '../utils/user-config.js';
9
+ const UNAUTHORIZED_STATUS = 401;
10
+ export default defineCommand({
11
+ description: 'End the stored session and remove its token; HOTCODEPUSH_TOKEN, when set, stays what it is.',
12
+ examples: ['hotcodepush logout', 'hotcodepush logout --json'],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const storedToken = readStoredToken();
16
+ if (storedToken === undefined) {
17
+ if (!process.env.HOTCODEPUSH_TOKEN) {
18
+ throw new NotLoggedInError();
19
+ }
20
+ printEnded({ id: null, name: null }, options.json);
21
+ return;
22
+ }
23
+ const authClient = createApiAuthClient(storedToken);
24
+ const knownSession = await fetchKnownSession(authClient);
25
+ const { error } = await authClient.revokeSession({
26
+ token: resolveUnsignedToken(storedToken),
27
+ });
28
+ // A 401 means the session already ended, so only the stored copies are left to clear.
29
+ if (error !== null && error.status !== UNAUTHORIZED_STATUS) {
30
+ throw resolveApiError(error);
31
+ }
32
+ deleteToken();
33
+ const storedSessionId = deleteSessionId();
34
+ printEnded({
35
+ id: knownSession?.session.id ?? storedSessionId ?? null,
36
+ name: knownSession?.user.name ?? null,
37
+ }, options.json);
38
+ },
39
+ });
40
+ function deleteSessionId() {
41
+ const userConfig = readUserConfig();
42
+ const { sessionId } = userConfig;
43
+ if (sessionId === undefined) {
44
+ return undefined;
45
+ }
46
+ delete userConfig.sessionId;
47
+ writeUserConfig(userConfig);
48
+ return sessionId;
49
+ }
50
+ async function fetchKnownSession(authClient) {
51
+ const { data, error } = await authClient.getSession();
52
+ if (error !== null) {
53
+ if (error.status === UNAUTHORIZED_STATUS) {
54
+ return null;
55
+ }
56
+ throw resolveApiError(error);
57
+ }
58
+ return data;
59
+ }
60
+ function printEnded(endedSession, isJson) {
61
+ if (isJson) {
62
+ printJson(endedSession);
63
+ return;
64
+ }
65
+ const variableText = process.env.HOTCODEPUSH_TOKEN
66
+ ? ' HOTCODEPUSH_TOKEN still authenticates.'
67
+ : '';
68
+ if (endedSession.id === null && endedSession.name === null) {
69
+ console.log(`No stored session to end.${variableText}`);
70
+ return;
71
+ }
72
+ const nameText = endedSession.name === null ? '' : ` ${endedSession.name}`;
73
+ console.log(`Logged out${nameText}.${variableText}`);
74
+ }
75
+ // A bearer can be signed, `<token>.<signature>`, but revoke-session looks up the token alone.
76
+ function resolveUnsignedToken(token) {
77
+ const [unsignedToken = token] = token.split('.');
78
+ return unsignedToken;
79
+ }
@@ -0,0 +1,35 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { fetchMember, memberOptionShape, resolveMemberEmail, } from '../../utils/member-resolution.js';
5
+ import { printJson } from '../../utils/output.js';
6
+ import { confirmConsequence } from '../../utils/prompts.js';
7
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: 'Remove a member from the organization, or yourself to leave it; the Owner leaves only after transferring the ownership.',
10
+ examples: [
11
+ 'hotcodepush member delete --member bob@example.com',
12
+ 'hotcodepush member delete --member 5e8a2c71-0b4d-4f39-9a6e-3c1d7b2f8e40 --yes --json',
13
+ ],
14
+ options: defineCommandOptions(memberOptionShape),
15
+ action: async (options) => {
16
+ const hotCodePush = createApiClient();
17
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
18
+ const fetchedMember = await fetchMember(hotCodePush, organizationId, options);
19
+ const email = resolveMemberEmail(fetchedMember);
20
+ const isConfirmed = await confirmConsequence(`removes member ${email} from the organization`, options);
21
+ if (!isConfirmed) {
22
+ return;
23
+ }
24
+ await hotCodePush.organizations.members.delete({
25
+ memberId: fetchedMember.id,
26
+ organizationId,
27
+ });
28
+ if (options.json) {
29
+ printJson({ id: fetchedMember.id, name: email });
30
+ }
31
+ else {
32
+ console.log(`Removed member ${email} (${fetchedMember.id}).`);
33
+ }
34
+ },
35
+ });
@@ -0,0 +1,37 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { fetchMember, memberOptionShape, resolveMemberEmail, } from '../../utils/member-resolution.js';
5
+ import { printDetails, printJson } from '../../utils/output.js';
6
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
7
+ export default defineCommand({
8
+ description: 'Print a member of the organization with their role and how they sign in.',
9
+ examples: [
10
+ 'hotcodepush member get --member anna@example.com',
11
+ 'hotcodepush member get --member 2a7d4e91-6c3b-4f58-8e0a-9b1c5d3f7e62 --json',
12
+ ],
13
+ options: defineCommandOptions(memberOptionShape),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const fetchedMember = await fetchMember(hotCodePush, await fetchOrganizationId(hotCodePush, options), options);
17
+ if (options.json) {
18
+ printJson(fetchedMember);
19
+ return;
20
+ }
21
+ printDetails([
22
+ ['ID', fetchedMember.id],
23
+ ['Name', fetchedMember.user?.name ?? ''],
24
+ ['Email', resolveMemberEmail(fetchedMember)],
25
+ ['Role', fetchedMember.role],
26
+ ['Password', hasPasswordAccount(fetchedMember) ? 'yes' : 'no'],
27
+ ['Two-factor', fetchedMember.user?.twoFactorEnabled ? 'yes' : 'no'],
28
+ ['Last seen', fetchedMember.lastSeenAt ?? 'never'],
29
+ ['Joined', fetchedMember.createdAt],
30
+ ]);
31
+ },
32
+ });
33
+ // Better Auth stores a password as an account of the provider `credential`.
34
+ function hasPasswordAccount({ user }) {
35
+ return (user?.accounts?.some(({ providerId }) => providerId === 'credential') ??
36
+ false);
37
+ }
@@ -0,0 +1,41 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { resolveMemberEmail } from '../../utils/member-resolution.js';
5
+ import { printJson, printTable, resolveDate } from '../../utils/output.js';
6
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
7
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: "List the organization's members with their roles, newest first.",
10
+ examples: [
11
+ 'hotcodepush member list --organization Acme',
12
+ 'hotcodepush member list --limit 10 --offset 10 --json',
13
+ ],
14
+ options: defineCommandOptions(paginationShape),
15
+ action: async (options) => {
16
+ const hotCodePush = createApiClient();
17
+ const listedMembers = await hotCodePush.organizations.members.list({
18
+ limit: options.limit,
19
+ offset: options.offset,
20
+ organizationId: await fetchOrganizationId(hotCodePush, options),
21
+ relations: ['user', 'user.accounts'],
22
+ });
23
+ const nextOffset = resolveNextOffset(listedMembers.length, options);
24
+ if (options.json) {
25
+ printJson({ members: listedMembers, nextOffset });
26
+ return;
27
+ }
28
+ printTable({
29
+ emptyText: 'No members.',
30
+ headers: ['ID', 'NAME', 'EMAIL', 'ROLE', 'JOINED'],
31
+ nextOffset,
32
+ rows: listedMembers.map(member => [
33
+ member.id,
34
+ member.user?.name ?? '',
35
+ resolveMemberEmail(member),
36
+ member.role,
37
+ resolveDate(member.createdAt),
38
+ ]),
39
+ });
40
+ },
41
+ });
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { defineCommand } from 'zodline';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { defineCommandOptions } from '../../utils/global-options.js';
5
+ import { fetchMember, memberOptionShape, resolveMemberEmail, } from '../../utils/member-resolution.js';
6
+ import { printJson } from '../../utils/output.js';
7
+ import { confirmConsequence, promptSelect } from '../../utils/prompts.js';
8
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
9
+ const MEMBER_ROLES = [
10
+ 'admin',
11
+ 'billing',
12
+ 'member',
13
+ 'owner',
14
+ ];
15
+ export default defineCommand({
16
+ description: "Change a member's role; --role owner transfers the ownership to them, makes you an admin and asks once, --yes confirming in scripts.",
17
+ examples: [
18
+ 'hotcodepush member update --member bob@example.com --role admin',
19
+ 'hotcodepush member update --member 5e8a2c71-0b4d-4f39-9a6e-3c1d7b2f8e40 --role owner --yes',
20
+ ],
21
+ options: defineCommandOptions({
22
+ ...memberOptionShape,
23
+ role: z
24
+ .enum(MEMBER_ROLES)
25
+ .optional()
26
+ .describe("The member's new role: admin, billing, member, or owner to transfer the ownership."),
27
+ }),
28
+ action: async (options) => {
29
+ const hotCodePush = createApiClient();
30
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
31
+ const fetchedMember = await fetchMember(hotCodePush, organizationId, options);
32
+ const role = options.role ??
33
+ (await promptSelect('--role', 'Which role does the member get?', MEMBER_ROLES.map(role => ({ label: role, value: role })), options));
34
+ const email = resolveMemberEmail(fetchedMember);
35
+ if (role === 'owner') {
36
+ const isConfirmed = await confirmConsequence(`transfers the ownership of the organization to ${email} and makes you an admin; only the new owner can transfer it back`, options);
37
+ if (!isConfirmed) {
38
+ return;
39
+ }
40
+ }
41
+ const updatedMember = await hotCodePush.organizations.members.update({
42
+ memberId: fetchedMember.id,
43
+ organizationId,
44
+ role,
45
+ });
46
+ if (options.json) {
47
+ printJson(updatedMember);
48
+ }
49
+ else {
50
+ console.log(`Updated member ${email} (${updatedMember.id}) to role ${updatedMember.role}.`);
51
+ }
52
+ },
53
+ });
@@ -0,0 +1,37 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../utils/api-client.js';
3
+ import { openBrowser } from '../utils/browser.js';
4
+ import { defineCommandOptions } from '../utils/global-options.js';
5
+ import { resolveConsoleBaseUrl } from '../utils/hosts.js';
6
+ import { printJson } from '../utils/output.js';
7
+ import { assertProjectConfigAppId, readProjectConfig, } from '../utils/project-config.js';
8
+ import { channelOptionShape, fetchAppId, fetchChannel, } from '../utils/resource-resolution.js';
9
+ import { readApiUrl } from '../utils/user-config.js';
10
+ export default defineCommand({
11
+ description: "Open the app's console page in the browser, or the channel's, from hotcodepush.json.",
12
+ examples: ['hotcodepush open', 'hotcodepush open --channel staging'],
13
+ options: defineCommandOptions(channelOptionShape),
14
+ action: async (options) => {
15
+ const url = await resolveConsolePageUrl(options);
16
+ if (options.json) {
17
+ printJson({ url });
18
+ return;
19
+ }
20
+ console.log(`Opening ${url}`);
21
+ await openBrowser(url);
22
+ },
23
+ });
24
+ async function resolveConsolePageUrl(options) {
25
+ const consoleBaseUrl = resolveConsoleBaseUrl(readApiUrl());
26
+ if (options.channel !== undefined) {
27
+ const channel = await fetchChannel(createApiClient(), options);
28
+ return `${consoleBaseUrl}/apps/${channel.appId}/channels/${channel.id}`;
29
+ }
30
+ const projectConfig = readProjectConfig(options.config);
31
+ if (options.app === undefined && projectConfig?.appId !== undefined) {
32
+ assertProjectConfigAppId(projectConfig.appId);
33
+ return `${consoleBaseUrl}/apps/${projectConfig.appId}`;
34
+ }
35
+ const appId = await fetchAppId(createApiClient(), options, projectConfig);
36
+ return `${consoleBaseUrl}/apps/${appId}`;
37
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ import { defineCommand } from 'zodline';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { defineCommandOptions } from '../../utils/global-options.js';
5
+ import { printJson } from '../../utils/output.js';
6
+ import { promptText } from '../../utils/prompts.js';
7
+ export default defineCommand({
8
+ description: 'Create an organization with you as its Owner.',
9
+ examples: [
10
+ 'hotcodepush organization create --name Acme',
11
+ 'hotcodepush organization create --name Acme --json',
12
+ ],
13
+ options: defineCommandOptions({
14
+ name: z.string().optional().describe("The organization's name."),
15
+ }),
16
+ action: async (options) => {
17
+ const hotCodePush = createApiClient();
18
+ const name = options.name ??
19
+ (await promptText('--name', 'What is the organization called?', options));
20
+ const createdOrganization = await hotCodePush.organizations.create({
21
+ name,
22
+ });
23
+ if (options.json) {
24
+ printJson(createdOrganization);
25
+ }
26
+ else {
27
+ console.log(`Created organization ${createdOrganization.name} (${createdOrganization.id}).`);
28
+ }
29
+ },
30
+ });
@@ -0,0 +1,39 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson, resolveQuantityText } from '../../utils/output.js';
5
+ import { confirmConsequence } from '../../utils/prompts.js';
6
+ import { fetchApps, fetchOrganizationId, } from '../../utils/resource-resolution.js';
7
+ export default defineCommand({
8
+ description: 'Delete an organization with everything in it; support can restore it for seven days.',
9
+ examples: [
10
+ 'hotcodepush organization delete --organization Acme',
11
+ 'hotcodepush organization delete --organization Acme --yes --json',
12
+ ],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
17
+ const [fetchedOrganization, fetchedApps] = await Promise.all([
18
+ hotCodePush.organizations.get({ organizationId }),
19
+ fetchApps(hotCodePush, organizationId),
20
+ ]);
21
+ const isConfirmed = await confirmConsequence(resolveDeletionConsequence(fetchedOrganization.name, fetchedApps.length), options);
22
+ if (!isConfirmed) {
23
+ return;
24
+ }
25
+ await hotCodePush.organizations.delete({ organizationId });
26
+ if (options.json) {
27
+ printJson({ id: organizationId, name: fetchedOrganization.name });
28
+ }
29
+ else {
30
+ console.log(`Deleted organization ${fetchedOrganization.name} (${organizationId}).`);
31
+ }
32
+ },
33
+ });
34
+ function resolveDeletionConsequence(organizationName, appCount) {
35
+ const deletion = `deletes organization ${organizationName}`;
36
+ return appCount === 0
37
+ ? deletion
38
+ : `${deletion} and its ${resolveQuantityText(appCount, 'app')}`;
39
+ }
@@ -0,0 +1,38 @@
1
+ import { defineCommand } from 'zodline';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printDetails, printJson } from '../../utils/output.js';
5
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
6
+ import { resolveDollarText } from '../../utils/spending-cap.js';
7
+ export default defineCommand({
8
+ description: 'Print an organization with its plan, spending cap and region.',
9
+ examples: [
10
+ 'hotcodepush organization get --organization Acme',
11
+ 'hotcodepush organization get --json',
12
+ ],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const fetchedOrganization = await hotCodePush.organizations.get({
17
+ organizationId: await fetchOrganizationId(hotCodePush, options),
18
+ });
19
+ if (options.json) {
20
+ printJson(fetchedOrganization);
21
+ return;
22
+ }
23
+ printOrganizationDetails(fetchedOrganization);
24
+ },
25
+ });
26
+ export function printOrganizationDetails(organization) {
27
+ printDetails([
28
+ ['ID', organization.id],
29
+ ['Name', organization.name],
30
+ ['Plan', organization.plan],
31
+ [
32
+ 'Spending cap',
33
+ `${resolveDollarText(organization.spendingCapCents)} a month`,
34
+ ],
35
+ ['Region', organization.region],
36
+ ['Created', organization.createdAt],
37
+ ]);
38
+ }