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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- 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 +68 -7
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { HotCodePushError } from '@hotcodepush/node';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { defineCommand } from 'zodline';
|
|
6
|
+
import { DOCS_URL, PROJECT_CONFIG_FILE_NAME, SIGNING_PRIVATE_KEY_FILE_NAME, } from '../config/consts.js';
|
|
7
|
+
import { createApiClient } from '../utils/api-client.js';
|
|
8
|
+
import { fetchCurrentUser, isUnauthenticatedError, resolveCredentialText, } from '../utils/credential.js';
|
|
9
|
+
import { isInteractive } from '../utils/environment.js';
|
|
10
|
+
import { ConfirmationRequiredError, InvalidParameterError, MissingParameterError, NotLoggedInError, ReportedFailureError, UnexpectedError, } from '../utils/errors.js';
|
|
11
|
+
import { detectFramework } from '../utils/framework.js';
|
|
12
|
+
import { resolveFrameworkModule } from '../utils/frameworks/index.js';
|
|
13
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
14
|
+
import { resolveConsoleBaseUrl } from '../utils/hosts.js';
|
|
15
|
+
import { InitRun, resolveStepRows } from '../utils/init-steps.js';
|
|
16
|
+
import { stringifyLikeSource } from '../utils/json-file.js';
|
|
17
|
+
import { printOutcomeRows } from '../utils/outcome.js';
|
|
18
|
+
import { printJson } from '../utils/output.js';
|
|
19
|
+
import { readPackageJson } from '../utils/package-json.js';
|
|
20
|
+
import { resolveCommandLineText, resolvePackageManager, resolveRunScriptCommandLine, runCommandLineVisibly, } from '../utils/package-manager.js';
|
|
21
|
+
import { ID_SCHEMA, locateProjectConfig, resolveProjectChannel, } from '../utils/project-config.js';
|
|
22
|
+
import { confirmConsequence, promptSelect, promptText, promptYesNo, } from '../utils/prompts.js';
|
|
23
|
+
import { fetchApps, fetchHasSigningKey, fetchOrganizations, } from '../utils/resource-resolution.js';
|
|
24
|
+
import { isEnvironmentToken, readToken } from '../utils/token-store.js';
|
|
25
|
+
import { readApiUrl } from '../utils/user-config.js';
|
|
26
|
+
import { logIn } from './login.js';
|
|
27
|
+
import releaseCreateCommand from './release/create.js';
|
|
28
|
+
import signingKeyCreateCommand from './signing-key/create.js';
|
|
29
|
+
const CREATE_CHOICE = 'create';
|
|
30
|
+
export default defineCommand({
|
|
31
|
+
description: 'Set a React Native, Expo, Capacitor or Cordova project up from sign-in to the first release, re-runnable at any step.',
|
|
32
|
+
examples: ['hotcodepush init', 'hotcodepush init --yes --json'],
|
|
33
|
+
options: defineCommandOptions({
|
|
34
|
+
androidPath: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("The Android project, when it is not capacitor.config's android.path or android/."),
|
|
38
|
+
framework: z
|
|
39
|
+
.enum(['react_native', 'expo', 'capacitor', 'cordova'])
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('The framework, react_native, expo, capacitor or cordova, over the one detected from package.json.'),
|
|
42
|
+
iosPath: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("The iOS project, when it is not capacitor.config's ios.path or ios/."),
|
|
46
|
+
xcodeTarget: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('The app target that gets the build step phase, when the iOS project has several.'),
|
|
50
|
+
}),
|
|
51
|
+
action: async (options) => {
|
|
52
|
+
const projectConfigLocation = locateProjectConfig(options.config);
|
|
53
|
+
const { directoryPath, projectConfig } = projectConfigLocation;
|
|
54
|
+
const framework = options.framework ?? detectFramework(directoryPath);
|
|
55
|
+
const frameworkModule = resolveFrameworkModule(framework);
|
|
56
|
+
const run = new InitRun();
|
|
57
|
+
const hotCodePush = await run.run('sign_in', () => signIn(options));
|
|
58
|
+
const scope = await run.run('organization', () => resolveOrganization(requireValue(hotCodePush), projectConfig, options), { dependsOn: ['sign_in'] });
|
|
59
|
+
const app = await run.run('app', () => resolveApp(requireValue(hotCodePush), requireValue(scope), projectConfigLocation, framework, options), { dependsOn: ['organization'] });
|
|
60
|
+
const projectFiles = await resolveProjectFiles(projectConfigLocation, frameworkModule, options);
|
|
61
|
+
const editBlocker = await resolveEditBlocker(projectFiles, app, options);
|
|
62
|
+
await run.run('package', () => installPackage(projectFiles, editBlocker));
|
|
63
|
+
await run.run('configuration', () => writeConfiguration(requireValue(hotCodePush), projectFiles, requireValue(app), editBlocker), { dependsOn: ['app'] });
|
|
64
|
+
await run.run('hook', () => projectFiles.wiring.wireBinaryCreateStep(editBlocker));
|
|
65
|
+
const privateKeyPath = await run.run('signing_key', () => createSigningKey(app, projectFiles, options));
|
|
66
|
+
const isReleaseWanted = run.stoppedCode === undefined &&
|
|
67
|
+
(await promptYesNo('Publish a first release now?', options));
|
|
68
|
+
const isBuilt = await run.run('build', () => buildProject(projectFiles, isReleaseWanted));
|
|
69
|
+
const isReleased = await run.run('release', () => releaseFirst(projectFiles, isReleaseWanted, isBuilt === true, privateKeyPath, options), { dependsOn: ['configuration', 'build'] });
|
|
70
|
+
const status = run.stoppedCode === undefined ? 'complete' : 'incomplete';
|
|
71
|
+
if (options.json) {
|
|
72
|
+
printJson({ status, steps: run.steps });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// A blank line separates the rows from the package manager and build output above them.
|
|
76
|
+
console.log('');
|
|
77
|
+
printOutcomeRows(resolveStepRows(run.steps));
|
|
78
|
+
if (app !== undefined && status === 'complete') {
|
|
79
|
+
printFinish(app, isReleased === true, privateKeyPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (run.stoppedCode !== undefined) {
|
|
83
|
+
throw new ReportedFailureError(run.stoppedCode, run.stoppedStep);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// A non-interactive login leaves the device code for the next run.
|
|
88
|
+
async function signIn(options) {
|
|
89
|
+
if (readToken() !== undefined) {
|
|
90
|
+
try {
|
|
91
|
+
return {
|
|
92
|
+
message: resolveCredentialText(await fetchCurrentUser(), isEnvironmentToken()),
|
|
93
|
+
status: 'skipped',
|
|
94
|
+
value: createApiClient(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!isUnauthenticatedError(error) &&
|
|
99
|
+
!(error instanceof NotLoggedInError)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const user = await logIn(options);
|
|
105
|
+
return {
|
|
106
|
+
message: `Logged in as ${user.name} (${user.email}).`,
|
|
107
|
+
status: 'done',
|
|
108
|
+
value: createApiClient(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Without `--organization`, a person picks or creates one. Non-interactively the only one is used.
|
|
112
|
+
async function resolveOrganization(hotCodePush, projectConfig, options) {
|
|
113
|
+
const app = options.app === undefined && projectConfig?.appId !== undefined
|
|
114
|
+
? await fetchExistingApp(hotCodePush, projectConfig.appId)
|
|
115
|
+
: undefined;
|
|
116
|
+
if (app !== undefined) {
|
|
117
|
+
const organization = await hotCodePush.organizations.get({
|
|
118
|
+
organizationId: app.organizationId,
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
message: `Used organization ${organization.name}, the one of ${PROJECT_CONFIG_FILE_NAME}'s app.`,
|
|
122
|
+
status: 'done',
|
|
123
|
+
value: { app, organization },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const organizations = await fetchOrganizations(hotCodePush);
|
|
127
|
+
if (options.organization !== undefined) {
|
|
128
|
+
const named = findNamed(organizations, options.organization);
|
|
129
|
+
if (named !== undefined) {
|
|
130
|
+
return resolveUsedScope(named);
|
|
131
|
+
}
|
|
132
|
+
assertName('--organization', options.organization);
|
|
133
|
+
await confirmCreation('organization', options.organization, options);
|
|
134
|
+
return resolveCreatedScope(await hotCodePush.organizations.create({ name: options.organization }));
|
|
135
|
+
}
|
|
136
|
+
const [onlyOrganization] = organizations;
|
|
137
|
+
if (!isInteractive(options) &&
|
|
138
|
+
onlyOrganization !== undefined &&
|
|
139
|
+
organizations.length === 1) {
|
|
140
|
+
return resolveUsedScope(onlyOrganization);
|
|
141
|
+
}
|
|
142
|
+
const choice = await promptCreateOrPick('organization', organizations, options);
|
|
143
|
+
if (choice !== CREATE_CHOICE) {
|
|
144
|
+
return resolveUsedScope(requireById(organizations, choice));
|
|
145
|
+
}
|
|
146
|
+
const name = await promptText('--organization', 'What is the new organization called?', options);
|
|
147
|
+
return resolveCreatedScope(await hotCodePush.organizations.create({ name }));
|
|
148
|
+
}
|
|
149
|
+
// An app in the file that the API lacks is refused non-interactively. A person picks or creates another.
|
|
150
|
+
async function resolveApp(hotCodePush, { app, organization }, { filePath, projectConfig }, framework, options) {
|
|
151
|
+
if (app !== undefined) {
|
|
152
|
+
return {
|
|
153
|
+
message: `Used app ${app.name}, as ${PROJECT_CONFIG_FILE_NAME} names it.`,
|
|
154
|
+
status: 'done',
|
|
155
|
+
value: app,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (options.app === undefined &&
|
|
159
|
+
projectConfig?.appId !== undefined &&
|
|
160
|
+
!isInteractive(options)) {
|
|
161
|
+
throw new InvalidParameterError(`The app ${projectConfig.appId} in ${filePath ?? PROJECT_CONFIG_FILE_NAME} does not exist.`, `Pass --app with the app's id or name, or delete ${PROJECT_CONFIG_FILE_NAME} to create a new app.`);
|
|
162
|
+
}
|
|
163
|
+
const apps = await fetchApps(hotCodePush, organization.id);
|
|
164
|
+
if (options.app !== undefined) {
|
|
165
|
+
const named = findNamed(apps, options.app);
|
|
166
|
+
if (named !== undefined) {
|
|
167
|
+
return resolveUsed('app', named);
|
|
168
|
+
}
|
|
169
|
+
assertName('--app', options.app);
|
|
170
|
+
await confirmCreation('app', options.app, options);
|
|
171
|
+
return resolveCreated('app', await hotCodePush.organizations.apps.create({
|
|
172
|
+
framework,
|
|
173
|
+
name: options.app,
|
|
174
|
+
organizationId: organization.id,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
const [onlyApp] = apps;
|
|
178
|
+
if (!isInteractive(options) && onlyApp !== undefined && apps.length === 1) {
|
|
179
|
+
return resolveUsed('app', onlyApp);
|
|
180
|
+
}
|
|
181
|
+
const choice = await promptCreateOrPick('app', apps, options);
|
|
182
|
+
if (choice !== CREATE_CHOICE) {
|
|
183
|
+
return resolveUsed('app', requireById(apps, choice));
|
|
184
|
+
}
|
|
185
|
+
const name = await promptText('--app', 'What is the app called?', options);
|
|
186
|
+
return resolveCreated('app', await hotCodePush.organizations.apps.create({
|
|
187
|
+
framework,
|
|
188
|
+
name,
|
|
189
|
+
organizationId: organization.id,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
async function resolveProjectFiles({ directoryPath, filePath, projectConfig }, framework, options) {
|
|
193
|
+
const packageJson = readPackageJson(directoryPath);
|
|
194
|
+
return {
|
|
195
|
+
configFilePath: filePath ?? join(directoryPath, PROJECT_CONFIG_FILE_NAME),
|
|
196
|
+
directoryPath,
|
|
197
|
+
framework,
|
|
198
|
+
packageJson,
|
|
199
|
+
projectConfig,
|
|
200
|
+
wiring: await framework.resolveWiring({ directoryPath, packageJson }, options),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Declined, or with nobody to ask, each editing step stops with that reason.
|
|
204
|
+
async function resolveEditBlocker(projectFiles, app, options) {
|
|
205
|
+
const filePaths = resolveFilesToChange(projectFiles, app);
|
|
206
|
+
if (filePaths.length === 0) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const consequence = `changes ${filePaths.join(', ')}`;
|
|
210
|
+
const manualStep = `Run "hotcodepush init --yes" to change ${filePaths.join(', ')}.`;
|
|
211
|
+
try {
|
|
212
|
+
return (await confirmConsequence(consequence, options))
|
|
213
|
+
? undefined
|
|
214
|
+
: new ConfirmationRequiredError(consequence, manualStep);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error instanceof ConfirmationRequiredError) {
|
|
218
|
+
return new ConfirmationRequiredError(consequence, manualStep);
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Without an app the configuration step waits, so `hotcodepush.json` is not among the files.
|
|
224
|
+
function resolveFilesToChange({ configFilePath, projectConfig, wiring }, app) {
|
|
225
|
+
return [
|
|
226
|
+
...wiring.packageFilePaths,
|
|
227
|
+
...(app === undefined || isConfigurationComplete(projectConfig, app)
|
|
228
|
+
? []
|
|
229
|
+
: [basename(configFilePath)]),
|
|
230
|
+
...wiring.nativeFilePaths,
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
function installPackage({ framework, wiring }, editBlocker) {
|
|
234
|
+
if (wiring.isPackageInstalled) {
|
|
235
|
+
return Promise.resolve({
|
|
236
|
+
message: `The package ${framework.packageName} is already installed.`,
|
|
237
|
+
status: 'skipped',
|
|
238
|
+
value: undefined,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (editBlocker !== undefined) {
|
|
242
|
+
throw editBlocker;
|
|
243
|
+
}
|
|
244
|
+
return Promise.resolve({
|
|
245
|
+
message: wiring.installPackage(),
|
|
246
|
+
status: 'done',
|
|
247
|
+
value: undefined,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function writeConfiguration(hotCodePush, { configFilePath, projectConfig }, app, editBlocker) {
|
|
251
|
+
if (isConfigurationComplete(projectConfig, app)) {
|
|
252
|
+
return {
|
|
253
|
+
message: `The file ${basename(configFilePath)} is already present.`,
|
|
254
|
+
status: 'skipped',
|
|
255
|
+
value: undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (editBlocker !== undefined) {
|
|
259
|
+
throw editBlocker;
|
|
260
|
+
}
|
|
261
|
+
const sourceText = existsSync(configFilePath)
|
|
262
|
+
? readFileSync(configFilePath, 'utf8')
|
|
263
|
+
: '';
|
|
264
|
+
writeFileSync(configFilePath, stringifyLikeSource({
|
|
265
|
+
...projectConfig,
|
|
266
|
+
appId: app.id,
|
|
267
|
+
...(projectConfig?.channel !== undefined
|
|
268
|
+
? {}
|
|
269
|
+
: { channel: await fetchDefaultChannelName(hotCodePush, app) }),
|
|
270
|
+
}, sourceText || '{}\n'));
|
|
271
|
+
return {
|
|
272
|
+
message: resolveConfigurationMessage(basename(configFilePath), projectConfig, app),
|
|
273
|
+
status: 'done',
|
|
274
|
+
value: undefined,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveConfigurationMessage(fileName, projectConfig, app) {
|
|
278
|
+
if (projectConfig === undefined) {
|
|
279
|
+
return `Wrote ${fileName}.`;
|
|
280
|
+
}
|
|
281
|
+
if (projectConfig.appId !== undefined && projectConfig.appId !== app.id) {
|
|
282
|
+
return `Set the app in ${fileName} to ${app.name}.`;
|
|
283
|
+
}
|
|
284
|
+
return `Completed ${fileName}.`;
|
|
285
|
+
}
|
|
286
|
+
async function fetchDefaultChannelName(hotCodePush, app) {
|
|
287
|
+
if (app.defaultChannelId === null) {
|
|
288
|
+
return resolveProjectChannel({});
|
|
289
|
+
}
|
|
290
|
+
const defaultChannel = await hotCodePush.apps.channels.get({
|
|
291
|
+
appId: app.id,
|
|
292
|
+
channelId: app.defaultChannelId,
|
|
293
|
+
});
|
|
294
|
+
return defaultChannel.name;
|
|
295
|
+
}
|
|
296
|
+
// Nobody to ask points at the command instead, since a key file written on a runner is lost with it.
|
|
297
|
+
async function createSigningKey(app, { configFilePath }, options) {
|
|
298
|
+
if (app !== undefined &&
|
|
299
|
+
(await fetchHasSigningKey(createApiClient(), app.id))) {
|
|
300
|
+
return {
|
|
301
|
+
message: 'The app already has a signing key.',
|
|
302
|
+
status: 'skipped',
|
|
303
|
+
value: undefined,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (app === undefined ||
|
|
307
|
+
!(await promptYesNo('Generate a signing key pair?', options))) {
|
|
308
|
+
return {
|
|
309
|
+
message: 'Run "hotcodepush signing-key create" to enable code signing.',
|
|
310
|
+
status: 'skipped',
|
|
311
|
+
value: undefined,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const privateKeyPath = SIGNING_PRIVATE_KEY_FILE_NAME;
|
|
315
|
+
await signingKeyCreateCommand.action({
|
|
316
|
+
app: app.id,
|
|
317
|
+
config: existsSync(configFilePath) ? configFilePath : undefined,
|
|
318
|
+
json: options.json,
|
|
319
|
+
privateKeyPath,
|
|
320
|
+
}, undefined);
|
|
321
|
+
return {
|
|
322
|
+
message: 'Generated a signing key pair, its private key in the file named above.',
|
|
323
|
+
status: 'done',
|
|
324
|
+
value: privateKeyPath,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function buildProject({ directoryPath, framework, packageJson }, isReleaseWanted) {
|
|
328
|
+
if (framework.packageBundles !== undefined) {
|
|
329
|
+
return Promise.resolve({
|
|
330
|
+
message: 'The command "hotcodepush release create" packages the bundles itself.',
|
|
331
|
+
status: 'skipped',
|
|
332
|
+
value: false,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const buildCommandLine = resolveBuildCommandLine(directoryPath, packageJson);
|
|
336
|
+
if (buildCommandLine === undefined) {
|
|
337
|
+
return Promise.resolve({
|
|
338
|
+
message: 'There is no build script in package.json.',
|
|
339
|
+
status: 'skipped',
|
|
340
|
+
value: false,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (!isReleaseWanted) {
|
|
344
|
+
return Promise.resolve({
|
|
345
|
+
message: 'No release follows.',
|
|
346
|
+
status: 'skipped',
|
|
347
|
+
value: false,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
runCommandLineVisibly(buildCommandLine, directoryPath);
|
|
351
|
+
return Promise.resolve({
|
|
352
|
+
message: `Ran "${resolveCommandLineText(buildCommandLine)}".`,
|
|
353
|
+
status: 'done',
|
|
354
|
+
value: true,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async function releaseFirst({ configFilePath, directoryPath, framework, packageJson }, isReleaseWanted, isBuilt, privateKeyPath, options) {
|
|
358
|
+
const releaseCommandText = resolveReleaseCommandText(privateKeyPath);
|
|
359
|
+
if (!isReleaseWanted) {
|
|
360
|
+
const buildCommandLine = framework.packageBundles === undefined
|
|
361
|
+
? resolveBuildCommandLine(directoryPath, packageJson)
|
|
362
|
+
: undefined;
|
|
363
|
+
return {
|
|
364
|
+
message: buildCommandLine === undefined
|
|
365
|
+
? `Run "${releaseCommandText}" to publish the first release.`
|
|
366
|
+
: `Run "${resolveCommandLineText(buildCommandLine)}", then "${releaseCommandText}" to publish the first release.`,
|
|
367
|
+
status: 'skipped',
|
|
368
|
+
value: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const buildDirectory = framework.readBuildDirectory(directoryPath);
|
|
372
|
+
if (framework.packageBundles === undefined &&
|
|
373
|
+
!isBuilt &&
|
|
374
|
+
'path' in buildDirectory &&
|
|
375
|
+
!existsSync(join(directoryPath, buildDirectory.path))) {
|
|
376
|
+
const buildCommandLine = resolveBuildCommandLine(directoryPath, packageJson);
|
|
377
|
+
return {
|
|
378
|
+
message: `There is ${buildCommandLine === undefined ? 'no build script and ' : ''}no output at ${buildDirectory.path}. Build, then run "${releaseCommandText}".`,
|
|
379
|
+
status: 'skipped',
|
|
380
|
+
value: false,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
await releaseCreateCommand.action({
|
|
384
|
+
config: configFilePath,
|
|
385
|
+
json: options.json,
|
|
386
|
+
privateKeyPath,
|
|
387
|
+
yes: true,
|
|
388
|
+
}, undefined);
|
|
389
|
+
return {
|
|
390
|
+
message: 'Released to production.',
|
|
391
|
+
status: 'done',
|
|
392
|
+
value: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// A skipped release row already names the release command, so it is repeated only after a release.
|
|
396
|
+
function printFinish(app, isReleased, privateKeyPath) {
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log(isReleased
|
|
399
|
+
? `Build the app natively once and run it; change a line, run the build and "${resolveReleaseCommandText(privateKeyPath)}", then reopen the app.`
|
|
400
|
+
: 'Build the app natively once and run it, then publish the first release as shown above.');
|
|
401
|
+
console.log(`Console: ${resolveConsoleBaseUrl(readApiUrl())}/apps/${app.id}`);
|
|
402
|
+
console.log(`Docs: ${DOCS_URL}`);
|
|
403
|
+
}
|
|
404
|
+
function assertName(flag, reference) {
|
|
405
|
+
if (ID_SCHEMA.safeParse(reference).success) {
|
|
406
|
+
throw new InvalidParameterError(`Nothing has the id ${reference} from ${flag}.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function confirmCreation(noun, name, options) {
|
|
410
|
+
const isConfirmed = await confirmConsequence(`creates the ${noun} ${name}, which does not exist yet`, options);
|
|
411
|
+
if (!isConfirmed) {
|
|
412
|
+
throw new MissingParameterError(`--${noun}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Such as a copied project, a cloned demo or a reset environment.
|
|
416
|
+
async function fetchExistingApp(hotCodePush, appId) {
|
|
417
|
+
try {
|
|
418
|
+
return await hotCodePush.apps.get({ appId });
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (error instanceof HotCodePushError && error.code === 'not_found') {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function findNamed(resources, reference) {
|
|
428
|
+
return ID_SCHEMA.safeParse(reference).success
|
|
429
|
+
? resources.find(({ id }) => id === reference)
|
|
430
|
+
: resources.find(({ name }) => name.toLowerCase() === reference.toLowerCase());
|
|
431
|
+
}
|
|
432
|
+
function isConfigurationComplete(projectConfig, app) {
|
|
433
|
+
return projectConfig?.appId === app.id && projectConfig.channel !== undefined;
|
|
434
|
+
}
|
|
435
|
+
function promptCreateOrPick(noun, resources, options) {
|
|
436
|
+
return promptSelect(`--${noun}`, `Which ${noun}?`, [
|
|
437
|
+
...resources.map(({ id, name }) => ({ label: name, value: id })),
|
|
438
|
+
{ label: `Create a new ${noun}`, value: CREATE_CHOICE },
|
|
439
|
+
], options);
|
|
440
|
+
}
|
|
441
|
+
function requireById(resources, id) {
|
|
442
|
+
const resource = resources.find(candidate => candidate.id === id);
|
|
443
|
+
if (resource === undefined) {
|
|
444
|
+
throw new InvalidParameterError(`Nothing has the id ${id}.`);
|
|
445
|
+
}
|
|
446
|
+
return resource;
|
|
447
|
+
}
|
|
448
|
+
// A stopped step skips the rest, so the run never gets here without the value.
|
|
449
|
+
function requireValue(value) {
|
|
450
|
+
if (value === undefined) {
|
|
451
|
+
throw new UnexpectedError(new Error('An earlier step did not run.'));
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function resolveBuildCommandLine(directoryPath, packageJson) {
|
|
456
|
+
return packageJson.scripts?.build === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: resolveRunScriptCommandLine(resolvePackageManager(directoryPath), 'build');
|
|
459
|
+
}
|
|
460
|
+
function resolveReleaseCommandText(privateKeyPath) {
|
|
461
|
+
return privateKeyPath === undefined
|
|
462
|
+
? 'hotcodepush release create'
|
|
463
|
+
: `hotcodepush release create --private-key-path ${privateKeyPath}`;
|
|
464
|
+
}
|
|
465
|
+
function resolveCreatedScope(organization) {
|
|
466
|
+
return {
|
|
467
|
+
message: `Created organization ${organization.name}.`,
|
|
468
|
+
status: 'done',
|
|
469
|
+
value: { app: undefined, organization },
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function resolveUsedScope(organization) {
|
|
473
|
+
return {
|
|
474
|
+
message: `Used organization ${organization.name}.`,
|
|
475
|
+
status: 'done',
|
|
476
|
+
value: { app: undefined, organization },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function resolveCreated(noun, resource) {
|
|
480
|
+
return {
|
|
481
|
+
message: `Created ${noun} ${resource.name}.`,
|
|
482
|
+
status: 'done',
|
|
483
|
+
value: resource,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function resolveUsed(noun, resource) {
|
|
487
|
+
return {
|
|
488
|
+
message: `Used ${noun} ${resource.name}.`,
|
|
489
|
+
status: 'done',
|
|
490
|
+
value: resource,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
4
|
+
import { isInteractive } from '../../utils/environment.js';
|
|
5
|
+
import { MissingParameterError } from '../../utils/errors.js';
|
|
6
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
7
|
+
import { printJson } from '../../utils/output.js';
|
|
8
|
+
import { promptSelect, promptText } from '../../utils/prompts.js';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Accept an invitation to an organization with the token from its mail.',
|
|
11
|
+
examples: [
|
|
12
|
+
'hotcodepush invitation accept',
|
|
13
|
+
'hotcodepush invitation accept --invitation 4c8e2a6f-1d3b-4e97-a5c0-8f2d6b4a1e93 --token p8Xc2LmQ4rTzN7vB --json',
|
|
14
|
+
],
|
|
15
|
+
options: defineCommandOptions({
|
|
16
|
+
invitation: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('The invitation, by the id its mail carries.'),
|
|
20
|
+
token: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('The token the invitation mail carries.'),
|
|
24
|
+
}),
|
|
25
|
+
action: async (options) => {
|
|
26
|
+
const hotCodePush = createApiClient();
|
|
27
|
+
const invitationId = options.invitation ?? (await promptInvitationId(hotCodePush, options));
|
|
28
|
+
const token = options.token ??
|
|
29
|
+
(await promptText('--token', 'What is the token from the invitation mail?', options));
|
|
30
|
+
const acceptedMember = await hotCodePush.invitations.accept({
|
|
31
|
+
invitationId,
|
|
32
|
+
token,
|
|
33
|
+
});
|
|
34
|
+
if (options.json) {
|
|
35
|
+
printJson(acceptedMember);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(`Joined organization ${acceptedMember.organizationId} with role ${acceptedMember.role}.`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
async function promptInvitationId(hotCodePush, options) {
|
|
43
|
+
if (!isInteractive(options)) {
|
|
44
|
+
throw new MissingParameterError('--invitation');
|
|
45
|
+
}
|
|
46
|
+
const fetchedInvitations = await hotCodePush.invitations.list({
|
|
47
|
+
relations: ['organization'],
|
|
48
|
+
});
|
|
49
|
+
return promptSelect('--invitation', 'Which invitation?', fetchedInvitations.map(({ id, organization, organizationId, role }) => ({
|
|
50
|
+
label: `${organization?.name ?? organizationId} with role ${role}`,
|
|
51
|
+
value: id,
|
|
52
|
+
})), options);
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
4
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
5
|
+
import { printJson } from '../../utils/output.js';
|
|
6
|
+
import { confirmConsequence, promptSelect, promptText, } from '../../utils/prompts.js';
|
|
7
|
+
import { fetchOrganizationId } from '../../utils/resource-resolution.js';
|
|
8
|
+
const INVITATION_ROLES = [
|
|
9
|
+
'admin',
|
|
10
|
+
'billing',
|
|
11
|
+
'member',
|
|
12
|
+
];
|
|
13
|
+
export default defineCommand({
|
|
14
|
+
description: 'Invite an address to the organization with a role; the invitation mail is valid for two days.',
|
|
15
|
+
examples: [
|
|
16
|
+
'hotcodepush invitation create --email anna@example.com --role member',
|
|
17
|
+
'hotcodepush invitation create --organization Acme --email anna@example.com --role admin --yes --json',
|
|
18
|
+
],
|
|
19
|
+
options: defineCommandOptions({
|
|
20
|
+
email: z.string().optional().describe('The address to invite.'),
|
|
21
|
+
role: z
|
|
22
|
+
.enum(INVITATION_ROLES)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('The role the invitee gets: admin, billing or member.'),
|
|
25
|
+
}),
|
|
26
|
+
action: async (options) => {
|
|
27
|
+
const hotCodePush = createApiClient();
|
|
28
|
+
const organizationId = await fetchOrganizationId(hotCodePush, options);
|
|
29
|
+
const email = options.email ??
|
|
30
|
+
(await promptText('--email', 'Which address do you invite?', options));
|
|
31
|
+
const role = options.role ??
|
|
32
|
+
(await promptSelect('--role', 'Which role does the invitee get?', INVITATION_ROLES.map(role => ({ label: role, value: role })), options));
|
|
33
|
+
const fetchedOrganization = await hotCodePush.organizations.get({
|
|
34
|
+
organizationId,
|
|
35
|
+
});
|
|
36
|
+
const isConfirmed = await confirmConsequence(`invites ${email} to organization ${fetchedOrganization.name} with role ${role}`, options);
|
|
37
|
+
if (!isConfirmed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const createdInvitation = await hotCodePush.organizations.invitations.create({
|
|
41
|
+
email,
|
|
42
|
+
organizationId,
|
|
43
|
+
role,
|
|
44
|
+
});
|
|
45
|
+
if (options.json) {
|
|
46
|
+
printJson(createdInvitation);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`Invited ${createdInvitation.email} (${createdInvitation.id}) to organization ${fetchedOrganization.name} with role ${createdInvitation.role}.`);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
4
|
+
import { InvalidParameterError } from '../../utils/errors.js';
|
|
5
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
6
|
+
import { printJson } from '../../utils/output.js';
|
|
7
|
+
import { fetchAllPages } from '../../utils/pagination.js';
|
|
8
|
+
import { confirmConsequence, promptSelect } from '../../utils/prompts.js';
|
|
9
|
+
import { fetchOrganizationId } from '../../utils/resource-resolution.js';
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: 'Withdraw a pending invitation; the link in its mail stops working at once.',
|
|
12
|
+
examples: [
|
|
13
|
+
'hotcodepush invitation delete --invitation anna@example.com',
|
|
14
|
+
'hotcodepush invitation delete --invitation 4c8e2a6f-1d3b-4e97-a5c0-8f2d6b4a1e93 --yes --json',
|
|
15
|
+
],
|
|
16
|
+
options: defineCommandOptions({
|
|
17
|
+
invitation: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('The pending invitation, by id or email.'),
|
|
21
|
+
}),
|
|
22
|
+
action: async (options) => {
|
|
23
|
+
const hotCodePush = createApiClient();
|
|
24
|
+
const organizationId = await fetchOrganizationId(hotCodePush, options);
|
|
25
|
+
// Invitations have no read by id, and only a pending one is unique by its email.
|
|
26
|
+
const pendingInvitations = await fetchAllPages(page => hotCodePush.organizations.invitations.list({
|
|
27
|
+
organizationId,
|
|
28
|
+
status: 'pending',
|
|
29
|
+
...page,
|
|
30
|
+
}));
|
|
31
|
+
const invitation = resolveInvitation(pendingInvitations, options.invitation ??
|
|
32
|
+
(await promptSelect('--invitation', 'Which invitation?', pendingInvitations.map(({ email, id }) => ({
|
|
33
|
+
label: email,
|
|
34
|
+
value: id,
|
|
35
|
+
})), options)));
|
|
36
|
+
const isConfirmed = await confirmConsequence(`withdraws the invitation to ${invitation.email}: the link in its mail stops working`, options);
|
|
37
|
+
if (!isConfirmed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await hotCodePush.organizations.invitations.delete({
|
|
41
|
+
invitationId: invitation.id,
|
|
42
|
+
organizationId,
|
|
43
|
+
});
|
|
44
|
+
if (options.json) {
|
|
45
|
+
printJson({ id: invitation.id, name: invitation.email });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(`Withdrew the invitation to ${invitation.email} (${invitation.id}).`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
// The API stores emails lowercased.
|
|
53
|
+
function resolveInvitation(invitations, reference) {
|
|
54
|
+
const lowercasedReference = reference.toLowerCase();
|
|
55
|
+
const invitation = invitations.find(({ email, id }) => id === reference || email.toLowerCase() === lowercasedReference);
|
|
56
|
+
if (invitation === undefined) {
|
|
57
|
+
throw new InvalidParameterError(`The organization has no pending invitation ${reference} from --invitation.`, 'Run "hotcodepush invitation list" for the ids and emails.');
|
|
58
|
+
}
|
|
59
|
+
return invitation;
|
|
60
|
+
}
|