hotcodepush 0.0.1 → 0.2.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 (165) 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 +191 -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 +177 -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 +37 -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 +364 -0
  32. package/dist/commands/fingerprint/diff.js +134 -0
  33. package/dist/commands/fingerprint.js +42 -0
  34. package/dist/commands/init.js +493 -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 +36 -0
  41. package/dist/commands/logout.js +81 -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 +254 -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 +78 -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/authentication.js +60 -0
  92. package/dist/utils/boolean-flag.js +3 -0
  93. package/dist/utils/browser.js +6 -0
  94. package/dist/utils/build-step.js +104 -0
  95. package/dist/utils/bundle-files.js +80 -0
  96. package/dist/utils/bundle-resolution.js +46 -0
  97. package/dist/utils/channel-fields.js +130 -0
  98. package/dist/utils/cli.js +80 -0
  99. package/dist/utils/command-registry.js +79 -0
  100. package/dist/utils/command.js +4 -0
  101. package/dist/utils/completion/bash.js +48 -0
  102. package/dist/utils/completion/command-tree.js +72 -0
  103. package/dist/utils/completion/fish.js +44 -0
  104. package/dist/utils/completion/posix-case-arms.js +9 -0
  105. package/dist/utils/completion/posix-quote.js +4 -0
  106. package/dist/utils/completion/script-files.js +26 -0
  107. package/dist/utils/completion/zsh.js +72 -0
  108. package/dist/utils/compressed-files.js +31 -0
  109. package/dist/utils/credential.js +15 -0
  110. package/dist/utils/device-resolution.js +17 -0
  111. package/dist/utils/diagnostics.js +20 -0
  112. package/dist/utils/duration.js +16 -0
  113. package/dist/utils/environment.js +22 -0
  114. package/dist/utils/error-mapping.js +75 -0
  115. package/dist/utils/errors.js +266 -0
  116. package/dist/utils/fingerprint.js +76 -0
  117. package/dist/utils/flag-name.js +4 -0
  118. package/dist/utils/fleet.js +9 -0
  119. package/dist/utils/framework.js +74 -0
  120. package/dist/utils/frameworks/binary-create-step.js +70 -0
  121. package/dist/utils/frameworks/capacitor.js +150 -0
  122. package/dist/utils/frameworks/cordova.js +110 -0
  123. package/dist/utils/frameworks/expo.js +152 -0
  124. package/dist/utils/frameworks/index.js +13 -0
  125. package/dist/utils/frameworks/native-glue.js +10 -0
  126. package/dist/utils/frameworks/native-projects.js +27 -0
  127. package/dist/utils/frameworks/react-native-build.js +182 -0
  128. package/dist/utils/frameworks/react-native.js +127 -0
  129. package/dist/utils/frameworks/sdk-package.js +63 -0
  130. package/dist/utils/git-provenance.js +71 -0
  131. package/dist/utils/global-options.js +34 -0
  132. package/dist/utils/hosts.js +53 -0
  133. package/dist/utils/init-steps.js +51 -0
  134. package/dist/utils/json-file.js +28 -0
  135. package/dist/utils/login.js +153 -0
  136. package/dist/utils/member-resolution.js +49 -0
  137. package/dist/utils/native-project-edit.js +41 -0
  138. package/dist/utils/nullable-field.js +10 -0
  139. package/dist/utils/outcome.js +24 -0
  140. package/dist/utils/output.js +38 -0
  141. package/dist/utils/pack.js +20 -0
  142. package/dist/utils/package-json.js +5 -0
  143. package/dist/utils/package-manager.js +50 -0
  144. package/dist/utils/pagination.js +29 -0
  145. package/dist/utils/progress.js +23 -0
  146. package/dist/utils/progression.js +119 -0
  147. package/dist/utils/project-config.js +61 -0
  148. package/dist/utils/prompts.js +53 -0
  149. package/dist/utils/react-native-project.js +119 -0
  150. package/dist/utils/release-conditions.js +79 -0
  151. package/dist/utils/release-output.js +13 -0
  152. package/dist/utils/release-resolution.js +83 -0
  153. package/dist/utils/resource-file.js +81 -0
  154. package/dist/utils/resource-resolution.js +107 -0
  155. package/dist/utils/signing-private-key.js +105 -0
  156. package/dist/utils/spending-cap.js +16 -0
  157. package/dist/utils/telemetry.js +172 -0
  158. package/dist/utils/token-store.js +63 -0
  159. package/dist/utils/update-notice.js +68 -0
  160. package/dist/utils/upload.js +148 -0
  161. package/dist/utils/usage-month.js +14 -0
  162. package/dist/utils/user-config.js +54 -0
  163. package/dist/utils/webhook-resolution.js +47 -0
  164. package/dist/utils/xcode-project.js +181 -0
  165. package/package.json +11 -9
