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,32 @@
1
+ import { createRequire } from 'node:module';
2
+ import { fileURLToPath } from 'node:url';
3
+ // Each SDK is installed from the pkg.pr.new build of one commit until its package is published.
4
+ export const CAPACITOR_PACKAGE_NAME = '@hotcodepush/capacitor-live-updates';
5
+ export const CAPACITOR_PACKAGE_SPEC = 'https://pkg.pr.new/hotcodepush-team/capacitor-live-updates/@hotcodepush/capacitor-live-updates@a97c157';
6
+ export const CLI_CLIENT_ID = 'hotcodepush-cli';
7
+ export const CORDOVA_PACKAGE_NAME = '@hotcodepush/cordova-code-push';
8
+ export const CORDOVA_PACKAGE_SPEC = 'https://pkg.pr.new/hotcodepush-team/cordova-code-push/@hotcodepush/cordova-code-push@087f83e';
9
+ export const CLIENT_HEADER_NAME = 'X-HotCodePush-Client';
10
+ // The build writes the completion scripts to dist/completion, beside dist/config.
11
+ export const COMPLETION_DIRECTORY_PATH = fileURLToPath(new URL('../completion/', import.meta.url));
12
+ export const CONFIG_DIRECTORY_NAME = 'hotcodepush';
13
+ export const CONFIG_FILE_NAME = 'config.json';
14
+ export const DEFAULT_API_URL = 'https://api.hotcodepush.com';
15
+ export const DOCS_URL = 'https://hotcodepush.com/docs';
16
+ export const ERRORS_DOCS_URL = 'https://hotcodepush.com/docs/cli/errors';
17
+ export const EXPO_PACKAGE_NAME = '@hotcodepush/expo-ota-updates';
18
+ export const EXPO_PACKAGE_SPEC = 'https://pkg.pr.new/hotcodepush-team/expo-ota-updates/@hotcodepush/expo-ota-updates@0219169';
19
+ export const INIT_MANUAL_STEP = 'Run "hotcodepush init".';
20
+ export const ISSUES_URL = 'https://github.com/hotcodepush-team/cli/issues';
21
+ export const KEYRING_ACCOUNT_NAME = 'token';
22
+ export const KEYRING_SERVICE_NAME = 'hotcodepush-cli';
23
+ export const PACKAGE_JSON = createRequire(import.meta.url)('../../package.json');
24
+ // Our reverse proxy to the PostHog EU host.
25
+ export const POSTHOG_HOST = 'https://hotcodepush.com/ingest';
26
+ export const POSTHOG_PROJECT_KEY = PACKAGE_JSON.telemetry?.posthogProjectKey ?? '';
27
+ export const PROJECT_CONFIG_FILE_NAME = 'hotcodepush.json';
28
+ export const REACT_NATIVE_PACKAGE_NAME = '@hotcodepush/react-native-code-push';
29
+ export const REACT_NATIVE_PACKAGE_SPEC = 'https://pkg.pr.new/hotcodepush-team/react-native-code-push/@hotcodepush/react-native-code-push@56112dd';
30
+ export const SENTRY_DSN = PACKAGE_JSON.telemetry?.sentryDsn ?? '';
31
+ export const SIGNING_PRIVATE_KEY_FILE_NAME = 'hotcodepush-private-key.pem';
32
+ export const USER_AGENT = `hotcodepush-cli/${PACKAGE_JSON.version}`;
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { PACKAGE_JSON } from './config/consts.js';
3
+ import { commandRegistry } from './registry.js';
4
+ import { runCli } from './utils/cli.js';
5
+ process.exitCode = await runCli(commandRegistry, process.argv.slice(2), PACKAGE_JSON);
@@ -0,0 +1,83 @@
1
+ // Lazy imports, so a command line loads only the module it runs.
2
+ export const commandRegistry = {
3
+ 'app create': () => import('./commands/app/create.js'),
4
+ 'app delete': () => import('./commands/app/delete.js'),
5
+ 'app get': () => import('./commands/app/get.js'),
6
+ 'app list': () => import('./commands/app/list.js'),
7
+ 'app transfer': () => import('./commands/app/transfer.js'),
8
+ 'app update': () => import('./commands/app/update.js'),
9
+ 'audience get': () => import('./commands/audience/get.js'),
10
+ 'audit-log list': () => import('./commands/audit-log/list.js'),
11
+ 'binary create': () => import('./commands/binary/create.js'),
12
+ 'binary get': () => import('./commands/binary/get.js'),
13
+ 'binary list': () => import('./commands/binary/list.js'),
14
+ 'bundle delete': () => import('./commands/bundle/delete.js'),
15
+ 'bundle get': () => import('./commands/bundle/get.js'),
16
+ 'bundle list': () => import('./commands/bundle/list.js'),
17
+ 'bundle upload': () => import('./commands/bundle/upload.js'),
18
+ 'channel create': () => import('./commands/channel/create.js'),
19
+ 'channel delete': () => import('./commands/channel/delete.js'),
20
+ 'channel get': () => import('./commands/channel/get.js'),
21
+ 'channel list': () => import('./commands/channel/list.js'),
22
+ 'channel pause': () => import('./commands/channel/pause.js'),
23
+ 'channel resume': () => import('./commands/channel/resume.js'),
24
+ 'channel update': () => import('./commands/channel/update.js'),
25
+ 'checkout create': () => import('./commands/checkout/create.js'),
26
+ 'completion': () => import('./commands/completion.js'),
27
+ 'customer-portal-session create': () => import('./commands/customer-portal-session/create.js'),
28
+ 'device delete': () => import('./commands/device/delete.js'),
29
+ 'device get': () => import('./commands/device/get.js'),
30
+ 'device list': () => import('./commands/device/list.js'),
31
+ 'device probe': () => import('./commands/device/probe.js'),
32
+ 'doctor': () => import('./commands/doctor.js'),
33
+ 'fingerprint': () => import('./commands/fingerprint.js'),
34
+ 'fingerprint diff': () => import('./commands/fingerprint/diff.js'),
35
+ 'init': () => import('./commands/init.js'),
36
+ 'invitation accept': () => import('./commands/invitation/accept.js'),
37
+ 'invitation create': () => import('./commands/invitation/create.js'),
38
+ 'invitation delete': () => import('./commands/invitation/delete.js'),
39
+ 'invitation list': () => import('./commands/invitation/list.js'),
40
+ 'limit list': () => import('./commands/limit/list.js'),
41
+ 'login': () => import('./commands/login.js'),
42
+ 'logout': () => import('./commands/logout.js'),
43
+ 'member delete': () => import('./commands/member/delete.js'),
44
+ 'member get': () => import('./commands/member/get.js'),
45
+ 'member list': () => import('./commands/member/list.js'),
46
+ 'member update': () => import('./commands/member/update.js'),
47
+ 'open': () => import('./commands/open.js'),
48
+ 'organization create': () => import('./commands/organization/create.js'),
49
+ 'organization delete': () => import('./commands/organization/delete.js'),
50
+ 'organization get': () => import('./commands/organization/get.js'),
51
+ 'organization list': () => import('./commands/organization/list.js'),
52
+ 'organization update': () => import('./commands/organization/update.js'),
53
+ 'release create': () => import('./commands/release/create.js'),
54
+ 'release get': () => import('./commands/release/get.js'),
55
+ 'release list': () => import('./commands/release/list.js'),
56
+ 'release pause': () => import('./commands/release/pause.js'),
57
+ 'release resume': () => import('./commands/release/resume.js'),
58
+ 'release revoke': () => import('./commands/release/revoke.js'),
59
+ 'release rollback': () => import('./commands/release/rollback.js'),
60
+ 'release rollout': () => import('./commands/release/rollout.js'),
61
+ 'release update': () => import('./commands/release/update.js'),
62
+ 'resource-file write': () => import('./commands/resource-file/write.js'),
63
+ 'signing-key add': () => import('./commands/signing-key/add.js'),
64
+ 'signing-key create': () => import('./commands/signing-key/create.js'),
65
+ 'signing-key delete': () => import('./commands/signing-key/delete.js'),
66
+ 'signing-key list': () => import('./commands/signing-key/list.js'),
67
+ 'sso-provider delete': () => import('./commands/sso-provider/delete.js'),
68
+ 'sso-provider get': () => import('./commands/sso-provider/get.js'),
69
+ 'sso-provider set': () => import('./commands/sso-provider/set.js'),
70
+ 'sso-provider verify': () => import('./commands/sso-provider/verify.js'),
71
+ 'statistics get': () => import('./commands/statistics/get.js'),
72
+ 'usage get': () => import('./commands/usage/get.js'),
73
+ 'usage list': () => import('./commands/usage/list.js'),
74
+ 'webhook create': () => import('./commands/webhook/create.js'),
75
+ 'webhook delete': () => import('./commands/webhook/delete.js'),
76
+ 'webhook get': () => import('./commands/webhook/get.js'),
77
+ 'webhook list': () => import('./commands/webhook/list.js'),
78
+ 'webhook test': () => import('./commands/webhook/test.js'),
79
+ 'webhook update': () => import('./commands/webhook/update.js'),
80
+ 'webhook-delivery list': () => import('./commands/webhook-delivery/list.js'),
81
+ 'webhook-delivery redeliver': () => import('./commands/webhook-delivery/redeliver.js'),
82
+ 'whoami': () => import('./commands/whoami.js'),
83
+ };
@@ -0,0 +1,17 @@
1
+ import { HotCodePush } from '@hotcodepush/node';
2
+ import { PACKAGE_JSON, USER_AGENT } from '../config/consts.js';
3
+ import { NotLoggedInError } from './errors.js';
4
+ import { readToken } from './token-store.js';
5
+ import { readApiUrl } from './user-config.js';
6
+ export function createApiClient() {
7
+ const token = readToken();
8
+ if (token === undefined) {
9
+ throw new NotLoggedInError();
10
+ }
11
+ return new HotCodePush({
12
+ baseUrl: readApiUrl(),
13
+ client: `cli/${PACKAGE_JSON.version}`,
14
+ token,
15
+ userAgent: USER_AGENT,
16
+ });
17
+ }
@@ -0,0 +1,9 @@
1
+ export function resolveAudienceText(audience, channelName, rolloutPercent) {
2
+ const reachText = `reaches ${resolveCountText(audience.reachedDeviceCount)} of ${resolveCountText(audience.deviceCount)} active devices in ${channelName}`;
3
+ return rolloutPercent === 100
4
+ ? reachText
5
+ : `${reachText}; about ${resolveCountText(audience.estimatedDeviceCount)} at a ${rolloutPercent} percent rollout`;
6
+ }
7
+ function resolveCountText(count) {
8
+ return count.toLocaleString('en-US');
9
+ }
@@ -0,0 +1,35 @@
1
+ import { createAuthClient } from 'better-auth/client';
2
+ import { deviceAuthorizationClient } from 'better-auth/client/plugins';
3
+ import { CLIENT_HEADER_NAME, PACKAGE_JSON, USER_AGENT, } from '../config/consts.js';
4
+ import { resolveApiError } from './error-mapping.js';
5
+ import { NotLoggedInError } from './errors.js';
6
+ import { readApiUrl } from './user-config.js';
7
+ // The bearer is sent only when given, since a stale stored token would fail the login that replaces it.
8
+ // The User-Agent names the CLI because the session row keeps that header, not the client header.
9
+ export function createApiAuthClient(token) {
10
+ return createAuthClient({
11
+ baseURL: `${readApiUrl()}/v1/auth`,
12
+ fetchOptions: {
13
+ auth: token === undefined ? undefined : { token, type: 'Bearer' },
14
+ headers: {
15
+ 'User-Agent': USER_AGENT,
16
+ [CLIENT_HEADER_NAME]: `cli/${PACKAGE_JSON.version}`,
17
+ },
18
+ },
19
+ plugins: [deviceAuthorizationClient()],
20
+ });
21
+ }
22
+ // An API token has no session, so it counts as not logged in.
23
+ export async function fetchSession(authClient) {
24
+ const session = resolveResponseData(await authClient.getSession());
25
+ if (session === null) {
26
+ throw new NotLoggedInError();
27
+ }
28
+ return session;
29
+ }
30
+ export function resolveResponseData(response) {
31
+ if (response.error !== null) {
32
+ throw resolveApiError(response.error);
33
+ }
34
+ return response.data;
35
+ }
@@ -0,0 +1,3 @@
1
+ import { z } from 'zod';
2
+ // A bare flag arrives as true and `--protected false` as a string, so both forms are accepted.
3
+ export const booleanFlagSchema = z.union([z.boolean(), z.stringbool()]);
@@ -0,0 +1,6 @@
1
+ import open from 'open';
2
+ // Failures are ignored, since the command always prints the URL too.
3
+ // A headless machine or a missing `xdg-open` opens no browser.
4
+ export async function openBrowser(url) {
5
+ await open(url).catch(() => undefined);
6
+ }
@@ -0,0 +1,104 @@
1
+ import { resolve } from 'node:path';
2
+ import { HotCodePushError } from '@hotcodepush/node';
3
+ import { z } from 'zod';
4
+ import { PROJECT_CONFIG_FILE_NAME } from '../config/consts.js';
5
+ import { collectBundleFiles } from './bundle-files.js';
6
+ import { isOfflineBuild } from './environment.js';
7
+ import { InvalidParameterError } from './errors.js';
8
+ import { readFingerprint } from './fingerprint.js';
9
+ import { assertMissingParameter, detectFramework, resolveInputDirectoryPath, } from './framework.js';
10
+ import { resolveFrameworkModule } from './frameworks/index.js';
11
+ import { omitNativeGlue } from './frameworks/native-glue.js';
12
+ import { resolveDeviceHosts } from './hosts.js';
13
+ import { assertProjectConfigAppId, ID_SCHEMA, locateProjectConfig, resolveProjectChannel, } from './project-config.js';
14
+ import { promptSelect } from './prompts.js';
15
+ import { buildResourceFile, writeResourceFile } from './resource-file.js';
16
+ import { fetchChannels, fetchResourceId } from './resource-resolution.js';
17
+ import { readToken } from './token-store.js';
18
+ import { readApiUrl } from './user-config.js';
19
+ const PLATFORMS = ['android', 'ios'];
20
+ export const buildStepShape = {
21
+ embeddedBundlePath: z
22
+ .string()
23
+ .optional()
24
+ .describe("The directory of the embedded bundle, the files the build puts into the app; the framework's build output by default."),
25
+ platform: z
26
+ .enum(PLATFORMS)
27
+ .optional()
28
+ .describe('The platform being built, ios or android.'),
29
+ resourceFilePath: z
30
+ .string()
31
+ .optional()
32
+ .describe('Where the resource file goes, in the app the build makes.'),
33
+ };
34
+ // Read in the order a failure should name them.
35
+ // The embedded bundle leaves out the native glue, so its manifest matches a release of the same web build.
36
+ export async function readBuildStep(options) {
37
+ const { directoryPath, projectConfig } = locateProjectConfig(options.config);
38
+ const completeProjectConfig = assertProjectConfig(projectConfig);
39
+ const platform = options.platform ??
40
+ (await promptSelect('--platform', 'Which platform?', PLATFORMS.map(platform => ({ label: platform, value: platform })), options));
41
+ const framework = resolveFrameworkModule(detectFramework(directoryPath));
42
+ const embeddedBundlePath = await resolveInputDirectoryPath({ ...options, path: options.embeddedBundlePath }, '--embedded-bundle-path', directoryPath, framework);
43
+ const embeddedFiles = framework.collectEmbeddedFiles === undefined
44
+ ? omitNativeGlue(await collectBundleFiles(embeddedBundlePath), framework.nativeGluePaths ?? [])
45
+ : await framework.collectEmbeddedFiles(platform, embeddedBundlePath);
46
+ const resourceFilePath = resolve(assertMissingParameter(options.resourceFilePath, '--resource-file-path'));
47
+ return {
48
+ channelReference: resolveChannelReference(completeProjectConfig),
49
+ embeddedFiles,
50
+ fingerprint: await readFingerprint(directoryPath, completeProjectConfig.extraFingerprintPaths ?? []),
51
+ platform,
52
+ projectConfig: completeProjectConfig,
53
+ resourceFilePath,
54
+ };
55
+ }
56
+ // An id needs no API, but only the API can resolve a name.
57
+ export function resolveChannelIdByShape(channelReference) {
58
+ return ID_SCHEMA.safeParse(channelReference.reference).success
59
+ ? channelReference.reference
60
+ : null;
61
+ }
62
+ export function fetchChannelId(hotCodePush, { channelReference, projectConfig }) {
63
+ return fetchResourceId('channel', channelReference.reference, () => fetchChannels(hotCodePush, projectConfig.appId), channelReference.source);
64
+ }
65
+ export function resolveOfflineCause() {
66
+ if (isOfflineBuild()) {
67
+ return 'offline-switch';
68
+ }
69
+ return readToken() === undefined ? 'no-token' : undefined;
70
+ }
71
+ export function resolveFailureText(error) {
72
+ if (error instanceof HotCodePushError) {
73
+ return `${error.code} ${error.message}`;
74
+ }
75
+ return error instanceof Error ? error.message : String(error);
76
+ }
77
+ export function writeBuildResourceFile({ fingerprint, platform, projectConfig, resourceFilePath }, channelId, embeddedBundle) {
78
+ writeResourceFile(resourceFilePath, buildResourceFile({
79
+ builtAt: new Date().toISOString(),
80
+ channelId,
81
+ embeddedBundle,
82
+ fingerprint,
83
+ hosts: resolveDeviceHosts(readApiUrl()),
84
+ platform,
85
+ projectConfig,
86
+ }));
87
+ }
88
+ function assertProjectConfig(projectConfig) {
89
+ if (projectConfig?.appId === undefined) {
90
+ throw new InvalidParameterError('The project has no hotcodepush.json with an appId.', 'Run "hotcodepush init", or pass the file with --config.');
91
+ }
92
+ assertProjectConfigAppId(projectConfig.appId);
93
+ return { ...projectConfig, appId: projectConfig.appId };
94
+ }
95
+ // A build flavour sets `HOTCODEPUSH_CHANNEL` to override the channel of hotcodepush.json.
96
+ function resolveChannelReference(projectConfig) {
97
+ const flavourChannel = process.env.HOTCODEPUSH_CHANNEL;
98
+ return flavourChannel
99
+ ? { reference: flavourChannel, source: 'HOTCODEPUSH_CHANNEL' }
100
+ : {
101
+ reference: resolveProjectChannel(projectConfig),
102
+ source: PROJECT_CONFIG_FILE_NAME,
103
+ };
104
+ }
@@ -0,0 +1,80 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { readdir, realpath, stat } from 'node:fs/promises';
4
+ import { join, relative, sep } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { InvalidParameterError } from './errors.js';
7
+ const DEAD_LINK_ERROR_CODES = new Set(['ELOOP', 'ENOENT']);
8
+ export async function collectBundleFiles(directoryPath) {
9
+ await assertDirectory(directoryPath);
10
+ const filePaths = await collectFilePaths(directoryPath, new Set());
11
+ const collectedFiles = await Promise.all(filePaths
12
+ .map(filePath => ({
13
+ filePath,
14
+ path: relative(directoryPath, filePath).split(sep).join('/'),
15
+ }))
16
+ .filter(({ path }) => isShipped(path))
17
+ .map(async ({ filePath, path }) => ({
18
+ filePath,
19
+ path,
20
+ sha256: await computeFileSha256(filePath),
21
+ sizeBytes: (await stat(filePath)).size,
22
+ })));
23
+ return collectedFiles.sort((a, b) => a.path.localeCompare(b.path));
24
+ }
25
+ // The manifest names files by this hash, and the upload path uses it too.
26
+ export async function computeFileSha256(filePath) {
27
+ const hash = createHash('sha256');
28
+ await pipeline(createReadStream(filePath), hash);
29
+ return hash.digest('hex');
30
+ }
31
+ // Source maps belong in the error tracker and dotfiles are tooling, so neither ships to devices.
32
+ export function isShipped(path) {
33
+ return (!path.endsWith('.map') &&
34
+ !path.split('/').some(segment => segment.startsWith('.')));
35
+ }
36
+ async function assertDirectory(directoryPath) {
37
+ const isDirectory = await stat(directoryPath)
38
+ .then(stats => stats.isDirectory())
39
+ .catch(() => false);
40
+ if (!isDirectory) {
41
+ throw new InvalidParameterError(`The directory ${directoryPath} from --path does not exist.`);
42
+ }
43
+ }
44
+ // Follows links like a web server, so the bundle carries what the native build copies through a link.
45
+ // A link back into a directory the walk is inside ends there instead of looping.
46
+ async function collectFilePaths(directoryPath, ancestorRealPaths) {
47
+ const realDirectoryPath = await realpath(directoryPath);
48
+ if (ancestorRealPaths.has(realDirectoryPath)) {
49
+ return [];
50
+ }
51
+ const walkedRealPaths = new Set([...ancestorRealPaths, realDirectoryPath]);
52
+ const entryNames = await readdir(directoryPath);
53
+ const collectedFilePaths = await Promise.all(entryNames.map(async (entryName) => {
54
+ const entryPath = join(directoryPath, entryName);
55
+ const stats = await readEntryStats(entryPath);
56
+ if (stats?.isDirectory()) {
57
+ return collectFilePaths(entryPath, walkedRealPaths);
58
+ }
59
+ return stats?.isFile() ? [entryPath] : [];
60
+ }));
61
+ return collectedFilePaths.flat();
62
+ }
63
+ function isDeadLinkError(error) {
64
+ return (error instanceof Error &&
65
+ 'code' in error &&
66
+ typeof error.code === 'string' &&
67
+ DEAD_LINK_ERROR_CODES.has(error.code));
68
+ }
69
+ // A link that points nowhere or to itself has no content to serve.
70
+ async function readEntryStats(entryPath) {
71
+ try {
72
+ return await stat(entryPath);
73
+ }
74
+ catch (error) {
75
+ if (isDeadLinkError(error)) {
76
+ return undefined;
77
+ }
78
+ throw error;
79
+ }
80
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import { InvalidParameterError } from './errors.js';
3
+ import { fetchAllPages } from './pagination.js';
4
+ import { ID_SCHEMA } from './project-config.js';
5
+ import { promptSelect } from './prompts.js';
6
+ const NUMBER_PATTERN = /^\d+$/;
7
+ export const bundleOptionShape = {
8
+ bundle: z.string().optional().describe('The bundle, by its number or id.'),
9
+ };
10
+ export async function fetchBundle(hotCodePush, appId, options) {
11
+ const bundleId = await fetchBundleId(hotCodePush, appId, options);
12
+ return hotCodePush.apps.bundles.get({ appId, bundleId });
13
+ }
14
+ // A bundle that a binary ships is never in this list.
15
+ export function fetchUploadedBundles(hotCodePush, appId) {
16
+ return fetchAllPages(page => hotCodePush.apps.bundles.list({ appId, ...page, source: 'upload' }));
17
+ }
18
+ // A bundle that a binary ships has no number, so it reads `embedded · 1.4.2`.
19
+ export function resolveBundleLabel({ number, version, }) {
20
+ return `${number === null ? 'embedded' : `#${number}`} · ${version}`;
21
+ }
22
+ async function fetchBundleId(hotCodePush, appId, options) {
23
+ if (options.bundle === undefined) {
24
+ const bundles = await fetchUploadedBundles(hotCodePush, appId);
25
+ return promptSelect('--bundle', 'Which bundle?', bundles.map(bundle => ({
26
+ label: `${resolveBundleLabel(bundle)} (${bundle.status})`,
27
+ value: bundle.id,
28
+ })), options);
29
+ }
30
+ if (ID_SCHEMA.safeParse(options.bundle).success) {
31
+ return options.bundle;
32
+ }
33
+ if (!NUMBER_PATTERN.test(options.bundle)) {
34
+ throw new InvalidParameterError(`The value "${options.bundle}" of --bundle is neither a number nor an id.`);
35
+ }
36
+ return fetchBundleIdByNumber(hotCodePush, appId, Number(options.bundle));
37
+ }
38
+ // The list search matches numbers that contain the given one, so the exact match is picked here.
39
+ async function fetchBundleIdByNumber(hotCodePush, appId, number) {
40
+ const searchedBundles = await fetchAllPages(page => hotCodePush.apps.bundles.list({ appId, ...page, query: String(number) }));
41
+ const numberedBundle = searchedBundles.find(candidate => candidate.number === number);
42
+ if (numberedBundle === undefined) {
43
+ throw new InvalidParameterError(`The app has no bundle #${number} from --bundle.`);
44
+ }
45
+ return numberedBundle.id;
46
+ }
@@ -0,0 +1,130 @@
1
+ import { z } from 'zod';
2
+ import { booleanFlagSchema } from './boolean-flag.js';
3
+ import { DURATION_PATTERN, resolveDurationMilliseconds } from './duration.js';
4
+ import { InvalidParameterError } from './errors.js';
5
+ import { resolveNullableField } from './nullable-field.js';
6
+ // The plan pins these defaults. The API and the console carry the same constant.
7
+ export const FAILURE_POLICY_DEFAULTS = {
8
+ failureAction: 'pause',
9
+ failureMinimumUpdateCount: 50,
10
+ failureThresholdPercent: 5,
11
+ };
12
+ export const failurePolicyShape = {
13
+ failureAction: z
14
+ .enum(['none', 'notify', 'pause', 'revoke'])
15
+ .optional()
16
+ .describe('What auto-pause does when a release fails too often: pause, revoke, notify or none.'),
17
+ failureMinimumUpdateCount: z.coerce
18
+ .number()
19
+ .int()
20
+ .optional()
21
+ .describe('The applied updates a release needs before auto-pause judges it.'),
22
+ failureThresholdPercent: z.coerce
23
+ .number()
24
+ .int()
25
+ .optional()
26
+ .describe('The failure percentage at which auto-pause acts.'),
27
+ };
28
+ export const channelFieldsShape = {
29
+ discoverable: booleanFlagSchema
30
+ .optional()
31
+ .describe("Whether the app's setChannel finds the channel by its name; pass false to turn it off."),
32
+ expiresAt: z
33
+ .string()
34
+ .optional()
35
+ .describe('When the preview channel expires, an ISO 8601 timestamp; not on protected or default channels.'),
36
+ expiresIn: z
37
+ .string()
38
+ .regex(DURATION_PATTERN, 'Expected a number of days or hours such as 14d or 12h')
39
+ .optional()
40
+ .describe('How long until the preview channel expires, 14d or 12h.'),
41
+ ...failurePolicyShape,
42
+ protected: booleanFlagSchema
43
+ .optional()
44
+ .describe('Whether only Owners and Admins change what the channel serves; pass false to turn it off.'),
45
+ };
46
+ export const channelFieldsClearShape = {
47
+ clearExpiresAt: z
48
+ .boolean()
49
+ .optional()
50
+ .describe('Sets the expiry back to null: the channel never expires.'),
51
+ ...resolveFailurePolicyClearShape('the default policy applies'),
52
+ };
53
+ export const releaseFailurePolicyClearShape = resolveFailurePolicyClearShape("the channel's value applies");
54
+ export function resolveChannelFields(options) {
55
+ return {
56
+ expiresAt: resolveExpiresAt(options),
57
+ ...resolveFailurePolicy(options),
58
+ isDiscoverable: options.discoverable,
59
+ isProtected: options.protected,
60
+ };
61
+ }
62
+ // Each field falls back from its own value to the channel, then to the default.
63
+ export function resolveEffectiveFailurePolicy(ownFields, channelFields) {
64
+ return {
65
+ failureAction: resolveEffectiveFailurePolicyValue(ownFields.failureAction, channelFields?.failureAction, FAILURE_POLICY_DEFAULTS.failureAction),
66
+ failureMinimumUpdateCount: resolveEffectiveFailurePolicyValue(ownFields.failureMinimumUpdateCount, channelFields?.failureMinimumUpdateCount, FAILURE_POLICY_DEFAULTS.failureMinimumUpdateCount),
67
+ failureThresholdPercent: resolveEffectiveFailurePolicyValue(ownFields.failureThresholdPercent, channelFields?.failureThresholdPercent, FAILURE_POLICY_DEFAULTS.failureThresholdPercent),
68
+ };
69
+ }
70
+ export function resolveFailurePolicy(options) {
71
+ return {
72
+ failureAction: resolveNullableField(options.failureAction, options.clearFailureAction, 'failure-action'),
73
+ failureMinimumUpdateCount: resolveNullableField(options.failureMinimumUpdateCount, options.clearFailureMinimumUpdateCount, 'failure-minimum-update-count'),
74
+ failureThresholdPercent: resolveNullableField(options.failureThresholdPercent, options.clearFailureThresholdPercent, 'failure-threshold-percent'),
75
+ };
76
+ }
77
+ // An inherited value names its level, such as `5% (channel)` or `pause (default)`.
78
+ export function resolveFailurePolicyDetails({ failureAction, failureMinimumUpdateCount, failureThresholdPercent, }) {
79
+ return [
80
+ [
81
+ 'Failure threshold',
82
+ resolveEffectiveFailurePolicyText(`${failureThresholdPercent.value}%`, failureThresholdPercent.level),
83
+ ],
84
+ [
85
+ 'Failure minimum update count',
86
+ resolveEffectiveFailurePolicyText(String(failureMinimumUpdateCount.value), failureMinimumUpdateCount.level),
87
+ ],
88
+ [
89
+ 'Failure action',
90
+ resolveEffectiveFailurePolicyText(failureAction.value, failureAction.level),
91
+ ],
92
+ ];
93
+ }
94
+ function resolveEffectiveFailurePolicyText(valueText, level) {
95
+ return level === 'own' ? valueText : `${valueText} (${level})`;
96
+ }
97
+ function resolveEffectiveFailurePolicyValue(ownValue, channelValue, defaultValue) {
98
+ if (ownValue !== null) {
99
+ return { level: 'own', value: ownValue };
100
+ }
101
+ if (channelValue !== null && channelValue !== undefined) {
102
+ return { level: 'channel', value: channelValue };
103
+ }
104
+ return { level: 'default', value: defaultValue };
105
+ }
106
+ function resolveExpiresAt({ clearExpiresAt, expiresAt, expiresIn, }) {
107
+ if (expiresIn === undefined) {
108
+ return resolveNullableField(expiresAt, clearExpiresAt, 'expires-at');
109
+ }
110
+ if (expiresAt !== undefined) {
111
+ throw new InvalidParameterError('The flags --expires-in and --expires-at were passed together.', 'Pass only one of them.');
112
+ }
113
+ return resolveNullableField(new Date(Date.now() + resolveDurationMilliseconds(expiresIn)).toISOString(), clearExpiresAt, 'expires-in', 'clear-expires-at');
114
+ }
115
+ function resolveFailurePolicyClearShape(nullMeaning) {
116
+ return {
117
+ clearFailureAction: z
118
+ .boolean()
119
+ .optional()
120
+ .describe(`Sets the action back to null: ${nullMeaning}.`),
121
+ clearFailureMinimumUpdateCount: z
122
+ .boolean()
123
+ .optional()
124
+ .describe(`Sets the minimum update count back to null: ${nullMeaning}.`),
125
+ clearFailureThresholdPercent: z
126
+ .boolean()
127
+ .optional()
128
+ .describe(`Sets the threshold back to null: ${nullMeaning}.`),
129
+ };
130
+ }
@@ -0,0 +1,75 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import { defineConfig, processConfig } from 'zodline';
3
+ import { loadCommands, resolveInvocation } from './command-registry.js';
4
+ import { isColorEnabled } from './environment.js';
5
+ import { printError, resolveCliError } from './error-mapping.js';
6
+ import { ExitCode, ReportedFailureError } from './errors.js';
7
+ import { printTelemetryNotice, reportFinishedCommand } from './telemetry.js';
8
+ import { printUpdateNotice, refreshLatestKnownVersion, } from './update-notice.js';
9
+ export async function runCli(commandRegistry, argv, meta) {
10
+ if (isShellStartup(argv)) {
11
+ const { cliError } = await runCommandLine(commandRegistry, argv, meta);
12
+ printCommandLineError(cliError, argv);
13
+ return cliError?.exitCode ?? ExitCode.Success;
14
+ }
15
+ const startedAt = performance.now();
16
+ printTelemetryNotice();
17
+ // The check runs beside the command, so the notice after it never waits long on the registry.
18
+ const latestKnownVersionRefresh = refreshLatestKnownVersion();
19
+ const { cliError, commandName } = await runCommandLine(commandRegistry, argv, meta);
20
+ printCommandLineError(cliError, argv);
21
+ if (commandName !== undefined) {
22
+ await reportFinishedCommand({
23
+ cliError,
24
+ commandName,
25
+ durationMs: performance.now() - startedAt,
26
+ });
27
+ }
28
+ await latestKnownVersionRefresh;
29
+ printUpdateNotice();
30
+ return cliError?.exitCode ?? ExitCode.Success;
31
+ }
32
+ // A shell profile runs `completion` at startup, so it must not wait on the network or print a notice.
33
+ function isShellStartup(argv) {
34
+ return argv[0] === 'completion';
35
+ }
36
+ function printCommandLineError(cliError, argv) {
37
+ if (cliError !== undefined && !(cliError instanceof ReportedFailureError)) {
38
+ printError(cliError, {
39
+ isJson: argv.includes('--json'),
40
+ isVerbose: argv.includes('--verbose'),
41
+ });
42
+ }
43
+ }
44
+ async function runCommandLine(commandRegistry, argv, meta) {
45
+ let commandName;
46
+ try {
47
+ const invocation = resolveInvocation(Object.keys(commandRegistry), argv);
48
+ // A line without a command shows the help or the version, and zodline ends the process there.
49
+ commandName =
50
+ invocation.commandNames.length === 1
51
+ ? invocation.commandNames[0]
52
+ : undefined;
53
+ const commands = await loadCommands(commandRegistry, invocation.commandNames);
54
+ const config = defineConfig({ commands, meta });
55
+ const { args, command, options } = isColorEnabled(process.stdout)
56
+ ? processConfig(config, invocation.argv)
57
+ : processConfigWithoutColor(config, invocation.argv);
58
+ await command.action(options, args);
59
+ return { cliError: undefined, commandName };
60
+ }
61
+ catch (error) {
62
+ return { cliError: resolveCliError(error), commandName };
63
+ }
64
+ }
65
+ // zodline always colours its help, so the colour codes are stripped while it runs.
66
+ function processConfigWithoutColor(config, argv) {
67
+ const log = console.log;
68
+ console.log = (...values) => log(...values.map(value => typeof value === 'string' ? stripVTControlCharacters(value) : value));
69
+ try {
70
+ return processConfig(config, argv);
71
+ }
72
+ finally {
73
+ console.log = log;
74
+ }
75
+ }