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
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ import { INIT_MANUAL_STEP } from '../../config/consts.js';
5
+ import { resolveInstallCommandLine, resolvePackageManager, runCommandLineVisibly, } from '../package-manager.js';
6
+ export function checkSdkPackage({ directoryPath, packageJson }, packageName) {
7
+ if (!isSdkPackageDeclared(packageJson, packageName)) {
8
+ return {
9
+ check: 'package',
10
+ code: 'package_missing',
11
+ manualStep: INIT_MANUAL_STEP,
12
+ message: `The package ${packageName} is not in package.json.`,
13
+ status: 'failed',
14
+ };
15
+ }
16
+ const installedVersion = readInstalledPackageVersion(directoryPath, packageName);
17
+ if (installedVersion === undefined) {
18
+ return {
19
+ check: 'package',
20
+ code: 'package_not_installed',
21
+ manualStep: 'Install the dependencies.',
22
+ message: `The package ${packageName} is declared but not in node_modules.`,
23
+ status: 'failed',
24
+ };
25
+ }
26
+ return {
27
+ check: 'package',
28
+ message: `The package ${packageName} ${installedVersion} is installed.`,
29
+ status: 'ok',
30
+ };
31
+ }
32
+ export function installSdkPackage(projectDirectoryPath, packageName, packageSpec) {
33
+ runCommandLineVisibly(resolveInstallCommandLine(resolvePackageManager(projectDirectoryPath), packageSpec), projectDirectoryPath);
34
+ return `Installed ${packageName} from ${packageSpec}.`;
35
+ }
36
+ export function isSdkPackageDeclared(packageJson, packageName) {
37
+ return (packageJson?.dependencies?.[packageName] !== undefined ||
38
+ packageJson?.devDependencies?.[packageName] !== undefined);
39
+ }
40
+ export function readInstalledPackageVersion(projectDirectoryPath, packageName) {
41
+ const packageJsonPath = join(projectDirectoryPath, 'node_modules', packageName, 'package.json');
42
+ if (!existsSync(packageJsonPath)) {
43
+ return undefined;
44
+ }
45
+ return readPackageJsonVersion(packageJsonPath);
46
+ }
47
+ function readPackageJsonVersion(packageJsonPath) {
48
+ return JSON.parse(readFileSync(packageJsonPath, 'utf8'))
49
+ .version;
50
+ }
51
+ // Resolved from the SDK package, since an isolated install such as pnpm's keeps it out of the project's `node_modules`.
52
+ export function readSdkDependencyVersion(projectDirectoryPath, sdkPackageName, packageName) {
53
+ try {
54
+ const projectRequire = createRequire(join(projectDirectoryPath, 'package.json'));
55
+ const sdkPackageJsonPath = projectRequire.resolve(`${sdkPackageName}/package.json`);
56
+ return readPackageJsonVersion(projectRequire.resolve(`${packageName}/package.json`, {
57
+ paths: [dirname(sdkPackageJsonPath)],
58
+ }));
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
@@ -0,0 +1,71 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ const EMPTY_PROVENANCE = {
5
+ gitMessage: null,
6
+ gitRef: null,
7
+ gitRemote: null,
8
+ gitSha: null,
9
+ isGitDirty: null,
10
+ };
11
+ // `--no-git` skips the detection for a pipeline without a checkout, while the typed flags still apply.
12
+ export async function resolveGitProvenance(directoryPath, options) {
13
+ const detected = options.noGit
14
+ ? EMPTY_PROVENANCE
15
+ : await detectGitProvenance(directoryPath);
16
+ return {
17
+ gitMessage: options.gitMessage ?? detected.gitMessage,
18
+ gitRef: options.gitRef ?? detected.gitRef,
19
+ gitRemote: detected.gitRemote,
20
+ gitSha: options.gitSha ?? detected.gitSha,
21
+ isGitDirty: detected.isGitDirty,
22
+ };
23
+ }
24
+ // Drops the credentials, so `github.com/acme/shop` comes from an HTTPS or an SSH URL alike.
25
+ export function resolveRemoteSlug(remoteUrl) {
26
+ const sshMatch = /^[^@]+@([^:]+):(.+)$/.exec(remoteUrl);
27
+ if (sshMatch?.[1] !== undefined && sshMatch[2] !== undefined) {
28
+ return `${sshMatch[1]}/${stripGitSuffix(sshMatch[2])}`;
29
+ }
30
+ try {
31
+ const url = new URL(remoteUrl);
32
+ return `${url.host}/${stripGitSuffix(url.pathname)}`;
33
+ }
34
+ catch {
35
+ return stripGitSuffix(remoteUrl);
36
+ }
37
+ }
38
+ async function detectGitProvenance(directoryPath) {
39
+ const [sha, ref, message, status, remoteUrl] = await Promise.all([
40
+ readGit(directoryPath, ['rev-parse', 'HEAD']),
41
+ readGit(directoryPath, ['rev-parse', '--abbrev-ref', 'HEAD']),
42
+ readGit(directoryPath, ['log', '-1', '--format=%s']),
43
+ readGit(directoryPath, ['status', '--porcelain']),
44
+ readGit(directoryPath, ['remote', 'get-url', 'origin']),
45
+ ]);
46
+ if (sha === null) {
47
+ return EMPTY_PROVENANCE;
48
+ }
49
+ return {
50
+ gitMessage: message,
51
+ gitRef: ref,
52
+ gitRemote: remoteUrl === null ? null : resolveRemoteSlug(remoteUrl),
53
+ gitSha: sha,
54
+ isGitDirty: status === null ? null : status.length > 0,
55
+ };
56
+ }
57
+ // Null when git is missing, the directory is no repository or the command fails.
58
+ async function readGit(directoryPath, args) {
59
+ try {
60
+ const { stdout } = await execFileAsync('git', args, {
61
+ cwd: directoryPath,
62
+ });
63
+ return stdout.trim();
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function stripGitSuffix(path) {
70
+ return path.replace(/^\/+/, '').replace(/\.git$/, '');
71
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { defineOptions } from 'zodline';
3
+ // zodline answers `--help` and `--version` before any command runs.
4
+ // Both options are here only so the help lists them.
5
+ const globalOptionsShape = {
6
+ app: z
7
+ .string()
8
+ .optional()
9
+ .describe('The app, by id or name, when no hotcodepush.json is found or another app is meant.'),
10
+ config: z
11
+ .string()
12
+ .optional()
13
+ .describe('The hotcodepush.json to use, when it is not the nearest one up from the working directory.'),
14
+ help: z.boolean().optional().describe('Print this help.'),
15
+ json: z
16
+ .boolean()
17
+ .optional()
18
+ .describe('Print machine output as JSON on stdout and ask nothing.'),
19
+ organization: z
20
+ .string()
21
+ .optional()
22
+ .describe('The organization, by id or name, when you belong to several.'),
23
+ verbose: z.boolean().optional().describe('Print the detail behind an error.'),
24
+ version: z.boolean().optional().describe("Print the CLI's version."),
25
+ yes: z
26
+ .boolean()
27
+ .optional()
28
+ .describe('Take every default, ask nothing, confirm nothing.'),
29
+ };
30
+ export function defineCommandOptions(shape) {
31
+ // The first spread puts the command options first in the help.
32
+ // The last spread overrides a global option of the same name.
33
+ return defineOptions(z.object({ ...shape, ...globalOptionsShape, ...shape }), { h: 'help', v: 'version', y: 'yes' });
34
+ }
@@ -0,0 +1,53 @@
1
+ import { DEFAULT_API_URL } from '../config/consts.js';
2
+ const LOCAL_CONSOLE_BASE_URL = 'http://localhost:4300';
3
+ const PRODUCTION_CONSOLE_BASE_URL = 'https://console.hotcodepush.com';
4
+ const PRODUCTION_FILES_BASE_URL = 'https://files.hotcodepush.com';
5
+ const PRODUCTION_UPDATES_BASE_URL = 'https://updates.hotcodepush.com';
6
+ const STAGING_API_URL = 'https://api.staging.hotcodepush.com';
7
+ const STAGING_CONSOLE_BASE_URL = 'https://console.staging.hotcodepush.com';
8
+ const STAGING_FILES_BASE_URL = 'https://files.staging.hotcodepush.com';
9
+ const STAGING_UPDATES_BASE_URL = 'https://updates.staging.hotcodepush.com';
10
+ // Any API other than production and staging is the local stack, which serves the hosts itself.
11
+ export function resolveDeviceHosts(apiUrl) {
12
+ const derived = resolveDerivedDeviceHosts(apiUrl);
13
+ return {
14
+ filesBaseUrl: process.env.HOTCODEPUSH_FILES_BASE_URL || derived.filesBaseUrl,
15
+ updatesBaseUrl: process.env.HOTCODEPUSH_UPDATES_BASE_URL || derived.updatesBaseUrl,
16
+ };
17
+ }
18
+ // The local console runs on the dev port of the monorepo.
19
+ export function resolveConsoleBaseUrl(apiUrl) {
20
+ if (process.env.HOTCODEPUSH_CONSOLE_BASE_URL) {
21
+ return process.env.HOTCODEPUSH_CONSOLE_BASE_URL;
22
+ }
23
+ const trimmedApiUrl = apiUrl.replace(/\/+$/, '');
24
+ if (trimmedApiUrl === DEFAULT_API_URL) {
25
+ return PRODUCTION_CONSOLE_BASE_URL;
26
+ }
27
+ return trimmedApiUrl === STAGING_API_URL
28
+ ? STAGING_CONSOLE_BASE_URL
29
+ : LOCAL_CONSOLE_BASE_URL;
30
+ }
31
+ // The production host applies where the resource file names none.
32
+ export function resolveFilesBaseUrl(apiUrl) {
33
+ return resolveDeviceHosts(apiUrl).filesBaseUrl ?? PRODUCTION_FILES_BASE_URL;
34
+ }
35
+ export function resolveUpdatesBaseUrl(apiUrl) {
36
+ return (resolveDeviceHosts(apiUrl).updatesBaseUrl ?? PRODUCTION_UPDATES_BASE_URL);
37
+ }
38
+ function resolveDerivedDeviceHosts(apiUrl) {
39
+ const trimmedApiUrl = apiUrl.replace(/\/+$/, '');
40
+ if (trimmedApiUrl === DEFAULT_API_URL) {
41
+ return { filesBaseUrl: undefined, updatesBaseUrl: undefined };
42
+ }
43
+ if (trimmedApiUrl === STAGING_API_URL) {
44
+ return {
45
+ filesBaseUrl: STAGING_FILES_BASE_URL,
46
+ updatesBaseUrl: STAGING_UPDATES_BASE_URL,
47
+ };
48
+ }
49
+ return {
50
+ filesBaseUrl: `${trimmedApiUrl}/files`,
51
+ updatesBaseUrl: `${trimmedApiUrl}/updates`,
52
+ };
53
+ }
@@ -0,0 +1,51 @@
1
+ import { resolveCliError, resolveSentence } from './error-mapping.js';
2
+ // Any error stops only its own step, so the run reports instead of throwing and independent steps still run.
3
+ // An API message without a fix carries the fix itself, so it doubles as the manual step.
4
+ export class InitRun {
5
+ steps = [];
6
+ unmetSteps = new Set();
7
+ get stoppedCode() {
8
+ return this.steps.find(({ status }) => status === 'stopped')?.code;
9
+ }
10
+ get stoppedStep() {
11
+ return this.steps.find(({ status }) => status === 'stopped')?.step;
12
+ }
13
+ async run(step, perform, { dependsOn = [] } = {}) {
14
+ const unmetStep = dependsOn.find(name => this.unmetSteps.has(name));
15
+ if (unmetStep !== undefined) {
16
+ this.steps.push({
17
+ message: `Waits on the ${unmetStep} step.`,
18
+ status: 'skipped',
19
+ step,
20
+ });
21
+ this.unmetSteps.add(step);
22
+ return undefined;
23
+ }
24
+ try {
25
+ const { message, status, value } = await perform();
26
+ this.steps.push({ message, status, step });
27
+ return value;
28
+ }
29
+ catch (error) {
30
+ const { code, fix, message } = resolveCliError(error);
31
+ const messageSentence = resolveSentence(message);
32
+ this.steps.push({
33
+ code,
34
+ manualStep: fix ?? messageSentence,
35
+ message: messageSentence,
36
+ status: 'stopped',
37
+ step,
38
+ });
39
+ this.unmetSteps.add(step);
40
+ return undefined;
41
+ }
42
+ }
43
+ }
44
+ export function resolveStepRows(steps) {
45
+ return steps.map(({ manualStep, message, status, step }) => ({
46
+ manualStep: manualStep === message ? undefined : manualStep,
47
+ message,
48
+ name: step,
49
+ status,
50
+ }));
51
+ }
@@ -0,0 +1,28 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { InvalidJsonError } from './errors.js';
3
+ // V8 quotes the text around an unexpected token, which in config.json can be the auth token.
4
+ // Matches that message so only the unexpected token is kept.
5
+ const QUOTED_TEXT_PATTERN = /^(Unexpected token '.+?'), .*$/s;
6
+ export function readJsonFile(filePath) {
7
+ return parseJsonFileText(filePath, readFileSync(filePath, 'utf8'));
8
+ }
9
+ export function parseJsonFileText(filePath, text) {
10
+ try {
11
+ return JSON.parse(text);
12
+ }
13
+ catch (error) {
14
+ if (!(error instanceof SyntaxError)) {
15
+ throw error;
16
+ }
17
+ throw new InvalidJsonError(filePath, resolveJsonSyntaxProblem(error));
18
+ }
19
+ }
20
+ // Keeps the indentation and final newline of the file, so the diff is only the edited line.
21
+ export function stringifyLikeSource(value, sourceText) {
22
+ const indent = /^(\s+)"/m.exec(sourceText)?.[1] ?? ' ';
23
+ const newline = sourceText.endsWith('\n') ? '\n' : '';
24
+ return `${JSON.stringify(value, null, indent)}${newline}`;
25
+ }
26
+ function resolveJsonSyntaxProblem({ message }) {
27
+ return message.replace(QUOTED_TEXT_PATTERN, '$1');
28
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import { isInteractive } from './environment.js';
3
+ import { MissingParameterError } from './errors.js';
4
+ import { fetchAllPages } from './pagination.js';
5
+ import { fetchResourceId, promptResourceId } from './resource-resolution.js';
6
+ export const memberOptionShape = {
7
+ member: z.string().optional().describe('The member, by id or email.'),
8
+ };
9
+ export async function fetchMember(hotCodePush, organizationId, options) {
10
+ return hotCodePush.organizations.members.get({
11
+ memberId: await fetchMemberId(hotCodePush, organizationId, options),
12
+ organizationId,
13
+ relations: ['user', 'user.accounts'],
14
+ });
15
+ }
16
+ // The email is unique in the organization, so it names a member the way a name does an app.
17
+ export function resolveMemberEmail({ user }) {
18
+ return user?.email ?? '';
19
+ }
20
+ // `users.get` embeds the member rows of the caller.
21
+ export function resolveMemberRole(members, organizationId) {
22
+ return (members.find(member => member.organizationId === organizationId)?.role ??
23
+ null);
24
+ }
25
+ // A UUID is the id. Anything else is an email, matched among the members whose email contains it.
26
+ async function fetchMemberId(hotCodePush, organizationId, options) {
27
+ const { member } = options;
28
+ if (member !== undefined) {
29
+ return fetchResourceId('member', member, async () => resolveNamedMembers(await fetchMembers(hotCodePush, organizationId, member)));
30
+ }
31
+ if (!isInteractive(options)) {
32
+ throw new MissingParameterError('--member');
33
+ }
34
+ return promptResourceId('member', resolveNamedMembers(await fetchMembers(hotCodePush, organizationId)), options);
35
+ }
36
+ function fetchMembers(hotCodePush, organizationId, query) {
37
+ return fetchAllPages(page => hotCodePush.organizations.members.list({
38
+ organizationId,
39
+ query,
40
+ relations: ['user'],
41
+ ...page,
42
+ }));
43
+ }
44
+ function resolveNamedMembers(members) {
45
+ return members.map(member => ({
46
+ id: member.id,
47
+ name: resolveMemberEmail(member),
48
+ }));
49
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { NativeProjectError } from './errors.js';
4
+ const GRADLE_EDIT_DESCRIPTION = 'the Gradle task that runs the build step';
5
+ const GRADLE_FILE_MARKER = 'hotcodepush.gradle';
6
+ const GRADLE_FILE_NAMES = ['build.gradle', 'build.gradle.kts'];
7
+ // The line resolves the SDK package through Node, so it finds the package wherever `node_modules` lies.
8
+ export function resolveGradleEdit(androidProjectPath, packageName) {
9
+ if (!existsSync(androidProjectPath)) {
10
+ return undefined;
11
+ }
12
+ const filePath = GRADLE_FILE_NAMES.map(fileName => join(androidProjectPath, 'app', fileName)).find(candidatePath => existsSync(candidatePath));
13
+ if (filePath === undefined) {
14
+ return resolveRefusedEdit(GRADLE_EDIT_DESCRIPTION, join(androidProjectPath, 'app', 'build.gradle'), new NativeProjectError(`The Android project ${androidProjectPath} has no build.gradle or build.gradle.kts in its app folder.`, `Apply node_modules/${packageName}/android/${GRADLE_FILE_MARKER} from the app module's Gradle file.`));
15
+ }
16
+ const resolvePackageScript = `require.resolve('${packageName}/package.json')`;
17
+ return {
18
+ description: GRADLE_EDIT_DESCRIPTION,
19
+ filePath,
20
+ isApplied: () => readFileSync(filePath, 'utf8').includes(GRADLE_FILE_MARKER),
21
+ apply: () => {
22
+ const source = readFileSync(filePath, 'utf8');
23
+ const applyLine = filePath.endsWith('.kts')
24
+ ? `apply(from = File(providers.exec { workingDir(rootDir); commandLine("node", "--print", "${resolvePackageScript}") }.standardOutput.asText.get().trim()).resolveSibling("android/${GRADLE_FILE_MARKER}"))`
25
+ : `apply from: new File(["node", "--print", "${resolvePackageScript}"].execute(null, rootDir).text.trim(), "../android/${GRADLE_FILE_MARKER}")`;
26
+ writeFileSync(filePath, `${source}${source.endsWith('\n') ? '' : '\n'}\n${applyLine}\n`);
27
+ },
28
+ };
29
+ }
30
+ // A file wired by hand holds the marker, so `doctor` passes it and `init` leaves it alone.
31
+ export function resolveRefusedEdit(description, filePath, error, marker) {
32
+ return {
33
+ description,
34
+ filePath,
35
+ isApplied: () => marker !== undefined && readFileSync(filePath, 'utf8').includes(marker),
36
+ isRefused: true,
37
+ apply: () => {
38
+ throw error;
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,10 @@
1
+ import { InvalidParameterError } from './errors.js';
2
+ export function resolveNullableField(value, isCleared, flagName, clearFlagName = `clear-${flagName}`) {
3
+ if (!isCleared) {
4
+ return value;
5
+ }
6
+ if (value !== undefined) {
7
+ throw new InvalidParameterError(`The flags --${flagName} and --${clearFlagName} were passed together.`, 'Pass only one of them.');
8
+ }
9
+ return null;
10
+ }
@@ -0,0 +1,24 @@
1
+ const STATUS_MARKS = {
2
+ done: '✓',
3
+ failed: '✗',
4
+ ok: '✓',
5
+ skipped: '–',
6
+ stopped: '✗',
7
+ };
8
+ export function printOutcomeRows(rows) {
9
+ const labeledRows = rows.map(row => ({
10
+ ...row,
11
+ label: resolveOutcomeLabel(row.name),
12
+ }));
13
+ const labelWidth = Math.max(...labeledRows.map(({ label }) => label.length));
14
+ for (const { label, manualStep, message, status } of labeledRows) {
15
+ console.log(`${STATUS_MARKS[status]} ${label.padEnd(labelWidth)} ${message}`);
16
+ if (manualStep !== undefined) {
17
+ console.log(`${' '.repeat(labelWidth + 4)}${manualStep}`);
18
+ }
19
+ }
20
+ }
21
+ function resolveOutcomeLabel(name) {
22
+ const words = name.replaceAll('_', ' ');
23
+ return words.charAt(0).toUpperCase() + words.slice(1);
24
+ }
@@ -0,0 +1,37 @@
1
+ export function printDetails(details) {
2
+ const labelWidth = Math.max(...details.map(([label]) => label.length));
3
+ for (const [label, value] of details) {
4
+ console.log(`${label.padEnd(labelWidth)} ${value}`);
5
+ }
6
+ }
7
+ export function printJson(data) {
8
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
9
+ }
10
+ // The API warns when a computed preview is likely not what was meant.
11
+ export function printWarnings(warnings) {
12
+ for (const { message } of warnings) {
13
+ process.stderr.write(`Warning: ${message}\n`);
14
+ }
15
+ }
16
+ export function printTable({ emptyText, headers, nextOffset, rows, }) {
17
+ if (rows.length === 0) {
18
+ console.log(emptyText);
19
+ return;
20
+ }
21
+ const columnWidths = headers.map((header, columnIndex) => Math.max(header.length, ...rows.map(row => row[columnIndex]?.length ?? 0)));
22
+ for (const cells of [headers, ...rows]) {
23
+ console.log(cells
24
+ .map((cell, columnIndex) => cell.padEnd(columnWidths[columnIndex] ?? 0))
25
+ .join(' ')
26
+ .trimEnd());
27
+ }
28
+ if (nextOffset !== null) {
29
+ console.log(`Next page: --offset ${nextOffset}`);
30
+ }
31
+ }
32
+ export function resolveDate(timestamp) {
33
+ return timestamp.slice(0, 10);
34
+ }
35
+ export function resolveQuantityText(count, noun, pluralNoun = `${noun}s`) {
36
+ return `${count} ${count === 1 ? noun : pluralNoun}`;
37
+ }
@@ -0,0 +1,20 @@
1
+ import { createReadStream, createWriteStream } from 'node:fs';
2
+ import { Readable } from 'node:stream';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { buildPack } from '@hotcodepush/protocol';
5
+ export async function writePack(files, outputFilePath) {
6
+ await pipeline(Readable.fromWeb(buildPack(readPackEntries(files))), createWriteStream(outputFilePath));
7
+ }
8
+ async function* readPackEntries(files) {
9
+ for (const { compressedFilePath, sha256, sizeBytes } of files) {
10
+ yield {
11
+ body: readBody(compressedFilePath),
12
+ sha256,
13
+ sizeBytes,
14
+ type: 'file',
15
+ };
16
+ }
17
+ }
18
+ function readBody(filePath) {
19
+ return Readable.toWeb(createReadStream(filePath));
20
+ }
@@ -0,0 +1,5 @@
1
+ import { join } from 'node:path';
2
+ import { readJsonFile } from './json-file.js';
3
+ export function readPackageJson(projectDirectoryPath) {
4
+ return readJsonFile(join(projectDirectoryPath, 'package.json'));
5
+ }
@@ -0,0 +1,50 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { CommandFailedError } from './errors.js';
5
+ const LOCKFILES = [
6
+ ['bun.lock', 'bun'],
7
+ ['bun.lockb', 'bun'],
8
+ ['pnpm-lock.yaml', 'pnpm'],
9
+ ['yarn.lock', 'yarn'],
10
+ ];
11
+ export function resolvePackageManager(projectDirectoryPath) {
12
+ const detected = LOCKFILES.find(([fileName]) => existsSync(join(projectDirectoryPath, fileName)));
13
+ return detected === undefined ? 'npm' : detected[1];
14
+ }
15
+ export function resolveInstallCommandLine(packageManager, packageSpec) {
16
+ switch (packageManager) {
17
+ case 'bun':
18
+ return { args: ['add', '--exact', packageSpec], command: 'bun' };
19
+ case 'npm':
20
+ return {
21
+ args: ['install', '--save-exact', packageSpec],
22
+ command: 'npm',
23
+ };
24
+ case 'pnpm':
25
+ return { args: ['add', '--save-exact', packageSpec], command: 'pnpm' };
26
+ case 'yarn':
27
+ return { args: ['add', '--exact', packageSpec], command: 'yarn' };
28
+ }
29
+ }
30
+ export function resolveRunScriptCommandLine(packageManager, scriptName) {
31
+ return packageManager === 'yarn'
32
+ ? { args: [scriptName], command: 'yarn' }
33
+ : { args: ['run', scriptName], command: packageManager };
34
+ }
35
+ export function resolveCommandLineText({ args, command }) {
36
+ return [command, ...args].join(' ');
37
+ }
38
+ // Output goes to stderr, so `--json` keeps stdout for data.
39
+ export function runCommandLineVisibly(commandLine, projectDirectoryPath) {
40
+ const commandLineText = resolveCommandLineText(commandLine);
41
+ process.stderr.write(`$ ${commandLineText}\n`);
42
+ const result = spawnSync(commandLine.command, commandLine.args, {
43
+ cwd: projectDirectoryPath,
44
+ shell: process.platform === 'win32',
45
+ stdio: ['ignore', process.stderr, process.stderr],
46
+ });
47
+ if (result.status !== 0) {
48
+ throw new CommandFailedError(commandLineText, result.status);
49
+ }
50
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ const DEFAULT_LIMIT = 50;
3
+ const MAX_LIMIT = 100;
4
+ export const paginationShape = {
5
+ limit: z.coerce
6
+ .number()
7
+ .int()
8
+ .optional()
9
+ .describe(`How many to list, at most ${MAX_LIMIT}; ${DEFAULT_LIMIT} by default.`),
10
+ offset: z.coerce
11
+ .number()
12
+ .int()
13
+ .optional()
14
+ .describe('How many to skip: the offset the previous page ended with.'),
15
+ };
16
+ export async function fetchAllPages(fetchPage, limit = MAX_LIMIT) {
17
+ const fetchedItems = [];
18
+ for (let offset = 0;; offset += limit) {
19
+ const fetchedPage = await fetchPage({ limit, offset });
20
+ fetchedItems.push(...fetchedPage);
21
+ if (fetchedPage.length < limit) {
22
+ return fetchedItems;
23
+ }
24
+ }
25
+ }
26
+ // The API answers a plain array without a total, so a full page implies another.
27
+ export function resolveNextOffset(itemCount, { limit = DEFAULT_LIMIT, offset = 0 }) {
28
+ return itemCount < limit ? null : offset + limit;
29
+ }
@@ -0,0 +1,23 @@
1
+ export function createReporter({ json }) {
2
+ const isReporting = Boolean(process.stderr.isTTY) && !json;
3
+ return {
4
+ report: line => {
5
+ if (isReporting) {
6
+ process.stderr.write(`${line}\n`);
7
+ }
8
+ },
9
+ };
10
+ }
11
+ // Decimal units, as the limits use.
12
+ export function resolveByteText(bytes) {
13
+ if (bytes < 1000) {
14
+ return `${bytes} B`;
15
+ }
16
+ if (bytes < 1_000_000) {
17
+ return `${(bytes / 1000).toFixed(1)} kB`;
18
+ }
19
+ if (bytes < 1_000_000_000) {
20
+ return `${(bytes / 1_000_000).toFixed(1)} MB`;
21
+ }
22
+ return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
23
+ }