@@ -0,0 +1,38 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.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
+ }
@@ -0,0 +1,41 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { resolveMemberRole } from '../../utils/member-resolution.js';
5
+ import { printJson, printTable, resolveDate } from '../../utils/output.js';
6
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
7
+ export default defineCommand({
8
+ description: 'List your organizations with your role in each, newest first.',
9
+ examples: [
10
+ 'hotcodepush organization list',
11
+ 'hotcodepush organization list --limit 10 --offset 10 --json',
12
+ ],
13
+ options: defineCommandOptions(paginationShape),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const listedOrganizations = await hotCodePush.organizations.list({
17
+ limit: options.limit,
18
+ offset: options.offset,
19
+ });
20
+ const nextOffset = resolveNextOffset(listedOrganizations.length, options);
21
+ if (options.json) {
22
+ printJson({ nextOffset, organizations: listedOrganizations });
23
+ return;
24
+ }
25
+ const { members = [] } = await hotCodePush.users.get({
26
+ relations: ['members'],
27
+ userId: 'me',
28
+ });
29
+ printTable({
30
+ emptyText: 'No organizations.',
31
+ headers: ['ID', 'NAME', 'ROLE', 'CREATED'],
32
+ nextOffset,
33
+ rows: listedOrganizations.map(({ createdAt, id, name }) => [
34
+ id,
35
+ name,
36
+ resolveMemberRole(members, id) ?? '',
37
+ resolveDate(createdAt),
38
+ ]),
39
+ });
40
+ },
41
+ });
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { defineCommandOptions } from '../../utils/global-options.js';
5
+ import { printJson } from '../../utils/output.js';
6
+ import { confirmConsequence, promptText } from '../../utils/prompts.js';
7
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
8
+ import { resolveCents, resolveDollarText, spendingCapShape, } from '../../utils/spending-cap.js';
9
+ import { printOrganizationDetails } from './get.js';
10
+ export default defineCommand({
11
+ description: 'Rename an organization or set its spending cap, which applies now.',
12
+ examples: [
13
+ 'hotcodepush organization update --organization Acme --name "Acme Inc"',
14
+ 'hotcodepush organization update --spending-cap 100 --yes --json',
15
+ ],
16
+ options: defineCommandOptions({
17
+ name: z.string().optional().describe("The organization's new name."),
18
+ ...spendingCapShape,
19
+ }),
20
+ action: async (options) => {
21
+ const hotCodePush = createApiClient();
22
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
23
+ const spendingCapCents = options.spendingCap === undefined
24
+ ? undefined
25
+ : resolveCents(options.spendingCap);
26
+ const name = options.name ??
27
+ (spendingCapCents === undefined
28
+ ? await promptText('--name', 'What is the organization called now?', options)
29
+ : undefined);
30
+ if (spendingCapCents !== undefined) {
31
+ const fetchedOrganization = await hotCodePush.organizations.get({
32
+ organizationId,
33
+ });
34
+ const isConfirmed = await confirmSpendingCapChange(fetchedOrganization, spendingCapCents, options);
35
+ if (!isConfirmed) {
36
+ return;
37
+ }
38
+ }
39
+ const updatedOrganization = await hotCodePush.organizations.update({
40
+ name,
41
+ organizationId,
42
+ spendingCapCents,
43
+ });
44
+ if (options.json) {
45
+ printJson(updatedOrganization);
46
+ }
47
+ else {
48
+ printOrganizationDetails(updatedOrganization);
49
+ }
50
+ },
51
+ });
52
+ // An unchanged cap needs no confirmation, since the API answers it with `no_change`.
53
+ async function confirmSpendingCapChange(organization, spendingCapCents, options) {
54
+ return (spendingCapCents === organization.spendingCapCents ||
55
+ confirmConsequence(resolveSpendingCapConsequence(organization, spendingCapCents), options));
56
+ }
57
+ function resolveSpendingCapConsequence(organization, spendingCapCents) {
58
+ const capText = `the spending cap of ${organization.name}`;
59
+ const previousText = resolveDollarText(organization.spendingCapCents);
60
+ const newText = resolveDollarText(spendingCapCents);
61
+ return spendingCapCents > organization.spendingCapCents
62
+ ? `raises ${capText} from ${previousText} to ${newText} a month and applies now`
63
+ : `lowers ${capText} from ${previousText} to ${newText} a month and applies now. Devices counted this month stay counted, and if they already exceed the new cap, no further device is counted until the cap is raised or the month turns`;
64
+ }
@@ -0,0 +1,237 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { resolveAudienceText } from '../../utils/audience.js';
4
+ import { booleanFlagSchema } from '../../utils/boolean-flag.js';
5
+ import { fetchBundle, resolveBundleLabel, } from '../../utils/bundle-resolution.js';
6
+ import { failurePolicyShape, resolveFailurePolicy, } from '../../utils/channel-fields.js';
7
+ import { defineCommand } from '../../utils/command.js';
8
+ import { withTemporaryDirectory } from '../../utils/compressed-files.js';
9
+ import { InvalidParameterError } from '../../utils/errors.js';
10
+ import { defineCommandOptions } from '../../utils/global-options.js';
11
+ import { printJson, printWarnings } from '../../utils/output.js';
12
+ import { progressionShape, resolveProgression, resolveProgressionText, } from '../../utils/progression.js';
13
+ import { readProjectConfig } from '../../utils/project-config.js';
14
+ import { confirmConsequence } from '../../utils/prompts.js';
15
+ import { buildAudienceQuery, buildReleaseConditions, conditionOptionShape, } from '../../utils/release-conditions.js';
16
+ import { printReleasedLine } from '../../utils/release-output.js';
17
+ import { fetchReleaseLog, waitUntilLive, } from '../../utils/release-resolution.js';
18
+ import { fetchAppId, fetchChannelId, fetchChannels, fetchResourceId, } from '../../utils/resource-resolution.js';
19
+ import { assertWithinBundleBytesLimit, collectUploadFiles, uploadBundle, } from '../../utils/upload.js';
20
+ import { bundleUploadOptionShape, resolveUploadBundleOptions, } from '../bundle/upload.js';
21
+ export default defineCommand({
22
+ description: 'Release a bundle to a channel, uploading the build first unless --bundle or --from-channel names one, and wait until it is live.',
23
+ examples: [
24
+ 'hotcodepush release create --path dist',
25
+ 'hotcodepush release create --from-channel staging --channel production --binary-version ">=2.3.0" --progression --dry-run',
26
+ ],
27
+ options: defineCommandOptions({
28
+ ...bundleUploadOptionShape,
29
+ ...conditionOptionShape,
30
+ ...failurePolicyShape,
31
+ ...progressionShape,
32
+ bundle: z
33
+ .string()
34
+ .optional()
35
+ .describe('A bundle already uploaded, by number or id. Without it the build is uploaded first.'),
36
+ channel: z
37
+ .array(z.string())
38
+ .optional()
39
+ .describe("The channel, by id or name, repeatable. Defaults to hotcodepush.json's channel."),
40
+ dryRun: z
41
+ .boolean()
42
+ .optional()
43
+ .describe('Validate and print the audience the release would reach. Nothing is signed, uploaded or published, so no private key is needed.'),
44
+ fromChannel: z
45
+ .string()
46
+ .optional()
47
+ .describe('Release what this channel serves, by id or name, instead of a bundle or a build.'),
48
+ mandatory: booleanFlagSchema
49
+ .optional()
50
+ .describe('Devices apply the release at once and restart.'),
51
+ notes: z
52
+ .string()
53
+ .optional()
54
+ .describe('The release notes. They ride the public index.'),
55
+ rolloutPercent: z.coerce
56
+ .number()
57
+ .int()
58
+ .min(0)
59
+ .max(100)
60
+ .optional()
61
+ .describe("The share of devices the release reaches, 0 to 100; 100 by default, the schedule's first percentage with --progression."),
62
+ }),
63
+ action: options => withTemporaryDirectory(packagingDirectoryPath => createReleases(options, packagingDirectoryPath)),
64
+ });
65
+ // The packaging directory is removed when the command ends.
66
+ async function createReleases(options, packagingDirectoryPath) {
67
+ const hotCodePush = createApiClient();
68
+ const progression = resolveProgression(options);
69
+ const bundleSource = await resolveBundleSource(hotCodePush, options, packagingDirectoryPath);
70
+ const fingerprint = resolveSourceFingerprint(bundleSource);
71
+ const isMandatory = options.mandatory ?? false;
72
+ const rolloutPercent = options.rolloutPercent ?? progression?.rolloutPercents[0] ?? 100;
73
+ const appId = 'bundle' in bundleSource ? bundleSource.bundle.appId : bundleSource.appId;
74
+ const channelAudiences = await fetchChannelAudiences(hotCodePush, appId, options, fingerprint, rolloutPercent);
75
+ const consequence = resolveReleaseConsequence(bundleSource, channelAudiences, { isMandatory, progression, rolloutPercent });
76
+ if (options.dryRun) {
77
+ await printDryRun(bundleSource, channelAudiences, consequence, options);
78
+ return;
79
+ }
80
+ if (!(await confirmConsequence(consequence, options))) {
81
+ return;
82
+ }
83
+ const releases = [];
84
+ for (const bundle of await resolveReleasedBundles(hotCodePush, bundleSource)) {
85
+ const releaseBody = {
86
+ ...('sourceChannel' in bundleSource
87
+ ? { fromChannelId: bundleSource.sourceChannel.id }
88
+ : { bundleId: bundle.id }),
89
+ conditions: buildReleaseConditions(options, fingerprint),
90
+ ...resolveFailurePolicy(options),
91
+ isMandatory,
92
+ notes: options.notes ?? null,
93
+ progression: progression ?? undefined,
94
+ rolloutPercent,
95
+ };
96
+ for (const { channel } of channelAudiences) {
97
+ const createdRelease = await hotCodePush.apps.channels.releases.create({
98
+ ...releaseBody,
99
+ appId: bundle.appId,
100
+ channelId: channel.id,
101
+ });
102
+ const liveRelease = await waitUntilLive(hotCodePush, bundle.appId, createdRelease.id, ['bundle', 'channel']);
103
+ releases.push(liveRelease);
104
+ if (!options.json) {
105
+ printReleasedLine(liveRelease, channel.name, resolveBundleLabel(bundle));
106
+ }
107
+ }
108
+ }
109
+ printAudienceWarnings(channelAudiences);
110
+ if (options.json) {
111
+ printJson(releases);
112
+ }
113
+ }
114
+ export function resolveReleaseConsequence(bundleSource, channelAudiences, { isMandatory, progression, rolloutPercent }) {
115
+ const audienceText = channelAudiences
116
+ .map(({ audience, channel }) => resolveAudienceText(audience, channel.name, rolloutPercent))
117
+ .join(' and ');
118
+ const progressionText = progression === null
119
+ ? ''
120
+ : `, progressing through ${resolveProgressionText(progression)}`;
121
+ return `${resolveReleaseText(bundleSource)} at ${rolloutPercent} percent${progressionText}${isMandatory ? ', mandatory' : ''}: ${audienceText}`;
122
+ }
123
+ // The audience preview checks the conditions before anything is uploaded.
124
+ async function fetchChannelAudiences(hotCodePush, appId, options, fingerprint, rolloutPercent) {
125
+ const channelReferences = options.channel === undefined || options.channel.length === 0
126
+ ? [undefined]
127
+ : options.channel;
128
+ const audienceQuery = buildAudienceQuery(options, fingerprint, rolloutPercent);
129
+ const projectConfig = readProjectConfig(options.config);
130
+ return Promise.all(channelReferences.map(async (channelReference) => {
131
+ const channel = await hotCodePush.apps.channels.get({
132
+ appId,
133
+ channelId: await fetchChannelId(hotCodePush, appId, { ...options, channel: channelReference }, projectConfig),
134
+ });
135
+ const audience = await hotCodePush.apps.channels.audience.get({
136
+ ...audienceQuery,
137
+ appId: channel.appId,
138
+ channelId: channel.id,
139
+ });
140
+ return { audience, channel };
141
+ }));
142
+ }
143
+ // Mirrors the API, which releases the newest active release of that channel.
144
+ async function fetchSourceRelease(hotCodePush, appId, reference) {
145
+ const sourceChannel = await hotCodePush.apps.channels.get({
146
+ appId,
147
+ channelId: await fetchResourceId('channel', reference, () => fetchChannels(hotCodePush, appId), '--from-channel'),
148
+ });
149
+ const sourceRelease = (await fetchReleaseLog(hotCodePush, sourceChannel)).find(({ status }) => status === 'active');
150
+ if (sourceRelease === undefined) {
151
+ throw new InvalidParameterError(`The channel ${sourceChannel.name} from --from-channel serves no active release.`);
152
+ }
153
+ const bundle = sourceRelease.bundle ??
154
+ (await hotCodePush.apps.bundles.get({
155
+ appId,
156
+ bundleId: sourceRelease.bundleId,
157
+ }));
158
+ return { bundle, sourceChannel, sourceRelease };
159
+ }
160
+ // A dry run still hashes a web build and checks the size limit, but uploads nothing.
161
+ async function printDryRun(bundleSource, channelAudiences, consequence, options) {
162
+ if ('bundleUploads' in bundleSource) {
163
+ for (const bundleUpload of bundleSource.bundleUploads) {
164
+ assertWithinBundleBytesLimit(await collectUploadFiles(bundleUpload));
165
+ }
166
+ }
167
+ printAudienceWarnings(channelAudiences);
168
+ if (options.json) {
169
+ printJson(channelAudiences.map(({ audience, channel }) => ({
170
+ ...audience,
171
+ channelId: channel.id,
172
+ })));
173
+ return;
174
+ }
175
+ console.log(`Dry run, nothing published: this ${consequence}.`);
176
+ }
177
+ // Each channel warns once, however many platform bundles the release carries.
178
+ function printAudienceWarnings(channelAudiences) {
179
+ for (const { audience } of channelAudiences) {
180
+ printWarnings(audience.warnings);
181
+ }
182
+ }
183
+ // The upload runs only once the release is confirmed.
184
+ async function resolveBundleSource(hotCodePush, options, packagingDirectoryPath) {
185
+ const sourceFlags = [
186
+ options.bundle === undefined ? undefined : '--bundle',
187
+ options.fromChannel === undefined ? undefined : '--from-channel',
188
+ options.path === undefined ? undefined : '--path',
189
+ ].filter(flag => flag !== undefined);
190
+ if (sourceFlags.length > 1) {
191
+ throw new InvalidParameterError(`The flags ${sourceFlags.join(' and ')} were passed together.`, 'Pass one of them: the bundle to release, the channel to release from or the build to upload.');
192
+ }
193
+ if (options.bundle === undefined && options.fromChannel === undefined) {
194
+ return resolveUploadBundleOptions(hotCodePush, options, packagingDirectoryPath, { isDryRun: options.dryRun });
195
+ }
196
+ const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
197
+ return options.fromChannel === undefined
198
+ ? { bundle: await fetchBundle(hotCodePush, appId, options) }
199
+ : fetchSourceRelease(hotCodePush, appId, options.fromChannel);
200
+ }
201
+ async function resolveReleasedBundles(hotCodePush, bundleSource) {
202
+ if ('bundle' in bundleSource) {
203
+ return [bundleSource.bundle];
204
+ }
205
+ const bundles = [];
206
+ for (const uploadBundleOptions of bundleSource.bundleUploads) {
207
+ bundles.push((await uploadBundle(hotCodePush, uploadBundleOptions)).bundle);
208
+ }
209
+ return bundles;
210
+ }
211
+ function resolveReleaseText(bundleSource) {
212
+ if ('bundleUploads' in bundleSource) {
213
+ return resolveUploadText(bundleSource.bundleUploads);
214
+ }
215
+ const bundleText = `releases bundle ${resolveBundleLabel(bundleSource.bundle)}`;
216
+ return 'sourceChannel' in bundleSource
217
+ ? `${bundleText}, release #${bundleSource.sourceRelease.number} of ${bundleSource.sourceChannel.name},`
218
+ : bundleText;
219
+ }
220
+ // A build still to upload has no number yet, so it is named by its version label.
221
+ // One bundle for both platforms is the web build. Per-platform bundles are named by their platform.
222
+ function resolveUploadText(bundleUploads) {
223
+ const bundleVersion = bundleUploads[0]?.bundleVersion ?? '';
224
+ const platformTexts = bundleUploads.map(({ platforms }) => platforms.join(' and '));
225
+ if (bundleUploads.length === 1 && platformTexts[0]?.includes(' and ')) {
226
+ return `uploads the web build as ${bundleVersion} and releases it`;
227
+ }
228
+ return bundleUploads.length === 1
229
+ ? `uploads the ${platformTexts[0]} bundle as ${bundleVersion} and releases it`
230
+ : `uploads the ${platformTexts.join(' and ')} bundles as ${bundleVersion} and releases each`;
231
+ }
232
+ // A project has one native contract, whatever the platform.
233
+ function resolveSourceFingerprint(bundleSource) {
234
+ return 'bundle' in bundleSource
235
+ ? bundleSource.bundle.fingerprint
236
+ : (bundleSource.bundleUploads[0]?.fingerprint ?? null);
237
+ }
@@ -0,0 +1,81 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { resolveEffectiveFailurePolicy, resolveFailurePolicyDetails, } from '../../utils/channel-fields.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { resolveReleaseDeviceCount } from '../../utils/fleet.js';
5
+ import { defineCommandOptions } from '../../utils/global-options.js';
6
+ import { printDetails, printJson } from '../../utils/output.js';
7
+ import { resolveProgressionStepText, resolveProgressionText, } from '../../utils/progression.js';
8
+ import { fetchReleaseInChannel, releaseOptionShape, resolveReleaseBundleLabel, } from '../../utils/release-resolution.js';
9
+ import { channelOptionShape } from '../../utils/resource-resolution.js';
10
+ export default defineCommand({
11
+ description: 'Print a release with its rollout, its progression, its effective auto-pause policy, its status and its counters: applied, confirmed and failed by reason.',
12
+ examples: [
13
+ 'hotcodepush release get --release 43',
14
+ 'hotcodepush release get --channel staging --release 43 --json',
15
+ ],
16
+ options: defineCommandOptions({
17
+ ...channelOptionShape,
18
+ ...releaseOptionShape,
19
+ }),
20
+ action: async (options) => {
21
+ const hotCodePush = createApiClient();
22
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
23
+ if (options.json) {
24
+ printJson(release);
25
+ return;
26
+ }
27
+ const fleet = await hotCodePush.apps.statistics.fleet.get({
28
+ appId: channel.appId,
29
+ channelId: channel.id,
30
+ });
31
+ printDetails([
32
+ ['ID', release.id],
33
+ ['Number', `#${release.number}`],
34
+ ['Channel', channel.name],
35
+ ['Bundle', `${resolveReleaseBundleLabel(release)} (${release.bundleId})`],
36
+ ['Status', resolveStatusText(release)],
37
+ ['Rollout', `${release.rolloutPercent}%`],
38
+ ...resolveProgressionDetails(release),
39
+ ...resolveFailurePolicyDetails(resolveEffectiveFailurePolicy(release, channel)),
40
+ ['Mandatory', release.isMandatory ? 'yes' : 'no'],
41
+ ['Notes', release.notes ?? 'none'],
42
+ ['Rolled back from', release.rolledBackFromReleaseId ?? 'nothing'],
43
+ ['Live', release.liveAt ?? 'not yet'],
44
+ ['Devices', String(resolveReleaseDeviceCount(fleet, release.id))],
45
+ ...resolveCounterDetails(release),
46
+ ['Created', release.createdAt],
47
+ ]);
48
+ },
49
+ });
50
+ function resolveCounterDetails({ releaseCounters, }) {
51
+ if (releaseCounters === undefined) {
52
+ return [];
53
+ }
54
+ return [
55
+ ['Applied', String(releaseCounters.appliedUpdateCount)],
56
+ ['Confirmed', String(releaseCounters.confirmedUpdateCount)],
57
+ ['Download failures', String(releaseCounters.downloadFailureCount)],
58
+ ['Verification failures', String(releaseCounters.verificationFailureCount)],
59
+ ['Crash failures', String(releaseCounters.crashFailureCount)],
60
+ [
61
+ 'Readiness timeout failures',
62
+ String(releaseCounters.readinessTimeoutFailureCount),
63
+ ],
64
+ ['Reported failures', String(releaseCounters.reportedFailureCount)],
65
+ ];
66
+ }
67
+ function resolveProgressionDetails(release) {
68
+ const { progression } = release;
69
+ if (progression === null) {
70
+ return [['Progression', 'none']];
71
+ }
72
+ return [
73
+ ['Progression', resolveProgressionText(progression)],
74
+ ['Progression step', resolveProgressionStepText(release, progression)],
75
+ ];
76
+ }
77
+ function resolveStatusText(release) {
78
+ return release.status === 'paused' && release.pausedAt !== null
79
+ ? `paused since ${release.pausedAt}`
80
+ : release.status;
81
+ }
@@ -0,0 +1,69 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { resolveReleaseDeviceCount } from '../../utils/fleet.js';
4
+ import { defineCommandOptions } from '../../utils/global-options.js';
5
+ import { printJson, printTable, resolveDate } from '../../utils/output.js';
6
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
7
+ import { resolveProgressionStepText } from '../../utils/progression.js';
8
+ import { resolveReleaseBundleLabel } from '../../utils/release-resolution.js';
9
+ import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
10
+ export default defineCommand({
11
+ description: "List a channel's releases, newest first, with the progression and the devices on each.",
12
+ examples: [
13
+ 'hotcodepush release list',
14
+ 'hotcodepush release list --channel staging --json',
15
+ ],
16
+ options: defineCommandOptions({ ...channelOptionShape, ...paginationShape }),
17
+ action: async (options) => {
18
+ const hotCodePush = createApiClient();
19
+ const fetchedChannel = await fetchChannel(hotCodePush, options);
20
+ const listedReleases = await hotCodePush.apps.releases.list({
21
+ appId: fetchedChannel.appId,
22
+ channelId: fetchedChannel.id,
23
+ limit: options.limit,
24
+ offset: options.offset,
25
+ relations: ['bundle', 'releaseCounters'],
26
+ });
27
+ const nextOffset = resolveNextOffset(listedReleases.length, options);
28
+ if (options.json) {
29
+ printJson({ nextOffset, releases: listedReleases });
30
+ return;
31
+ }
32
+ const fleet = await hotCodePush.apps.statistics.fleet.get({
33
+ appId: fetchedChannel.appId,
34
+ channelId: fetchedChannel.id,
35
+ });
36
+ printTable({
37
+ emptyText: `No releases in channel ${fetchedChannel.name}.`,
38
+ headers: [
39
+ 'NUMBER',
40
+ 'BUNDLE',
41
+ 'STATUS',
42
+ 'ROLLOUT',
43
+ 'PROGRESSION',
44
+ 'MANDATORY',
45
+ 'DEVICES',
46
+ 'LIVE',
47
+ 'CREATED',
48
+ ],
49
+ nextOffset,
50
+ rows: listedReleases.map(release => [
51
+ `#${release.number}`,
52
+ resolveReleaseBundleLabel(release),
53
+ release.status,
54
+ `${release.rolloutPercent}%`,
55
+ resolveProgressionCell(release),
56
+ release.isMandatory ? 'yes' : 'no',
57
+ String(resolveReleaseDeviceCount(fleet, release.id)),
58
+ release.liveAt === null ? 'pending' : resolveDate(release.liveAt),
59
+ resolveDate(release.createdAt),
60
+ ]),
61
+ });
62
+ },
63
+ });
64
+ function resolveProgressionCell(release) {
65
+ const { progression } = release;
66
+ return progression === null
67
+ ? 'none'
68
+ : `${progression.rolloutPercents.join(',')}% · ${resolveProgressionStepText(release, progression)}`;
69
+ }
@@ -0,0 +1,36 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson } from '../../utils/output.js';
5
+ import { confirmConsequence } from '../../utils/prompts.js';
6
+ import { fetchReleaseInChannel, releaseOptionShape, } from '../../utils/release-resolution.js';
7
+ import { channelOptionShape } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: 'Pause a release: devices on it keep it, and nobody new gets it until it is resumed.',
10
+ examples: [
11
+ 'hotcodepush release pause --release 43',
12
+ 'hotcodepush release pause --channel staging --release 43 --yes --json',
13
+ ],
14
+ options: defineCommandOptions({
15
+ ...channelOptionShape,
16
+ ...releaseOptionShape,
17
+ }),
18
+ action: async (options) => {
19
+ const hotCodePush = createApiClient();
20
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
21
+ const isConfirmed = await confirmConsequence(`pauses release #${release.number} of ${channel.name}: devices on it keep it and no new device gets it`, options);
22
+ if (!isConfirmed) {
23
+ return;
24
+ }
25
+ const pausedRelease = await hotCodePush.apps.releases.pause({
26
+ appId: channel.appId,
27
+ releaseId: release.id,
28
+ });
29
+ if (options.json) {
30
+ printJson(pausedRelease);
31
+ }
32
+ else {
33
+ console.log(`Paused release #${pausedRelease.number} of ${channel.name} (${pausedRelease.id}).`);
34
+ }
35
+ },
36
+ });
@@ -0,0 +1,36 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { defineCommand } from '../../utils/command.js';
3
+ import { defineCommandOptions } from '../../utils/global-options.js';
4
+ import { printJson } from '../../utils/output.js';
5
+ import { confirmConsequence } from '../../utils/prompts.js';
6
+ import { fetchReleaseInChannel, releaseOptionShape, } from '../../utils/release-resolution.js';
7
+ import { channelOptionShape } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: 'Resume a paused release: devices get it again, and a new auto-pause window starts.',
10
+ examples: [
11
+ 'hotcodepush release resume --release 43',
12
+ 'hotcodepush release resume --channel staging --release 43 --yes --json',
13
+ ],
14
+ options: defineCommandOptions({
15
+ ...channelOptionShape,
16
+ ...releaseOptionShape,
17
+ }),
18
+ action: async (options) => {
19
+ const hotCodePush = createApiClient();
20
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
21
+ const isConfirmed = await confirmConsequence(`resumes release #${release.number} of ${channel.name}: devices get it again and a new auto-pause window starts`, options);
22
+ if (!isConfirmed) {
23
+ return;
24
+ }
25
+ const resumedRelease = await hotCodePush.apps.releases.resume({
26
+ appId: channel.appId,
27
+ releaseId: release.id,
28
+ });
29
+ if (options.json) {
30
+ printJson(resumedRelease);
31
+ }
32
+ else {
33
+ console.log(`Resumed release #${resumedRelease.number} of ${channel.name} (${resumedRelease.id}).`);
34
+ }
35
+ },
36
+ });