hotcodepush 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commands/app/create.js +49 -0
- package/dist/commands/app/delete.js +34 -0
- package/dist/commands/app/get.js +33 -0
- package/dist/commands/app/list.js +38 -0
- package/dist/commands/app/transfer.js +58 -0
- package/dist/commands/app/update.js +34 -0
- package/dist/commands/audience/get.js +69 -0
- package/dist/commands/audit-log/list.js +82 -0
- package/dist/commands/binary/create.js +191 -0
- package/dist/commands/binary/get.js +56 -0
- package/dist/commands/binary/list.js +58 -0
- package/dist/commands/bundle/delete.js +35 -0
- package/dist/commands/bundle/get.js +63 -0
- package/dist/commands/bundle/list.js +66 -0
- package/dist/commands/bundle/upload.js +177 -0
- package/dist/commands/channel/create.js +59 -0
- package/dist/commands/channel/delete.js +68 -0
- package/dist/commands/channel/get.js +45 -0
- package/dist/commands/channel/list.js +39 -0
- package/dist/commands/channel/pause.js +32 -0
- package/dist/commands/channel/resume.js +32 -0
- package/dist/commands/channel/update.js +40 -0
- package/dist/commands/checkout/create.js +25 -0
- package/dist/commands/completion.js +37 -0
- package/dist/commands/customer-portal-session/create.js +25 -0
- package/dist/commands/device/delete.js +29 -0
- package/dist/commands/device/get.js +60 -0
- package/dist/commands/device/list.js +97 -0
- package/dist/commands/device/probe.js +253 -0
- package/dist/commands/doctor.js +364 -0
- package/dist/commands/fingerprint/diff.js +134 -0
- package/dist/commands/fingerprint.js +42 -0
- package/dist/commands/init.js +493 -0
- package/dist/commands/invitation/accept.js +53 -0
- package/dist/commands/invitation/create.js +52 -0
- package/dist/commands/invitation/delete.js +60 -0
- package/dist/commands/invitation/list.js +40 -0
- package/dist/commands/limit/list.js +34 -0
- package/dist/commands/login.js +36 -0
- package/dist/commands/logout.js +81 -0
- package/dist/commands/member/delete.js +35 -0
- package/dist/commands/member/get.js +37 -0
- package/dist/commands/member/list.js +41 -0
- package/dist/commands/member/update.js +53 -0
- package/dist/commands/open.js +37 -0
- package/dist/commands/organization/create.js +30 -0
- package/dist/commands/organization/delete.js +39 -0
- package/dist/commands/organization/get.js +38 -0
- package/dist/commands/organization/list.js +41 -0
- package/dist/commands/organization/update.js +64 -0
- package/dist/commands/release/create.js +237 -0
- package/dist/commands/release/get.js +81 -0
- package/dist/commands/release/list.js +69 -0
- package/dist/commands/release/pause.js +36 -0
- package/dist/commands/release/resume.js +36 -0
- package/dist/commands/release/revoke.js +254 -0
- package/dist/commands/release/rollback.js +73 -0
- package/dist/commands/release/rollout.js +63 -0
- package/dist/commands/release/update.js +82 -0
- package/dist/commands/resource-file/write.js +78 -0
- package/dist/commands/signing-key/add.js +45 -0
- package/dist/commands/signing-key/create.js +84 -0
- package/dist/commands/signing-key/delete.js +67 -0
- package/dist/commands/signing-key/list.js +38 -0
- package/dist/commands/sso-provider/delete.js +32 -0
- package/dist/commands/sso-provider/get.js +63 -0
- package/dist/commands/sso-provider/set.js +161 -0
- package/dist/commands/sso-provider/verify.js +31 -0
- package/dist/commands/statistics/get.js +189 -0
- package/dist/commands/usage/get.js +31 -0
- package/dist/commands/usage/list.js +41 -0
- package/dist/commands/webhook/create.js +72 -0
- package/dist/commands/webhook/delete.js +32 -0
- package/dist/commands/webhook/get.js +34 -0
- package/dist/commands/webhook/list.js +40 -0
- package/dist/commands/webhook/test.js +32 -0
- package/dist/commands/webhook/update.js +56 -0
- package/dist/commands/webhook-delivery/list.js +63 -0
- package/dist/commands/webhook-delivery/redeliver.js +48 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/completion/hotcodepush.bash +536 -0
- package/dist/completion/hotcodepush.fish +979 -0
- package/dist/completion/hotcodepush.zsh +1372 -0
- package/dist/config/consts.js +32 -0
- package/dist/index.js +5 -0
- package/dist/registry.js +83 -0
- package/dist/utils/api-client.js +17 -0
- package/dist/utils/audience.js +9 -0
- package/dist/utils/auth-client.js +35 -0
- package/dist/utils/authentication.js +60 -0
- package/dist/utils/boolean-flag.js +3 -0
- package/dist/utils/browser.js +6 -0
- package/dist/utils/build-step.js +104 -0
- package/dist/utils/bundle-files.js +80 -0
- package/dist/utils/bundle-resolution.js +46 -0
- package/dist/utils/channel-fields.js +130 -0
- package/dist/utils/cli.js +80 -0
- package/dist/utils/command-registry.js +79 -0
- package/dist/utils/command.js +4 -0
- package/dist/utils/completion/bash.js +48 -0
- package/dist/utils/completion/command-tree.js +72 -0
- package/dist/utils/completion/fish.js +44 -0
- package/dist/utils/completion/posix-case-arms.js +9 -0
- package/dist/utils/completion/posix-quote.js +4 -0
- package/dist/utils/completion/script-files.js +26 -0
- package/dist/utils/completion/zsh.js +72 -0
- package/dist/utils/compressed-files.js +31 -0
- package/dist/utils/credential.js +15 -0
- package/dist/utils/device-resolution.js +17 -0
- package/dist/utils/diagnostics.js +20 -0
- package/dist/utils/duration.js +16 -0
- package/dist/utils/environment.js +22 -0
- package/dist/utils/error-mapping.js +75 -0
- package/dist/utils/errors.js +266 -0
- package/dist/utils/fingerprint.js +76 -0
- package/dist/utils/flag-name.js +4 -0
- package/dist/utils/fleet.js +9 -0
- package/dist/utils/framework.js +74 -0
- package/dist/utils/frameworks/binary-create-step.js +70 -0
- package/dist/utils/frameworks/capacitor.js +150 -0
- package/dist/utils/frameworks/cordova.js +110 -0
- package/dist/utils/frameworks/expo.js +152 -0
- package/dist/utils/frameworks/index.js +13 -0
- package/dist/utils/frameworks/native-glue.js +10 -0
- package/dist/utils/frameworks/native-projects.js +27 -0
- package/dist/utils/frameworks/react-native-build.js +182 -0
- package/dist/utils/frameworks/react-native.js +127 -0
- package/dist/utils/frameworks/sdk-package.js +63 -0
- package/dist/utils/git-provenance.js +71 -0
- package/dist/utils/global-options.js +34 -0
- package/dist/utils/hosts.js +53 -0
- package/dist/utils/init-steps.js +51 -0
- package/dist/utils/json-file.js +28 -0
- package/dist/utils/login.js +153 -0
- package/dist/utils/member-resolution.js +49 -0
- package/dist/utils/native-project-edit.js +41 -0
- package/dist/utils/nullable-field.js +10 -0
- package/dist/utils/outcome.js +24 -0
- package/dist/utils/output.js +38 -0
- package/dist/utils/pack.js +20 -0
- package/dist/utils/package-json.js +5 -0
- package/dist/utils/package-manager.js +50 -0
- package/dist/utils/pagination.js +29 -0
- package/dist/utils/progress.js +23 -0
- package/dist/utils/progression.js +119 -0
- package/dist/utils/project-config.js +61 -0
- package/dist/utils/prompts.js +53 -0
- package/dist/utils/react-native-project.js +119 -0
- package/dist/utils/release-conditions.js +79 -0
- package/dist/utils/release-output.js +13 -0
- package/dist/utils/release-resolution.js +83 -0
- package/dist/utils/resource-file.js +81 -0
- package/dist/utils/resource-resolution.js +107 -0
- package/dist/utils/signing-private-key.js +105 -0
- package/dist/utils/spending-cap.js +16 -0
- package/dist/utils/telemetry.js +172 -0
- package/dist/utils/token-store.js +63 -0
- package/dist/utils/update-notice.js +68 -0
- package/dist/utils/upload.js +148 -0
- package/dist/utils/usage-month.js +14 -0
- package/dist/utils/user-config.js +54 -0
- package/dist/utils/webhook-resolution.js +47 -0
- package/dist/utils/xcode-project.js +181 -0
- package/package.json +11 -9
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { openAsBlob } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { HotCodePushError } from '@hotcodepush/node';
|
|
4
|
+
import { BUNDLE_BYTES_LIMIT, signManifest } from '@hotcodepush/protocol';
|
|
5
|
+
import { collectBundleFiles } from './bundle-files.js';
|
|
6
|
+
import { compressFiles, withTemporaryDirectory } from './compressed-files.js';
|
|
7
|
+
import { BundleTooLargeError } from './errors.js';
|
|
8
|
+
import { omitNativeGlue } from './frameworks/native-glue.js';
|
|
9
|
+
import { resolveQuantityText } from './output.js';
|
|
10
|
+
import { writePack } from './pack.js';
|
|
11
|
+
import { fetchAllPages } from './pagination.js';
|
|
12
|
+
import { resolveByteText } from './progress.js';
|
|
13
|
+
// The most the bundle files list takes, more than any other list.
|
|
14
|
+
const BUNDLE_FILE_PAGE_LIMIT = 1000;
|
|
15
|
+
export function assertWithinBundleBytesLimit(files) {
|
|
16
|
+
const largeFile = files.find(file => file.sizeBytes > BUNDLE_BYTES_LIMIT);
|
|
17
|
+
if (largeFile !== undefined) {
|
|
18
|
+
throw new BundleTooLargeError(`The file ${largeFile.path}`, BUNDLE_BYTES_LIMIT);
|
|
19
|
+
}
|
|
20
|
+
const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
21
|
+
if (totalBytes > BUNDLE_BYTES_LIMIT) {
|
|
22
|
+
throw new BundleTooLargeError('The bundle', BUNDLE_BYTES_LIMIT);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function collectUploadFiles({ directoryPath, nativeGluePaths = [], reporter, }) {
|
|
26
|
+
const collectedFiles = await collectBundleFiles(directoryPath);
|
|
27
|
+
const files = omitNativeGlue(collectedFiles, nativeGluePaths);
|
|
28
|
+
const omittedFileCount = collectedFiles.length - files.length;
|
|
29
|
+
if (omittedFileCount > 0) {
|
|
30
|
+
reporter.report(`Left out ${resolveQuantityText(omittedFileCount, 'file')} of the native glue the binary carries: ${nativeGluePaths.join(', ')}.`);
|
|
31
|
+
}
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
export async function uploadBundle(hotCodePush, { appId, bundleVersion, directoryPath, fingerprint, gitProvenance, nativeGluePaths, platforms, reporter, signingPrivateKey, }) {
|
|
35
|
+
reporter.report(`Hashing the files under ${directoryPath}…`);
|
|
36
|
+
const files = await collectUploadFiles({
|
|
37
|
+
directoryPath,
|
|
38
|
+
nativeGluePaths,
|
|
39
|
+
reporter,
|
|
40
|
+
});
|
|
41
|
+
assertWithinBundleBytesLimit(files);
|
|
42
|
+
return withTemporaryDirectory(async (temporaryDirectoryPath) => {
|
|
43
|
+
const compressedFiles = await compressFiles(files, temporaryDirectoryPath);
|
|
44
|
+
const manifest = buildManifestToSign({
|
|
45
|
+
appId,
|
|
46
|
+
bundleVersion,
|
|
47
|
+
files,
|
|
48
|
+
fingerprint,
|
|
49
|
+
platforms,
|
|
50
|
+
});
|
|
51
|
+
const signature = signingPrivateKey === null
|
|
52
|
+
? null
|
|
53
|
+
: (await signManifest(manifest, signingPrivateKey)).signature;
|
|
54
|
+
if (signature !== null) {
|
|
55
|
+
reporter.report(`Signed the manifest with key ${signature.keyId}.`);
|
|
56
|
+
}
|
|
57
|
+
const createdBundle = await hotCodePush.apps.bundles.create({
|
|
58
|
+
appId,
|
|
59
|
+
files: manifest.files,
|
|
60
|
+
fingerprint,
|
|
61
|
+
platforms: manifest.platforms,
|
|
62
|
+
signature,
|
|
63
|
+
version: bundleVersion,
|
|
64
|
+
...gitProvenance,
|
|
65
|
+
});
|
|
66
|
+
const missingSha256s = await fetchMissingSha256s(hotCodePush, {
|
|
67
|
+
appId,
|
|
68
|
+
bundleId: createdBundle.id,
|
|
69
|
+
});
|
|
70
|
+
reporter.report(`Uploading ${missingSha256s.length} of ${files.length} files the app lacks…`);
|
|
71
|
+
const uploadedFiles = await uploadMissingFiles(hotCodePush, appId, missingSha256s, compressedFiles, reporter);
|
|
72
|
+
const packFilePath = join(temporaryDirectoryPath, 'pack');
|
|
73
|
+
await writePack([...compressedFiles.values()], packFilePath);
|
|
74
|
+
reporter.report('Uploading the full pack…');
|
|
75
|
+
await uploadPack(hotCodePush, { appId, bundleId: createdBundle.id }, packFilePath);
|
|
76
|
+
const completedBundle = await hotCodePush.apps.bundles.complete({
|
|
77
|
+
appId,
|
|
78
|
+
bundleId: createdBundle.id,
|
|
79
|
+
});
|
|
80
|
+
return { bundle: completedBundle, upload: uploadedFiles };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// The API rebuilds this manifest before it checks the signature, so the order must match.
|
|
84
|
+
// Canonical JSON leaves array order to the writer: files sort by path and platforms by UTF-16 code units.
|
|
85
|
+
export function buildManifestToSign({ appId, bundleVersion, files, fingerprint, platforms, }) {
|
|
86
|
+
return {
|
|
87
|
+
appId,
|
|
88
|
+
bundleVersion,
|
|
89
|
+
files: files
|
|
90
|
+
.map(({ path, sha256, sizeBytes }) => ({ path, sha256, sizeBytes }))
|
|
91
|
+
.sort((left, right) => compareCodeUnits(left.path, right.path)),
|
|
92
|
+
fingerprint,
|
|
93
|
+
platforms: [...platforms].sort(compareCodeUnits),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function uploadMissingFiles(hotCodePush, appId, missingSha256s, compressedFiles, reporter) {
|
|
97
|
+
let sizeBytes = 0;
|
|
98
|
+
for (const [index, sha256] of missingSha256s.entries()) {
|
|
99
|
+
const compressedFile = compressedFiles.get(sha256);
|
|
100
|
+
if (compressedFile === undefined) {
|
|
101
|
+
throw new Error(`The app lacks ${sha256}, which is not in the build.`);
|
|
102
|
+
}
|
|
103
|
+
reporter.report(` ${index + 1}/${missingSha256s.length} ${sha256.slice(0, 12)} (${resolveByteText(compressedFile.sizeBytes)})`);
|
|
104
|
+
await uploadFile(hotCodePush, appId, compressedFile);
|
|
105
|
+
sizeBytes += compressedFile.sizeBytes;
|
|
106
|
+
}
|
|
107
|
+
return { fileCount: missingSha256s.length, sizeBytes };
|
|
108
|
+
}
|
|
109
|
+
export async function uploadPack(hotCodePush, pathParameters, packFilePath) {
|
|
110
|
+
await hotCodePush.apps.bundles.pack.upload({
|
|
111
|
+
...pathParameters,
|
|
112
|
+
body: await openAsBlob(packFilePath),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// `upload_incomplete` names the missing hashes in its details.
|
|
116
|
+
export function resolveMissingSha256s(error) {
|
|
117
|
+
if (!(error instanceof HotCodePushError) ||
|
|
118
|
+
error.code !== 'upload_incomplete') {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const details = error.details;
|
|
122
|
+
const missingSha256s = details?.missingSha256s;
|
|
123
|
+
return Array.isArray(missingSha256s)
|
|
124
|
+
? missingSha256s.filter((sha256) => typeof sha256 === 'string')
|
|
125
|
+
: [];
|
|
126
|
+
}
|
|
127
|
+
function compareCodeUnits(left, right) {
|
|
128
|
+
if (left === right) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
return left < right ? -1 : 1;
|
|
132
|
+
}
|
|
133
|
+
// Several paths can share a hash, so each is listed once.
|
|
134
|
+
async function fetchMissingSha256s(hotCodePush, pathParameters) {
|
|
135
|
+
const missingFiles = await fetchAllPages(page => hotCodePush.apps.bundles.files.list({
|
|
136
|
+
...pathParameters,
|
|
137
|
+
isStored: false,
|
|
138
|
+
...page,
|
|
139
|
+
}), BUNDLE_FILE_PAGE_LIMIT);
|
|
140
|
+
return [...new Set(missingFiles.map(({ sha256 }) => sha256))];
|
|
141
|
+
}
|
|
142
|
+
async function uploadFile(hotCodePush, appId, { compressedFilePath, sha256 }) {
|
|
143
|
+
await hotCodePush.apps.files.upload({
|
|
144
|
+
appId,
|
|
145
|
+
body: await openAsBlob(compressedFilePath),
|
|
146
|
+
sha256,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const monthOptionShape = {
|
|
3
|
+
month: z
|
|
4
|
+
.string()
|
|
5
|
+
.optional()
|
|
6
|
+
.describe('The month, YYYY-MM. Defaults to the current one.'),
|
|
7
|
+
};
|
|
8
|
+
export function resolveMonthText(month) {
|
|
9
|
+
return new Date(`${month}-01T00:00:00.000Z`).toLocaleString('en-US', {
|
|
10
|
+
month: 'long',
|
|
11
|
+
timeZone: 'UTC',
|
|
12
|
+
year: 'numeric',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CONFIG_DIRECTORY_NAME, CONFIG_FILE_NAME, DEFAULT_API_URL, } from '../config/consts.js';
|
|
5
|
+
import { readJsonFile } from './json-file.js';
|
|
6
|
+
// A config.json the CLI cannot read or write is for the command to report, never for a notice.
|
|
7
|
+
export function ignoreUserConfigErrors(readOrWrite) {
|
|
8
|
+
try {
|
|
9
|
+
return readOrWrite();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// The variable comes first, so a CI reaches the API its `HOTCODEPUSH_TOKEN` belongs to.
|
|
16
|
+
export function readApiUrl() {
|
|
17
|
+
return (process.env.HOTCODEPUSH_API_BASE_URL ||
|
|
18
|
+
readUserConfig().apiUrl ||
|
|
19
|
+
DEFAULT_API_URL).replace(/\/+$/, '');
|
|
20
|
+
}
|
|
21
|
+
export function readUserConfig() {
|
|
22
|
+
const filePath = resolveConfigFilePath();
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
return readJsonFile(filePath);
|
|
27
|
+
}
|
|
28
|
+
export function resolveConfigDirectoryPath() {
|
|
29
|
+
const baseDirectoryPath = process.platform === 'win32'
|
|
30
|
+
? (process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'))
|
|
31
|
+
: process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
32
|
+
return join(baseDirectoryPath, CONFIG_DIRECTORY_NAME);
|
|
33
|
+
}
|
|
34
|
+
// The expiry is the local hint that a session may have ended; the API's answer is the fact.
|
|
35
|
+
export function writeStoredSession({ expiresAt, id }) {
|
|
36
|
+
writeUserConfig({
|
|
37
|
+
...readUserConfig(),
|
|
38
|
+
sessionExpiresAt: new Date(expiresAt).toISOString(),
|
|
39
|
+
sessionId: id,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function writeUserConfig(userConfig) {
|
|
43
|
+
const filePath = resolveConfigFilePath();
|
|
44
|
+
mkdirSync(resolveConfigDirectoryPath(), { recursive: true });
|
|
45
|
+
// The file can hold the token, so only its owner may read it.
|
|
46
|
+
// The mode covers a new file and chmod an existing one.
|
|
47
|
+
writeFileSync(filePath, `${JSON.stringify(userConfig, null, 2)}\n`, {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
chmodSync(filePath, 0o600);
|
|
51
|
+
}
|
|
52
|
+
function resolveConfigFilePath() {
|
|
53
|
+
return join(resolveConfigDirectoryPath(), CONFIG_FILE_NAME);
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { fetchApps, fetchOrganizationId, fetchResourceId, promptResourceId, } from './resource-resolution.js';
|
|
6
|
+
export const webhookAppOptionShape = {
|
|
7
|
+
app: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("The app, by id or name, whose events alone the webhook receives. Defaults to every app's and the organization's events, never to hotcodepush.json's app."),
|
|
11
|
+
};
|
|
12
|
+
export const webhookEventOptionShape = {
|
|
13
|
+
event: z
|
|
14
|
+
.array(z.string())
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('An event type the webhook receives, such as release.created, repeatable.'),
|
|
17
|
+
};
|
|
18
|
+
export const webhookOptionShape = {
|
|
19
|
+
webhook: z.string().optional().describe('The webhook, by id or name.'),
|
|
20
|
+
};
|
|
21
|
+
// The name is unique in the organization, so it names a webhook the way a name does an app.
|
|
22
|
+
export async function fetchWebhook(hotCodePush, options) {
|
|
23
|
+
const organizationId = await fetchOrganizationId(hotCodePush, options);
|
|
24
|
+
return hotCodePush.organizations.webhooks.get({
|
|
25
|
+
organizationId,
|
|
26
|
+
webhookId: await fetchWebhookId(hotCodePush, organizationId, options),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function fetchWebhookAppId(hotCodePush, organizationId, app) {
|
|
30
|
+
return fetchResourceId('app', app, () => fetchApps(hotCodePush, organizationId));
|
|
31
|
+
}
|
|
32
|
+
// The API checks the types against its catalog and names the one it refuses.
|
|
33
|
+
export function resolveWebhookEventTypes(events) {
|
|
34
|
+
return events;
|
|
35
|
+
}
|
|
36
|
+
async function fetchWebhookId(hotCodePush, organizationId, options) {
|
|
37
|
+
if (options.webhook !== undefined) {
|
|
38
|
+
return fetchResourceId('webhook', options.webhook, () => fetchWebhooks(hotCodePush, organizationId));
|
|
39
|
+
}
|
|
40
|
+
if (!isInteractive(options)) {
|
|
41
|
+
throw new MissingParameterError('--webhook');
|
|
42
|
+
}
|
|
43
|
+
return promptResourceId('webhook', await fetchWebhooks(hotCodePush, organizationId), options);
|
|
44
|
+
}
|
|
45
|
+
function fetchWebhooks(hotCodePush, organizationId) {
|
|
46
|
+
return fetchAllPages(page => hotCodePush.organizations.webhooks.list({ organizationId, ...page }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { project as createXcodeProject } from 'xcode';
|
|
5
|
+
import { InvalidParameterError, MissingParameterError, XcodeProjectError, } from './errors.js';
|
|
6
|
+
import { promptSelect } from './prompts.js';
|
|
7
|
+
const APPLICATION_PRODUCT_TYPE = 'com.apple.product-type.application';
|
|
8
|
+
const BINARY_CREATE_PHASE_MARKER = 'binary-create-xcode.sh';
|
|
9
|
+
const BINARY_CREATE_PHASE_NAME = 'Create HotCodePush binary';
|
|
10
|
+
const KEPT_NUMBER_PLACEHOLDER_PATTERN = /hotcodepushKeptNumber(\d+)_/g;
|
|
11
|
+
// Matches a value of digits alone, after `=`, `(` or `,` and before `;`, `,` or a line break.
|
|
12
|
+
const NUMBER_VALUE_PATTERN = /(?<=[=(,]\s*)\d+(?=[;,\n])/g;
|
|
13
|
+
// The `xcode` package's project parses only a file, so its grammar module parses the text.
|
|
14
|
+
const pbxprojParser = createRequire(import.meta.url)('xcode/lib/parser/pbxproj.js');
|
|
15
|
+
// The script reads the version and build from the processed Info.plist.
|
|
16
|
+
// As the phase's input, Xcode processes the plist before the phase runs.
|
|
17
|
+
const INFO_PLIST_INPUT_PATH = '"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)"';
|
|
18
|
+
const RESOURCE_FILE_NAME = 'hotcodepush.json';
|
|
19
|
+
// `cap add ios` lays out `App/App.xcodeproj`. Any other layout takes the first project found.
|
|
20
|
+
export function resolveXcodeProjectFilePath(iosProjectPath) {
|
|
21
|
+
const capacitorProjectPath = join(iosProjectPath, 'App', 'App.xcodeproj');
|
|
22
|
+
const projectPath = existsSync(capacitorProjectPath)
|
|
23
|
+
? capacitorProjectPath
|
|
24
|
+
: findXcodeProjectPath(iosProjectPath);
|
|
25
|
+
return projectPath === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: join(projectPath, 'project.pbxproj');
|
|
28
|
+
}
|
|
29
|
+
// Every SDK names its script file the same, so the file name recognises the phase.
|
|
30
|
+
export function hasBinaryCreatePhase(projectFilePath) {
|
|
31
|
+
return readFileSync(projectFilePath, 'utf8').includes(BINARY_CREATE_PHASE_MARKER);
|
|
32
|
+
}
|
|
33
|
+
// A pbxproj string carries its line breaks escaped.
|
|
34
|
+
export function buildPbxprojShellScript(lines) {
|
|
35
|
+
return lines.join('\\n');
|
|
36
|
+
}
|
|
37
|
+
export async function addBinaryCreatePhase(projectFilePath, phase, options) {
|
|
38
|
+
if (hasBinaryCreatePhase(projectFilePath)) {
|
|
39
|
+
return 'present';
|
|
40
|
+
}
|
|
41
|
+
const editableProject = parseProject(projectFilePath, phase.fix);
|
|
42
|
+
const { project } = editableProject;
|
|
43
|
+
const target = await resolveAppTarget(project, options);
|
|
44
|
+
const nativeTarget = project.pbxNativeTargetSection()[target.key];
|
|
45
|
+
const buildPhases = typeof nativeTarget === 'object' ? nativeTarget.buildPhases : [];
|
|
46
|
+
const anchorPhaseIndex = phase.anchorPhaseName === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: buildPhases.findIndex(({ comment }) => comment === phase.anchorPhaseName);
|
|
49
|
+
if (anchorPhaseIndex === -1) {
|
|
50
|
+
throw new XcodeProjectError(`The Xcode project ${projectFilePath} has no "${phase.anchorPhaseName}" phase to run the build step after.`, undefined, projectFilePath, phase.fix);
|
|
51
|
+
}
|
|
52
|
+
const { buildPhase } = project.addBuildPhase([], 'PBXShellScriptBuildPhase', BINARY_CREATE_PHASE_NAME, target.key, {
|
|
53
|
+
inputPaths: [INFO_PLIST_INPUT_PATH],
|
|
54
|
+
shellPath: '/bin/sh',
|
|
55
|
+
shellScript: phase.shellScript,
|
|
56
|
+
});
|
|
57
|
+
// The phase writes hotcodepush.json with the build time on every build.
|
|
58
|
+
// Xcode warns about a phase without outputs that is not marked so.
|
|
59
|
+
buildPhase.alwaysOutOfDate = 1;
|
|
60
|
+
if (anchorPhaseIndex !== undefined) {
|
|
61
|
+
// The package appends the phase, but it must run right after the phase whose output it reads.
|
|
62
|
+
const binaryCreatePhase = buildPhases.pop();
|
|
63
|
+
if (binaryCreatePhase !== undefined) {
|
|
64
|
+
buildPhases.splice(anchorPhaseIndex + 1, 0, binaryCreatePhase);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
writeProject(projectFilePath, editableProject);
|
|
68
|
+
return 'added';
|
|
69
|
+
}
|
|
70
|
+
// An earlier `init` added this reference to Capacitor's app target.
|
|
71
|
+
// A project that cannot be parsed counts as without it, and the phase's edit reports the parse error.
|
|
72
|
+
export function hasReadableResourceReference(projectFilePath) {
|
|
73
|
+
try {
|
|
74
|
+
return (findResourceReferenceKeys(parseProject(projectFilePath).project).length >
|
|
75
|
+
0);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// The build step now writes the file into the app, and a reference to a missing file fails the build.
|
|
82
|
+
export function removeResourceReference(projectFilePath) {
|
|
83
|
+
const editableProject = parseProject(projectFilePath);
|
|
84
|
+
const { project } = editableProject;
|
|
85
|
+
const { objects } = project.hash.project;
|
|
86
|
+
const fileReferenceKeys = findResourceReferenceKeys(project);
|
|
87
|
+
const buildFileKeys = Object.entries(objects.PBXBuildFile)
|
|
88
|
+
.filter(([, buildFile]) => typeof buildFile === 'object' &&
|
|
89
|
+
fileReferenceKeys.includes(String(buildFile.fileRef)))
|
|
90
|
+
.map(([key]) => key);
|
|
91
|
+
deleteObjects(objects.PBXFileReference, fileReferenceKeys);
|
|
92
|
+
deleteObjects(objects.PBXBuildFile, buildFileKeys);
|
|
93
|
+
removeChildren(objects.PBXResourcesBuildPhase ?? {}, 'files', buildFileKeys);
|
|
94
|
+
removeChildren(objects.PBXGroup, 'children', fileReferenceKeys);
|
|
95
|
+
writeProject(projectFilePath, editableProject);
|
|
96
|
+
}
|
|
97
|
+
async function resolveAppTarget(project, options) {
|
|
98
|
+
const appTargets = resolveAppTargets(project);
|
|
99
|
+
if (options.xcodeTarget !== undefined) {
|
|
100
|
+
const namedTarget = appTargets.find(({ name }) => name === options.xcodeTarget);
|
|
101
|
+
if (namedTarget === undefined) {
|
|
102
|
+
throw new InvalidParameterError(`No app target is named ${options.xcodeTarget}. The targets are ${appTargets.map(({ name }) => name).join(', ')}.`, 'Pass one of them with --xcode-target.');
|
|
103
|
+
}
|
|
104
|
+
return namedTarget;
|
|
105
|
+
}
|
|
106
|
+
const [onlyTarget] = appTargets;
|
|
107
|
+
if (onlyTarget !== undefined && appTargets.length === 1) {
|
|
108
|
+
return onlyTarget;
|
|
109
|
+
}
|
|
110
|
+
const targetKey = await promptSelect('--xcode-target', 'Which app target?', appTargets.map(({ key, name }) => ({ label: name, value: key })), options);
|
|
111
|
+
const selectedTarget = appTargets.find(({ key }) => key === targetKey);
|
|
112
|
+
if (selectedTarget === undefined) {
|
|
113
|
+
throw new MissingParameterError('--xcode-target');
|
|
114
|
+
}
|
|
115
|
+
return selectedTarget;
|
|
116
|
+
}
|
|
117
|
+
function deleteObjects(section, keys) {
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
delete section[key];
|
|
120
|
+
delete section[`${key}_comment`];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function findResourceReferenceKeys(project) {
|
|
124
|
+
return Object.entries(project.pbxFileReferenceSection())
|
|
125
|
+
.filter(([, fileReference]) => typeof fileReference === 'object' &&
|
|
126
|
+
String(fileReference.path).replaceAll('"', '') === RESOURCE_FILE_NAME)
|
|
127
|
+
.map(([key]) => key);
|
|
128
|
+
}
|
|
129
|
+
function removeChildren(section, field, keys) {
|
|
130
|
+
for (const sectionObject of Object.values(section)) {
|
|
131
|
+
const entries = typeof sectionObject === 'object' ? sectionObject[field] : undefined;
|
|
132
|
+
if (typeof sectionObject === 'object' && Array.isArray(entries)) {
|
|
133
|
+
sectionObject[field] = entries.filter(({ value }) => !keys.includes(value));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function findXcodeProjectPath(iosProjectPath) {
|
|
138
|
+
if (!existsSync(iosProjectPath)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
for (const directoryPath of [iosProjectPath, join(iosProjectPath, 'App')]) {
|
|
142
|
+
const projectName = existsSync(directoryPath)
|
|
143
|
+
? readdirSync(directoryPath).find(name => name.endsWith('.xcodeproj'))
|
|
144
|
+
: undefined;
|
|
145
|
+
if (projectName !== undefined) {
|
|
146
|
+
return join(directoryPath, projectName);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
// The `xcode` package reads digits as a JavaScript number, so `0920` would come back as `920` and long values lose digits.
|
|
152
|
+
// Such values pass through as text placeholders, and `writeProject` puts them back.
|
|
153
|
+
function parseProject(projectFilePath, fix) {
|
|
154
|
+
try {
|
|
155
|
+
const keptNumbers = [];
|
|
156
|
+
const text = readFileSync(projectFilePath, 'utf8').replace(NUMBER_VALUE_PATTERN, number => {
|
|
157
|
+
if (String(Number.parseInt(number, 10)) === number) {
|
|
158
|
+
return number;
|
|
159
|
+
}
|
|
160
|
+
keptNumbers.push(number);
|
|
161
|
+
return `hotcodepushKeptNumber${keptNumbers.length - 1}_`;
|
|
162
|
+
});
|
|
163
|
+
const project = createXcodeProject(projectFilePath);
|
|
164
|
+
project.hash = pbxprojParser.parse(text);
|
|
165
|
+
return { keptNumbers, project };
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new XcodeProjectError(`The Xcode project ${projectFilePath} could not be parsed.`, error, projectFilePath, fix);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function writeProject(projectFilePath, { keptNumbers, project }) {
|
|
172
|
+
writeFileSync(projectFilePath, project
|
|
173
|
+
.writeSync()
|
|
174
|
+
.replace(KEPT_NUMBER_PLACEHOLDER_PATTERN, (placeholder, index) => keptNumbers[Number(index)] ?? placeholder));
|
|
175
|
+
}
|
|
176
|
+
function resolveAppTargets(project) {
|
|
177
|
+
return Object.entries(project.pbxNativeTargetSection())
|
|
178
|
+
.filter((entry) => typeof entry[1] === 'object' &&
|
|
179
|
+
entry[1].productType.replaceAll('"', '') === APPLICATION_PRODUCT_TYPE)
|
|
180
|
+
.map(([key, target]) => ({ key, name: target.name.replaceAll('"', '') }));
|
|
181
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hotcodepush",
|
|
3
3
|
"description": "The HotCodePush CLI: set up, release and manage over-the-air (OTA) updates from the terminal and CI",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://hotcodepush.com",
|
|
@@ -33,25 +33,26 @@
|
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"telemetry": {
|
|
36
|
-
"posthogProjectKey": "",
|
|
37
|
-
"sentryDsn": ""
|
|
36
|
+
"posthogProjectKey": "phc_AdeADMfxpV9qp8ZxanDNjoXFhnFMr7JDiB3pXyjJ9WRs",
|
|
37
|
+
"sentryDsn": "https://8e8933170cb380e4079e70cd2b987668@o4507446340747264.ingest.de.sentry.io/4512356211556432"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsc --project tsconfig.build.json && node scripts/generate-completion.mjs",
|
|
40
|
+
"build": "rimraf ./dist && tsc --project tsconfig.build.json && node scripts/generate-completion.mjs",
|
|
41
41
|
"fmt": "prettier --write .",
|
|
42
42
|
"lint": "eslint .",
|
|
43
|
+
"prepublishOnly": "npm run build",
|
|
43
44
|
"test": "vitest run",
|
|
44
45
|
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@clack/prompts": "1.8.1",
|
|
48
|
-
"@hotcodepush/node": "https://pkg.pr.new/hotcodepush-team/hotcodepush-node/@hotcodepush/node@
|
|
49
|
-
"@hotcodepush/protocol": "https://pkg.pr.new/hotcodepush-team/protocol/@hotcodepush/protocol@
|
|
49
|
+
"@hotcodepush/node": "https://pkg.pr.new/hotcodepush-team/hotcodepush-node/@hotcodepush/node@9e671df",
|
|
50
|
+
"@hotcodepush/protocol": "https://pkg.pr.new/hotcodepush-team/protocol/@hotcodepush/protocol@5b27c65",
|
|
50
51
|
"@napi-rs/keyring": "2.1.0",
|
|
51
|
-
"better-auth": "1.7.
|
|
52
|
+
"better-auth": "1.7.12",
|
|
52
53
|
"fast-xml-parser": "5.11.2",
|
|
53
54
|
"open": "11.0.4",
|
|
54
|
-
"xcode": "
|
|
55
|
+
"xcode": "4.0.0",
|
|
55
56
|
"zod": "4.6.7",
|
|
56
57
|
"zodline": "0.3.1"
|
|
57
58
|
},
|
|
@@ -64,8 +65,9 @@
|
|
|
64
65
|
"eslint-plugin-import-x": "4.17.1",
|
|
65
66
|
"pkg-pr-new": "0.0.88",
|
|
66
67
|
"prettier": "3.9.8",
|
|
68
|
+
"rimraf": "6.1.3",
|
|
67
69
|
"typescript": "5.9.3",
|
|
68
70
|
"typescript-eslint": "8.70.0",
|
|
69
|
-
"vitest": "5.0.
|
|
71
|
+
"vitest": "5.0.5"
|
|
70
72
|
}
|
|
71
73
|
}
|