@aetherpush/cli 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 +112 -0
- package/bin/script/acquisition-sdk.js +178 -0
- package/bin/script/acquisition-sdk.js.map +1 -0
- package/bin/script/cli.js +20 -0
- package/bin/script/cli.js.map +1 -0
- package/bin/script/command-executor.js +1301 -0
- package/bin/script/command-executor.js.map +1 -0
- package/bin/script/command-parser.js +1103 -0
- package/bin/script/command-parser.js.map +1 -0
- package/bin/script/commands/debug.js +127 -0
- package/bin/script/commands/debug.js.map +1 -0
- package/bin/script/errors.js +16 -0
- package/bin/script/errors.js.map +1 -0
- package/bin/script/hash-utils.js +183 -0
- package/bin/script/hash-utils.js.map +1 -0
- package/bin/script/index.js +5 -0
- package/bin/script/index.js.map +1 -0
- package/bin/script/management-sdk.js +360 -0
- package/bin/script/management-sdk.js.map +1 -0
- package/bin/script/react-native-utils.js +264 -0
- package/bin/script/react-native-utils.js.map +1 -0
- package/bin/script/sign.js +74 -0
- package/bin/script/sign.js.map +1 -0
- package/bin/script/types/cli.js +39 -0
- package/bin/script/types/cli.js.map +1 -0
- package/bin/script/types/index.js +18 -0
- package/bin/script/types/index.js.map +1 -0
- package/bin/script/types/rest-definitions.js +3 -0
- package/bin/script/types/rest-definitions.js.map +1 -0
- package/bin/script/types.js +4 -0
- package/bin/script/types.js.map +1 -0
- package/bin/script/utils/file-utils.js +50 -0
- package/bin/script/utils/file-utils.js.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,1301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Aether. All rights reserved.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.runReactNativeBundleCommand = exports.releaseReact = exports.release = exports.deploymentList = exports.createEmptyTempReleaseFolder = exports.confirm = exports.execSync = exports.spawn = exports.sdk = exports.log = void 0;
|
|
5
|
+
exports.execute = execute;
|
|
6
|
+
const AccountManager = require("./management-sdk");
|
|
7
|
+
const childProcess = require("child_process");
|
|
8
|
+
const debug_1 = require("./commands/debug");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const chalk = require("chalk");
|
|
11
|
+
const g2js = require("gradle-to-js/lib/parser");
|
|
12
|
+
const moment = require("moment");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const plist = require("plist");
|
|
16
|
+
const prompt = require("prompt");
|
|
17
|
+
const rimraf = require("rimraf");
|
|
18
|
+
const semver = require("semver");
|
|
19
|
+
const Table = require("cli-table");
|
|
20
|
+
const wordwrap = require("wordwrap");
|
|
21
|
+
const cli = require("../script/types/cli");
|
|
22
|
+
const sign_1 = require("./sign");
|
|
23
|
+
const xcode = require("xcode");
|
|
24
|
+
const react_native_utils_1 = require("./react-native-utils");
|
|
25
|
+
const file_utils_1 = require("./utils/file-utils");
|
|
26
|
+
const configFilePath = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".aether", "config.json");
|
|
27
|
+
const DEFAULT_AETHER_SERVER_URL = "https://api-staging.aetherpush.com";
|
|
28
|
+
const emailValidator = require("email-validator");
|
|
29
|
+
const packageJson = require("../../package.json");
|
|
30
|
+
const properties = require("properties");
|
|
31
|
+
const CLI_HEADERS = {
|
|
32
|
+
"X-Aether-CLI-Version": packageJson.version,
|
|
33
|
+
};
|
|
34
|
+
const log = (message) => console.log(message);
|
|
35
|
+
exports.log = log;
|
|
36
|
+
exports.spawn = childProcess.spawn;
|
|
37
|
+
exports.execSync = childProcess.execSync;
|
|
38
|
+
let connectionInfo;
|
|
39
|
+
const confirm = (message = "Are you sure?") => {
|
|
40
|
+
message += " (y/N):";
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
prompt.message = "";
|
|
43
|
+
prompt.delimiter = "";
|
|
44
|
+
prompt.start();
|
|
45
|
+
prompt.get({
|
|
46
|
+
properties: {
|
|
47
|
+
response: {
|
|
48
|
+
description: chalk.cyan(message),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}, (err, result) => {
|
|
52
|
+
const accepted = result.response && result.response.toLowerCase() === "y";
|
|
53
|
+
const rejected = !result.response || result.response.toLowerCase() === "n";
|
|
54
|
+
if (accepted) {
|
|
55
|
+
resolve(true);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (!rejected) {
|
|
59
|
+
console.log('Invalid response: "' + result.response + '"');
|
|
60
|
+
}
|
|
61
|
+
resolve(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
exports.confirm = confirm;
|
|
67
|
+
function accessKeyAdd(command) {
|
|
68
|
+
return exports.sdk.addAccessKey(command.name, command.ttl).then((accessKey) => {
|
|
69
|
+
(0, exports.log)(`Successfully created the "${command.name}" access key: ${accessKey.name}`);
|
|
70
|
+
(0, exports.log)("Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function accessKeyPatch(command) {
|
|
74
|
+
const willUpdateName = isCommandOptionSpecified(command.newName) && command.oldName !== command.newName;
|
|
75
|
+
const willUpdateTtl = isCommandOptionSpecified(command.ttl);
|
|
76
|
+
if (!willUpdateName && !willUpdateTtl) {
|
|
77
|
+
throw new Error("A new name and/or TTL must be provided.");
|
|
78
|
+
}
|
|
79
|
+
return exports.sdk.patchAccessKey(command.oldName, command.newName, command.ttl).then((accessKey) => {
|
|
80
|
+
let logMessage = "Successfully ";
|
|
81
|
+
if (willUpdateName) {
|
|
82
|
+
logMessage += `renamed the access key "${command.oldName}" to "${command.newName}"`;
|
|
83
|
+
}
|
|
84
|
+
if (willUpdateTtl) {
|
|
85
|
+
const expirationDate = moment(accessKey.expires).format("LLLL");
|
|
86
|
+
if (willUpdateName) {
|
|
87
|
+
logMessage += ` and changed its expiration date to ${expirationDate}`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logMessage += `changed the expiration date of the "${command.oldName}" access key to ${expirationDate}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
(0, exports.log)(`${logMessage}.`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function accessKeyList(command) {
|
|
97
|
+
throwForInvalidOutputFormat(command.format);
|
|
98
|
+
return exports.sdk.getAccessKeys().then((accessKeys) => {
|
|
99
|
+
printAccessKeys(command.format, accessKeys);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function accessKeyRemove(command) {
|
|
103
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
104
|
+
if (wasConfirmed) {
|
|
105
|
+
return exports.sdk.removeAccessKey(command.accessKey).then(() => {
|
|
106
|
+
(0, exports.log)(`Successfully removed the "${command.accessKey}" access key.`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
(0, exports.log)("Access key removal cancelled.");
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function appAdd(command) {
|
|
113
|
+
return exports.sdk.addApp(command.appName).then((app) => {
|
|
114
|
+
(0, exports.log)('Successfully added the "' + command.appName + '" app, along with the following default deployments:');
|
|
115
|
+
const deploymentListCommand = {
|
|
116
|
+
type: cli.CommandType.deploymentList,
|
|
117
|
+
appName: app.name,
|
|
118
|
+
format: "table",
|
|
119
|
+
displayKeys: true,
|
|
120
|
+
};
|
|
121
|
+
return (0, exports.deploymentList)(deploymentListCommand, /*showPackage=*/ false);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function appList(command) {
|
|
125
|
+
throwForInvalidOutputFormat(command.format);
|
|
126
|
+
return exports.sdk.getApps().then((retrievedApps) => {
|
|
127
|
+
printAppList(command.format, retrievedApps);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function appRemove(command) {
|
|
131
|
+
return (0, exports.confirm)("Are you sure you want to remove this app? Note that its deployment keys will be PERMANENTLY unrecoverable.").then((wasConfirmed) => {
|
|
132
|
+
if (wasConfirmed) {
|
|
133
|
+
return exports.sdk.removeApp(command.appName).then(() => {
|
|
134
|
+
(0, exports.log)('Successfully removed the "' + command.appName + '" app.');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
(0, exports.log)("App removal cancelled.");
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function appRename(command) {
|
|
141
|
+
return exports.sdk.renameApp(command.currentAppName, command.newAppName).then(() => {
|
|
142
|
+
(0, exports.log)('Successfully renamed the "' + command.currentAppName + '" app to "' + command.newAppName + '".');
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const createEmptyTempReleaseFolder = (folderPath) => {
|
|
146
|
+
return deleteFolder(folderPath).then(() => {
|
|
147
|
+
fs.mkdirSync(folderPath);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
exports.createEmptyTempReleaseFolder = createEmptyTempReleaseFolder;
|
|
151
|
+
function appTransfer(command) {
|
|
152
|
+
throwForInvalidEmail(command.email);
|
|
153
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
154
|
+
if (wasConfirmed) {
|
|
155
|
+
return exports.sdk.transferApp(command.appName, command.email).then(() => {
|
|
156
|
+
(0, exports.log)('Successfully transferred the ownership of app "' + command.appName + '" to the account with email "' + command.email + '".');
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
(0, exports.log)("App transfer cancelled.");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function addCollaborator(command) {
|
|
163
|
+
throwForInvalidEmail(command.email);
|
|
164
|
+
return exports.sdk.addCollaborator(command.appName, command.email).then(() => {
|
|
165
|
+
(0, exports.log)('Successfully added "' + command.email + '" as a collaborator to the app "' + command.appName + '".');
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function listCollaborators(command) {
|
|
169
|
+
throwForInvalidOutputFormat(command.format);
|
|
170
|
+
return exports.sdk.getCollaborators(command.appName).then((retrievedCollaborators) => {
|
|
171
|
+
printCollaboratorsList(command.format, retrievedCollaborators);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function removeCollaborator(command) {
|
|
175
|
+
throwForInvalidEmail(command.email);
|
|
176
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
177
|
+
if (wasConfirmed) {
|
|
178
|
+
return exports.sdk.removeCollaborator(command.appName, command.email).then(() => {
|
|
179
|
+
(0, exports.log)('Successfully removed "' + command.email + '" as a collaborator from the app "' + command.appName + '".');
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
(0, exports.log)("App collaborator removal cancelled.");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function deleteConnectionInfoCache(printMessage = true) {
|
|
186
|
+
try {
|
|
187
|
+
fs.unlinkSync(configFilePath);
|
|
188
|
+
if (printMessage) {
|
|
189
|
+
(0, exports.log)(`Logged out. The session file at ${chalk.cyan(configFilePath)} has been deleted.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (ex) { }
|
|
193
|
+
}
|
|
194
|
+
function deleteFolder(folderPath) {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
rimraf(folderPath, (err) => {
|
|
197
|
+
if (err) {
|
|
198
|
+
reject(err);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
resolve();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function deploymentAdd(command) {
|
|
207
|
+
return exports.sdk.addDeployment(command.appName, command.deploymentName, command.key).then((deployment) => {
|
|
208
|
+
(0, exports.log)('Successfully added the "' +
|
|
209
|
+
command.deploymentName +
|
|
210
|
+
'" deployment with key "' +
|
|
211
|
+
deployment.key +
|
|
212
|
+
'" to the "' +
|
|
213
|
+
command.appName +
|
|
214
|
+
'" app.');
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function deploymentHistoryClear(command) {
|
|
218
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
219
|
+
if (wasConfirmed) {
|
|
220
|
+
return exports.sdk.clearDeploymentHistory(command.appName, command.deploymentName).then(() => {
|
|
221
|
+
(0, exports.log)('Successfully cleared the release history associated with the "' +
|
|
222
|
+
command.deploymentName +
|
|
223
|
+
'" deployment from the "' +
|
|
224
|
+
command.appName +
|
|
225
|
+
'" app.');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
(0, exports.log)("Clear deployment cancelled.");
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const deploymentList = (command, showPackage = true) => {
|
|
232
|
+
throwForInvalidOutputFormat(command.format);
|
|
233
|
+
let deployments;
|
|
234
|
+
return exports.sdk
|
|
235
|
+
.getDeployments(command.appName)
|
|
236
|
+
.then((retrievedDeployments) => {
|
|
237
|
+
deployments = retrievedDeployments;
|
|
238
|
+
if (showPackage) {
|
|
239
|
+
const metricsPromises = deployments.map((deployment) => {
|
|
240
|
+
if (deployment.package) {
|
|
241
|
+
return exports.sdk.getDeploymentMetrics(command.appName, deployment.name).then((metrics) => {
|
|
242
|
+
if (metrics[deployment.package.label]) {
|
|
243
|
+
const totalActive = getTotalActiveFromDeploymentMetrics(metrics);
|
|
244
|
+
deployment.package.metrics = {
|
|
245
|
+
active: metrics[deployment.package.label].active,
|
|
246
|
+
downloaded: metrics[deployment.package.label].downloaded,
|
|
247
|
+
failed: metrics[deployment.package.label].failed,
|
|
248
|
+
installed: metrics[deployment.package.label].installed,
|
|
249
|
+
totalActive: totalActive,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return Promise.all(metricsPromises).then(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
.then(() => {
|
|
262
|
+
printDeploymentList(command, deployments, showPackage);
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
exports.deploymentList = deploymentList;
|
|
266
|
+
function deploymentRemove(command) {
|
|
267
|
+
return (0, exports.confirm)("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.").then((wasConfirmed) => {
|
|
268
|
+
if (wasConfirmed) {
|
|
269
|
+
return exports.sdk.removeDeployment(command.appName, command.deploymentName).then(() => {
|
|
270
|
+
(0, exports.log)('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
(0, exports.log)("Deployment removal cancelled.");
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function deploymentRename(command) {
|
|
277
|
+
return exports.sdk.renameDeployment(command.appName, command.currentDeploymentName, command.newDeploymentName).then(() => {
|
|
278
|
+
(0, exports.log)('Successfully renamed the "' +
|
|
279
|
+
command.currentDeploymentName +
|
|
280
|
+
'" deployment to "' +
|
|
281
|
+
command.newDeploymentName +
|
|
282
|
+
'" for the "' +
|
|
283
|
+
command.appName +
|
|
284
|
+
'" app.');
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function deploymentHistory(command) {
|
|
288
|
+
throwForInvalidOutputFormat(command.format);
|
|
289
|
+
return Promise.all([
|
|
290
|
+
exports.sdk.getAccountInfo(),
|
|
291
|
+
exports.sdk.getDeploymentHistory(command.appName, command.deploymentName),
|
|
292
|
+
exports.sdk.getDeploymentMetrics(command.appName, command.deploymentName),
|
|
293
|
+
]).then(([account, deploymentHistory, metrics]) => {
|
|
294
|
+
const totalActive = getTotalActiveFromDeploymentMetrics(metrics);
|
|
295
|
+
deploymentHistory.forEach((packageObject) => {
|
|
296
|
+
if (metrics[packageObject.label]) {
|
|
297
|
+
packageObject.metrics = {
|
|
298
|
+
active: metrics[packageObject.label].active,
|
|
299
|
+
downloaded: metrics[packageObject.label].downloaded,
|
|
300
|
+
failed: metrics[packageObject.label].failed,
|
|
301
|
+
installed: metrics[packageObject.label].installed,
|
|
302
|
+
totalActive: totalActive,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
printDeploymentHistory(command, deploymentHistory, account.email);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function deserializeConnectionInfo() {
|
|
310
|
+
try {
|
|
311
|
+
const savedConnection = fs.readFileSync(configFilePath, {
|
|
312
|
+
encoding: "utf8",
|
|
313
|
+
});
|
|
314
|
+
let connectionInfo = JSON.parse(savedConnection);
|
|
315
|
+
// If the connection info is in the legacy format, convert it to the modern format
|
|
316
|
+
if (connectionInfo.accessKeyName) {
|
|
317
|
+
connectionInfo = {
|
|
318
|
+
accessKey: connectionInfo.accessKeyName,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const connInfo = connectionInfo;
|
|
322
|
+
return connInfo;
|
|
323
|
+
}
|
|
324
|
+
catch (ex) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function execute(command) {
|
|
329
|
+
connectionInfo = deserializeConnectionInfo();
|
|
330
|
+
return Promise.resolve().then(() => {
|
|
331
|
+
switch (command.type) {
|
|
332
|
+
// Must not be logged in
|
|
333
|
+
case cli.CommandType.login:
|
|
334
|
+
case cli.CommandType.register:
|
|
335
|
+
if (connectionInfo) {
|
|
336
|
+
throw new Error("You are already logged in from this machine.");
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
// Must be logged in
|
|
340
|
+
default:
|
|
341
|
+
if (!!exports.sdk)
|
|
342
|
+
break; // Used by unit tests to skip authentication
|
|
343
|
+
if (!connectionInfo) {
|
|
344
|
+
throw new Error("You are not currently logged in. Run 'aether login' to authenticate with Aether.");
|
|
345
|
+
}
|
|
346
|
+
exports.sdk = getSdk(connectionInfo.accessKey, CLI_HEADERS, connectionInfo.customServerUrl);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
switch (command.type) {
|
|
350
|
+
case cli.CommandType.accessKeyAdd:
|
|
351
|
+
return accessKeyAdd(command);
|
|
352
|
+
case cli.CommandType.accessKeyPatch:
|
|
353
|
+
return accessKeyPatch(command);
|
|
354
|
+
case cli.CommandType.accessKeyList:
|
|
355
|
+
return accessKeyList(command);
|
|
356
|
+
case cli.CommandType.accessKeyRemove:
|
|
357
|
+
return accessKeyRemove(command);
|
|
358
|
+
case cli.CommandType.appAdd:
|
|
359
|
+
return appAdd(command);
|
|
360
|
+
case cli.CommandType.appList:
|
|
361
|
+
return appList(command);
|
|
362
|
+
case cli.CommandType.appRemove:
|
|
363
|
+
return appRemove(command);
|
|
364
|
+
case cli.CommandType.appRename:
|
|
365
|
+
return appRename(command);
|
|
366
|
+
case cli.CommandType.appTransfer:
|
|
367
|
+
return appTransfer(command);
|
|
368
|
+
case cli.CommandType.collaboratorAdd:
|
|
369
|
+
return addCollaborator(command);
|
|
370
|
+
case cli.CommandType.collaboratorList:
|
|
371
|
+
return listCollaborators(command);
|
|
372
|
+
case cli.CommandType.collaboratorRemove:
|
|
373
|
+
return removeCollaborator(command);
|
|
374
|
+
case cli.CommandType.debug:
|
|
375
|
+
return (0, debug_1.default)(command);
|
|
376
|
+
case cli.CommandType.deploymentAdd:
|
|
377
|
+
return deploymentAdd(command);
|
|
378
|
+
case cli.CommandType.deploymentHistoryClear:
|
|
379
|
+
return deploymentHistoryClear(command);
|
|
380
|
+
case cli.CommandType.deploymentHistory:
|
|
381
|
+
return deploymentHistory(command);
|
|
382
|
+
case cli.CommandType.deploymentList:
|
|
383
|
+
return (0, exports.deploymentList)(command);
|
|
384
|
+
case cli.CommandType.deploymentRemove:
|
|
385
|
+
return deploymentRemove(command);
|
|
386
|
+
case cli.CommandType.deploymentRename:
|
|
387
|
+
return deploymentRename(command);
|
|
388
|
+
case cli.CommandType.login:
|
|
389
|
+
return login(command);
|
|
390
|
+
case cli.CommandType.logout:
|
|
391
|
+
return logout(command);
|
|
392
|
+
case cli.CommandType.patch:
|
|
393
|
+
return patch(command);
|
|
394
|
+
case cli.CommandType.promote:
|
|
395
|
+
return promote(command);
|
|
396
|
+
case cli.CommandType.register:
|
|
397
|
+
return register(command);
|
|
398
|
+
case cli.CommandType.release:
|
|
399
|
+
return (0, exports.release)(command);
|
|
400
|
+
case cli.CommandType.releaseReact:
|
|
401
|
+
return (0, exports.releaseReact)(command);
|
|
402
|
+
case cli.CommandType.rollback:
|
|
403
|
+
return rollback(command);
|
|
404
|
+
case cli.CommandType.sessionList:
|
|
405
|
+
return sessionList(command);
|
|
406
|
+
case cli.CommandType.sessionRemove:
|
|
407
|
+
return sessionRemove(command);
|
|
408
|
+
case cli.CommandType.whoami:
|
|
409
|
+
return whoami(command);
|
|
410
|
+
default:
|
|
411
|
+
// We should never see this message as invalid commands should be caught by the argument parser.
|
|
412
|
+
throw new Error("Invalid command: " + JSON.stringify(command));
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function getTotalActiveFromDeploymentMetrics(metrics) {
|
|
417
|
+
let totalActive = 0;
|
|
418
|
+
Object.keys(metrics).forEach((label) => {
|
|
419
|
+
totalActive += metrics[label].active;
|
|
420
|
+
});
|
|
421
|
+
return totalActive;
|
|
422
|
+
}
|
|
423
|
+
async function login(command) {
|
|
424
|
+
const serverUrl = command.serverUrl || DEFAULT_AETHER_SERVER_URL;
|
|
425
|
+
if (command.accessKey) {
|
|
426
|
+
exports.sdk = getSdk(command.accessKey, CLI_HEADERS, serverUrl);
|
|
427
|
+
const authenticated = await exports.sdk.isAuthenticated();
|
|
428
|
+
if (!authenticated) {
|
|
429
|
+
throw new Error("Invalid access key.");
|
|
430
|
+
}
|
|
431
|
+
serializeConnectionInfo(command.accessKey, /*preserveAccessKeyOnLogout*/ true, serverUrl);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const { email, password } = await promptForLoginCredentials();
|
|
435
|
+
if (!email) {
|
|
436
|
+
throw new Error("Email is required.");
|
|
437
|
+
}
|
|
438
|
+
if (!password) {
|
|
439
|
+
throw new Error("Password is required.");
|
|
440
|
+
}
|
|
441
|
+
const url = serverUrl.replace(/\/$/, "") + "/v1/auth/login";
|
|
442
|
+
let res;
|
|
443
|
+
try {
|
|
444
|
+
res = await fetch(url, {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
447
|
+
body: JSON.stringify({ email, password }),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
throw new Error(`Unable to reach Aether at ${serverUrl}. Are you offline, or behind a firewall or proxy?`);
|
|
452
|
+
}
|
|
453
|
+
const body = await res.json().catch(() => ({}));
|
|
454
|
+
if (!res.ok) {
|
|
455
|
+
throw new Error(body.error || body.message || `Login failed (HTTP ${res.status}).`);
|
|
456
|
+
}
|
|
457
|
+
const accessKey = body.accessKey;
|
|
458
|
+
if (!accessKey) {
|
|
459
|
+
throw new Error("Server returned an empty access key.");
|
|
460
|
+
}
|
|
461
|
+
exports.sdk = getSdk(accessKey, CLI_HEADERS, serverUrl);
|
|
462
|
+
serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl);
|
|
463
|
+
(0, exports.log)(chalk.green(`Successfully logged in as ${email}.`));
|
|
464
|
+
}
|
|
465
|
+
function logout(command) {
|
|
466
|
+
exports.sdk = null;
|
|
467
|
+
deleteConnectionInfoCache();
|
|
468
|
+
return Promise.resolve();
|
|
469
|
+
}
|
|
470
|
+
function formatDate(unixOffset) {
|
|
471
|
+
const date = moment(unixOffset);
|
|
472
|
+
const now = moment();
|
|
473
|
+
if (Math.abs(now.diff(date, "days")) < 30) {
|
|
474
|
+
return date.fromNow(); // "2 hours ago"
|
|
475
|
+
}
|
|
476
|
+
else if (now.year() === date.year()) {
|
|
477
|
+
return date.format("MMM D"); // "Nov 6"
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
return date.format("MMM D, YYYY"); // "Nov 6, 2014"
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function printAppList(format, apps) {
|
|
484
|
+
if (format === "json") {
|
|
485
|
+
printJson(apps);
|
|
486
|
+
}
|
|
487
|
+
else if (format === "table") {
|
|
488
|
+
const headers = ["Name", "Deployments"];
|
|
489
|
+
printTable(headers, (dataSource) => {
|
|
490
|
+
apps.forEach((app, index) => {
|
|
491
|
+
const row = [app.name, wordwrap(50)(app.deployments.join(", "))];
|
|
492
|
+
dataSource.push(row);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function getCollaboratorDisplayName(email, collaboratorProperties) {
|
|
498
|
+
return collaboratorProperties.permission === AccountManager.AppPermission.OWNER ? email + chalk.magenta(" (Owner)") : email;
|
|
499
|
+
}
|
|
500
|
+
function printCollaboratorsList(format, collaborators) {
|
|
501
|
+
if (format === "json") {
|
|
502
|
+
const dataSource = { collaborators: collaborators };
|
|
503
|
+
printJson(dataSource);
|
|
504
|
+
}
|
|
505
|
+
else if (format === "table") {
|
|
506
|
+
const headers = ["E-mail Address"];
|
|
507
|
+
printTable(headers, (dataSource) => {
|
|
508
|
+
Object.keys(collaborators).forEach((email) => {
|
|
509
|
+
const row = [getCollaboratorDisplayName(email, collaborators[email])];
|
|
510
|
+
dataSource.push(row);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function printDeploymentList(command, deployments, showPackage = true) {
|
|
516
|
+
if (command.format === "json") {
|
|
517
|
+
printJson(deployments);
|
|
518
|
+
}
|
|
519
|
+
else if (command.format === "table") {
|
|
520
|
+
const headers = ["Name"];
|
|
521
|
+
if (command.displayKeys) {
|
|
522
|
+
headers.push("Deployment Key");
|
|
523
|
+
}
|
|
524
|
+
if (showPackage) {
|
|
525
|
+
headers.push("Update Metadata");
|
|
526
|
+
headers.push("Install Metrics");
|
|
527
|
+
}
|
|
528
|
+
printTable(headers, (dataSource) => {
|
|
529
|
+
deployments.forEach((deployment) => {
|
|
530
|
+
const row = [deployment.name];
|
|
531
|
+
if (command.displayKeys) {
|
|
532
|
+
row.push(deployment.key);
|
|
533
|
+
}
|
|
534
|
+
if (showPackage) {
|
|
535
|
+
row.push(getPackageString(deployment.package));
|
|
536
|
+
row.push(getPackageMetricsString(deployment.package));
|
|
537
|
+
}
|
|
538
|
+
dataSource.push(row);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function printDeploymentHistory(command, deploymentHistory, currentUserEmail) {
|
|
544
|
+
if (command.format === "json") {
|
|
545
|
+
printJson(deploymentHistory);
|
|
546
|
+
}
|
|
547
|
+
else if (command.format === "table") {
|
|
548
|
+
const headers = ["Label", "Release Time", "App Version", "Mandatory"];
|
|
549
|
+
if (command.displayAuthor) {
|
|
550
|
+
headers.push("Released By");
|
|
551
|
+
}
|
|
552
|
+
headers.push("Description", "Install Metrics");
|
|
553
|
+
printTable(headers, (dataSource) => {
|
|
554
|
+
deploymentHistory.forEach((packageObject) => {
|
|
555
|
+
let releaseTime = formatDate(packageObject.uploadTime);
|
|
556
|
+
let releaseSource;
|
|
557
|
+
if (packageObject.releaseMethod === "Promote") {
|
|
558
|
+
releaseSource = `Promoted ${packageObject.originalLabel} from "${packageObject.originalDeployment}"`;
|
|
559
|
+
}
|
|
560
|
+
else if (packageObject.releaseMethod === "Rollback") {
|
|
561
|
+
const labelNumber = parseInt(packageObject.label.substring(1));
|
|
562
|
+
const lastLabel = "v" + (labelNumber - 1);
|
|
563
|
+
releaseSource = `Rolled back ${lastLabel} to ${packageObject.originalLabel}`;
|
|
564
|
+
}
|
|
565
|
+
if (releaseSource) {
|
|
566
|
+
releaseTime += "\n" + chalk.magenta(`(${releaseSource})`).toString();
|
|
567
|
+
}
|
|
568
|
+
let row = [packageObject.label, releaseTime, packageObject.appVersion, packageObject.isMandatory ? "Yes" : "No"];
|
|
569
|
+
if (command.displayAuthor) {
|
|
570
|
+
let releasedBy = packageObject.releasedBy ? packageObject.releasedBy : "";
|
|
571
|
+
if (currentUserEmail && releasedBy === currentUserEmail) {
|
|
572
|
+
releasedBy = "You";
|
|
573
|
+
}
|
|
574
|
+
row.push(releasedBy);
|
|
575
|
+
}
|
|
576
|
+
row.push(packageObject.description ? wordwrap(30)(packageObject.description) : "");
|
|
577
|
+
row.push(getPackageMetricsString(packageObject) + (packageObject.isDisabled ? `\n${chalk.green("Disabled:")} Yes` : ""));
|
|
578
|
+
if (packageObject.isDisabled) {
|
|
579
|
+
row = row.map((cellContents) => applyChalkSkippingLineBreaks(cellContents, chalk.dim));
|
|
580
|
+
}
|
|
581
|
+
dataSource.push(row);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function applyChalkSkippingLineBreaks(applyString, chalkMethod) {
|
|
587
|
+
// Used to prevent "chalk" from applying styles to linebreaks which
|
|
588
|
+
// causes table border chars to have the style applied as well.
|
|
589
|
+
return applyString
|
|
590
|
+
.split("\n")
|
|
591
|
+
.map((token) => chalkMethod(token))
|
|
592
|
+
.join("\n");
|
|
593
|
+
}
|
|
594
|
+
function getPackageString(packageObject) {
|
|
595
|
+
if (!packageObject) {
|
|
596
|
+
return chalk.magenta("No updates released").toString();
|
|
597
|
+
}
|
|
598
|
+
let packageString = chalk.green("Label: ") +
|
|
599
|
+
packageObject.label +
|
|
600
|
+
"\n" +
|
|
601
|
+
chalk.green("App Version: ") +
|
|
602
|
+
packageObject.appVersion +
|
|
603
|
+
"\n" +
|
|
604
|
+
chalk.green("Mandatory: ") +
|
|
605
|
+
(packageObject.isMandatory ? "Yes" : "No") +
|
|
606
|
+
"\n" +
|
|
607
|
+
chalk.green("Release Time: ") +
|
|
608
|
+
formatDate(packageObject.uploadTime) +
|
|
609
|
+
"\n" +
|
|
610
|
+
chalk.green("Released By: ") +
|
|
611
|
+
(packageObject.releasedBy ? packageObject.releasedBy : "") +
|
|
612
|
+
(packageObject.description ? wordwrap(70)("\n" + chalk.green("Description: ") + packageObject.description) : "");
|
|
613
|
+
if (packageObject.isDisabled) {
|
|
614
|
+
packageString += `\n${chalk.green("Disabled:")} Yes`;
|
|
615
|
+
}
|
|
616
|
+
return packageString;
|
|
617
|
+
}
|
|
618
|
+
function getPackageMetricsString(obj) {
|
|
619
|
+
const packageObject = obj;
|
|
620
|
+
const rolloutString = obj && obj.rollout && obj.rollout !== 100 ? `\n${chalk.green("Rollout:")} ${obj.rollout.toLocaleString()}%` : "";
|
|
621
|
+
if (!packageObject || !packageObject.metrics) {
|
|
622
|
+
return chalk.magenta("No installs recorded").toString() + (rolloutString || "");
|
|
623
|
+
}
|
|
624
|
+
const activePercent = packageObject.metrics.totalActive
|
|
625
|
+
? (packageObject.metrics.active / packageObject.metrics.totalActive) * 100
|
|
626
|
+
: 0.0;
|
|
627
|
+
let percentString;
|
|
628
|
+
if (activePercent === 100.0) {
|
|
629
|
+
percentString = "100%";
|
|
630
|
+
}
|
|
631
|
+
else if (activePercent === 0.0) {
|
|
632
|
+
percentString = "0%";
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
percentString = activePercent.toPrecision(2) + "%";
|
|
636
|
+
}
|
|
637
|
+
const numPending = packageObject.metrics.downloaded - packageObject.metrics.installed - packageObject.metrics.failed;
|
|
638
|
+
let returnString = chalk.green("Active: ") +
|
|
639
|
+
percentString +
|
|
640
|
+
" (" +
|
|
641
|
+
packageObject.metrics.active.toLocaleString() +
|
|
642
|
+
" of " +
|
|
643
|
+
packageObject.metrics.totalActive.toLocaleString() +
|
|
644
|
+
")\n" +
|
|
645
|
+
chalk.green("Total: ") +
|
|
646
|
+
packageObject.metrics.installed.toLocaleString();
|
|
647
|
+
if (numPending > 0) {
|
|
648
|
+
returnString += " (" + numPending.toLocaleString() + " pending)";
|
|
649
|
+
}
|
|
650
|
+
if (packageObject.metrics.failed) {
|
|
651
|
+
returnString += "\n" + chalk.green("Rollbacks: ") + chalk.red(packageObject.metrics.failed.toLocaleString() + "");
|
|
652
|
+
}
|
|
653
|
+
if (rolloutString) {
|
|
654
|
+
returnString += rolloutString;
|
|
655
|
+
}
|
|
656
|
+
return returnString;
|
|
657
|
+
}
|
|
658
|
+
function getReactNativeProjectAppVersion(command, projectName) {
|
|
659
|
+
(0, exports.log)(chalk.cyan(`Detecting ${command.platform} app version:\n`));
|
|
660
|
+
if (command.platform === "ios") {
|
|
661
|
+
let resolvedPlistFile = command.plistFile;
|
|
662
|
+
if (resolvedPlistFile) {
|
|
663
|
+
// If a plist file path is explicitly provided, then we don't
|
|
664
|
+
// need to attempt to "resolve" it within the well-known locations.
|
|
665
|
+
if (!(0, file_utils_1.fileExists)(resolvedPlistFile)) {
|
|
666
|
+
throw new Error("The specified plist file doesn't exist. Please check that the provided path is correct.");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// Allow the plist prefix to be specified with or without a trailing
|
|
671
|
+
// separator character, but prescribe the use of a hyphen when omitted,
|
|
672
|
+
// since this is the most commonly used convetion for plist files.
|
|
673
|
+
if (command.plistFilePrefix && /.+[^-.]$/.test(command.plistFilePrefix)) {
|
|
674
|
+
command.plistFilePrefix += "-";
|
|
675
|
+
}
|
|
676
|
+
const iOSDirectory = "ios";
|
|
677
|
+
const plistFileName = `${command.plistFilePrefix || ""}Info.plist`;
|
|
678
|
+
const knownLocations = [path.join(iOSDirectory, projectName, plistFileName), path.join(iOSDirectory, plistFileName)];
|
|
679
|
+
resolvedPlistFile = knownLocations.find(file_utils_1.fileExists);
|
|
680
|
+
if (!resolvedPlistFile) {
|
|
681
|
+
throw new Error(`Unable to find either of the following plist files in order to infer your app's binary version: "${knownLocations.join('", "')}". If your plist has a different name, or is located in a different directory, consider using either the "--plistFile" or "--plistFilePrefix" parameters to help inform the CLI how to find it.`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const plistContents = fs.readFileSync(resolvedPlistFile).toString();
|
|
685
|
+
let parsedPlist;
|
|
686
|
+
try {
|
|
687
|
+
parsedPlist = plist.parse(plistContents);
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
throw new Error(`Unable to parse "${resolvedPlistFile}". Please ensure it is a well-formed plist file.`);
|
|
691
|
+
}
|
|
692
|
+
if (parsedPlist && parsedPlist.CFBundleShortVersionString) {
|
|
693
|
+
if ((0, react_native_utils_1.isValidVersion)(parsedPlist.CFBundleShortVersionString)) {
|
|
694
|
+
(0, exports.log)(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
|
|
695
|
+
return Promise.resolve(parsedPlist.CFBundleShortVersionString);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") {
|
|
699
|
+
throw new Error(`The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`);
|
|
700
|
+
}
|
|
701
|
+
return getAppVersionFromXcodeProject(command, projectName);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else if (command.platform === "android") {
|
|
709
|
+
let buildGradlePath = path.join("android", "app");
|
|
710
|
+
if (command.gradleFile) {
|
|
711
|
+
buildGradlePath = command.gradleFile;
|
|
712
|
+
}
|
|
713
|
+
if (fs.lstatSync(buildGradlePath).isDirectory()) {
|
|
714
|
+
buildGradlePath = path.join(buildGradlePath, "build.gradle");
|
|
715
|
+
}
|
|
716
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(buildGradlePath)) {
|
|
717
|
+
throw new Error(`Unable to find gradle file "${buildGradlePath}".`);
|
|
718
|
+
}
|
|
719
|
+
return g2js
|
|
720
|
+
.parseFile(buildGradlePath)
|
|
721
|
+
.catch(() => {
|
|
722
|
+
throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`);
|
|
723
|
+
})
|
|
724
|
+
.then((buildGradle) => {
|
|
725
|
+
let versionName = null;
|
|
726
|
+
// First 'if' statement was implemented as workaround for case
|
|
727
|
+
// when 'build.gradle' file contains several 'android' nodes.
|
|
728
|
+
// In this case 'buildGradle.android' prop represents array instead of object
|
|
729
|
+
// due to parsing issue in 'g2js.parseFile' method.
|
|
730
|
+
if (buildGradle.android instanceof Array) {
|
|
731
|
+
for (let i = 0; i < buildGradle.android.length; i++) {
|
|
732
|
+
const gradlePart = buildGradle.android[i];
|
|
733
|
+
if (gradlePart.defaultConfig && gradlePart.defaultConfig.versionName) {
|
|
734
|
+
versionName = gradlePart.defaultConfig.versionName;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
else if (buildGradle.android && buildGradle.android.defaultConfig && buildGradle.android.defaultConfig.versionName) {
|
|
740
|
+
versionName = buildGradle.android.defaultConfig.versionName;
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
throw new Error(`The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`);
|
|
744
|
+
}
|
|
745
|
+
if (typeof versionName !== "string") {
|
|
746
|
+
throw new Error(`The "android.defaultConfig.versionName" property value in "${buildGradlePath}" is not a valid string. If this is expected, consider using the --targetBinaryVersion option to specify the value manually.`);
|
|
747
|
+
}
|
|
748
|
+
let appVersion = versionName.replace(/"/g, "").trim();
|
|
749
|
+
if ((0, react_native_utils_1.isValidVersion)(appVersion)) {
|
|
750
|
+
// The versionName property is a valid semver string,
|
|
751
|
+
// so we can safely use that and move on.
|
|
752
|
+
(0, exports.log)(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`);
|
|
753
|
+
return appVersion;
|
|
754
|
+
}
|
|
755
|
+
else if (/^\d.*/.test(appVersion)) {
|
|
756
|
+
// The versionName property isn't a valid semver string,
|
|
757
|
+
// but it starts with a number, and therefore, it can't
|
|
758
|
+
// be a valid Gradle property reference.
|
|
759
|
+
throw new Error(`The "android.defaultConfig.versionName" property in the "${buildGradlePath}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`);
|
|
760
|
+
}
|
|
761
|
+
// The version property isn't a valid semver string
|
|
762
|
+
// so we assume it is a reference to a property variable.
|
|
763
|
+
const propertyName = appVersion.replace("project.", "");
|
|
764
|
+
const propertiesFileName = "gradle.properties";
|
|
765
|
+
const knownLocations = [path.join("android", "app", propertiesFileName), path.join("android", propertiesFileName)];
|
|
766
|
+
// Search for gradle properties across all `gradle.properties` files
|
|
767
|
+
let propertiesFile = null;
|
|
768
|
+
for (let i = 0; i < knownLocations.length; i++) {
|
|
769
|
+
propertiesFile = knownLocations[i];
|
|
770
|
+
if ((0, file_utils_1.fileExists)(propertiesFile)) {
|
|
771
|
+
const propertiesContent = fs.readFileSync(propertiesFile).toString();
|
|
772
|
+
try {
|
|
773
|
+
const parsedProperties = properties.parse(propertiesContent);
|
|
774
|
+
appVersion = parsedProperties[propertyName];
|
|
775
|
+
if (appVersion) {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (e) {
|
|
780
|
+
throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!appVersion) {
|
|
785
|
+
throw new Error(`No property named "${propertyName}" exists in the "${propertiesFile}" file.`);
|
|
786
|
+
}
|
|
787
|
+
if (!(0, react_native_utils_1.isValidVersion)(appVersion)) {
|
|
788
|
+
throw new Error(`The "${propertyName}" property in the "${propertiesFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`);
|
|
789
|
+
}
|
|
790
|
+
(0, exports.log)(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`);
|
|
791
|
+
return appVersion.toString();
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
throw new Error(`Unsupported platform "${command.platform}". Use "ios" or "android".`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function getAppVersionFromXcodeProject(command, projectName) {
|
|
799
|
+
const pbxprojFileName = "project.pbxproj";
|
|
800
|
+
let resolvedPbxprojFile = command.xcodeProjectFile;
|
|
801
|
+
if (resolvedPbxprojFile) {
|
|
802
|
+
// If the xcode project file path is explicitly provided, then we don't
|
|
803
|
+
// need to attempt to "resolve" it within the well-known locations.
|
|
804
|
+
if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) {
|
|
805
|
+
// Specify path to pbxproj file if the provided file path is an Xcode project file.
|
|
806
|
+
resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName);
|
|
807
|
+
}
|
|
808
|
+
if (!(0, file_utils_1.fileExists)(resolvedPbxprojFile)) {
|
|
809
|
+
throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct.");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
const iOSDirectory = "ios";
|
|
814
|
+
const xcodeprojDirectory = `${projectName}.xcodeproj`;
|
|
815
|
+
const pbxprojKnownLocations = [
|
|
816
|
+
path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName),
|
|
817
|
+
path.join(iOSDirectory, pbxprojFileName),
|
|
818
|
+
];
|
|
819
|
+
resolvedPbxprojFile = pbxprojKnownLocations.find(file_utils_1.fileExists);
|
|
820
|
+
if (!resolvedPbxprojFile) {
|
|
821
|
+
throw new Error(`Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join('", "')}".`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync();
|
|
825
|
+
const marketingVersion = xcodeProj.getBuildProperty("MARKETING_VERSION", command.buildConfigurationName, command.xcodeTargetName);
|
|
826
|
+
if (!(0, react_native_utils_1.isValidVersion)(marketingVersion)) {
|
|
827
|
+
throw new Error(`The "MARKETING_VERSION" key in the "${resolvedPbxprojFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`);
|
|
828
|
+
}
|
|
829
|
+
console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`);
|
|
830
|
+
return Promise.resolve(marketingVersion);
|
|
831
|
+
}
|
|
832
|
+
function printJson(object) {
|
|
833
|
+
(0, exports.log)(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2));
|
|
834
|
+
}
|
|
835
|
+
function printAccessKeys(format, keys) {
|
|
836
|
+
if (format === "json") {
|
|
837
|
+
printJson(keys);
|
|
838
|
+
}
|
|
839
|
+
else if (format === "table") {
|
|
840
|
+
printTable(["Name", "Created", "Expires"], (dataSource) => {
|
|
841
|
+
const now = new Date().getTime();
|
|
842
|
+
function isExpired(key) {
|
|
843
|
+
return now >= key.expires;
|
|
844
|
+
}
|
|
845
|
+
function keyToTableRow(key, dim) {
|
|
846
|
+
const row = [key.friendlyName, key.createdTime ? formatDate(key.createdTime) : "", formatDate(key.expires)];
|
|
847
|
+
if (dim) {
|
|
848
|
+
row.forEach((col, index) => {
|
|
849
|
+
row[index] = chalk.dim(col);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return row;
|
|
853
|
+
}
|
|
854
|
+
keys.forEach((key) => !isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ false)));
|
|
855
|
+
keys.forEach((key) => isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ true)));
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function printSessions(format, sessions) {
|
|
860
|
+
if (format === "json") {
|
|
861
|
+
printJson(sessions);
|
|
862
|
+
}
|
|
863
|
+
else if (format === "table") {
|
|
864
|
+
printTable(["Created From", "Logged in"], (dataSource) => {
|
|
865
|
+
sessions.forEach((session) => dataSource.push([session.createdBy, formatDate(session.loggedInTime)]));
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function printTable(columnNames, readData) {
|
|
870
|
+
const table = new Table({
|
|
871
|
+
head: columnNames,
|
|
872
|
+
style: { head: ["cyan"] },
|
|
873
|
+
});
|
|
874
|
+
readData(table);
|
|
875
|
+
(0, exports.log)(table.toString());
|
|
876
|
+
}
|
|
877
|
+
async function register(command) {
|
|
878
|
+
const serverUrl = command.serverUrl || DEFAULT_AETHER_SERVER_URL;
|
|
879
|
+
const { email, name, password, confirmPassword } = await promptForRegistration();
|
|
880
|
+
if (!email) {
|
|
881
|
+
throw new Error("Email is required.");
|
|
882
|
+
}
|
|
883
|
+
if (!password) {
|
|
884
|
+
throw new Error("Password is required.");
|
|
885
|
+
}
|
|
886
|
+
if (password !== confirmPassword) {
|
|
887
|
+
throw new Error("Passwords do not match.");
|
|
888
|
+
}
|
|
889
|
+
const reqBody = { email, password };
|
|
890
|
+
if (name) {
|
|
891
|
+
reqBody.name = name;
|
|
892
|
+
}
|
|
893
|
+
const url = serverUrl.replace(/\/$/, "") + "/v1/auth/register";
|
|
894
|
+
let res;
|
|
895
|
+
try {
|
|
896
|
+
res = await fetch(url, {
|
|
897
|
+
method: "POST",
|
|
898
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
899
|
+
body: JSON.stringify(reqBody),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
throw new Error(`Unable to reach Aether at ${serverUrl}. Are you offline, or behind a firewall or proxy?`);
|
|
904
|
+
}
|
|
905
|
+
const body = await res.json().catch(() => ({}));
|
|
906
|
+
if (!res.ok) {
|
|
907
|
+
if (Array.isArray(body.errors) && body.errors.length > 0) {
|
|
908
|
+
const lines = body.errors
|
|
909
|
+
.map((e) => (e && typeof e === "object" ? e.message || JSON.stringify(e) : String(e)))
|
|
910
|
+
.join("\n ");
|
|
911
|
+
throw new Error(`Registration failed:\n ${lines}`);
|
|
912
|
+
}
|
|
913
|
+
throw new Error(body.error || body.message || `Registration failed (HTTP ${res.status}).`);
|
|
914
|
+
}
|
|
915
|
+
(0, exports.log)(chalk.green(`Account created for ${email}.`));
|
|
916
|
+
(0, exports.log)(`Check your inbox for a verification link, then run ${chalk.cyan("aether login")} to sign in.`);
|
|
917
|
+
}
|
|
918
|
+
function promote(command) {
|
|
919
|
+
const packageInfo = {
|
|
920
|
+
appVersion: command.appStoreVersion,
|
|
921
|
+
description: command.description,
|
|
922
|
+
label: command.label,
|
|
923
|
+
isDisabled: command.disabled,
|
|
924
|
+
isMandatory: command.mandatory,
|
|
925
|
+
rollout: command.rollout,
|
|
926
|
+
};
|
|
927
|
+
return exports.sdk
|
|
928
|
+
.promote(command.appName, command.sourceDeploymentName, command.destDeploymentName, packageInfo)
|
|
929
|
+
.then(() => {
|
|
930
|
+
(0, exports.log)("Successfully promoted " +
|
|
931
|
+
(command.label !== null ? '"' + command.label + '" of ' : "") +
|
|
932
|
+
'the "' +
|
|
933
|
+
command.sourceDeploymentName +
|
|
934
|
+
'" deployment of the "' +
|
|
935
|
+
command.appName +
|
|
936
|
+
'" app to the "' +
|
|
937
|
+
command.destDeploymentName +
|
|
938
|
+
'" deployment.');
|
|
939
|
+
})
|
|
940
|
+
.catch((err) => releaseErrorHandler(err, command));
|
|
941
|
+
}
|
|
942
|
+
function patch(command) {
|
|
943
|
+
const packageInfo = {
|
|
944
|
+
appVersion: command.appStoreVersion,
|
|
945
|
+
description: command.description,
|
|
946
|
+
isMandatory: command.mandatory,
|
|
947
|
+
isDisabled: command.disabled,
|
|
948
|
+
rollout: command.rollout,
|
|
949
|
+
};
|
|
950
|
+
for (const updateProperty in packageInfo) {
|
|
951
|
+
if (packageInfo[updateProperty] !== null) {
|
|
952
|
+
return exports.sdk.patchRelease(command.appName, command.deploymentName, command.label, packageInfo).then(() => {
|
|
953
|
+
(0, exports.log)(`Successfully updated the "${command.label ? command.label : `latest`}" release of "${command.appName}" app's "${command.deploymentName}" deployment.`);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
throw new Error("At least one property must be specified to patch a release.");
|
|
958
|
+
}
|
|
959
|
+
const release = (command) => {
|
|
960
|
+
if ((0, file_utils_1.isBinaryOrZip)(command.package)) {
|
|
961
|
+
throw new Error("It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle).");
|
|
962
|
+
}
|
|
963
|
+
throwForInvalidSemverRange(command.appStoreVersion);
|
|
964
|
+
const filePath = command.package;
|
|
965
|
+
let isSingleFilePackage = true;
|
|
966
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
967
|
+
isSingleFilePackage = false;
|
|
968
|
+
}
|
|
969
|
+
const updateMetadata = {
|
|
970
|
+
description: command.description,
|
|
971
|
+
isDisabled: command.disabled,
|
|
972
|
+
isMandatory: command.mandatory,
|
|
973
|
+
rollout: command.rollout,
|
|
974
|
+
};
|
|
975
|
+
return exports.sdk
|
|
976
|
+
.isAuthenticated(true)
|
|
977
|
+
.then((isAuth) => {
|
|
978
|
+
(0, exports.log)("Uploading release package...");
|
|
979
|
+
return exports.sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata);
|
|
980
|
+
})
|
|
981
|
+
.then(() => {
|
|
982
|
+
(0, exports.log)('Successfully released an update containing the "' +
|
|
983
|
+
command.package +
|
|
984
|
+
'" ' +
|
|
985
|
+
(isSingleFilePackage ? "file" : "directory") +
|
|
986
|
+
' to the "' +
|
|
987
|
+
command.deploymentName +
|
|
988
|
+
'" deployment of the "' +
|
|
989
|
+
command.appName +
|
|
990
|
+
'" app.');
|
|
991
|
+
})
|
|
992
|
+
.catch((err) => releaseErrorHandler(err, command));
|
|
993
|
+
};
|
|
994
|
+
exports.release = release;
|
|
995
|
+
const releaseReact = (command) => {
|
|
996
|
+
let bundleName = command.bundleName;
|
|
997
|
+
let entryFile = command.entryFile;
|
|
998
|
+
const outputFolder = command.outputDir || path.join(os.tmpdir(), "Aether");
|
|
999
|
+
const platform = (command.platform = command.platform.toLowerCase());
|
|
1000
|
+
const releaseCommand = command;
|
|
1001
|
+
// Check for app and deployment exist before releasing an update.
|
|
1002
|
+
// This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name.
|
|
1003
|
+
return (exports.sdk
|
|
1004
|
+
.getDeployment(command.appName, command.deploymentName)
|
|
1005
|
+
.then(() => {
|
|
1006
|
+
releaseCommand.package = outputFolder;
|
|
1007
|
+
switch (platform) {
|
|
1008
|
+
case "android":
|
|
1009
|
+
case "ios":
|
|
1010
|
+
if (!bundleName) {
|
|
1011
|
+
bundleName = platform === "ios" ? "main.jsbundle" : `index.${platform}.bundle`;
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
default:
|
|
1015
|
+
throw new Error('Platform must be either "android" or "ios".');
|
|
1016
|
+
}
|
|
1017
|
+
let projectName;
|
|
1018
|
+
try {
|
|
1019
|
+
const projectPackageJson = require(path.join(process.cwd(), "package.json"));
|
|
1020
|
+
projectName = projectPackageJson.name;
|
|
1021
|
+
if (!projectName) {
|
|
1022
|
+
throw new Error('The "package.json" file in the CWD does not have the "name" field set.');
|
|
1023
|
+
}
|
|
1024
|
+
if (!projectPackageJson.dependencies["react-native"]) {
|
|
1025
|
+
throw new Error("The project in the CWD is not a React Native project.");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
throw new Error('Unable to find or read "package.json" in the CWD. The "release-react" command must be executed in a React Native project folder.');
|
|
1030
|
+
}
|
|
1031
|
+
if (!entryFile) {
|
|
1032
|
+
entryFile = `index.${platform}.js`;
|
|
1033
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) {
|
|
1034
|
+
entryFile = "index.js";
|
|
1035
|
+
}
|
|
1036
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) {
|
|
1037
|
+
throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(entryFile)) {
|
|
1042
|
+
throw new Error(`Entry file "${entryFile}" does not exist.`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const appVersionPromise = command.appStoreVersion
|
|
1046
|
+
? Promise.resolve(command.appStoreVersion)
|
|
1047
|
+
: getReactNativeProjectAppVersion(command, projectName);
|
|
1048
|
+
if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) {
|
|
1049
|
+
command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map");
|
|
1050
|
+
}
|
|
1051
|
+
return appVersionPromise;
|
|
1052
|
+
})
|
|
1053
|
+
.then((appVersion) => {
|
|
1054
|
+
throwForInvalidSemverRange(appVersion);
|
|
1055
|
+
releaseCommand.appStoreVersion = appVersion;
|
|
1056
|
+
return (0, exports.createEmptyTempReleaseFolder)(outputFolder);
|
|
1057
|
+
})
|
|
1058
|
+
// This is needed to clear the react native bundler cache:
|
|
1059
|
+
// https://github.com/facebook/react-native/issues/4289
|
|
1060
|
+
.then(() => deleteFolder(`${os.tmpdir()}/react-*`))
|
|
1061
|
+
.then(() => (0, exports.runReactNativeBundleCommand)(bundleName, command.development || false, entryFile, outputFolder, platform, command.sourcemapOutput))
|
|
1062
|
+
.then(async () => {
|
|
1063
|
+
const isHermesEnabled = command.useHermes ||
|
|
1064
|
+
(platform === "android" && (await (0, react_native_utils_1.getAndroidHermesEnabled)(command.gradleFile))) || // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in build.gradle and we're releasing an Android build
|
|
1065
|
+
(platform === "ios" && (await (0, react_native_utils_1.getiOSHermesEnabled)(command.podFile))); // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build
|
|
1066
|
+
if (isHermesEnabled) {
|
|
1067
|
+
(0, exports.log)(chalk.cyan("\nRunning hermes compiler...\n"));
|
|
1068
|
+
await (0, react_native_utils_1.runHermesEmitBinaryCommand)(bundleName, outputFolder, command.sourcemapOutput, command.extraHermesFlags, command.gradleFile);
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
.then(async () => {
|
|
1072
|
+
if (command.privateKeyPath) {
|
|
1073
|
+
(0, exports.log)(chalk.cyan("\nSigning the bundle:\n"));
|
|
1074
|
+
await (0, sign_1.default)(command.privateKeyPath, outputFolder);
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
console.log("private key was not provided");
|
|
1078
|
+
}
|
|
1079
|
+
})
|
|
1080
|
+
.then(() => {
|
|
1081
|
+
(0, exports.log)(chalk.cyan("\nReleasing update contents to Aether:\n"));
|
|
1082
|
+
return (0, exports.release)(releaseCommand);
|
|
1083
|
+
})
|
|
1084
|
+
.then(() => {
|
|
1085
|
+
if (!command.outputDir) {
|
|
1086
|
+
deleteFolder(outputFolder);
|
|
1087
|
+
}
|
|
1088
|
+
})
|
|
1089
|
+
.catch((err) => {
|
|
1090
|
+
deleteFolder(outputFolder);
|
|
1091
|
+
throw err;
|
|
1092
|
+
}));
|
|
1093
|
+
};
|
|
1094
|
+
exports.releaseReact = releaseReact;
|
|
1095
|
+
function rollback(command) {
|
|
1096
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
1097
|
+
if (!wasConfirmed) {
|
|
1098
|
+
(0, exports.log)("Rollback cancelled.");
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
return exports.sdk.rollback(command.appName, command.deploymentName, command.targetRelease || undefined).then(() => {
|
|
1102
|
+
(0, exports.log)('Successfully performed a rollback on the "' + command.deploymentName + '" deployment of the "' + command.appName + '" app.');
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
function promptForLoginCredentials() {
|
|
1107
|
+
return new Promise((resolve, reject) => {
|
|
1108
|
+
prompt.message = "";
|
|
1109
|
+
prompt.delimiter = "";
|
|
1110
|
+
prompt.start();
|
|
1111
|
+
prompt.get({
|
|
1112
|
+
properties: {
|
|
1113
|
+
email: { description: chalk.cyan("Email: ") },
|
|
1114
|
+
password: { description: chalk.cyan("Password: "), hidden: true, replace: "*" },
|
|
1115
|
+
},
|
|
1116
|
+
}, (err, result) => {
|
|
1117
|
+
if (err) {
|
|
1118
|
+
reject(err);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
resolve({
|
|
1122
|
+
email: (result.email || "").toString().trim(),
|
|
1123
|
+
password: (result.password || "").toString(),
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
function promptForRegistration() {
|
|
1129
|
+
return new Promise((resolve, reject) => {
|
|
1130
|
+
prompt.message = "";
|
|
1131
|
+
prompt.delimiter = "";
|
|
1132
|
+
prompt.start();
|
|
1133
|
+
prompt.get({
|
|
1134
|
+
properties: {
|
|
1135
|
+
email: { description: chalk.cyan("Email: ") },
|
|
1136
|
+
name: { description: chalk.cyan("Name (optional): "), default: "" },
|
|
1137
|
+
password: {
|
|
1138
|
+
description: chalk.cyan("Password (min 12 characters): "),
|
|
1139
|
+
hidden: true,
|
|
1140
|
+
replace: "*",
|
|
1141
|
+
},
|
|
1142
|
+
confirmPassword: {
|
|
1143
|
+
description: chalk.cyan("Confirm password: "),
|
|
1144
|
+
hidden: true,
|
|
1145
|
+
replace: "*",
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
}, (err, result) => {
|
|
1149
|
+
if (err) {
|
|
1150
|
+
reject(err);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
resolve({
|
|
1154
|
+
email: (result.email || "").toString().trim(),
|
|
1155
|
+
name: (result.name || "").toString().trim(),
|
|
1156
|
+
password: (result.password || "").toString(),
|
|
1157
|
+
confirmPassword: (result.confirmPassword || "").toString(),
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
const runReactNativeBundleCommand = (bundleName, development, entryFile, outputFolder, platform, sourcemapOutput) => {
|
|
1163
|
+
const reactNativeBundleArgs = [];
|
|
1164
|
+
const envNodeArgs = process.env.CODE_PUSH_NODE_ARGS;
|
|
1165
|
+
if (typeof envNodeArgs !== "undefined") {
|
|
1166
|
+
Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/));
|
|
1167
|
+
}
|
|
1168
|
+
const isOldCLI = fs.existsSync(path.join("node_modules", "react-native", "local-cli", "cli.js"));
|
|
1169
|
+
Array.prototype.push.apply(reactNativeBundleArgs, [
|
|
1170
|
+
isOldCLI ? path.join("node_modules", "react-native", "local-cli", "cli.js") : path.join("node_modules", "react-native", "cli.js"),
|
|
1171
|
+
"bundle",
|
|
1172
|
+
"--assets-dest",
|
|
1173
|
+
outputFolder,
|
|
1174
|
+
"--bundle-output",
|
|
1175
|
+
path.join(outputFolder, bundleName),
|
|
1176
|
+
"--dev",
|
|
1177
|
+
development,
|
|
1178
|
+
"--entry-file",
|
|
1179
|
+
entryFile,
|
|
1180
|
+
"--platform",
|
|
1181
|
+
platform,
|
|
1182
|
+
]);
|
|
1183
|
+
if (sourcemapOutput) {
|
|
1184
|
+
reactNativeBundleArgs.push("--sourcemap-output", sourcemapOutput);
|
|
1185
|
+
}
|
|
1186
|
+
(0, exports.log)(chalk.cyan('Running "react-native bundle" command:\n'));
|
|
1187
|
+
const reactNativeBundleProcess = (0, exports.spawn)("node", reactNativeBundleArgs);
|
|
1188
|
+
(0, exports.log)(`node ${reactNativeBundleArgs.join(" ")}`);
|
|
1189
|
+
return new Promise((resolve, reject) => {
|
|
1190
|
+
reactNativeBundleProcess.stdout.on("data", (data) => {
|
|
1191
|
+
(0, exports.log)(data.toString().trim());
|
|
1192
|
+
});
|
|
1193
|
+
reactNativeBundleProcess.stderr.on("data", (data) => {
|
|
1194
|
+
console.error(data.toString().trim());
|
|
1195
|
+
});
|
|
1196
|
+
reactNativeBundleProcess.on("close", (exitCode) => {
|
|
1197
|
+
if (exitCode) {
|
|
1198
|
+
reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
|
|
1199
|
+
}
|
|
1200
|
+
resolve();
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
exports.runReactNativeBundleCommand = runReactNativeBundleCommand;
|
|
1205
|
+
function serializeConnectionInfo(accessKey, preserveAccessKeyOnLogout, customServerUrl) {
|
|
1206
|
+
const connectionInfo = {
|
|
1207
|
+
accessKey: accessKey,
|
|
1208
|
+
preserveAccessKeyOnLogout: preserveAccessKeyOnLogout,
|
|
1209
|
+
};
|
|
1210
|
+
if (customServerUrl) {
|
|
1211
|
+
connectionInfo.customServerUrl = customServerUrl;
|
|
1212
|
+
}
|
|
1213
|
+
fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
|
|
1214
|
+
const json = JSON.stringify(connectionInfo);
|
|
1215
|
+
fs.writeFileSync(configFilePath, json, { encoding: "utf8" });
|
|
1216
|
+
(0, exports.log)(`Session file written to ${chalk.cyan(configFilePath)}. Run ${chalk.cyan("aether logout")} to terminate the session.`);
|
|
1217
|
+
}
|
|
1218
|
+
function sessionList(command) {
|
|
1219
|
+
throwForInvalidOutputFormat(command.format);
|
|
1220
|
+
return exports.sdk.getSessions().then((sessions) => {
|
|
1221
|
+
printSessions(command.format, sessions);
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function sessionRemove(command) {
|
|
1225
|
+
if (os.hostname() === command.machineName) {
|
|
1226
|
+
throw new Error("Cannot remove the current login session via this command. Please run 'aether logout' instead.");
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
1230
|
+
if (wasConfirmed) {
|
|
1231
|
+
return exports.sdk.removeSessions(command.machineName).then(() => {
|
|
1232
|
+
(0, exports.log)(`Successfully removed the login session for "${command.machineName}".`);
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
(0, exports.log)("Session removal cancelled.");
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function releaseErrorHandler(error, command) {
|
|
1240
|
+
if (command.noDuplicateReleaseError && error.statusCode === 409) {
|
|
1241
|
+
console.warn(chalk.yellow("[Warning] " + error.message));
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
throw error;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function throwForInvalidEmail(email) {
|
|
1248
|
+
if (!emailValidator.validate(email)) {
|
|
1249
|
+
throw new Error('"' + email + '" is an invalid e-mail address.');
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function throwForInvalidSemverRange(semverRange) {
|
|
1253
|
+
if (semver.validRange(semverRange) === null) {
|
|
1254
|
+
throw new Error('Please use a semver-compliant target binary version range, for example "1.0.0", "*" or "^1.2.3".');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function throwForInvalidOutputFormat(format) {
|
|
1258
|
+
switch (format) {
|
|
1259
|
+
case "json":
|
|
1260
|
+
case "table":
|
|
1261
|
+
break;
|
|
1262
|
+
default:
|
|
1263
|
+
throw new Error("Invalid format: " + format + ".");
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function whoami(command) {
|
|
1267
|
+
return exports.sdk.getAccountInfo().then((account) => {
|
|
1268
|
+
(0, exports.log)(account.email);
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
function isCommandOptionSpecified(option) {
|
|
1272
|
+
return option !== undefined && option !== null;
|
|
1273
|
+
}
|
|
1274
|
+
function getSdk(accessKey, headers, customServerUrl) {
|
|
1275
|
+
const sdk = new AccountManager(accessKey, CLI_HEADERS, customServerUrl);
|
|
1276
|
+
/*
|
|
1277
|
+
* If the server returns `Unauthorized`, it must be due to an invalid
|
|
1278
|
+
* (or expired) access key. For convenience, we patch every SDK call
|
|
1279
|
+
* to delete the cached connection so the user can simply
|
|
1280
|
+
* login again instead of having to log out first.
|
|
1281
|
+
*/
|
|
1282
|
+
Object.getOwnPropertyNames(AccountManager.prototype).forEach((functionName) => {
|
|
1283
|
+
if (typeof sdk[functionName] === "function") {
|
|
1284
|
+
const originalFunction = sdk[functionName];
|
|
1285
|
+
sdk[functionName] = function () {
|
|
1286
|
+
let maybePromise = originalFunction.apply(sdk, arguments);
|
|
1287
|
+
if (maybePromise && maybePromise.then !== undefined) {
|
|
1288
|
+
maybePromise = maybePromise.catch((error) => {
|
|
1289
|
+
if (error.statusCode && error.statusCode === 401) {
|
|
1290
|
+
deleteConnectionInfoCache(/* printMessage */ false);
|
|
1291
|
+
}
|
|
1292
|
+
throw error;
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
return maybePromise;
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
return sdk;
|
|
1300
|
+
}
|
|
1301
|
+
//# sourceMappingURL=command-executor.js.map
|