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.
- 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 +189 -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 +176 -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 +36 -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 +363 -0
- package/dist/commands/fingerprint/diff.js +133 -0
- package/dist/commands/fingerprint.js +41 -0
- package/dist/commands/init.js +492 -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 +185 -0
- package/dist/commands/logout.js +79 -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 +253 -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 +76 -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/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 +75 -0
- package/dist/utils/command-registry.js +79 -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/duration.js +16 -0
- package/dist/utils/environment.js +22 -0
- package/dist/utils/error-mapping.js +79 -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/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 +37 -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 +80 -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 +171 -0
- package/dist/utils/token-store.js +63 -0
- package/dist/utils/update-notice.js +67 -0
- package/dist/utils/upload.js +148 -0
- package/dist/utils/usage-month.js +14 -0
- package/dist/utils/user-config.js +46 -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,363 @@
|
|
|
1
|
+
import { ProjectConfigurationSchema, resolveSigningKeyId, } from '@hotcodepush/protocol';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { INIT_MANUAL_STEP, PACKAGE_JSON, PROJECT_CONFIG_FILE_NAME, } from '../config/consts.js';
|
|
4
|
+
import { createApiClient } from '../utils/api-client.js';
|
|
5
|
+
import { fetchCurrentUser, isUnauthenticatedError, resolveCredentialText, } from '../utils/credential.js';
|
|
6
|
+
import { resolveCliError, resolveSentence } from '../utils/error-mapping.js';
|
|
7
|
+
import { CliError, InvalidJsonError, NotLoggedInError, ReportedFailureError, SigningKeyUnavailableError, UnknownFrameworkError, } from '../utils/errors.js';
|
|
8
|
+
import { detectFramework } from '../utils/framework.js';
|
|
9
|
+
import { resolveFrameworkModule } from '../utils/frameworks/index.js';
|
|
10
|
+
import { readInstalledPackageVersion, readSdkDependencyVersion, } from '../utils/frameworks/sdk-package.js';
|
|
11
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
12
|
+
import { resolveFilesBaseUrl, resolveUpdatesBaseUrl } from '../utils/hosts.js';
|
|
13
|
+
import { printOutcomeRows } from '../utils/outcome.js';
|
|
14
|
+
import { printJson, resolveQuantityText } from '../utils/output.js';
|
|
15
|
+
import { readPackageJson } from '../utils/package-json.js';
|
|
16
|
+
import { ID_SCHEMA, locateProjectConfigFile, readProjectConfigFile, resolveProjectChannel, } from '../utils/project-config.js';
|
|
17
|
+
import { fetchChannels, fetchHasSigningKey, fetchResourceId, fetchSigningKeys, } from '../utils/resource-resolution.js';
|
|
18
|
+
import { readSigningKeyPair } from '../utils/signing-private-key.js';
|
|
19
|
+
import { isEnvironmentToken, readToken } from '../utils/token-store.js';
|
|
20
|
+
import { readApiUrl } from '../utils/user-config.js';
|
|
21
|
+
const CHANNEL_NAME_SCHEMA = ProjectConfigurationSchema.shape.channel;
|
|
22
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
23
|
+
export default defineCommand({
|
|
24
|
+
description: 'Check the project: its configuration, the hook wiring, the signing key, the hosts and the versions a bug report needs.',
|
|
25
|
+
examples: ['hotcodepush doctor', 'hotcodepush doctor --json'],
|
|
26
|
+
options: defineCommandOptions({}),
|
|
27
|
+
action: async (options) => {
|
|
28
|
+
const project = readProject(options.config);
|
|
29
|
+
const { app, checks: sessionAndAppChecks } = await checkSessionAndApp(project);
|
|
30
|
+
const checks = [
|
|
31
|
+
checkConfiguration(project),
|
|
32
|
+
...sessionAndAppChecks,
|
|
33
|
+
...checkFramework(project),
|
|
34
|
+
await checkHosts(),
|
|
35
|
+
await checkSigningKey(project, app),
|
|
36
|
+
checkVersions(project),
|
|
37
|
+
];
|
|
38
|
+
const status = checks.some(({ status }) => status === 'failed')
|
|
39
|
+
? 'failed'
|
|
40
|
+
: 'clean';
|
|
41
|
+
if (options.json) {
|
|
42
|
+
printJson({ checks, status });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
printOutcomeRows(checks.map(({ check, manualStep, message, status }) => ({
|
|
46
|
+
manualStep,
|
|
47
|
+
message,
|
|
48
|
+
name: check,
|
|
49
|
+
status,
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
if (status === 'failed') {
|
|
53
|
+
throw new ReportedFailureError('doctor_failed');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
function readProject(configPath) {
|
|
58
|
+
const { directoryPath, filePath } = locateProjectConfigFile(configPath);
|
|
59
|
+
const framework = resolveProjectFramework(directoryPath);
|
|
60
|
+
const project = {
|
|
61
|
+
directoryPath,
|
|
62
|
+
framework,
|
|
63
|
+
packageJson: framework instanceof CliError
|
|
64
|
+
? undefined
|
|
65
|
+
: readPackageJson(directoryPath),
|
|
66
|
+
projectConfig: undefined,
|
|
67
|
+
};
|
|
68
|
+
if (filePath === undefined) {
|
|
69
|
+
return project;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return { ...project, projectConfig: readProjectConfigFile(filePath) };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof InvalidJsonError)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return { ...project, projectConfigError: error };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function checkConfiguration({ directoryPath, projectConfig, projectConfigError, }) {
|
|
82
|
+
if (projectConfigError !== undefined) {
|
|
83
|
+
return resolveFailedCheck('configuration', projectConfigError);
|
|
84
|
+
}
|
|
85
|
+
if (projectConfig === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
check: 'configuration',
|
|
88
|
+
code: 'project_config_missing',
|
|
89
|
+
manualStep: INIT_MANUAL_STEP,
|
|
90
|
+
message: `There is no ${PROJECT_CONFIG_FILE_NAME} in ${directoryPath} or a directory above it.`,
|
|
91
|
+
status: 'failed',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const problems = resolveConfigurationProblems(projectConfig);
|
|
95
|
+
if (problems.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
check: 'configuration',
|
|
98
|
+
code: 'project_config_invalid',
|
|
99
|
+
manualStep: INIT_MANUAL_STEP,
|
|
100
|
+
message: `The file ${PROJECT_CONFIG_FILE_NAME} ${problems.join(' and ')}.`,
|
|
101
|
+
status: 'failed',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
check: 'configuration',
|
|
106
|
+
message: `The file ${PROJECT_CONFIG_FILE_NAME} names app ${projectConfig.appId} and channel ${resolveProjectChannel(projectConfig)}.`,
|
|
107
|
+
status: 'ok',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// A file without `channel` follows the schema default.
|
|
111
|
+
function resolveConfigurationProblems(projectConfig) {
|
|
112
|
+
const problems = [];
|
|
113
|
+
if (projectConfig.appId === undefined) {
|
|
114
|
+
problems.push('lacks appId');
|
|
115
|
+
}
|
|
116
|
+
else if (!ID_SCHEMA.safeParse(projectConfig.appId).success) {
|
|
117
|
+
problems.push('has an appId that is not an id');
|
|
118
|
+
}
|
|
119
|
+
if (projectConfig.channel !== undefined &&
|
|
120
|
+
!CHANNEL_NAME_SCHEMA.safeParse(projectConfig.channel).success) {
|
|
121
|
+
problems.push('has a channel that is not a channel name');
|
|
122
|
+
}
|
|
123
|
+
return problems;
|
|
124
|
+
}
|
|
125
|
+
// A refused credential, or an API that cannot be asked, fails the session check and leaves the app out.
|
|
126
|
+
async function checkSessionAndApp({ projectConfig, }) {
|
|
127
|
+
let user;
|
|
128
|
+
try {
|
|
129
|
+
if (readToken() === undefined) {
|
|
130
|
+
return {
|
|
131
|
+
app: undefined,
|
|
132
|
+
checks: [
|
|
133
|
+
{
|
|
134
|
+
check: 'session',
|
|
135
|
+
message: 'You are not logged in, so the app is not checked against the API.',
|
|
136
|
+
status: 'skipped',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
user = await fetchCurrentUser();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return { app: undefined, checks: [resolveFailedSessionCheck(error)] };
|
|
145
|
+
}
|
|
146
|
+
const sessionCheck = {
|
|
147
|
+
check: 'session',
|
|
148
|
+
message: resolveCredentialText(user, isEnvironmentToken()),
|
|
149
|
+
status: 'ok',
|
|
150
|
+
};
|
|
151
|
+
const { app, check } = await checkApp(projectConfig);
|
|
152
|
+
return { app, checks: [sessionCheck, check] };
|
|
153
|
+
}
|
|
154
|
+
function resolveFailedSessionCheck(error) {
|
|
155
|
+
const { code, fix, message } = resolveCliError(error);
|
|
156
|
+
if (isUnauthenticatedError(error) || error instanceof NotLoggedInError) {
|
|
157
|
+
return {
|
|
158
|
+
check: 'session',
|
|
159
|
+
code,
|
|
160
|
+
manualStep: 'Run "hotcodepush login", or set a valid HOTCODEPUSH_TOKEN.',
|
|
161
|
+
message: 'The API does not accept the credential.',
|
|
162
|
+
status: 'failed',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
check: 'session',
|
|
167
|
+
code,
|
|
168
|
+
manualStep: fix ?? undefined,
|
|
169
|
+
message: `The credential cannot be checked. ${resolveSentence(message)}`,
|
|
170
|
+
status: 'failed',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function checkApp(projectConfig) {
|
|
174
|
+
if (projectConfig?.appId === undefined ||
|
|
175
|
+
!ID_SCHEMA.safeParse(projectConfig.appId).success) {
|
|
176
|
+
return {
|
|
177
|
+
app: undefined,
|
|
178
|
+
check: {
|
|
179
|
+
check: 'app',
|
|
180
|
+
message: 'The configuration names no app and channel to check.',
|
|
181
|
+
status: 'skipped',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const { appId } = projectConfig;
|
|
186
|
+
const hotCodePush = createApiClient();
|
|
187
|
+
try {
|
|
188
|
+
const [app, channel] = await Promise.all([
|
|
189
|
+
hotCodePush.apps.get({ appId }),
|
|
190
|
+
fetchResourceId('channel', resolveProjectChannel(projectConfig), () => fetchChannels(hotCodePush, appId), PROJECT_CONFIG_FILE_NAME).then(channelId => hotCodePush.apps.channels.get({ appId, channelId })),
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
app,
|
|
194
|
+
check: {
|
|
195
|
+
check: 'app',
|
|
196
|
+
message: `The app ${app.name} and the channel ${channel.name} exist.`,
|
|
197
|
+
status: 'ok',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
app: undefined,
|
|
204
|
+
check: {
|
|
205
|
+
check: 'app',
|
|
206
|
+
code: resolveCliError(error).code,
|
|
207
|
+
manualStep: INIT_MANUAL_STEP,
|
|
208
|
+
message: `The API does not know the app or the channel. ${resolveSentence(error instanceof Error ? error.message : String(error))}`,
|
|
209
|
+
status: 'failed',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// No key file is looked for, since an upload is told where its key is.
|
|
215
|
+
// A key the app has but the file does not list fails, since an upload from that project would go unsigned.
|
|
216
|
+
async function checkSigningKey({ projectConfig }, app) {
|
|
217
|
+
const publicKeys = projectConfig?.appId === undefined ? [] : (projectConfig.publicKeys ?? []);
|
|
218
|
+
try {
|
|
219
|
+
if (publicKeys.length === 0 &&
|
|
220
|
+
app !== undefined &&
|
|
221
|
+
(await fetchHasSigningKey(createApiClient(), app.id))) {
|
|
222
|
+
return {
|
|
223
|
+
check: 'signing_key',
|
|
224
|
+
code: 'public_key_unlisted',
|
|
225
|
+
manualStep: `Run "hotcodepush signing-key list --json" and add each publicKey to publicKeys in ${PROJECT_CONFIG_FILE_NAME}.`,
|
|
226
|
+
message: `The app has a signing key and ${PROJECT_CONFIG_FILE_NAME} lists none.`,
|
|
227
|
+
status: 'failed',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const unregisteredKeyCount = app === undefined || publicKeys.length === 0
|
|
231
|
+
? 0
|
|
232
|
+
: await fetchUnregisteredPublicKeyCount(app.id, publicKeys);
|
|
233
|
+
if (unregisteredKeyCount > 0) {
|
|
234
|
+
return {
|
|
235
|
+
check: 'signing_key',
|
|
236
|
+
code: 'public_key_unregistered',
|
|
237
|
+
manualStep: 'Run "hotcodepush signing-key list --json" and keep in publicKeys only the keys it prints.',
|
|
238
|
+
message: `The file ${PROJECT_CONFIG_FILE_NAME} lists ${resolveQuantityText(unregisteredKeyCount, 'public key')} the app has not registered.`,
|
|
239
|
+
status: 'failed',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return await checkSigningPrivateKey(publicKeys);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return resolveFailedCheck('signing_key', error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// No variable while keys are listed is fine, since an upload is told where its key is.
|
|
249
|
+
async function checkSigningPrivateKey(publicKeys) {
|
|
250
|
+
try {
|
|
251
|
+
const signingKeyPair = await readSigningKeyPair(publicKeys, undefined);
|
|
252
|
+
return signingKeyPair === null
|
|
253
|
+
? {
|
|
254
|
+
check: 'signing_key',
|
|
255
|
+
message: 'Code signing is off. Run "hotcodepush signing-key create" to turn it on.',
|
|
256
|
+
status: 'skipped',
|
|
257
|
+
}
|
|
258
|
+
: {
|
|
259
|
+
check: 'signing_key',
|
|
260
|
+
message: `HOTCODEPUSH_SIGNING_KEY signs with key ${resolveSigningKeyId(signingKeyPair.publicKey)}.`,
|
|
261
|
+
status: 'ok',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (error instanceof SigningKeyUnavailableError) {
|
|
266
|
+
return {
|
|
267
|
+
check: 'signing_key',
|
|
268
|
+
message: 'Code signing is on. An upload signs with --private-key-path or HOTCODEPUSH_SIGNING_KEY.',
|
|
269
|
+
status: 'ok',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// An upload signed with an unregistered key is refused.
|
|
276
|
+
async function fetchUnregisteredPublicKeyCount(appId, publicKeys) {
|
|
277
|
+
const registeredPublicKeys = new Set((await fetchSigningKeys(createApiClient(), appId)).map(({ publicKey }) => publicKey));
|
|
278
|
+
return publicKeys.filter(publicKey => !registeredPublicKeys.has(publicKey))
|
|
279
|
+
.length;
|
|
280
|
+
}
|
|
281
|
+
// The resource file is not checked, since every build writes it into the app, never into the project.
|
|
282
|
+
function checkFramework(project) {
|
|
283
|
+
const { framework } = project;
|
|
284
|
+
if (framework instanceof CliError) {
|
|
285
|
+
return [resolveFailedCheck('framework', framework)];
|
|
286
|
+
}
|
|
287
|
+
return framework.checkWiring(project);
|
|
288
|
+
}
|
|
289
|
+
// Any HTTP answer counts and a network error does not.
|
|
290
|
+
async function checkHosts() {
|
|
291
|
+
let apiUrl;
|
|
292
|
+
try {
|
|
293
|
+
apiUrl = readApiUrl();
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return resolveFailedCheck('hosts', error);
|
|
297
|
+
}
|
|
298
|
+
const hosts = [
|
|
299
|
+
['api', `${apiUrl.replace(/\/+$/, '')}/health`],
|
|
300
|
+
['files', resolveFilesBaseUrl(apiUrl)],
|
|
301
|
+
['updates', resolveUpdatesBaseUrl(apiUrl)],
|
|
302
|
+
];
|
|
303
|
+
const unreachable = (await Promise.all(hosts.map(async ([name, url]) => (await isReachable(url)) ? undefined : `${name} (${url})`))).filter(host => host !== undefined);
|
|
304
|
+
if (unreachable.length > 0) {
|
|
305
|
+
return {
|
|
306
|
+
check: 'hosts',
|
|
307
|
+
code: 'host_unreachable',
|
|
308
|
+
manualStep: 'Check the network, the API URL in config.json and the HOTCODEPUSH_*_BASE_URL variables.',
|
|
309
|
+
message: `The CLI cannot reach ${unreachable.join(', ')}.`,
|
|
310
|
+
status: 'failed',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
check: 'hosts',
|
|
315
|
+
message: `The hosts ${hosts.map(([name]) => name).join(', ')} are reachable.`,
|
|
316
|
+
status: 'ok',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function checkVersions({ directoryPath, framework, packageJson, }) {
|
|
320
|
+
const versions = [
|
|
321
|
+
`${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`,
|
|
322
|
+
`node ${process.version}`,
|
|
323
|
+
];
|
|
324
|
+
if (!(framework instanceof CliError)) {
|
|
325
|
+
versions.push(...framework.versionedPackageNames.map(packageName => `${packageName} ${readInstalledPackageVersion(directoryPath, packageName) ?? packageJson?.dependencies?.[packageName] ?? 'missing'}`), ...(framework.versionedSdkDependencyNames ?? []).map(packageName => `${packageName} ${readSdkDependencyVersion(directoryPath, framework.packageName, packageName) ?? 'missing'}`));
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
check: 'versions',
|
|
329
|
+
message: `The versions are ${versions.join(', ')}.`,
|
|
330
|
+
status: 'ok',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function resolveFailedCheck(check, error) {
|
|
334
|
+
const { code, fix, message } = resolveCliError(error);
|
|
335
|
+
return {
|
|
336
|
+
check,
|
|
337
|
+
code,
|
|
338
|
+
manualStep: fix ?? undefined,
|
|
339
|
+
message: resolveSentence(message),
|
|
340
|
+
status: 'failed',
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function isReachable(url) {
|
|
344
|
+
try {
|
|
345
|
+
await fetch(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function resolveProjectFramework(directoryPath) {
|
|
353
|
+
try {
|
|
354
|
+
return resolveFrameworkModule(detectFramework(directoryPath));
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error instanceof UnknownFrameworkError ||
|
|
358
|
+
error instanceof InvalidJsonError) {
|
|
359
|
+
return error;
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineCommand } from 'zodline';
|
|
4
|
+
import { InvalidParameterError } from '../../utils/errors.js';
|
|
5
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
6
|
+
import { printJson, printTable } from '../../utils/output.js';
|
|
7
|
+
import { promptText } from '../../utils/prompts.js';
|
|
8
|
+
const FINGERPRINT_FILE_FIX = 'Write each file with "hotcodepush fingerprint --json > <file>", the earlier one at the commit of the store build.';
|
|
9
|
+
const FingerprintFileSchema = z.object({
|
|
10
|
+
fingerprint: z.string(),
|
|
11
|
+
extraFingerprintPaths: z.array(z.object({ path: z.string(), sha256: z.string() })),
|
|
12
|
+
packages: z.array(z.object({
|
|
13
|
+
integrity: z.string().nullable(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
version: z.string(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
export default defineCommand({
|
|
19
|
+
description: 'Print the packages and extra fingerprint paths added, removed or moved between <file-a> and <file-b>, two files written by fingerprint --json, the earlier first; local, no login.',
|
|
20
|
+
examples: [
|
|
21
|
+
'hotcodepush fingerprint diff store-build.json current.json',
|
|
22
|
+
'hotcodepush fingerprint diff store-build.json current.json --json',
|
|
23
|
+
],
|
|
24
|
+
args: z.array(z.string()),
|
|
25
|
+
options: defineCommandOptions({}),
|
|
26
|
+
action: async (options, args) => {
|
|
27
|
+
if (args.length > 2) {
|
|
28
|
+
throw new InvalidParameterError(`The fingerprint diff command compares two files, <file-a> and <file-b>. It does not take ${args.slice(2).join(' ')}.`);
|
|
29
|
+
}
|
|
30
|
+
const beforeFile = await readFingerprintFile('<file-a>', args[0] ??
|
|
31
|
+
(await promptText('<file-a>', "Which file holds the earlier fingerprint, the store build's?", options)));
|
|
32
|
+
const afterFile = await readFingerprintFile('<file-b>', args[1] ??
|
|
33
|
+
(await promptText('<file-b>', 'Which file holds the later fingerprint?', options)));
|
|
34
|
+
const diff = resolveFingerprintDiff(beforeFile, afterFile);
|
|
35
|
+
if (options.json) {
|
|
36
|
+
printJson(diff);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (diff.packages.length === 0 && diff.extraFingerprintPaths.length === 0) {
|
|
40
|
+
console.log(`Nothing moved: both files hold ${beforeFile.fingerprint}.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
printTable({
|
|
44
|
+
emptyText: 'No package moved.',
|
|
45
|
+
headers: ['PACKAGE', 'BEFORE', 'AFTER'],
|
|
46
|
+
nextOffset: null,
|
|
47
|
+
rows: diff.packages.map(({ after, before, name }) => [
|
|
48
|
+
name,
|
|
49
|
+
resolveInstalledVersionText(before, after),
|
|
50
|
+
resolveInstalledVersionText(after, before),
|
|
51
|
+
]),
|
|
52
|
+
});
|
|
53
|
+
if (diff.extraFingerprintPaths.length > 0) {
|
|
54
|
+
printTable({
|
|
55
|
+
emptyText: '',
|
|
56
|
+
headers: ['EXTRA FINGERPRINT PATH', 'BEFORE', 'AFTER'],
|
|
57
|
+
nextOffset: null,
|
|
58
|
+
rows: diff.extraFingerprintPaths.map(({ after, before, path }) => [
|
|
59
|
+
path,
|
|
60
|
+
before ?? 'none',
|
|
61
|
+
after ?? 'none',
|
|
62
|
+
]),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
async function readFingerprintFile(argumentName, filePath) {
|
|
68
|
+
const text = await readFile(filePath, 'utf8').catch((error) => {
|
|
69
|
+
throw new InvalidParameterError(`The file ${filePath} from ${argumentName} cannot be read.`, FINGERPRINT_FILE_FIX, error);
|
|
70
|
+
});
|
|
71
|
+
const parsedFile = FingerprintFileSchema.safeParse(parseJson(text));
|
|
72
|
+
if (!parsedFile.success) {
|
|
73
|
+
throw new InvalidParameterError(`The file ${filePath} from ${argumentName} is not a fingerprint written by fingerprint --json.`, FINGERPRINT_FILE_FIX, parsedFile.error);
|
|
74
|
+
}
|
|
75
|
+
return parsedFile.data;
|
|
76
|
+
}
|
|
77
|
+
function parseJson(text) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(text);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function resolveFingerprintDiff(before, after) {
|
|
86
|
+
return {
|
|
87
|
+
extraFingerprintPaths: resolveExtraFingerprintPathChanges(before.extraFingerprintPaths, after.extraFingerprintPaths),
|
|
88
|
+
packages: resolvePackageChanges(before.packages, after.packages),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Pairs the versions in order: a pair moved, the rest was added or removed.
|
|
92
|
+
function resolvePackageChanges(before, after) {
|
|
93
|
+
const names = [...new Set([...before, ...after].map(({ name }) => name))];
|
|
94
|
+
return names.sort().flatMap(name => {
|
|
95
|
+
const beforeVersions = resolveVersionsOnlyIn(before, after, name);
|
|
96
|
+
const afterVersions = resolveVersionsOnlyIn(after, before, name);
|
|
97
|
+
return Array.from({ length: Math.max(beforeVersions.length, afterVersions.length) }, (_, index) => ({
|
|
98
|
+
after: afterVersions[index] ?? null,
|
|
99
|
+
before: beforeVersions[index] ?? null,
|
|
100
|
+
name,
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function resolveVersionsOnlyIn(packages, otherPackages, name) {
|
|
105
|
+
return packages
|
|
106
|
+
.filter(locked => locked.name === name &&
|
|
107
|
+
!otherPackages.some(other => other.name === name &&
|
|
108
|
+
other.version === locked.version &&
|
|
109
|
+
other.integrity === locked.integrity))
|
|
110
|
+
.map(({ integrity, version }) => ({ integrity, version }));
|
|
111
|
+
}
|
|
112
|
+
function resolveExtraFingerprintPathChanges(before, after) {
|
|
113
|
+
const beforeHashes = new Map(before.map(({ path, sha256 }) => [path, sha256]));
|
|
114
|
+
const afterHashes = new Map(after.map(({ path, sha256 }) => [path, sha256]));
|
|
115
|
+
const paths = [...new Set([...beforeHashes.keys(), ...afterHashes.keys()])];
|
|
116
|
+
return paths
|
|
117
|
+
.sort()
|
|
118
|
+
.map(path => ({
|
|
119
|
+
after: afterHashes.get(path) ?? null,
|
|
120
|
+
before: beforeHashes.get(path) ?? null,
|
|
121
|
+
path,
|
|
122
|
+
}))
|
|
123
|
+
.filter(({ after, before }) => after !== before);
|
|
124
|
+
}
|
|
125
|
+
// Shows the integrity where the version did not move, so a rebuild under the same version shows.
|
|
126
|
+
function resolveInstalledVersionText(installedVersion, otherInstalledVersion) {
|
|
127
|
+
if (installedVersion === null) {
|
|
128
|
+
return 'none';
|
|
129
|
+
}
|
|
130
|
+
return installedVersion.version === otherInstalledVersion?.version
|
|
131
|
+
? (installedVersion.integrity ?? 'no integrity')
|
|
132
|
+
: installedVersion.version;
|
|
133
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { computeFingerprint } from '@hotcodepush/protocol/fingerprint';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { readProjectFingerprintContributors } from '../utils/fingerprint.js';
|
|
4
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
5
|
+
import { printJson, printTable } from '../utils/output.js';
|
|
6
|
+
import { locateProjectConfig } from '../utils/project-config.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: "Print the project's native fingerprint with the packages and the files under the extra fingerprint paths that contribute to it; local, no login.",
|
|
9
|
+
examples: [
|
|
10
|
+
'hotcodepush fingerprint',
|
|
11
|
+
'hotcodepush fingerprint --json > fingerprint.json',
|
|
12
|
+
],
|
|
13
|
+
options: defineCommandOptions({}),
|
|
14
|
+
action: async (options) => {
|
|
15
|
+
const { directoryPath, projectConfig } = locateProjectConfig(options.config);
|
|
16
|
+
const contributors = await readProjectFingerprintContributors(directoryPath, projectConfig?.extraFingerprintPaths ?? []);
|
|
17
|
+
const fingerprint = computeFingerprint(contributors);
|
|
18
|
+
if (options.json) {
|
|
19
|
+
printJson({ fingerprint, ...contributors });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`Fingerprint ${fingerprint}`);
|
|
23
|
+
printTable({
|
|
24
|
+
emptyText: 'No installed package ships native code.',
|
|
25
|
+
headers: ['PACKAGE', 'VERSION'],
|
|
26
|
+
nextOffset: null,
|
|
27
|
+
rows: contributors.packages.map(({ name, version }) => [name, version]),
|
|
28
|
+
});
|
|
29
|
+
if (contributors.extraFingerprintPaths.length > 0) {
|
|
30
|
+
printTable({
|
|
31
|
+
emptyText: '',
|
|
32
|
+
headers: ['EXTRA FINGERPRINT PATH', 'SHA-256'],
|
|
33
|
+
nextOffset: null,
|
|
34
|
+
rows: contributors.extraFingerprintPaths.map(({ path, sha256 }) => [
|
|
35
|
+
path,
|
|
36
|
+
sha256,
|
|
37
|
+
]),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|