hotcodepush 0.0.0 → 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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/commands/app/create.js +49 -0
  4. package/dist/commands/app/delete.js +34 -0
  5. package/dist/commands/app/get.js +33 -0
  6. package/dist/commands/app/list.js +38 -0
  7. package/dist/commands/app/transfer.js +58 -0
  8. package/dist/commands/app/update.js +34 -0
  9. package/dist/commands/audience/get.js +69 -0
  10. package/dist/commands/audit-log/list.js +82 -0
  11. package/dist/commands/binary/create.js +189 -0
  12. package/dist/commands/binary/get.js +56 -0
  13. package/dist/commands/binary/list.js +58 -0
  14. package/dist/commands/bundle/delete.js +35 -0
  15. package/dist/commands/bundle/get.js +63 -0
  16. package/dist/commands/bundle/list.js +66 -0
  17. package/dist/commands/bundle/upload.js +176 -0
  18. package/dist/commands/channel/create.js +59 -0
  19. package/dist/commands/channel/delete.js +68 -0
  20. package/dist/commands/channel/get.js +45 -0
  21. package/dist/commands/channel/list.js +39 -0
  22. package/dist/commands/channel/pause.js +32 -0
  23. package/dist/commands/channel/resume.js +32 -0
  24. package/dist/commands/channel/update.js +40 -0
  25. package/dist/commands/checkout/create.js +25 -0
  26. package/dist/commands/completion.js +36 -0
  27. package/dist/commands/customer-portal-session/create.js +25 -0
  28. package/dist/commands/device/delete.js +29 -0
  29. package/dist/commands/device/get.js +60 -0
  30. package/dist/commands/device/list.js +97 -0
  31. package/dist/commands/device/probe.js +253 -0
  32. package/dist/commands/doctor.js +363 -0
  33. package/dist/commands/fingerprint/diff.js +133 -0
  34. package/dist/commands/fingerprint.js +41 -0
  35. package/dist/commands/init.js +492 -0
  36. package/dist/commands/invitation/accept.js +53 -0
  37. package/dist/commands/invitation/create.js +52 -0
  38. package/dist/commands/invitation/delete.js +60 -0
  39. package/dist/commands/invitation/list.js +40 -0
  40. package/dist/commands/limit/list.js +34 -0
  41. package/dist/commands/login.js +185 -0
  42. package/dist/commands/logout.js +79 -0
  43. package/dist/commands/member/delete.js +35 -0
  44. package/dist/commands/member/get.js +37 -0
  45. package/dist/commands/member/list.js +41 -0
  46. package/dist/commands/member/update.js +53 -0
  47. package/dist/commands/open.js +37 -0
  48. package/dist/commands/organization/create.js +30 -0
  49. package/dist/commands/organization/delete.js +39 -0
  50. package/dist/commands/organization/get.js +38 -0
  51. package/dist/commands/organization/list.js +41 -0
  52. package/dist/commands/organization/update.js +64 -0
  53. package/dist/commands/release/create.js +237 -0
  54. package/dist/commands/release/get.js +81 -0
  55. package/dist/commands/release/list.js +69 -0
  56. package/dist/commands/release/pause.js +36 -0
  57. package/dist/commands/release/resume.js +36 -0
  58. package/dist/commands/release/revoke.js +253 -0
  59. package/dist/commands/release/rollback.js +73 -0
  60. package/dist/commands/release/rollout.js +63 -0
  61. package/dist/commands/release/update.js +82 -0
  62. package/dist/commands/resource-file/write.js +76 -0
  63. package/dist/commands/signing-key/add.js +45 -0
  64. package/dist/commands/signing-key/create.js +84 -0
  65. package/dist/commands/signing-key/delete.js +67 -0
  66. package/dist/commands/signing-key/list.js +38 -0
  67. package/dist/commands/sso-provider/delete.js +32 -0
  68. package/dist/commands/sso-provider/get.js +63 -0
  69. package/dist/commands/sso-provider/set.js +161 -0
  70. package/dist/commands/sso-provider/verify.js +31 -0
  71. package/dist/commands/statistics/get.js +189 -0
  72. package/dist/commands/usage/get.js +31 -0
  73. package/dist/commands/usage/list.js +41 -0
  74. package/dist/commands/webhook/create.js +72 -0
  75. package/dist/commands/webhook/delete.js +32 -0
  76. package/dist/commands/webhook/get.js +34 -0
  77. package/dist/commands/webhook/list.js +40 -0
  78. package/dist/commands/webhook/test.js +32 -0
  79. package/dist/commands/webhook/update.js +56 -0
  80. package/dist/commands/webhook-delivery/list.js +63 -0
  81. package/dist/commands/webhook-delivery/redeliver.js +48 -0
  82. package/dist/commands/whoami.js +24 -0
  83. package/dist/completion/hotcodepush.bash +536 -0
  84. package/dist/completion/hotcodepush.fish +979 -0
  85. package/dist/completion/hotcodepush.zsh +1372 -0
  86. package/dist/config/consts.js +32 -0
  87. package/dist/index.js +5 -0
  88. package/dist/registry.js +83 -0
  89. package/dist/utils/api-client.js +17 -0
  90. package/dist/utils/audience.js +9 -0
  91. package/dist/utils/auth-client.js +35 -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 +75 -0
  99. package/dist/utils/command-registry.js +79 -0
  100. package/dist/utils/completion/bash.js +48 -0
  101. package/dist/utils/completion/command-tree.js +72 -0
  102. package/dist/utils/completion/fish.js +44 -0
  103. package/dist/utils/completion/posix-case-arms.js +9 -0
  104. package/dist/utils/completion/posix-quote.js +4 -0
  105. package/dist/utils/completion/script-files.js +26 -0
  106. package/dist/utils/completion/zsh.js +72 -0
  107. package/dist/utils/compressed-files.js +31 -0
  108. package/dist/utils/credential.js +15 -0
  109. package/dist/utils/device-resolution.js +17 -0
  110. package/dist/utils/duration.js +16 -0
  111. package/dist/utils/environment.js +22 -0
  112. package/dist/utils/error-mapping.js +79 -0
  113. package/dist/utils/errors.js +266 -0
  114. package/dist/utils/fingerprint.js +76 -0
  115. package/dist/utils/flag-name.js +4 -0
  116. package/dist/utils/fleet.js +9 -0
  117. package/dist/utils/framework.js +74 -0
  118. package/dist/utils/frameworks/binary-create-step.js +70 -0
  119. package/dist/utils/frameworks/capacitor.js +150 -0
  120. package/dist/utils/frameworks/cordova.js +110 -0
  121. package/dist/utils/frameworks/expo.js +152 -0
  122. package/dist/utils/frameworks/index.js +13 -0
  123. package/dist/utils/frameworks/native-glue.js +10 -0
  124. package/dist/utils/frameworks/native-projects.js +27 -0
  125. package/dist/utils/frameworks/react-native-build.js +182 -0
  126. package/dist/utils/frameworks/react-native.js +127 -0
  127. package/dist/utils/frameworks/sdk-package.js +63 -0
  128. package/dist/utils/git-provenance.js +71 -0
  129. package/dist/utils/global-options.js +34 -0
  130. package/dist/utils/hosts.js +53 -0
  131. package/dist/utils/init-steps.js +51 -0
  132. package/dist/utils/json-file.js +28 -0
  133. package/dist/utils/member-resolution.js +49 -0
  134. package/dist/utils/native-project-edit.js +41 -0
  135. package/dist/utils/nullable-field.js +10 -0
  136. package/dist/utils/outcome.js +24 -0
  137. package/dist/utils/output.js +37 -0
  138. package/dist/utils/pack.js +20 -0
  139. package/dist/utils/package-json.js +5 -0
  140. package/dist/utils/package-manager.js +50 -0
  141. package/dist/utils/pagination.js +29 -0
  142. package/dist/utils/progress.js +23 -0
  143. package/dist/utils/progression.js +119 -0
  144. package/dist/utils/project-config.js +61 -0
  145. package/dist/utils/prompts.js +53 -0
  146. package/dist/utils/react-native-project.js +119 -0
  147. package/dist/utils/release-conditions.js +80 -0
  148. package/dist/utils/release-output.js +13 -0
  149. package/dist/utils/release-resolution.js +83 -0
  150. package/dist/utils/resource-file.js +81 -0
  151. package/dist/utils/resource-resolution.js +107 -0
  152. package/dist/utils/signing-private-key.js +105 -0
  153. package/dist/utils/spending-cap.js +16 -0
  154. package/dist/utils/telemetry.js +171 -0
  155. package/dist/utils/token-store.js +63 -0
  156. package/dist/utils/update-notice.js +67 -0
  157. package/dist/utils/upload.js +148 -0
  158. package/dist/utils/usage-month.js +14 -0
  159. package/dist/utils/user-config.js +46 -0
  160. package/dist/utils/webhook-resolution.js +47 -0
  161. package/dist/utils/xcode-project.js +181 -0
  162. package/package.json +68 -7
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Genz IT Solutions GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # hotcodepush
2
+
3
+ The HotCodePush CLI: set up, release and manage over-the-air (OTA) updates for React Native, Expo, Capacitor and Cordova apps from the terminal and CI.
4
+
5
+ ## Installation
6
+
7
+ The CLI is not on npm yet. Install the preview build of a commit on `main` as a dev dependency, `<sha>` being the commit's short hash:
8
+
9
+ ```sh
10
+ npm install --save-dev https://pkg.pr.new/hotcodepush-team/cli/hotcodepush@<sha>
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```sh
16
+ npx hotcodepush login
17
+ npx hotcodepush init # wires the SDK and the build step into your project
18
+ npx hotcodepush release create --path dist # uploads the build and releases it to the project's channel
19
+ npx hotcodepush release create --channel staging --rollout-percent 10
20
+ npx hotcodepush doctor # checks the setup
21
+ npx hotcodepush --help # every command; <command> --help for its flags
22
+ ```
23
+
24
+ Every command takes `--json` for machine-readable output and `--yes` to skip confirmations.
25
+ In CI, set `HOTCODEPUSH_TOKEN` instead of logging in, and `HOTCODEPUSH_API_BASE_URL` when the token belongs to an API other than production.
26
+
27
+ ### Telemetry
28
+
29
+ After each command, the CLI sends one anonymous event (`command`, `outcome`, `stopped_step` for `init`, `duration_ms`, `cli_version`, `is_ci`, `framework` and `source`) to PostHog, and unexpected errors without their message to Sentry.
30
+ No argument, path or name from your project is ever sent.
31
+ Set `HOTCODEPUSH_TELEMETRY_DISABLED=1` or `DO_NOT_TRACK=1` to turn it off.
32
+
33
+ ## Documentation
34
+
35
+ See [hotcodepush.com/docs/cli](https://hotcodepush.com/docs/cli).
36
+
37
+ ## Development
38
+
39
+ ```sh
40
+ nvm use
41
+ npm ci
42
+ npm run build # compiles src/ into dist/; node dist/index.js --help runs it
43
+ npm test
44
+ npm run lint && npm run typecheck
45
+ npm run fmt # Prettier, run before every commit
46
+ ```
47
+
48
+ Tests live next to the code they test as `*.test.ts`.
49
+ `CLAUDE.md` holds the layout, the rules and how to add a command or a framework.
50
+
51
+ ## License
52
+
53
+ See [LICENSE](./LICENSE).
@@ -0,0 +1,49 @@
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 { promptSelect, promptText } from '../../utils/prompts.js';
7
+ import { fetchOrganizationId } from '../../utils/resource-resolution.js';
8
+ const FRAMEWORKS = [
9
+ 'react_native',
10
+ 'expo',
11
+ 'capacitor',
12
+ 'cordova',
13
+ ];
14
+ export default defineCommand({
15
+ description: 'Create an app with its default channel, production.',
16
+ examples: [
17
+ 'hotcodepush app create --name "My App" --framework capacitor',
18
+ 'hotcodepush app create --organization Acme --name "My App" --framework expo --json',
19
+ ],
20
+ options: defineCommandOptions({
21
+ framework: z
22
+ .enum(FRAMEWORKS)
23
+ .optional()
24
+ .describe('The framework: react_native, expo, capacitor or cordova.'),
25
+ name: z
26
+ .string()
27
+ .optional()
28
+ .describe("The app's name, unique in the organization."),
29
+ }),
30
+ action: async (options) => {
31
+ const hotCodePush = createApiClient();
32
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
33
+ const name = options.name ??
34
+ (await promptText('--name', 'What is the app called?', options));
35
+ const framework = options.framework ??
36
+ (await promptSelect('--framework', 'Which framework does the app use?', FRAMEWORKS.map(framework => ({ label: framework, value: framework })), options));
37
+ const createdApp = await hotCodePush.organizations.apps.create({
38
+ framework,
39
+ name,
40
+ organizationId,
41
+ });
42
+ if (options.json) {
43
+ printJson(createdApp);
44
+ }
45
+ else {
46
+ console.log(`Created app ${createdApp.name} (${createdApp.id}).`);
47
+ }
48
+ },
49
+ });
@@ -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, resolveQuantityText } from '../../utils/output.js';
5
+ import { readProjectConfig } from '../../utils/project-config.js';
6
+ import { confirmConsequence } from '../../utils/prompts.js';
7
+ import { fetchAppId, fetchChannels } from '../../utils/resource-resolution.js';
8
+ export default defineCommand({
9
+ description: 'Delete an app with its channels, bundles and releases; support can restore it for seven days.',
10
+ examples: [
11
+ 'hotcodepush app delete --app "My App"',
12
+ 'hotcodepush app delete --app 7c9e6679-7425-40de-944b-e07fc1f90ae7 --yes --json',
13
+ ],
14
+ options: defineCommandOptions({}),
15
+ action: async (options) => {
16
+ const hotCodePush = createApiClient();
17
+ const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
18
+ const [fetchedApp, fetchedChannels] = await Promise.all([
19
+ hotCodePush.apps.get({ appId }),
20
+ fetchChannels(hotCodePush, appId),
21
+ ]);
22
+ const isConfirmed = await confirmConsequence(`deletes app ${fetchedApp.name} and its ${resolveQuantityText(fetchedChannels.length, 'channel')}`, options);
23
+ if (!isConfirmed) {
24
+ return;
25
+ }
26
+ await hotCodePush.apps.delete({ appId });
27
+ if (options.json) {
28
+ printJson({ id: appId, name: fetchedApp.name });
29
+ }
30
+ else {
31
+ console.log(`Deleted app ${fetchedApp.name} (${appId}).`);
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,33 @@
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 { readProjectConfig } from '../../utils/project-config.js';
6
+ import { fetchAppId } from '../../utils/resource-resolution.js';
7
+ export default defineCommand({
8
+ description: "Print an app, hotcodepush.json's by default.",
9
+ examples: [
10
+ 'hotcodepush app get',
11
+ 'hotcodepush app get --app "My App" --organization Acme --json',
12
+ ],
13
+ options: defineCommandOptions({}),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const fetchedApp = await hotCodePush.apps.get({
17
+ appId: await fetchAppId(hotCodePush, options, readProjectConfig(options.config)),
18
+ });
19
+ if (options.json) {
20
+ printJson(fetchedApp);
21
+ return;
22
+ }
23
+ printDetails([
24
+ ['ID', fetchedApp.id],
25
+ ['Name', fetchedApp.name],
26
+ ['Framework', fetchedApp.framework],
27
+ ['Organization', fetchedApp.organizationId],
28
+ ['Default channel', fetchedApp.defaultChannelId ?? 'none'],
29
+ ['Channel link template', fetchedApp.channelLinkTemplate ?? 'none'],
30
+ ['Created', fetchedApp.createdAt],
31
+ ]);
32
+ },
33
+ });
@@ -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 { 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 an organization's apps, newest first.",
9
+ examples: [
10
+ 'hotcodepush app list',
11
+ 'hotcodepush app list --organization Acme --json',
12
+ ],
13
+ options: defineCommandOptions(paginationShape),
14
+ action: async (options) => {
15
+ const hotCodePush = createApiClient();
16
+ const listedApps = await hotCodePush.organizations.apps.list({
17
+ limit: options.limit,
18
+ offset: options.offset,
19
+ organizationId: await fetchOrganizationId(hotCodePush, options),
20
+ });
21
+ const nextOffset = resolveNextOffset(listedApps.length, options);
22
+ if (options.json) {
23
+ printJson({ apps: listedApps, nextOffset });
24
+ return;
25
+ }
26
+ printTable({
27
+ emptyText: 'No apps.',
28
+ headers: ['ID', 'NAME', 'FRAMEWORK', 'CREATED'],
29
+ nextOffset,
30
+ rows: listedApps.map(({ createdAt, framework, id, name }) => [
31
+ id,
32
+ name,
33
+ framework,
34
+ resolveDate(createdAt),
35
+ ]),
36
+ });
37
+ },
38
+ });
@@ -0,0 +1,58 @@
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 { readProjectConfig } from '../../utils/project-config.js';
7
+ import { confirmConsequence } from '../../utils/prompts.js';
8
+ import { fetchAppId, fetchOrganizations, fetchResourceId, promptResourceId, } from '../../utils/resource-resolution.js';
9
+ export default defineCommand({
10
+ description: 'Move an app to the organization --organization names, one you are an Admin of.',
11
+ examples: [
12
+ 'hotcodepush app transfer --organization Globex',
13
+ 'hotcodepush app transfer --app "My App" --organization Globex --yes --json',
14
+ ],
15
+ options: defineCommandOptions({
16
+ organization: z
17
+ .string()
18
+ .optional()
19
+ .describe('The organization the app moves to, by id or name.'),
20
+ }),
21
+ action: async (options) => {
22
+ const hotCodePush = createApiClient();
23
+ // Here `--organization` names the target, so the app name is looked up in every organization of the user.
24
+ const appId = await fetchAppId(hotCodePush, { ...options, organization: undefined }, readProjectConfig(options.config));
25
+ const [fetchedApp, fetchedOrganizations] = await Promise.all([
26
+ hotCodePush.apps.get({ appId }),
27
+ fetchOrganizations(hotCodePush),
28
+ ]);
29
+ const targetOrganizationId = await pickTargetOrganizationId(fetchedApp, fetchedOrganizations, options);
30
+ const sourceOrganizationName = resolveOrganizationName(fetchedOrganizations, fetchedApp.organizationId);
31
+ const targetOrganizationName = resolveOrganizationName(fetchedOrganizations, targetOrganizationId);
32
+ const isConfirmed = await confirmConsequence(`moves app ${fetchedApp.name} from organization ${sourceOrganizationName} to organization ${targetOrganizationName}`, options);
33
+ if (!isConfirmed) {
34
+ return;
35
+ }
36
+ const transferredApp = await hotCodePush.apps.transfer({
37
+ appId,
38
+ organizationId: targetOrganizationId,
39
+ });
40
+ if (options.json) {
41
+ printJson(transferredApp);
42
+ }
43
+ else {
44
+ console.log(`Transferred app ${transferredApp.name} (${transferredApp.id}) to organization ${targetOrganizationName}.`);
45
+ }
46
+ },
47
+ });
48
+ function pickTargetOrganizationId(app, organizations, options) {
49
+ if (options.organization === undefined) {
50
+ return promptResourceId('organization', organizations.filter(({ id }) => id !== app.organizationId), options);
51
+ }
52
+ return fetchResourceId('organization', options.organization, async () => organizations);
53
+ }
54
+ // Falls back to the id when the organizations of the user do not include it.
55
+ function resolveOrganizationName(organizations, organizationId) {
56
+ return (organizations.find(({ id }) => id === organizationId)?.name ??
57
+ organizationId);
58
+ }
@@ -0,0 +1,34 @@
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 { readProjectConfig } from '../../utils/project-config.js';
7
+ import { promptText } from '../../utils/prompts.js';
8
+ import { fetchAppId } from '../../utils/resource-resolution.js';
9
+ export default defineCommand({
10
+ description: "Rename an app, hotcodepush.json's by default.",
11
+ examples: [
12
+ 'hotcodepush app update --name "My New App"',
13
+ 'hotcodepush app update --app "My App" --name "My New App" --json',
14
+ ],
15
+ options: defineCommandOptions({
16
+ name: z
17
+ .string()
18
+ .optional()
19
+ .describe("The app's new name, unique in the organization."),
20
+ }),
21
+ action: async (options) => {
22
+ const hotCodePush = createApiClient();
23
+ const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
24
+ const name = options.name ??
25
+ (await promptText('--name', 'What is the app called now?', options));
26
+ const updatedApp = await hotCodePush.apps.update({ appId, name });
27
+ if (options.json) {
28
+ printJson(updatedApp);
29
+ }
30
+ else {
31
+ console.log(`Updated app ${updatedApp.name} (${updatedApp.id}).`);
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { defineCommand } from 'zodline';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { resolveAudienceText } from '../../utils/audience.js';
5
+ import { hasLockfile, readFingerprint } from '../../utils/fingerprint.js';
6
+ import { defineCommandOptions } from '../../utils/global-options.js';
7
+ import { printJson, printTable, printWarnings } from '../../utils/output.js';
8
+ import { locateProjectConfig } from '../../utils/project-config.js';
9
+ import { buildAudienceQuery, conditionOptionShape, } from '../../utils/release-conditions.js';
10
+ import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
11
+ export default defineCommand({
12
+ description: "Preview the devices a release's conditions would reach in a channel, before it exists, the project's fingerprint among them; what release create --dry-run prints.",
13
+ examples: [
14
+ 'hotcodepush audience get --binary-version ">=2.3.0" --rollout-percent 10',
15
+ 'hotcodepush audience get --channel production --attribute tier=beta --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259 --json',
16
+ ],
17
+ options: defineCommandOptions({
18
+ ...channelOptionShape,
19
+ ...conditionOptionShape,
20
+ rolloutPercent: z.coerce
21
+ .number()
22
+ .int()
23
+ .min(0)
24
+ .max(100)
25
+ .optional()
26
+ .describe('The rollout percentage to estimate, 0 to 100; 100 by default.'),
27
+ }),
28
+ action: async (options) => {
29
+ const hotCodePush = createApiClient();
30
+ const channel = await fetchChannel(hotCodePush, options);
31
+ const rolloutPercent = options.rolloutPercent ?? 100;
32
+ const audience = await hotCodePush.apps.channels.audience.get({
33
+ ...buildAudienceQuery(options, await readProjectFingerprint(options.config), rolloutPercent),
34
+ appId: channel.appId,
35
+ channelId: channel.id,
36
+ });
37
+ printWarnings(audience.warnings);
38
+ if (options.json) {
39
+ printJson(audience);
40
+ return;
41
+ }
42
+ const audienceText = resolveAudienceText(audience, channel.name, rolloutPercent);
43
+ console.log(`${audienceText.charAt(0).toUpperCase()}${audienceText.slice(1)}.`);
44
+ if (audience.reachedDeviceCount === 0) {
45
+ return;
46
+ }
47
+ for (const [header, deviceCounts] of [
48
+ ['PLATFORM', audience.platform],
49
+ ['BINARY VERSION', audience.binaryVersion],
50
+ ]) {
51
+ printTable({
52
+ emptyText: '',
53
+ headers: [header, 'DEVICES'],
54
+ nextOffset: null,
55
+ rows: deviceCounts.map(({ deviceCount, value }) => [
56
+ value ?? 'none',
57
+ String(deviceCount),
58
+ ]),
59
+ });
60
+ }
61
+ },
62
+ });
63
+ // Uses the fingerprint that `release create` would set, so the preview counts the same devices.
64
+ async function readProjectFingerprint(configPath) {
65
+ const { directoryPath, projectConfig } = locateProjectConfig(configPath);
66
+ return hasLockfile(directoryPath)
67
+ ? readFingerprint(directoryPath, projectConfig?.extraFingerprintPaths ?? [])
68
+ : null;
69
+ }
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod';
2
+ import { defineCommand } from 'zodline';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { resolveTimeBound } from '../../utils/duration.js';
5
+ import { defineCommandOptions } from '../../utils/global-options.js';
6
+ import { printJson, printTable } from '../../utils/output.js';
7
+ import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
8
+ import { fetchApps, fetchOrganizationId, fetchResourceId, } from '../../utils/resource-resolution.js';
9
+ const ACTOR_TYPES = ['system', 'user'];
10
+ export default defineCommand({
11
+ description: "List the organization's audit log, newest first, filtered by type, actor, app and time, on a plan that includes the audit log.",
12
+ examples: [
13
+ 'hotcodepush audit-log list --type "channel.*" --created-since 7d',
14
+ 'hotcodepush audit-log list --app "My App" --actor 4f2c8a1e-6b3d-4e9f-a7c5-2d8b1e6f3a90 --json',
15
+ ],
16
+ options: defineCommandOptions({
17
+ ...paginationShape,
18
+ actor: z.string().optional().describe('The user who acted, by id.'),
19
+ actorType: z
20
+ .enum(ACTOR_TYPES)
21
+ .optional()
22
+ .describe('system for the rows no user wrote, user for the rest.'),
23
+ app: z
24
+ .string()
25
+ .optional()
26
+ .describe("The app, by id or name, to narrow the log to; the whole organization by default, never hotcodepush.json's app."),
27
+ createdSince: z
28
+ .string()
29
+ .optional()
30
+ .describe('The earliest row, inclusive: an ISO 8601 timestamp or a duration ago such as 2h or 7d.'),
31
+ createdUntil: z
32
+ .string()
33
+ .optional()
34
+ .describe('The latest row, inclusive: an ISO 8601 timestamp or a duration ago such as 2h or 7d.'),
35
+ type: z
36
+ .string()
37
+ .optional()
38
+ .describe('The type, such as channel.created, or channel.* for every action on the object.'),
39
+ }),
40
+ action: async (options) => {
41
+ const hotCodePush = createApiClient();
42
+ const organizationId = await fetchOrganizationId(hotCodePush, options);
43
+ // The log belongs to the organization, so only `--app` filters it, never the app in hotcodepush.json.
44
+ const listedAuditLogs = await hotCodePush.organizations.auditLogs.list({
45
+ actorType: options.actorType,
46
+ appId: options.app === undefined
47
+ ? undefined
48
+ : await fetchResourceId('app', options.app, () => fetchApps(hotCodePush, organizationId)),
49
+ createdSince: resolveTimeBound(options.createdSince),
50
+ createdUntil: resolveTimeBound(options.createdUntil),
51
+ limit: options.limit,
52
+ offset: options.offset,
53
+ organizationId,
54
+ relations: ['user'],
55
+ type: options.type,
56
+ userId: options.actor,
57
+ });
58
+ const nextOffset = resolveNextOffset(listedAuditLogs.length, options);
59
+ if (options.json) {
60
+ printJson({ auditLogs: listedAuditLogs, nextOffset });
61
+ return;
62
+ }
63
+ printTable({
64
+ emptyText: 'No audit log rows match.',
65
+ headers: ['TIME', 'TYPE', 'ACTOR', 'OBJECT'],
66
+ nextOffset,
67
+ rows: listedAuditLogs.map(auditLog => [
68
+ auditLog.createdAt,
69
+ auditLog.type,
70
+ resolveActorText(auditLog),
71
+ auditLog.objectId ?? '',
72
+ ]),
73
+ });
74
+ },
75
+ });
76
+ // Matches the console: the email while the user exists, the id after.
77
+ function resolveActorText({ actorType, user, userId }) {
78
+ if (actorType === 'system') {
79
+ return 'System';
80
+ }
81
+ return user?.email ?? userId ?? '';
82
+ }
@@ -0,0 +1,189 @@
1
+ import { z } from 'zod';
2
+ import { defineCommand } from 'zodline';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { buildStepShape, fetchChannelId, readBuildStep, resolveChannelIdByShape, resolveFailureText, resolveOfflineCause, writeBuildResourceFile, } from '../../utils/build-step.js';
5
+ import { compressFiles, withTemporaryDirectory, } from '../../utils/compressed-files.js';
6
+ import { isCi } from '../../utils/environment.js';
7
+ import { CliError, PipelineNotLoggedInError } from '../../utils/errors.js';
8
+ import { assertMissingParameter } from '../../utils/framework.js';
9
+ import { defineCommandOptions } from '../../utils/global-options.js';
10
+ import { printJson } from '../../utils/output.js';
11
+ import { createReporter, resolveByteText } from '../../utils/progress.js';
12
+ import { assertWithinBundleBytesLimit, resolveMissingSha256s, uploadMissingFiles, } from '../../utils/upload.js';
13
+ const NO_UPLOAD = { fileCount: 0, sizeBytes: 0 };
14
+ export default defineCommand({
15
+ description: 'The build step of a store build: creates the binary with the embedded bundle it ships and writes the resource file the SDK reads, naming that bundle; every other build runs resource-file write. HOTCODEPUSH_OFFLINE=1 builds without the API, naming no channel unless it is given by id and creating no binary.',
16
+ examples: [
17
+ 'hotcodepush binary create --platform ios --embedded-bundle-path build/App.app/public --resource-file-path build/App.app/hotcodepush.json --binary-version 2.4.1 --binary-build 57',
18
+ 'hotcodepush binary create --platform android --embedded-bundle-path build/assets/public --resource-file-path build/assets/hotcodepush.json --binary-version 2.4.1 --binary-build 57 --force',
19
+ ],
20
+ options: defineCommandOptions({
21
+ ...buildStepShape,
22
+ binaryBuild: z
23
+ .string()
24
+ .optional()
25
+ .describe("The store build's build number, from the build's variables."),
26
+ binaryVersion: z
27
+ .string()
28
+ .optional()
29
+ .describe("The store build's version, from the build's variables."),
30
+ force: z
31
+ .boolean()
32
+ .optional()
33
+ .describe('Replace a binary whose fingerprint conflicts, for the deliberate pre-ship rebuild.'),
34
+ }),
35
+ action: async (options) => {
36
+ const buildStep = await readBuildStep(options);
37
+ const { embeddedFiles, platform, resourceFilePath } = buildStep;
38
+ if (embeddedFiles === undefined) {
39
+ writeBuildResourceFile(buildStep, resolveChannelIdByShape(buildStep.channelReference), null);
40
+ process.stderr.write(`No binary created: the ${platform} build bundled no JavaScript, as a debug build served by the development server does. Wrote ${resourceFilePath} without an embedded bundle: over-the-air (OTA) updates are off in this build.\n`);
41
+ if (options.json) {
42
+ printJson({ binary: null, resourceFilePath, upload: NO_UPLOAD });
43
+ }
44
+ return;
45
+ }
46
+ assertWithinBundleBytesLimit(embeddedFiles);
47
+ const identity = resolveBinaryIdentity(options);
48
+ const binaryCreation = await createBuildBinary({
49
+ ...identity,
50
+ appId: buildStep.projectConfig.appId,
51
+ fingerprint: buildStep.fingerprint,
52
+ force: options.force ?? false,
53
+ platform,
54
+ }, buildStep, embeddedFiles, createReporter(options));
55
+ writeBuildResourceFile(buildStep, binaryCreation.channelId, {
56
+ bundleVersion: identity.binaryVersion,
57
+ files: embeddedFiles,
58
+ id: binaryCreation.binary?.bundleId ?? null,
59
+ });
60
+ if (binaryCreation.pipelineFailure !== undefined) {
61
+ throw binaryCreation.pipelineFailure;
62
+ }
63
+ if (binaryCreation.skippedReason !== null) {
64
+ process.stderr.write(`Warning: ${binaryCreation.skippedReason}\n`);
65
+ }
66
+ if (options.json) {
67
+ printJson({
68
+ binary: binaryCreation.binary,
69
+ resourceFilePath,
70
+ upload: binaryCreation.upload,
71
+ });
72
+ return;
73
+ }
74
+ console.log(`Wrote ${resourceFilePath} for ${platform}.`);
75
+ if (binaryCreation.binary !== null) {
76
+ console.log(`Created the binary ${platform} ${identity.binaryVersion} (${identity.binaryBuild}): ${binaryCreation.upload.fileCount} files uploaded, ${resolveByteText(binaryCreation.upload.sizeBytes)}.`);
77
+ }
78
+ },
79
+ });
80
+ async function createBuildBinary(request, buildStep, files, reporter) {
81
+ const channelIdByShape = resolveChannelIdByShape(buildStep.channelReference);
82
+ const offlineCause = resolveOfflineCause();
83
+ if (offlineCause === 'no-token' && isCi()) {
84
+ return resolveFailedCreation(channelIdByShape, new PipelineNotLoggedInError());
85
+ }
86
+ if (offlineCause !== undefined) {
87
+ return {
88
+ binary: null,
89
+ channelId: channelIdByShape,
90
+ skippedReason: resolveOfflineText(offlineCause, channelIdByShape),
91
+ upload: NO_UPLOAD,
92
+ };
93
+ }
94
+ const hotCodePush = createApiClient();
95
+ let channelId;
96
+ try {
97
+ channelId =
98
+ channelIdByShape ?? (await fetchChannelId(hotCodePush, buildStep));
99
+ }
100
+ catch (error) {
101
+ if (error instanceof CliError) {
102
+ throw error;
103
+ }
104
+ if (isCi()) {
105
+ return resolveFailedCreation(null, error);
106
+ }
107
+ return {
108
+ binary: null,
109
+ channelId: null,
110
+ skippedReason: `the channel could not be resolved, so the build names none and takes no OTA updates, and no binary was created: ${resolveFailureText(error)}`,
111
+ upload: NO_UPLOAD,
112
+ };
113
+ }
114
+ try {
115
+ return {
116
+ ...(await createBinaryWithUploads(hotCodePush, request, files, reporter)),
117
+ channelId,
118
+ skippedReason: null,
119
+ };
120
+ }
121
+ catch (error) {
122
+ // A local build never breaks: a failed creation is one warning. CI fails with it, since what it ships must name its channel.
123
+ if (isCi()) {
124
+ return resolveFailedCreation(channelId, error);
125
+ }
126
+ return {
127
+ binary: null,
128
+ channelId,
129
+ skippedReason: `the binary was not created: ${resolveFailureText(error)}`,
130
+ upload: NO_UPLOAD,
131
+ };
132
+ }
133
+ }
134
+ async function createBinaryWithUploads(hotCodePush, request, files, reporter) {
135
+ const { binaryBuild, binaryVersion, ...identityless } = request;
136
+ const createOptions = {
137
+ ...identityless,
138
+ build: binaryBuild,
139
+ files: files.map(({ path, sha256, sizeBytes }) => ({
140
+ path,
141
+ sha256,
142
+ sizeBytes,
143
+ })),
144
+ version: binaryVersion,
145
+ };
146
+ try {
147
+ const binary = await hotCodePush.apps.binaries.create(createOptions);
148
+ return { binary, upload: NO_UPLOAD };
149
+ }
150
+ catch (error) {
151
+ const missingSha256s = resolveMissingSha256s(error);
152
+ if (missingSha256s === undefined) {
153
+ throw error;
154
+ }
155
+ reporter.report(`Uploading ${missingSha256s.length} files the app lacks…`);
156
+ const uploadedFiles = await withTemporaryDirectory(async (temporaryDirectoryPath) => uploadMissingFiles(hotCodePush, request.appId, missingSha256s, await compressFiles(files.filter(({ sha256 }) => missingSha256s.includes(sha256)), temporaryDirectoryPath), reporter));
157
+ const binary = await hotCodePush.apps.binaries.create(createOptions);
158
+ return { binary, upload: uploadedFiles };
159
+ }
160
+ }
161
+ // The CLI reads no project file for the identity.
162
+ function resolveBinaryIdentity(options) {
163
+ const binaryVersion = assertMissingParameter(options.binaryVersion, '--binary-version');
164
+ return {
165
+ binaryBuild: assertMissingParameter(options.binaryBuild, '--binary-build'),
166
+ binaryVersion,
167
+ };
168
+ }
169
+ function resolveFailedCreation(channelId, pipelineFailure) {
170
+ return {
171
+ binary: null,
172
+ channelId,
173
+ pipelineFailure,
174
+ skippedReason: null,
175
+ upload: NO_UPLOAD,
176
+ };
177
+ }
178
+ function resolveOfflineText(offlineCause, channelId) {
179
+ const causeText = offlineCause === 'offline-switch'
180
+ ? 'HOTCODEPUSH_OFFLINE is set'
181
+ : 'not logged in';
182
+ const consequenceText = channelId === null
183
+ ? ': it names no channel and takes no OTA updates until it is built with a token, and no binary was created'
184
+ : ' and no binary was created';
185
+ const fixText = offlineCause === 'offline-switch'
186
+ ? ''
187
+ : '; run "hotcodepush login" or set HOTCODEPUSH_TOKEN';
188
+ return `${causeText}, so the build was made offline${consequenceText}${fixText}.`;
189
+ }