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,119 @@
1
+ import { z } from 'zod';
2
+ import { InvalidParameterError } from './errors.js';
3
+ const DEFAULT_PROGRESSION = {
4
+ rolloutPercents: [10, 50, 100],
5
+ stepMinimumSeconds: 3600,
6
+ stepMinimumUpdateCount: 50,
7
+ };
8
+ // Parses `10,50,100` in the schema, so a wrong percentage is reported under the flag name.
9
+ const percentageListSchema = z
10
+ .string()
11
+ .transform(value => value.split(',').map(percentage => percentage.trim()))
12
+ .pipe(z.array(z.coerce.number().int()));
13
+ // The API validates the schedule itself.
14
+ export const progressionShape = {
15
+ progression: z
16
+ .boolean()
17
+ .optional()
18
+ .describe('Widen the rollout step by step while the release stays healthy: 10, 50 and 100 percent by default, each step after at least an hour and 50 applied updates.'),
19
+ progressionMinimumSeconds: z.coerce
20
+ .number()
21
+ .int()
22
+ .optional()
23
+ .describe('The seconds a step lasts at least before the next one, with --progression; 3600 by default.'),
24
+ progressionMinimumUpdateCount: z.coerce
25
+ .number()
26
+ .int()
27
+ .optional()
28
+ .describe('The applied updates each step needs before the next one, with --progression; 50 by default.'),
29
+ progressionRolloutPercents: percentageListSchema
30
+ .optional()
31
+ .describe('The percentages the rollout widens through, rising, the last above the starting rollout, with --progression; 10,50,100 by default.'),
32
+ };
33
+ // A schedule flag without `--progression` is refused rather than taken as one.
34
+ export function resolveProgression(options) {
35
+ if (!options.progression) {
36
+ assertNoProgressionFlags(options);
37
+ return null;
38
+ }
39
+ return {
40
+ rolloutPercents: options.progressionRolloutPercents ?? DEFAULT_PROGRESSION.rolloutPercents,
41
+ stepMinimumSeconds: options.progressionMinimumSeconds ??
42
+ DEFAULT_PROGRESSION.stepMinimumSeconds,
43
+ stepMinimumUpdateCount: options.progressionMinimumUpdateCount ??
44
+ DEFAULT_PROGRESSION.stepMinimumUpdateCount,
45
+ };
46
+ }
47
+ // Mirrors how the progression job of the API reads the release.
48
+ // Nothing widens a paused or revoked release, so it has no step.
49
+ export function resolveProgressionStep(release) {
50
+ const { progression, progressionStepStartedAt, releaseCounters } = release;
51
+ if (progression === null || release.status !== 'active') {
52
+ return null;
53
+ }
54
+ const number = progression.rolloutPercents.filter(percentage => percentage <= release.rolloutPercent).length;
55
+ if (number === progression.rolloutPercents.length) {
56
+ return null;
57
+ }
58
+ const appliedUpdateCount = progressionStepStartedAt === null || releaseCounters === undefined
59
+ ? 0
60
+ : releaseCounters.appliedUpdateCount -
61
+ release.stepStartAppliedUpdateCount;
62
+ if (progressionStepStartedAt === null ||
63
+ appliedUpdateCount < progression.stepMinimumUpdateCount) {
64
+ return {
65
+ gate: {
66
+ appliedUpdateCount,
67
+ requiredUpdateCount: progression.stepMinimumUpdateCount,
68
+ type: 'sample',
69
+ },
70
+ number,
71
+ };
72
+ }
73
+ return {
74
+ gate: {
75
+ readyAt: new Date(Date.parse(progressionStepStartedAt) +
76
+ progression.stepMinimumSeconds * 1000).toISOString(),
77
+ type: 'time',
78
+ },
79
+ number,
80
+ };
81
+ }
82
+ export function resolveProgressionStepText(release, progression) {
83
+ const progressionStep = resolveProgressionStep(release);
84
+ if (progressionStep !== null) {
85
+ return `${progressionStep.number} of ${progression.rolloutPercents.length}, ${resolveGateText(progressionStep.gate)}`;
86
+ }
87
+ switch (release.status) {
88
+ case 'active':
89
+ return 'complete';
90
+ case 'paused':
91
+ return 'held while paused';
92
+ case 'revoked':
93
+ return 'ended by the revocation';
94
+ }
95
+ }
96
+ export function resolveProgressionText({ rolloutPercents, stepMinimumSeconds, stepMinimumUpdateCount, }) {
97
+ return `${rolloutPercents.join(', ')} percent, each step at least ${stepMinimumSeconds} seconds and ${stepMinimumUpdateCount} applied updates`;
98
+ }
99
+ function assertNoProgressionFlags(options) {
100
+ const progressionFlags = [
101
+ options.progressionMinimumSeconds === undefined
102
+ ? undefined
103
+ : '--progression-minimum-seconds',
104
+ options.progressionMinimumUpdateCount === undefined
105
+ ? undefined
106
+ : '--progression-minimum-update-count',
107
+ options.progressionRolloutPercents === undefined
108
+ ? undefined
109
+ : '--progression-rollout-percents',
110
+ ].filter(flag => flag !== undefined);
111
+ if (progressionFlags.length > 0) {
112
+ throw new InvalidParameterError(`The schedule's flags ${progressionFlags.join(' and ')} apply only with --progression.`, 'Pass --progression to give the release a schedule.');
113
+ }
114
+ }
115
+ function resolveGateText(gate) {
116
+ return gate.type === 'sample'
117
+ ? `Waiting on updates: ${gate.appliedUpdateCount} of ${gate.requiredUpdateCount} applied`
118
+ : `ready at ${gate.readyAt}`;
119
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { ProjectConfigurationSchema } from '@hotcodepush/protocol';
4
+ import { z } from 'zod';
5
+ import { PROJECT_CONFIG_FILE_NAME } from '../config/consts.js';
6
+ import { InvalidParameterError } from './errors.js';
7
+ import { readJsonFile, stringifyLikeSource } from './json-file.js';
8
+ export const ID_SCHEMA = z.guid();
9
+ // The file comes with the repository, and the id reaches console URLs, the browser and the resource file.
10
+ export function assertProjectConfigAppId(appId) {
11
+ if (!ID_SCHEMA.safeParse(appId).success) {
12
+ throw new InvalidParameterError(`The appId in ${PROJECT_CONFIG_FILE_NAME} is not an id.`, 'Set appId to the id "hotcodepush app list" prints.');
13
+ }
14
+ }
15
+ export function resolveProjectChannel(projectConfig) {
16
+ return (projectConfig.channel ??
17
+ ProjectConfigurationSchema.shape.channel.parse(undefined));
18
+ }
19
+ // Walks up from the working directory to the nearest file, the way git finds its root.
20
+ export function readProjectConfig(configPath) {
21
+ return locateProjectConfig(configPath).projectConfig;
22
+ }
23
+ // Without a file, the working directory is the project root.
24
+ export function locateProjectConfig(configPath) {
25
+ const fileLocation = locateProjectConfigFile(configPath);
26
+ return {
27
+ ...fileLocation,
28
+ projectConfig: fileLocation.filePath === undefined
29
+ ? undefined
30
+ : readProjectConfigFile(fileLocation.filePath),
31
+ };
32
+ }
33
+ // The file is not read yet, so `doctor` can report one that does not parse.
34
+ export function locateProjectConfigFile(configPath) {
35
+ if (configPath !== undefined && !existsSync(configPath)) {
36
+ throw new InvalidParameterError(`The file ${configPath} from --config does not exist.`);
37
+ }
38
+ const filePath = configPath === undefined
39
+ ? findProjectConfigFilePath(process.cwd())
40
+ : resolve(configPath);
41
+ return {
42
+ directoryPath: filePath === undefined ? process.cwd() : dirname(filePath),
43
+ filePath,
44
+ };
45
+ }
46
+ export function readProjectConfigFile(filePath) {
47
+ return readJsonFile(filePath);
48
+ }
49
+ export function writeProjectConfig(filePath, projectConfig) {
50
+ writeFileSync(filePath, stringifyLikeSource(projectConfig, readFileSync(filePath, 'utf8')));
51
+ }
52
+ function findProjectConfigFilePath(directoryPath) {
53
+ const filePath = join(directoryPath, PROJECT_CONFIG_FILE_NAME);
54
+ if (existsSync(filePath)) {
55
+ return filePath;
56
+ }
57
+ const parentDirectoryPath = dirname(directoryPath);
58
+ return parentDirectoryPath === directoryPath
59
+ ? undefined
60
+ : findProjectConfigFilePath(parentDirectoryPath);
61
+ }
@@ -0,0 +1,53 @@
1
+ import { confirm, isCancel, password, select, text } from '@clack/prompts';
2
+ import { isInteractive } from './environment.js';
3
+ import { ConfirmationRequiredError, MissingParameterError } from './errors.js';
4
+ export async function confirmConsequence(consequence, options) {
5
+ if (options.yes) {
6
+ return true;
7
+ }
8
+ if (!isInteractive(options)) {
9
+ throw new ConfirmationRequiredError(consequence);
10
+ }
11
+ const answer = await confirm({
12
+ initialValue: false,
13
+ message: `This ${consequence}. Continue?`,
14
+ });
15
+ return answer === true;
16
+ }
17
+ export async function promptYesNo(message, options) {
18
+ if (!isInteractive(options)) {
19
+ return false;
20
+ }
21
+ const answer = await confirm({ initialValue: true, message });
22
+ return answer === true;
23
+ }
24
+ export async function promptSelect(flag, message, choices, options) {
25
+ if (choices.length === 0 || !isInteractive(options)) {
26
+ throw new MissingParameterError(flag);
27
+ }
28
+ // clack types an option through a conditional type that a generic value cannot satisfy.
29
+ const answer = await select({ message, options: choices });
30
+ if (isCancel(answer)) {
31
+ throw new MissingParameterError(flag);
32
+ }
33
+ return answer;
34
+ }
35
+ export function promptSecret(flag, message, options) {
36
+ return promptInput(password, flag, message, options);
37
+ }
38
+ export function promptText(flag, message, options) {
39
+ return promptInput(text, flag, message, options);
40
+ }
41
+ async function promptInput(ask, flag, message, options) {
42
+ if (!isInteractive(options)) {
43
+ throw new MissingParameterError(flag);
44
+ }
45
+ const answer = await ask({
46
+ message,
47
+ validate: value => (value?.trim() ? undefined : 'Enter a value.'),
48
+ });
49
+ if (isCancel(answer)) {
50
+ throw new MissingParameterError(flag);
51
+ }
52
+ return answer;
53
+ }
@@ -0,0 +1,119 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { NativeProjectError } from './errors.js';
4
+ import { resolveRefusedEdit } from './native-project-edit.js';
5
+ const APP_DELEGATE_FILE_NAMES = ['AppDelegate.swift', 'AppDelegate.mm'];
6
+ const BUNDLE_URL_CALL = 'HotCodePush.bundleURL()';
7
+ const BUNDLE_URL_FIX = `Import HotcodepushReactNativeCodePush in the file that overrides bundleURL() and return ${BUNDLE_URL_CALL} from it where it returns the embedded bundle.`;
8
+ const BUNDLE_URL_OVERRIDE = 'func bundleURL()';
9
+ const EMBEDDED_BUNDLE_URL_CALL = 'Bundle.main.url(forResource: "main", withExtension: "jsbundle")';
10
+ const OBJECTIVE_C_BUNDLE_URL_CALL = '[HotCodePush bundleURL]';
11
+ const REACT_HOST_CLASS = 'com.hotcodepush.reactnative.HotCodePushReactHost';
12
+ const REACT_HOST_EDIT_DESCRIPTION = 'HotCodePushReactHost in MainApplication.kt';
13
+ const REACT_HOST_IMPORT = `import ${REACT_HOST_CLASS}.getDefaultReactHost`;
14
+ const REACT_NATIVE_REACT_HOST_IMPORT = 'import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost';
15
+ const SWIFT_MODULE_IMPORT = 'import HotcodepushReactNativeCodePush';
16
+ // React Native 0.88's template overrides `bundleURL()` in `SceneDelegate.swift`, older ones in `AppDelegate.swift`.
17
+ // The debug build's Metro line stays as it is.
18
+ export function resolveBundleUrlEdit(iosProjectPath) {
19
+ if (!existsSync(iosProjectPath)) {
20
+ return undefined;
21
+ }
22
+ const appDelegateFilePath = findAppDelegateFilePath(iosProjectPath);
23
+ if (appDelegateFilePath === undefined ||
24
+ !appDelegateFilePath.endsWith('.swift')) {
25
+ return resolveRefusedBundleUrlEdit(iosProjectPath, appDelegateFilePath);
26
+ }
27
+ const sceneDelegateFilePath = join(dirname(appDelegateFilePath), 'SceneDelegate.swift');
28
+ const bundleUrlFilePath = [sceneDelegateFilePath, appDelegateFilePath].find(hasBundleUrlOverride);
29
+ const filePath = bundleUrlFilePath ?? appDelegateFilePath;
30
+ return {
31
+ description: `${BUNDLE_URL_CALL} in ${basename(filePath)}`,
32
+ filePath,
33
+ isApplied: () => readFileSync(filePath, 'utf8').includes(BUNDLE_URL_CALL),
34
+ apply: () => {
35
+ if (bundleUrlFilePath === undefined) {
36
+ throw new NativeProjectError(`Neither ${sceneDelegateFilePath} nor ${appDelegateFilePath} returns the template's main.jsbundle from bundleURL().`, BUNDLE_URL_FIX);
37
+ }
38
+ const source = readFileSync(filePath, 'utf8');
39
+ const lastImportLine = source
40
+ .split('\n')
41
+ .findLast(line => line.startsWith('import '));
42
+ if (!source.includes(EMBEDDED_BUNDLE_URL_CALL) ||
43
+ lastImportLine === undefined) {
44
+ throw new NativeProjectError(`The file ${filePath} does not return the template's main.jsbundle from bundleURL().`, `Import HotcodepushReactNativeCodePush in ${filePath} and return ${BUNDLE_URL_CALL} from bundleURL() where it returns the embedded bundle.`);
45
+ }
46
+ writeFileSync(filePath, source
47
+ .replace(lastImportLine, `${lastImportLine}\n${SWIFT_MODULE_IMPORT}`)
48
+ .replace(EMBEDDED_BUNDLE_URL_CALL, BUNDLE_URL_CALL));
49
+ },
50
+ };
51
+ }
52
+ // The SDK's `getDefaultReactHost` asks for the served bundle at every start and reload.
53
+ // Only the import changes, so the call and its parameters stay.
54
+ export function resolveReactHostEdit(androidProjectPath) {
55
+ if (!existsSync(androidProjectPath)) {
56
+ return undefined;
57
+ }
58
+ const sourceDirectoryPath = join(androidProjectPath, 'app', 'src', 'main', 'java');
59
+ const filePath = findFilePath(sourceDirectoryPath, 'MainApplication.kt');
60
+ if (filePath === undefined) {
61
+ return resolveRefusedReactHostEdit(androidProjectPath, findFilePath(sourceDirectoryPath, 'MainApplication.java'));
62
+ }
63
+ return {
64
+ description: REACT_HOST_EDIT_DESCRIPTION,
65
+ filePath,
66
+ isApplied: () => readFileSync(filePath, 'utf8').includes(REACT_HOST_IMPORT),
67
+ apply: () => {
68
+ const source = readFileSync(filePath, 'utf8');
69
+ if (!source.includes(REACT_NATIVE_REACT_HOST_IMPORT)) {
70
+ throw new NativeProjectError(`The file ${filePath} does not import the template's DefaultReactHost.getDefaultReactHost.`, resolveReactHostFix(filePath));
71
+ }
72
+ writeFileSync(filePath, source.replace(REACT_NATIVE_REACT_HOST_IMPORT, REACT_HOST_IMPORT));
73
+ },
74
+ };
75
+ }
76
+ // Templates before React Native 0.77 have an Objective-C app delegate.
77
+ function findAppDelegateFilePath(iosProjectPath) {
78
+ const appDirectoryPaths = readdirSync(iosProjectPath, { withFileTypes: true })
79
+ .filter(entry => entry.isDirectory() && entry.name !== 'Pods')
80
+ .map(entry => join(iosProjectPath, entry.name));
81
+ return APP_DELEGATE_FILE_NAMES.flatMap(fileName => appDirectoryPaths.map(directoryPath => join(directoryPath, fileName))).find(filePath => existsSync(filePath));
82
+ }
83
+ function findFilePath(directoryPath, fileName) {
84
+ if (!existsSync(directoryPath)) {
85
+ return undefined;
86
+ }
87
+ for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
88
+ const entryPath = join(directoryPath, entry.name);
89
+ if (entry.isFile() && entry.name === fileName) {
90
+ return entryPath;
91
+ }
92
+ const foundPath = entry.isDirectory()
93
+ ? findFilePath(entryPath, fileName)
94
+ : undefined;
95
+ if (foundPath !== undefined) {
96
+ return foundPath;
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ function hasBundleUrlOverride(filePath) {
102
+ return (existsSync(filePath) &&
103
+ readFileSync(filePath, 'utf8').includes(BUNDLE_URL_OVERRIDE));
104
+ }
105
+ function resolveReactHostFix(location) {
106
+ return `Build the React host in ${location} with ${REACT_HOST_CLASS}.getDefaultReactHost, which takes DefaultReactHost.getDefaultReactHost's parameters.`;
107
+ }
108
+ function resolveRefusedBundleUrlEdit(iosProjectPath, objectiveCFilePath) {
109
+ if (objectiveCFilePath === undefined) {
110
+ return resolveRefusedEdit(`${BUNDLE_URL_CALL} in AppDelegate.swift`, iosProjectPath, new NativeProjectError(`The iOS project ${iosProjectPath} has no AppDelegate.swift in an app folder.`, BUNDLE_URL_FIX));
111
+ }
112
+ return resolveRefusedEdit(`${BUNDLE_URL_CALL} in ${basename(objectiveCFilePath)}`, objectiveCFilePath, new NativeProjectError(`The app delegate ${objectiveCFilePath} is Objective-C, and init edits only the Swift file that overrides bundleURL().`, `Import <HotcodepushReactNativeCodePush/HotcodepushReactNativeCodePush-Swift.h> in ${objectiveCFilePath} and return ${OBJECTIVE_C_BUNDLE_URL_CALL} from bundleURL where it returns the embedded bundle.`), OBJECTIVE_C_BUNDLE_URL_CALL);
113
+ }
114
+ function resolveRefusedReactHostEdit(androidProjectPath, javaFilePath) {
115
+ if (javaFilePath === undefined) {
116
+ return resolveRefusedEdit(REACT_HOST_EDIT_DESCRIPTION, androidProjectPath, new NativeProjectError(`The Android project ${androidProjectPath} has no MainApplication.kt in its app's sources.`, resolveReactHostFix("the app's Application class")));
117
+ }
118
+ return resolveRefusedEdit(REACT_HOST_EDIT_DESCRIPTION, javaFilePath, new NativeProjectError(`The file ${javaFilePath} is Java, and init edits only the MainApplication.kt of React Native's template.`, resolveReactHostFix(javaFilePath)), REACT_HOST_CLASS);
119
+ }
@@ -0,0 +1,80 @@
1
+ import { hashAttribute, hashDeviceId, isValidAttributeKey, isValidAttributeValue, } from '@hotcodepush/protocol';
2
+ import { z } from 'zod';
3
+ import { InvalidParameterError } from './errors.js';
4
+ export const conditionOptionShape = {
5
+ attribute: z
6
+ .array(z.string())
7
+ .optional()
8
+ .describe('An attribute the app sets that the device must carry, key=value, repeatable.'),
9
+ binaryVersion: z
10
+ .string()
11
+ .optional()
12
+ .describe('The app versions served, a semver range such as ">=2.3.0 <3.0.0".'),
13
+ device: z
14
+ .array(z.string())
15
+ .optional()
16
+ .describe('A device the release is limited to, by id, repeatable.'),
17
+ osVersion: z
18
+ .string()
19
+ .optional()
20
+ .describe('The OS versions served, a range such as ">=17".'),
21
+ };
22
+ // A release of a bundle carries its fingerprint beside the conditions.
23
+ export function buildAudienceQuery(options, fingerprint, rolloutPercent) {
24
+ return {
25
+ attribute: resolveAttributePairs(options.attribute).map(({ key, value }) => `${key}=${value}`),
26
+ binary: resolveList(options.binaryVersion),
27
+ device: options.device ?? [],
28
+ fingerprint: resolveList(fingerprint ?? undefined),
29
+ os: resolveList(options.osVersion),
30
+ rolloutPercent,
31
+ };
32
+ }
33
+ // Matches the index: attribute values and device ids are hashed, and all device ids share one condition.
34
+ export function buildReleaseConditions(options, fingerprint) {
35
+ const deviceIds = options.device ?? [];
36
+ return [
37
+ ...resolveAttributePairs(options.attribute).map(({ key, value }) => ({
38
+ key,
39
+ type: 'attribute',
40
+ valueSha256: hashAttribute(key, value),
41
+ })),
42
+ ...resolveList(options.binaryVersion).map((range) => ({
43
+ range,
44
+ type: 'binary',
45
+ })),
46
+ ...(deviceIds.length === 0
47
+ ? []
48
+ : [
49
+ {
50
+ hashedIds: deviceIds.map(hashDeviceId),
51
+ type: 'device',
52
+ },
53
+ ]),
54
+ ...resolveList(fingerprint ?? undefined).map((fingerprint) => ({
55
+ fingerprint,
56
+ type: 'fingerprint',
57
+ })),
58
+ ...resolveList(options.osVersion).map((range) => ({
59
+ range,
60
+ type: 'os',
61
+ })),
62
+ ];
63
+ }
64
+ // The API requires the key to be an identifier and the value to be printable.
65
+ export function resolveAttributePairs(attributes) {
66
+ return (attributes ?? []).map(pair => {
67
+ const separatorIndex = pair.indexOf('=');
68
+ const key = pair.slice(0, separatorIndex);
69
+ const value = pair.slice(separatorIndex + 1);
70
+ if (separatorIndex === -1 ||
71
+ !isValidAttributeKey(key) ||
72
+ !isValidAttributeValue(value)) {
73
+ throw new InvalidParameterError(`The value "${pair}" of --attribute is not a key=value pair.`, 'Pass key=value. The key takes letters, digits, _, - and . up to 64 characters, the value printable characters up to 256.');
74
+ }
75
+ return { key, value };
76
+ });
77
+ }
78
+ function resolveList(value) {
79
+ return value === undefined ? [] : [value];
80
+ }
@@ -0,0 +1,13 @@
1
+ export function printReleasedLine(release, channelName, bundleLabel) {
2
+ console.log(`Released bundle ${bundleLabel} to ${channelName} as release #${release.number} at ${release.rolloutPercent} percent${resolveProgressingText(release)}${resolveLiveText(release)}.`);
3
+ }
4
+ export function resolveProgressingText({ progression }) {
5
+ return progression === null
6
+ ? ''
7
+ : `, progressing through ${progression.rolloutPercents.join(', ')} percent`;
8
+ }
9
+ function resolveLiveText(release) {
10
+ return release.liveAt === null
11
+ ? '; not live yet, and "release get" shows when it is'
12
+ : `, live since ${release.liveAt}`;
13
+ }
@@ -0,0 +1,83 @@
1
+ import { z } from 'zod';
2
+ import { resolveBundleLabel } from './bundle-resolution.js';
3
+ import { InvalidParameterError } from './errors.js';
4
+ import { fetchAllPages } from './pagination.js';
5
+ import { ID_SCHEMA } from './project-config.js';
6
+ import { promptSelect } from './prompts.js';
7
+ import { fetchChannel } from './resource-resolution.js';
8
+ const LIVE_POLL_INTERVAL_MS = 1000;
9
+ const LIVE_WAIT_MS = 60_000;
10
+ const NUMBER_PATTERN = /^\d+$/;
11
+ export const releaseOptionShape = {
12
+ release: z
13
+ .string()
14
+ .optional()
15
+ .describe('The release, by its number in the channel or by its id.'),
16
+ };
17
+ export async function fetchReleaseInChannel(hotCodePush, options) {
18
+ const channel = await fetchChannel(hotCodePush, options);
19
+ const releaseId = await resolveReleaseId(hotCodePush, channel, options);
20
+ const release = await hotCodePush.apps.releases.get({
21
+ appId: channel.appId,
22
+ relations: ['bundle', 'channel', 'releaseCounters'],
23
+ releaseId,
24
+ });
25
+ if (release.channelId !== channel.id) {
26
+ throw new InvalidParameterError(`The release ${releaseId} from --release is not in channel ${channel.name}.`);
27
+ }
28
+ return { channel, release };
29
+ }
30
+ export function fetchReleaseLog(hotCodePush, channel) {
31
+ return fetchAllPages(page => hotCodePush.apps.releases.list({
32
+ appId: channel.appId,
33
+ channelId: channel.id,
34
+ relations: ['bundle'],
35
+ ...page,
36
+ }));
37
+ }
38
+ export function resolveReleaseBundleLabel(release) {
39
+ return release.bundle === undefined
40
+ ? release.bundleId
41
+ : resolveBundleLabel(release.bundle);
42
+ }
43
+ export function resolveReleaseInLog(releases, reference, flag) {
44
+ const release = ID_SCHEMA.safeParse(reference).success
45
+ ? releases.find(({ id }) => id === reference)
46
+ : NUMBER_PATTERN.test(reference)
47
+ ? releases.find(({ number }) => number === Number(reference))
48
+ : undefined;
49
+ if (release === undefined) {
50
+ throw new InvalidParameterError(`The channel has no release "${reference}" from ${flag}.`, `Name a release of the channel by number or id with ${flag}.`);
51
+ }
52
+ return release;
53
+ }
54
+ // The materializer stamps `liveAt` once the index is written.
55
+ // Returns the last read release after the deadline, so the caller can say it is still pending.
56
+ export async function waitUntilLive(hotCodePush, appId, releaseId, relations) {
57
+ const deadline = Date.now() + LIVE_WAIT_MS;
58
+ for (;;) {
59
+ const release = await hotCodePush.apps.releases.get({
60
+ appId,
61
+ relations,
62
+ releaseId,
63
+ });
64
+ if (release.liveAt !== null || Date.now() >= deadline) {
65
+ return release;
66
+ }
67
+ await new Promise(resolve => setTimeout(resolve, LIVE_POLL_INTERVAL_MS));
68
+ }
69
+ }
70
+ async function resolveReleaseId(hotCodePush, channel, options) {
71
+ if (options.release !== undefined &&
72
+ ID_SCHEMA.safeParse(options.release).success) {
73
+ return options.release;
74
+ }
75
+ const releases = await fetchReleaseLog(hotCodePush, channel);
76
+ if (options.release !== undefined) {
77
+ return resolveReleaseInLog(releases, options.release, '--release').id;
78
+ }
79
+ return promptSelect('--release', 'Which release?', releases.map(release => ({
80
+ label: `#${release.number} · ${resolveReleaseBundleLabel(release)} (${release.status})`,
81
+ value: release.id,
82
+ })), options);
83
+ }
@@ -0,0 +1,81 @@
1
+ import { createPublicKey } from 'node:crypto';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { ConfigurationSchema, resolveSigningKeyId, SIGNING_KEY_BITS_MINIMUM, SigningPublicKeySchema, } from '@hotcodepush/protocol';
5
+ import { PROJECT_CONFIG_FILE_NAME } from '../config/consts.js';
6
+ import { InvalidParameterError } from './errors.js';
7
+ // The manifest is the unsigned bundle manifest without patches, whether or not a binary was created.
8
+ // The public keys take the form that the platform API imports.
9
+ export function buildResourceFile({ builtAt, channelId, embeddedBundle, fingerprint, hosts, platform, projectConfig, }) {
10
+ return ConfigurationSchema.parse({
11
+ ...omitProjectOnlyKeys(projectConfig),
12
+ builtAt,
13
+ channelId,
14
+ embeddedBundleId: embeddedBundle?.id ?? null,
15
+ embeddedBundleManifest: embeddedBundle === null
16
+ ? null
17
+ : {
18
+ appId: projectConfig.appId,
19
+ bundleVersion: embeddedBundle.bundleVersion,
20
+ files: embeddedBundle.files.map(({ path, sha256, sizeBytes }) => ({
21
+ path,
22
+ sha256,
23
+ sizeBytes,
24
+ })),
25
+ fingerprint,
26
+ keyId: null,
27
+ platforms: [platform],
28
+ },
29
+ fingerprint,
30
+ publicKeys: (projectConfig.publicKeys ?? []).map(publicKey => resolveDevicePublicKey(publicKey, platform)),
31
+ ...(hosts.filesBaseUrl === undefined
32
+ ? {}
33
+ : { filesBaseUrl: hosts.filesBaseUrl }),
34
+ ...(hosts.updatesBaseUrl === undefined
35
+ ? {}
36
+ : { updatesBaseUrl: hosts.updatesBaseUrl }),
37
+ });
38
+ }
39
+ export function writeResourceFile(filePath, configuration) {
40
+ mkdirSync(dirname(filePath), { recursive: true });
41
+ writeFileSync(filePath, `${JSON.stringify(configuration, null, 2)}\n`);
42
+ }
43
+ // The device imports the DER with its platform API and handles no ASN.1 itself.
44
+ // iOS `SecKeyCreateWithData` takes PKCS #1 and Android `X509EncodedKeySpec` takes SPKI.
45
+ function resolveDevicePublicKey(publicKey, platform) {
46
+ if (!SigningPublicKeySchema.safeParse(publicKey).success) {
47
+ throw buildUnusablePublicKeyError(publicKey);
48
+ }
49
+ let keyObject;
50
+ try {
51
+ keyObject = createPublicKey({
52
+ format: 'der',
53
+ key: Buffer.from(publicKey.slice(publicKey.indexOf(':') + 1), 'base64'),
54
+ type: 'spki',
55
+ });
56
+ }
57
+ catch (error) {
58
+ throw buildUnusablePublicKeyError(publicKey, error);
59
+ }
60
+ if (keyObject.asymmetricKeyType !== 'rsa' ||
61
+ (keyObject.asymmetricKeyDetails?.modulusLength ?? 0) <
62
+ SIGNING_KEY_BITS_MINIMUM) {
63
+ throw buildUnusablePublicKeyError(publicKey);
64
+ }
65
+ return {
66
+ der: keyObject
67
+ .export({ format: 'der', type: platform === 'ios' ? 'pkcs1' : 'spki' })
68
+ .toString('base64'),
69
+ keyId: resolveSigningKeyId(publicKey),
70
+ };
71
+ }
72
+ function buildUnusablePublicKeyError(publicKey, cause) {
73
+ return new InvalidParameterError(`The publicKeys in ${PROJECT_CONFIG_FILE_NAME} list ${publicKey.slice(0, 32)}…, which is not an rsa-v1_5-sha256 key of ${SIGNING_KEY_BITS_MINIMUM} bits or more.`, 'Replace it with the public key "hotcodepush signing-key create" writes.', cause);
74
+ }
75
+ // The resolved id replaces the channel, and only the CLI reads the fingerprint paths.
76
+ function omitProjectOnlyKeys(projectConfig) {
77
+ const configuration = { ...projectConfig };
78
+ delete configuration.channel;
79
+ delete configuration.extraFingerprintPaths;
80
+ return configuration;
81
+ }