@appcircle/codepush-cli 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.eslintrc.json +17 -0
  2. package/.github/pre-push +30 -0
  3. package/.github/prepare-commit--msg +2 -0
  4. package/CONTRIBUTING.md +71 -0
  5. package/Dockerfile +9 -0
  6. package/Jenkinsfile +45 -0
  7. package/README.md +837 -0
  8. package/bin/script/acquisition-sdk.js +178 -0
  9. package/bin/script/cli.js +23 -0
  10. package/bin/script/command-executor.js +1292 -0
  11. package/bin/script/command-parser.js +1123 -0
  12. package/bin/script/commands/debug.js +125 -0
  13. package/bin/script/hash-utils.js +203 -0
  14. package/bin/script/index.js +5 -0
  15. package/bin/script/management-sdk.js +454 -0
  16. package/bin/script/react-native-utils.js +249 -0
  17. package/bin/script/sign.js +69 -0
  18. package/bin/script/types/cli.js +40 -0
  19. package/bin/script/types/rest-definitions.js +19 -0
  20. package/bin/script/types.js +4 -0
  21. package/bin/script/utils/file-utils.js +50 -0
  22. package/bin/test/acquisition-rest-mock.js +108 -0
  23. package/bin/test/acquisition-sdk.js +188 -0
  24. package/bin/test/cli.js +1342 -0
  25. package/bin/test/hash-utils.js +149 -0
  26. package/bin/test/management-sdk.js +338 -0
  27. package/package.json +74 -0
  28. package/prettier.config.js +7 -0
  29. package/script/acquisition-sdk.ts +273 -0
  30. package/script/cli.ts +27 -0
  31. package/script/command-executor.ts +1614 -0
  32. package/script/command-parser.ts +1340 -0
  33. package/script/commands/debug.ts +148 -0
  34. package/script/hash-utils.ts +241 -0
  35. package/script/index.ts +5 -0
  36. package/script/management-sdk.ts +627 -0
  37. package/script/react-native-utils.ts +283 -0
  38. package/script/sign.ts +80 -0
  39. package/script/types/cli.ts +234 -0
  40. package/script/types/rest-definitions.ts +152 -0
  41. package/script/types.ts +35 -0
  42. package/script/utils/check-package.mjs +11 -0
  43. package/script/utils/file-utils.ts +46 -0
  44. package/test/acquisition-rest-mock.ts +125 -0
  45. package/test/acquisition-sdk.ts +272 -0
  46. package/test/cli.ts +1692 -0
  47. package/test/hash-utils.ts +170 -0
  48. package/test/management-sdk.ts +438 -0
  49. package/test/resources/TestApp/android/app/build.gradle +56 -0
  50. package/test/resources/TestApp/iOS/TestApp/Info.plist +49 -0
  51. package/test/resources/TestApp/index.android.js +2 -0
  52. package/test/resources/TestApp/index.ios.js +2 -0
  53. package/test/resources/TestApp/index.windows.js +2 -0
  54. package/test/resources/TestApp/package.json +6 -0
  55. package/test/resources/TestApp/windows/TestApp/Package.appxmanifest +46 -0
  56. package/test/resources/ignoredMetadata.zip +0 -0
  57. package/test/resources/test.zip +0 -0
  58. package/test/superagent-mock-config.js +58 -0
  59. package/tsconfig.json +13 -0
@@ -0,0 +1,1614 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ import AccountManager = require("./management-sdk");
5
+ const childProcess = require("child_process");
6
+ import debugCommand from "./commands/debug";
7
+ import * as fs from "fs";
8
+ import * as chalk from "chalk";
9
+ const g2js = require("gradle-to-js/lib/parser");
10
+ import * as moment from "moment";
11
+ const opener = require("opener");
12
+ import * as os from "os";
13
+ import * as path from "path";
14
+ const plist = require("plist");
15
+ const progress = require("progress");
16
+ const prompt = require("prompt");
17
+ import * as Q from "q";
18
+ const rimraf = require("rimraf");
19
+ import * as semver from "semver";
20
+ const Table = require("cli-table");
21
+ const which = require("which");
22
+ import wordwrap = require("wordwrap");
23
+ import * as cli from "../script/types/cli";
24
+ import sign from "./sign";
25
+ const xcode = require("xcode");
26
+ import {
27
+ AccessKey,
28
+ Account,
29
+ App,
30
+ CodePushError,
31
+ CollaboratorMap,
32
+ CollaboratorProperties,
33
+ Deployment,
34
+ DeploymentMetrics,
35
+ Headers,
36
+ Package,
37
+ PackageInfo,
38
+ Session,
39
+ UpdateMetrics,
40
+ } from "../script/types";
41
+ import {
42
+ getAndroidHermesEnabled,
43
+ getiOSHermesEnabled,
44
+ runHermesEmitBinaryCommand,
45
+ isValidVersion
46
+ } from "./react-native-utils";
47
+ import {
48
+ fileDoesNotExistOrIsDirectory,
49
+ isBinaryOrZip,
50
+ fileExists
51
+ } from "./utils/file-utils";
52
+
53
+ const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config");
54
+ const emailValidator = require("email-validator");
55
+ const packageJson = require("../../package.json");
56
+ const parseXml = Q.denodeify(require("xml2js").parseString);
57
+ import Promise = Q.Promise;
58
+ const properties = require("properties");
59
+
60
+ const CLI_HEADERS: Headers = {
61
+ "X-CodePush-CLI-Version": packageJson.version,
62
+ };
63
+
64
+ /** Deprecated */
65
+ interface ILegacyLoginConnectionInfo {
66
+ accessKeyName: string;
67
+ }
68
+
69
+ interface ILoginConnectionInfo {
70
+ accessKey: string;
71
+ customServerUrl?: string; // A custom serverUrl for internal debugging purposes or self-hosted releases
72
+ customAuthUrl?: string; // A custom serverUrl for internal debugging purposes or self-hosted releases
73
+ preserveAccessKeyOnLogout?: boolean;
74
+ }
75
+
76
+ export interface UpdateMetricsWithTotalActive extends UpdateMetrics {
77
+ totalActive: number;
78
+ }
79
+
80
+ export interface PackageWithMetrics {
81
+ metrics?: UpdateMetricsWithTotalActive;
82
+ }
83
+
84
+ export const log = (message: string | any): void => console.log(message);
85
+ export let sdk: AccountManager;
86
+ export const spawn = childProcess.spawn;
87
+ export const execSync = childProcess.execSync;
88
+
89
+ let connectionInfo: ILoginConnectionInfo;
90
+
91
+ export const confirm = (message: string = "Are you sure?"): Promise<boolean> => {
92
+ message += " (y/N):";
93
+ return Promise<boolean>((resolve, reject, notify): void => {
94
+ prompt.message = "";
95
+ prompt.delimiter = "";
96
+
97
+ prompt.start();
98
+
99
+ prompt.get(
100
+ {
101
+ properties: {
102
+ response: {
103
+ description: chalk.cyan(message),
104
+ },
105
+ },
106
+ },
107
+ (err: any, result: any): void => {
108
+ const accepted = result.response && result.response.toLowerCase() === "y";
109
+ const rejected = !result.response || result.response.toLowerCase() === "n";
110
+
111
+ if (accepted) {
112
+ resolve(true);
113
+ } else {
114
+ if (!rejected) {
115
+ console.log('Invalid response: "' + result.response + '"');
116
+ }
117
+ resolve(false);
118
+ }
119
+ }
120
+ );
121
+ });
122
+ };
123
+
124
+ function accessKeyAdd(command: cli.IAccessKeyAddCommand): Promise<void> {
125
+ return sdk.addAccessKey(command.name, command.ttl).then((accessKey: AccessKey) => {
126
+ log(`Successfully created the "${command.name}" access key: ${accessKey.key}`);
127
+ log("Make sure to save this key value somewhere safe, since you won't be able to view it from the CLI again!");
128
+ });
129
+ }
130
+
131
+ function accessKeyPatch(command: cli.IAccessKeyPatchCommand): Promise<void> {
132
+ const willUpdateName: boolean = isCommandOptionSpecified(command.newName) && command.oldName !== command.newName;
133
+ const willUpdateTtl: boolean = isCommandOptionSpecified(command.ttl);
134
+
135
+ if (!willUpdateName && !willUpdateTtl) {
136
+ throw new Error("A new name and/or TTL must be provided.");
137
+ }
138
+
139
+ return sdk.patchAccessKey(command.oldName, command.newName, command.ttl).then((accessKey: AccessKey) => {
140
+ let logMessage: string = "Successfully ";
141
+ if (willUpdateName) {
142
+ logMessage += `renamed the access key "${command.oldName}" to "${command.newName}"`;
143
+ }
144
+
145
+ if (willUpdateTtl) {
146
+ const expirationDate = moment(accessKey.expires).format("LLLL");
147
+ if (willUpdateName) {
148
+ logMessage += ` and changed its expiration date to ${expirationDate}`;
149
+ } else {
150
+ logMessage += `changed the expiration date of the "${command.oldName}" access key to ${expirationDate}`;
151
+ }
152
+ }
153
+
154
+ log(`${logMessage}.`);
155
+ });
156
+ }
157
+
158
+ function accessKeyList(command: cli.IAccessKeyListCommand): Promise<void> {
159
+ throwForInvalidOutputFormat(command.format);
160
+
161
+ return sdk.getAccessKeys().then((accessKeys: AccessKey[]): void => {
162
+ printAccessKeys(command.format, accessKeys);
163
+ });
164
+ }
165
+
166
+ function accessKeyRemove(command: cli.IAccessKeyRemoveCommand): Promise<void> {
167
+ return confirm().then((wasConfirmed: boolean): Promise<void> => {
168
+ if (wasConfirmed) {
169
+ return sdk.removeAccessKey(command.accessKey).then((): void => {
170
+ log(`Successfully removed the "${command.accessKey}" access key.`);
171
+ });
172
+ }
173
+
174
+ log("Access key removal cancelled.");
175
+ });
176
+ }
177
+
178
+ function appAdd(command: cli.IAppAddCommand): Promise<void> {
179
+ return sdk.addApp(command.appName).then((app: App): Promise<void> => {
180
+ log('Successfully added the "' + command.appName + '" app, along with the following default deployments:');
181
+ const deploymentListCommand: cli.IDeploymentListCommand = {
182
+ type: cli.CommandType.deploymentList,
183
+ appName: app.name,
184
+ format: "table",
185
+ displayKeys: true,
186
+ };
187
+ return deploymentList(deploymentListCommand, /*showPackage=*/ false);
188
+ });
189
+ }
190
+
191
+ function appList(command: cli.IAppListCommand): Promise<void> {
192
+ throwForInvalidOutputFormat(command.format);
193
+ let apps: App[];
194
+ return sdk.getApps().then((retrievedApps: App[]): void => {
195
+ printAppList(command.format, retrievedApps);
196
+ });
197
+ }
198
+
199
+ function appRemove(command: cli.IAppRemoveCommand): Promise<void> {
200
+ return confirm("Are you sure you want to remove this app? Note that its deployment keys will be PERMANENTLY unrecoverable.").then(
201
+ (wasConfirmed: boolean): Promise<void> => {
202
+ if (wasConfirmed) {
203
+ return sdk.removeApp(command.appName).then((): void => {
204
+ log('Successfully removed the "' + command.appName + '" app.');
205
+ });
206
+ }
207
+
208
+ log("App removal cancelled.");
209
+ }
210
+ );
211
+ }
212
+
213
+ function appRename(command: cli.IAppRenameCommand): Promise<void> {
214
+ return sdk.renameApp(command.currentAppName, command.newAppName).then((): void => {
215
+ log('Successfully renamed the "' + command.currentAppName + '" app to "' + command.newAppName + '".');
216
+ });
217
+ }
218
+
219
+ export const createEmptyTempReleaseFolder = (folderPath: string) => {
220
+ return deleteFolder(folderPath).then(() => {
221
+ fs.mkdirSync(folderPath);
222
+ });
223
+ };
224
+
225
+ function appTransfer(command: cli.IAppTransferCommand): Promise<void> {
226
+ throwForInvalidEmail(command.email);
227
+
228
+ return confirm().then((wasConfirmed: boolean): Promise<void> => {
229
+ if (wasConfirmed) {
230
+ return sdk.transferApp(command.appName, command.email).then((): void => {
231
+ log(
232
+ 'Successfully transferred the ownership of app "' + command.appName + '" to the account with email "' + command.email + '".'
233
+ );
234
+ });
235
+ }
236
+
237
+ log("App transfer cancelled.");
238
+ });
239
+ }
240
+
241
+ function addCollaborator(command: cli.ICollaboratorAddCommand): Promise<void> {
242
+ throwForInvalidEmail(command.email);
243
+
244
+ return sdk.addCollaborator(command.appName, command.email).then((): void => {
245
+ log('Successfully added "' + command.email + '" as a collaborator to the app "' + command.appName + '".');
246
+ });
247
+ }
248
+
249
+ function listCollaborators(command: cli.ICollaboratorListCommand): Promise<void> {
250
+ throwForInvalidOutputFormat(command.format);
251
+
252
+ return sdk.getCollaborators(command.appName).then((retrievedCollaborators: CollaboratorMap): void => {
253
+ printCollaboratorsList(command.format, retrievedCollaborators);
254
+ });
255
+ }
256
+
257
+ function removeCollaborator(command: cli.ICollaboratorRemoveCommand): Promise<void> {
258
+ throwForInvalidEmail(command.email);
259
+
260
+ return confirm().then((wasConfirmed: boolean): Promise<void> => {
261
+ if (wasConfirmed) {
262
+ return sdk.removeCollaborator(command.appName, command.email).then((): void => {
263
+ log('Successfully removed "' + command.email + '" as a collaborator from the app "' + command.appName + '".');
264
+ });
265
+ }
266
+
267
+ log("App collaborator removal cancelled.");
268
+ });
269
+ }
270
+
271
+ function deleteConnectionInfoCache(printMessage: boolean = true): void {
272
+ try {
273
+ fs.unlinkSync(configFilePath);
274
+
275
+ if (printMessage) {
276
+ log(`Successfully logged-out. The session file located at ${chalk.cyan(configFilePath)} has been deleted.\r\n`);
277
+ }
278
+ } catch (ex) {}
279
+ }
280
+
281
+ function deleteFolder(folderPath: string): Promise<void> {
282
+ return Promise<void>((resolve, reject, notify) => {
283
+ rimraf(folderPath, (err: any) => {
284
+ if (err) {
285
+ reject(err);
286
+ } else {
287
+ resolve(<void>null);
288
+ }
289
+ });
290
+ });
291
+ }
292
+
293
+ function deploymentAdd(command: cli.IDeploymentAddCommand): Promise<void> {
294
+ return sdk.addDeployment(command.appName, command.deploymentName, command.key).then((deployment: Deployment): void => {
295
+ log(
296
+ 'Successfully added the "' +
297
+ command.deploymentName +
298
+ '" deployment with key "' +
299
+ deployment.key +
300
+ '" to the "' +
301
+ command.appName +
302
+ '" app.'
303
+ );
304
+ });
305
+ }
306
+
307
+ function deploymentHistoryClear(command: cli.IDeploymentHistoryClearCommand): Promise<void> {
308
+ return confirm().then((wasConfirmed: boolean): Promise<void> => {
309
+ if (wasConfirmed) {
310
+ return sdk.clearDeploymentHistory(command.appName, command.deploymentName).then((): void => {
311
+ log(
312
+ 'Successfully cleared the release history associated with the "' +
313
+ command.deploymentName +
314
+ '" deployment from the "' +
315
+ command.appName +
316
+ '" app.'
317
+ );
318
+ });
319
+ }
320
+
321
+ log("Clear deployment cancelled.");
322
+ });
323
+ }
324
+
325
+ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage: boolean = true): Promise<void> => {
326
+ throwForInvalidOutputFormat(command.format);
327
+ let deployments: Deployment[];
328
+
329
+ return sdk
330
+ .getDeployments(command.appName)
331
+ .then((retrievedDeployments: Deployment[]) => {
332
+ deployments = retrievedDeployments;
333
+ if (showPackage) {
334
+ const metricsPromises: Promise<void>[] = deployments.map((deployment: Deployment) => {
335
+ if (deployment.package) {
336
+ return sdk.getDeploymentMetrics(command.appName, deployment.name).then((metrics: DeploymentMetrics): void => {
337
+ if (metrics[deployment.package.label]) {
338
+ const totalActive: number = getTotalActiveFromDeploymentMetrics(metrics);
339
+ (<PackageWithMetrics>deployment.package).metrics = {
340
+ active: metrics[deployment.package.label].active,
341
+ downloaded: metrics[deployment.package.label].downloaded,
342
+ failed: metrics[deployment.package.label].failed,
343
+ installed: metrics[deployment.package.label].installed,
344
+ totalActive: totalActive,
345
+ };
346
+ }
347
+ });
348
+ } else {
349
+ return Q(<void>null);
350
+ }
351
+ });
352
+
353
+ return Q.all(metricsPromises);
354
+ }
355
+ })
356
+ .then(() => {
357
+ printDeploymentList(command, deployments, showPackage);
358
+ });
359
+ };
360
+
361
+ function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise<void> {
362
+ return confirm(
363
+ "Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable."
364
+ ).then((wasConfirmed: boolean): Promise<void> => {
365
+ if (wasConfirmed) {
366
+ return sdk.removeDeployment(command.appName, command.deploymentName).then((): void => {
367
+ log('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
368
+ });
369
+ }
370
+
371
+ log("Deployment removal cancelled.");
372
+ });
373
+ }
374
+
375
+ function deploymentRename(command: cli.IDeploymentRenameCommand): Promise<void> {
376
+ return sdk.renameDeployment(command.appName, command.currentDeploymentName, command.newDeploymentName).then((): void => {
377
+ log(
378
+ 'Successfully renamed the "' +
379
+ command.currentDeploymentName +
380
+ '" deployment to "' +
381
+ command.newDeploymentName +
382
+ '" for the "' +
383
+ command.appName +
384
+ '" app.'
385
+ );
386
+ });
387
+ }
388
+
389
+ function deploymentHistory(command: cli.IDeploymentHistoryCommand): Promise<void> {
390
+ throwForInvalidOutputFormat(command.format);
391
+
392
+ return Q.all<any>([
393
+ sdk.getAccountInfo(),
394
+ sdk.getDeploymentHistory(command.appName, command.deploymentName),
395
+ sdk.getDeploymentMetrics(command.appName, command.deploymentName),
396
+ ]).spread<void>((account: Account, deploymentHistory: Package[], metrics: DeploymentMetrics): void => {
397
+ const totalActive: number = getTotalActiveFromDeploymentMetrics(metrics);
398
+ deploymentHistory.forEach((packageObject: Package) => {
399
+ if (metrics[packageObject.label]) {
400
+ (<PackageWithMetrics>packageObject).metrics = {
401
+ active: metrics[packageObject.label].active,
402
+ downloaded: metrics[packageObject.label].downloaded,
403
+ failed: metrics[packageObject.label].failed,
404
+ installed: metrics[packageObject.label].installed,
405
+ totalActive: totalActive,
406
+ };
407
+ }
408
+ });
409
+ printDeploymentHistory(command, <Package[]>deploymentHistory, account.email);
410
+ });
411
+ }
412
+
413
+ function deserializeConnectionInfo(): ILoginConnectionInfo {
414
+ try {
415
+ const savedConnection: string = fs.readFileSync(configFilePath, {
416
+ encoding: "utf8",
417
+ });
418
+ let connectionInfo: ILegacyLoginConnectionInfo | ILoginConnectionInfo = JSON.parse(savedConnection);
419
+
420
+ // If the connection info is in the legacy format, convert it to the modern format
421
+ if ((<ILegacyLoginConnectionInfo>connectionInfo).accessKeyName) {
422
+ connectionInfo = <ILoginConnectionInfo>{
423
+ accessKey: (<ILegacyLoginConnectionInfo>connectionInfo).accessKeyName,
424
+ };
425
+ }
426
+
427
+ const connInfo = <ILoginConnectionInfo>connectionInfo;
428
+
429
+ return connInfo;
430
+ } catch (ex) {
431
+ return;
432
+ }
433
+ }
434
+
435
+ export function execute(command: cli.ICommand) {
436
+ connectionInfo = deserializeConnectionInfo();
437
+
438
+ return Q(<void>null).then(() => {
439
+ switch (command.type) {
440
+ // Must not be logged in
441
+ case cli.CommandType.login:
442
+ case cli.CommandType.register:
443
+ if (connectionInfo) {
444
+ throw new Error("You are already logged in from this machine.");
445
+ }
446
+ break;
447
+
448
+ // It does not matter whether you are logged in or not
449
+ case cli.CommandType.link:
450
+ break;
451
+
452
+ // Must be logged in
453
+ default:
454
+ if (!!sdk) break; // Used by unit tests to skip authentication
455
+
456
+ if (!connectionInfo) {
457
+ throw new Error(
458
+ "You are not currently logged in. Run the 'appcircle-code-push login' command to authenticate with the CodePush server."
459
+ );
460
+ }
461
+
462
+ sdk = getSdk(connectionInfo.accessKey, null,CLI_HEADERS, connectionInfo.customServerUrl, connectionInfo.customAuthUrl);
463
+ break;
464
+ }
465
+
466
+ switch (command.type) {
467
+ case cli.CommandType.accessKeyAdd:
468
+ return accessKeyAdd(<cli.IAccessKeyAddCommand>command);
469
+
470
+ case cli.CommandType.accessKeyPatch:
471
+ return accessKeyPatch(<cli.IAccessKeyPatchCommand>command);
472
+
473
+ case cli.CommandType.accessKeyList:
474
+ return accessKeyList(<cli.IAccessKeyListCommand>command);
475
+
476
+ case cli.CommandType.accessKeyRemove:
477
+ return accessKeyRemove(<cli.IAccessKeyRemoveCommand>command);
478
+
479
+ case cli.CommandType.appAdd:
480
+ return appAdd(<cli.IAppAddCommand>command);
481
+
482
+ case cli.CommandType.appList:
483
+ return appList(<cli.IAppListCommand>command);
484
+
485
+ case cli.CommandType.appRemove:
486
+ return appRemove(<cli.IAppRemoveCommand>command);
487
+
488
+ case cli.CommandType.appRename:
489
+ return appRename(<cli.IAppRenameCommand>command);
490
+
491
+ case cli.CommandType.appTransfer:
492
+ return appTransfer(<cli.IAppTransferCommand>command);
493
+
494
+ case cli.CommandType.collaboratorAdd:
495
+ return addCollaborator(<cli.ICollaboratorAddCommand>command);
496
+
497
+ case cli.CommandType.collaboratorList:
498
+ return listCollaborators(<cli.ICollaboratorListCommand>command);
499
+
500
+ case cli.CommandType.collaboratorRemove:
501
+ return removeCollaborator(<cli.ICollaboratorRemoveCommand>command);
502
+
503
+ case cli.CommandType.debug:
504
+ return debugCommand(<cli.IDebugCommand>command);
505
+
506
+ case cli.CommandType.deploymentAdd:
507
+ return deploymentAdd(<cli.IDeploymentAddCommand>command);
508
+
509
+ case cli.CommandType.deploymentHistoryClear:
510
+ return deploymentHistoryClear(<cli.IDeploymentHistoryClearCommand>command);
511
+
512
+ case cli.CommandType.deploymentHistory:
513
+ return deploymentHistory(<cli.IDeploymentHistoryCommand>command);
514
+
515
+ case cli.CommandType.deploymentList:
516
+ return deploymentList(<cli.IDeploymentListCommand>command);
517
+
518
+ case cli.CommandType.deploymentRemove:
519
+ return deploymentRemove(<cli.IDeploymentRemoveCommand>command);
520
+
521
+ case cli.CommandType.deploymentRename:
522
+ return deploymentRename(<cli.IDeploymentRenameCommand>command);
523
+
524
+ case cli.CommandType.link:
525
+ return link(<cli.ILinkCommand>command);
526
+
527
+ case cli.CommandType.login:
528
+ return login(<cli.ILoginCommand>command);
529
+
530
+ case cli.CommandType.logout:
531
+ return logout(command);
532
+
533
+ case cli.CommandType.patch:
534
+ return patch(<cli.IPatchCommand>command);
535
+
536
+ case cli.CommandType.promote:
537
+ return promote(<cli.IPromoteCommand>command);
538
+
539
+ case cli.CommandType.register:
540
+ return register(<cli.IRegisterCommand>command);
541
+
542
+ case cli.CommandType.release:
543
+ return release(<cli.IReleaseCommand>command);
544
+
545
+ case cli.CommandType.releaseReact:
546
+ return releaseReact(<cli.IReleaseReactCommand>command);
547
+
548
+ case cli.CommandType.rollback:
549
+ return rollback(<cli.IRollbackCommand>command);
550
+
551
+ case cli.CommandType.sessionList:
552
+ return sessionList(<cli.ISessionListCommand>command);
553
+
554
+ case cli.CommandType.sessionRemove:
555
+ return sessionRemove(<cli.ISessionRemoveCommand>command);
556
+
557
+ case cli.CommandType.whoami:
558
+ return whoami(command);
559
+
560
+ default:
561
+ // We should never see this message as invalid commands should be caught by the argument parser.
562
+ throw new Error("Invalid command: " + JSON.stringify(command));
563
+ }
564
+ });
565
+ }
566
+
567
+ function getTotalActiveFromDeploymentMetrics(metrics: DeploymentMetrics): number {
568
+ let totalActive = 0;
569
+ Object.keys(metrics).forEach((label: string) => {
570
+ totalActive += metrics[label].active;
571
+ });
572
+
573
+ return totalActive;
574
+ }
575
+
576
+ function initiateExternalAuthenticationAsync(action: string, serverUrl?: string): void {
577
+ const message: string =
578
+ `A browser is being launched to authenticate your account. Follow the instructions ` +
579
+ `it displays to complete your ${action === "register" ? "registration" : action}.`;
580
+
581
+ log(message);
582
+ const hostname: string = os.hostname();
583
+ const url: string = `${serverUrl || AccountManager.SERVER_URL}/auth/${action}?hostname=${hostname}`;
584
+ opener(url);
585
+ }
586
+
587
+ function link(command: cli.ILinkCommand): Promise<void> {
588
+ initiateExternalAuthenticationAsync("link", command.serverUrl);
589
+ return Q(<void>null);
590
+ }
591
+
592
+ function login(command: cli.ILoginCommand): Promise<void> {
593
+ // Check if one of the flags were provided.
594
+ if (command.pat) {
595
+ sdk = getSdk(null, command.pat, CLI_HEADERS, command.serverUrl, command.authUrl);
596
+ return sdk.isAuthenticated().then((isAuthenticated: boolean): void => {
597
+ if (isAuthenticated) {
598
+ serializeConnectionInfo(sdk.accessKey, /*preserveAccessKeyOnLogout*/ true, command.serverUrl, command.authUrl);
599
+ } else {
600
+ throw new Error("Invalid access key.");
601
+ }
602
+ });
603
+ } else {
604
+ return loginWithExternalAuthentication("login", command.serverUrl, command.authUrl);
605
+ }
606
+ }
607
+
608
+ function loginWithExternalAuthentication(action: string, serverUrl?: string, authUrl?: string): Promise<void> {
609
+ log(""); // Insert newline
610
+
611
+ return requestAccessKey().then((accessKey: string): Promise<void> => {
612
+ if (accessKey === null) {
613
+ // The user has aborted the synchronous prompt (e.g.: via [CTRL]+[C]).
614
+ return;
615
+ }
616
+
617
+ sdk = getSdk(accessKey, null, CLI_HEADERS, serverUrl, authUrl);
618
+
619
+ return sdk.isAuthenticated().then((isAuthenticated: boolean): void => {
620
+ if (isAuthenticated) {
621
+ serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl, authUrl);
622
+ } else {
623
+ throw new Error("Invalid access key.");
624
+ }
625
+ });
626
+ });
627
+ }
628
+
629
+ function logout(command: cli.ICommand): Promise<void> {
630
+ return Q(<void>null)
631
+ .then((): Promise<void> => {
632
+ if (!connectionInfo.preserveAccessKeyOnLogout) {
633
+ const machineName: string = os.hostname();
634
+ return sdk.removeSession(machineName).catch((error: CodePushError) => {
635
+ // If we are not authenticated or the session doesn't exist anymore, just swallow the error instead of displaying it
636
+ if (error.statusCode !== AccountManager.ERROR_UNAUTHORIZED && error.statusCode !== AccountManager.ERROR_NOT_FOUND) {
637
+ throw error;
638
+ }
639
+ });
640
+ }
641
+ })
642
+ .then((): void => {
643
+ sdk = null;
644
+ deleteConnectionInfoCache();
645
+ });
646
+ }
647
+
648
+ function formatDate(unixOffset: number): string {
649
+ const date: moment.Moment = moment(unixOffset);
650
+ const now: moment.Moment = moment();
651
+ if (Math.abs(now.diff(date, "days")) < 30) {
652
+ return date.fromNow(); // "2 hours ago"
653
+ } else if (now.year() === date.year()) {
654
+ return date.format("MMM D"); // "Nov 6"
655
+ } else {
656
+ return date.format("MMM D, YYYY"); // "Nov 6, 2014"
657
+ }
658
+ }
659
+
660
+ function printAppList(format: string, apps: App[]): void {
661
+ if (format === "json") {
662
+ printJson(apps);
663
+ } else if (format === "table") {
664
+ const headers = ["Name", "Deployments"];
665
+ printTable(headers, (dataSource: any[]): void => {
666
+ apps.forEach((app: App, index: number): void => {
667
+ const row = [app.name, wordwrap(50)(app.deployments.join(", "))];
668
+ dataSource.push(row);
669
+ });
670
+ });
671
+ }
672
+ }
673
+
674
+ function getCollaboratorDisplayName(email: string, collaboratorProperties: CollaboratorProperties): string {
675
+ return collaboratorProperties.permission === AccountManager.AppPermission.OWNER ? email + chalk.magenta(" (Owner)") : email;
676
+ }
677
+
678
+ function printCollaboratorsList(format: string, collaborators: CollaboratorMap): void {
679
+ if (format === "json") {
680
+ const dataSource = { collaborators: collaborators };
681
+ printJson(dataSource);
682
+ } else if (format === "table") {
683
+ const headers = ["E-mail Address"];
684
+ printTable(headers, (dataSource: any[]): void => {
685
+ Object.keys(collaborators).forEach((email: string): void => {
686
+ const row = [getCollaboratorDisplayName(email, collaborators[email])];
687
+ dataSource.push(row);
688
+ });
689
+ });
690
+ }
691
+ }
692
+
693
+ function printDeploymentList(command: cli.IDeploymentListCommand, deployments: Deployment[], showPackage: boolean = true): void {
694
+ if (command.format === "json") {
695
+ printJson(deployments);
696
+ } else if (command.format === "table") {
697
+ const headers = ["Name"];
698
+ if (command.displayKeys) {
699
+ headers.push("Deployment Key");
700
+ }
701
+
702
+ if (showPackage) {
703
+ headers.push("Update Metadata");
704
+ headers.push("Install Metrics");
705
+ }
706
+
707
+ printTable(headers, (dataSource: any[]): void => {
708
+ deployments.forEach((deployment: Deployment): void => {
709
+ const row = [deployment.name];
710
+ if (command.displayKeys) {
711
+ row.push(deployment.key);
712
+ }
713
+
714
+ if (showPackage) {
715
+ row.push(getPackageString(deployment.package));
716
+ row.push(getPackageMetricsString(deployment.package));
717
+ }
718
+
719
+ dataSource.push(row);
720
+ });
721
+ });
722
+ }
723
+ }
724
+
725
+ function printDeploymentHistory(command: cli.IDeploymentHistoryCommand, deploymentHistory: Package[], currentUserEmail: string): void {
726
+ if (command.format === "json") {
727
+ printJson(deploymentHistory);
728
+ } else if (command.format === "table") {
729
+ const headers = ["Label", "Release Time", "App Version", "Mandatory"];
730
+ if (command.displayAuthor) {
731
+ headers.push("Released By");
732
+ }
733
+
734
+ headers.push("Description", "Install Metrics");
735
+
736
+ printTable(headers, (dataSource: any[]) => {
737
+ deploymentHistory.forEach((packageObject: Package) => {
738
+ let releaseTime: string = formatDate(packageObject.uploadTime);
739
+ let releaseSource: string;
740
+ if (packageObject.releaseMethod === "Promote") {
741
+ releaseSource = `Promoted ${packageObject.originalLabel} from "${packageObject.originalDeployment}"`;
742
+ } else if (packageObject.releaseMethod === "Rollback") {
743
+ const labelNumber: number = parseInt(packageObject.label.substring(1));
744
+ const lastLabel: string = "v" + (labelNumber - 1);
745
+ releaseSource = `Rolled back ${lastLabel} to ${packageObject.originalLabel}`;
746
+ }
747
+
748
+ if (releaseSource) {
749
+ releaseTime += "\n" + chalk.magenta(`(${releaseSource})`).toString();
750
+ }
751
+
752
+ let row: string[] = [packageObject.label, releaseTime, packageObject.appVersion, packageObject.isMandatory ? "Yes" : "No"];
753
+ if (command.displayAuthor) {
754
+ let releasedBy: string = packageObject.releasedBy ? packageObject.releasedBy : "";
755
+ if (currentUserEmail && releasedBy === currentUserEmail) {
756
+ releasedBy = "You";
757
+ }
758
+
759
+ row.push(releasedBy);
760
+ }
761
+
762
+ row.push(packageObject.description ? wordwrap(30)(packageObject.description) : "");
763
+ row.push(getPackageMetricsString(packageObject) + (packageObject.isDisabled ? `\n${chalk.green("Disabled:")} Yes` : ""));
764
+ if (packageObject.isDisabled) {
765
+ row = row.map((cellContents: string) => applyChalkSkippingLineBreaks(cellContents, (<any>chalk).dim));
766
+ }
767
+
768
+ dataSource.push(row);
769
+ });
770
+ });
771
+ }
772
+ }
773
+
774
+ function applyChalkSkippingLineBreaks(applyString: string, chalkMethod: (string: string) => any): string {
775
+ // Used to prevent "chalk" from applying styles to linebreaks which
776
+ // causes table border chars to have the style applied as well.
777
+ return applyString
778
+ .split("\n")
779
+ .map((token: string) => chalkMethod(token))
780
+ .join("\n");
781
+ }
782
+
783
+ function getPackageString(packageObject: Package): string {
784
+ if (!packageObject) {
785
+ return chalk.magenta("No updates released").toString();
786
+ }
787
+
788
+ let packageString: string =
789
+ chalk.green("Label: ") +
790
+ packageObject.label +
791
+ "\n" +
792
+ chalk.green("App Version: ") +
793
+ packageObject.appVersion +
794
+ "\n" +
795
+ chalk.green("Mandatory: ") +
796
+ (packageObject.isMandatory ? "Yes" : "No") +
797
+ "\n" +
798
+ chalk.green("Release Time: ") +
799
+ formatDate(packageObject.uploadTime) +
800
+ "\n" +
801
+ chalk.green("Released By: ") +
802
+ (packageObject.releasedBy ? packageObject.releasedBy : "") +
803
+ (packageObject.description ? wordwrap(70)("\n" + chalk.green("Description: ") + packageObject.description) : "");
804
+
805
+ if (packageObject.isDisabled) {
806
+ packageString += `\n${chalk.green("Disabled:")} Yes`;
807
+ }
808
+
809
+ return packageString;
810
+ }
811
+
812
+ function getPackageMetricsString(obj: Package): string {
813
+ const packageObject = <PackageWithMetrics>obj;
814
+ const rolloutString: string =
815
+ obj && obj.rollout && obj.rollout !== 100 ? `\n${chalk.green("Rollout:")} ${obj.rollout.toLocaleString()}%` : "";
816
+
817
+ if (!packageObject || !packageObject.metrics) {
818
+ return chalk.magenta("No installs recorded").toString() + (rolloutString || "");
819
+ }
820
+
821
+ const activePercent: number = packageObject.metrics.totalActive
822
+ ? (packageObject.metrics.active / packageObject.metrics.totalActive) * 100
823
+ : 0.0;
824
+ let percentString: string;
825
+ if (activePercent === 100.0) {
826
+ percentString = "100%";
827
+ } else if (activePercent === 0.0) {
828
+ percentString = "0%";
829
+ } else {
830
+ percentString = activePercent.toPrecision(2) + "%";
831
+ }
832
+
833
+ const numPending: number = packageObject.metrics.downloaded - packageObject.metrics.installed - packageObject.metrics.failed;
834
+ let returnString: string =
835
+ chalk.green("Active: ") +
836
+ percentString +
837
+ " (" +
838
+ packageObject.metrics.active.toLocaleString() +
839
+ " of " +
840
+ packageObject.metrics.totalActive.toLocaleString() +
841
+ ")\n" +
842
+ chalk.green("Total: ") +
843
+ packageObject.metrics.installed.toLocaleString();
844
+
845
+ if (numPending > 0) {
846
+ returnString += " (" + numPending.toLocaleString() + " pending)";
847
+ }
848
+
849
+ if (packageObject.metrics.failed) {
850
+ returnString += "\n" + chalk.green("Rollbacks: ") + chalk.red(packageObject.metrics.failed.toLocaleString() + "");
851
+ }
852
+
853
+ if (rolloutString) {
854
+ returnString += rolloutString;
855
+ }
856
+
857
+ return returnString;
858
+ }
859
+
860
+ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
861
+ log(chalk.cyan(`Detecting ${command.platform} app version:\n`));
862
+
863
+ if (command.platform === "ios") {
864
+ let resolvedPlistFile: string = command.plistFile;
865
+ if (resolvedPlistFile) {
866
+ // If a plist file path is explicitly provided, then we don't
867
+ // need to attempt to "resolve" it within the well-known locations.
868
+ if (!fileExists(resolvedPlistFile)) {
869
+ throw new Error("The specified plist file doesn't exist. Please check that the provided path is correct.");
870
+ }
871
+ } else {
872
+ // Allow the plist prefix to be specified with or without a trailing
873
+ // separator character, but prescribe the use of a hyphen when omitted,
874
+ // since this is the most commonly used convetion for plist files.
875
+ if (command.plistFilePrefix && /.+[^-.]$/.test(command.plistFilePrefix)) {
876
+ command.plistFilePrefix += "-";
877
+ }
878
+
879
+ const iOSDirectory: string = "ios";
880
+ const plistFileName = `${command.plistFilePrefix || ""}Info.plist`;
881
+
882
+ const knownLocations = [path.join(iOSDirectory, projectName, plistFileName), path.join(iOSDirectory, plistFileName)];
883
+
884
+ resolvedPlistFile = (<any>knownLocations).find(fileExists);
885
+
886
+ if (!resolvedPlistFile) {
887
+ throw new Error(
888
+ `Unable to find either of the following plist files in order to infer your app's binary version: "${knownLocations.join(
889
+ '", "'
890
+ )}". 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.`
891
+ );
892
+ }
893
+ }
894
+
895
+ const plistContents = fs.readFileSync(resolvedPlistFile).toString();
896
+
897
+ let parsedPlist;
898
+
899
+ try {
900
+ parsedPlist = plist.parse(plistContents);
901
+ } catch (e) {
902
+ throw new Error(`Unable to parse "${resolvedPlistFile}". Please ensure it is a well-formed plist file.`);
903
+ }
904
+
905
+ if (parsedPlist && parsedPlist.CFBundleShortVersionString) {
906
+ if (isValidVersion(parsedPlist.CFBundleShortVersionString)) {
907
+ log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
908
+ return Q(parsedPlist.CFBundleShortVersionString);
909
+ } else {
910
+ if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") {
911
+ throw new Error(
912
+ `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).`
913
+ );
914
+ }
915
+
916
+ return getAppVersionFromXcodeProject(command, projectName);
917
+ }
918
+ } else {
919
+ throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
920
+ }
921
+ } else if (command.platform === "android") {
922
+ let buildGradlePath: string = path.join("android", "app");
923
+ if (command.gradleFile) {
924
+ buildGradlePath = command.gradleFile;
925
+ }
926
+ if (fs.lstatSync(buildGradlePath).isDirectory()) {
927
+ buildGradlePath = path.join(buildGradlePath, "build.gradle");
928
+ }
929
+
930
+ if (fileDoesNotExistOrIsDirectory(buildGradlePath)) {
931
+ throw new Error(`Unable to find gradle file "${buildGradlePath}".`);
932
+ }
933
+
934
+ return g2js
935
+ .parseFile(buildGradlePath)
936
+ .catch(() => {
937
+ throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`);
938
+ })
939
+ .then((buildGradle: any) => {
940
+ let versionName: string = null;
941
+
942
+ // First 'if' statement was implemented as workaround for case
943
+ // when 'build.gradle' file contains several 'android' nodes.
944
+ // In this case 'buildGradle.android' prop represents array instead of object
945
+ // due to parsing issue in 'g2js.parseFile' method.
946
+ if (buildGradle.android instanceof Array) {
947
+ for (let i = 0; i < buildGradle.android.length; i++) {
948
+ const gradlePart = buildGradle.android[i];
949
+ if (gradlePart.defaultConfig && gradlePart.defaultConfig.versionName) {
950
+ versionName = gradlePart.defaultConfig.versionName;
951
+ break;
952
+ }
953
+ }
954
+ } else if (buildGradle.android && buildGradle.android.defaultConfig && buildGradle.android.defaultConfig.versionName) {
955
+ versionName = buildGradle.android.defaultConfig.versionName;
956
+ } else {
957
+ throw new Error(
958
+ `The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`
959
+ );
960
+ }
961
+
962
+ if (typeof versionName !== "string") {
963
+ throw new Error(
964
+ `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.`
965
+ );
966
+ }
967
+
968
+ let appVersion: string = versionName.replace(/"/g, "").trim();
969
+
970
+ if (isValidVersion(appVersion)) {
971
+ // The versionName property is a valid semver string,
972
+ // so we can safely use that and move on.
973
+ log(`Using the target binary version value "${appVersion}" from "${buildGradlePath}".\n`);
974
+ return appVersion;
975
+ } else if (/^\d.*/.test(appVersion)) {
976
+ // The versionName property isn't a valid semver string,
977
+ // but it starts with a number, and therefore, it can't
978
+ // be a valid Gradle property reference.
979
+ throw new Error(
980
+ `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).`
981
+ );
982
+ }
983
+
984
+ // The version property isn't a valid semver string
985
+ // so we assume it is a reference to a property variable.
986
+ const propertyName = appVersion.replace("project.", "");
987
+ const propertiesFileName = "gradle.properties";
988
+
989
+ const knownLocations = [path.join("android", "app", propertiesFileName), path.join("android", propertiesFileName)];
990
+
991
+ // Search for gradle properties across all `gradle.properties` files
992
+ let propertiesFile: string = null;
993
+ for (let i = 0; i < knownLocations.length; i++) {
994
+ propertiesFile = knownLocations[i];
995
+ if (fileExists(propertiesFile)) {
996
+ const propertiesContent: string = fs.readFileSync(propertiesFile).toString();
997
+ try {
998
+ const parsedProperties: any = properties.parse(propertiesContent);
999
+ appVersion = parsedProperties[propertyName];
1000
+ if (appVersion) {
1001
+ break;
1002
+ }
1003
+ } catch (e) {
1004
+ throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`);
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ if (!appVersion) {
1010
+ throw new Error(`No property named "${propertyName}" exists in the "${propertiesFile}" file.`);
1011
+ }
1012
+
1013
+ if (!isValidVersion(appVersion)) {
1014
+ throw new Error(
1015
+ `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).`
1016
+ );
1017
+ }
1018
+
1019
+ log(`Using the target binary version value "${appVersion}" from the "${propertyName}" key in the "${propertiesFile}" file.\n`);
1020
+ return appVersion.toString();
1021
+ });
1022
+ } else {
1023
+ const appxManifestFileName: string = "Package.appxmanifest";
1024
+ let appxManifestContainingFolder: string;
1025
+ let appxManifestContents: string;
1026
+
1027
+ try {
1028
+ appxManifestContainingFolder = path.join("windows", projectName);
1029
+ appxManifestContents = fs.readFileSync(path.join(appxManifestContainingFolder, "Package.appxmanifest")).toString();
1030
+ } catch (err) {
1031
+ throw new Error(`Unable to find or read "${appxManifestFileName}" in the "${path.join("windows", projectName)}" folder.`);
1032
+ }
1033
+
1034
+ return parseXml(appxManifestContents)
1035
+ .catch((err: any) => {
1036
+ throw new Error(
1037
+ `Unable to parse the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file, it could be malformed.`
1038
+ );
1039
+ })
1040
+ .then((parsedAppxManifest: any) => {
1041
+ try {
1042
+ return parsedAppxManifest.Package.Identity[0]["$"].Version.match(/^\d+\.\d+\.\d+/)[0];
1043
+ } catch (e) {
1044
+ throw new Error(
1045
+ `Unable to parse the package version from the "${path.join(appxManifestContainingFolder, appxManifestFileName)}" file.`
1046
+ );
1047
+ }
1048
+ });
1049
+ }
1050
+ }
1051
+
1052
+ function getAppVersionFromXcodeProject(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
1053
+ const pbxprojFileName = "project.pbxproj";
1054
+ let resolvedPbxprojFile: string = command.xcodeProjectFile;
1055
+ if (resolvedPbxprojFile) {
1056
+ // If the xcode project file path is explicitly provided, then we don't
1057
+ // need to attempt to "resolve" it within the well-known locations.
1058
+ if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) {
1059
+ // Specify path to pbxproj file if the provided file path is an Xcode project file.
1060
+ resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName);
1061
+ }
1062
+ if (!fileExists(resolvedPbxprojFile)) {
1063
+ throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct.");
1064
+ }
1065
+ } else {
1066
+ const iOSDirectory = "ios";
1067
+ const xcodeprojDirectory = `${projectName}.xcodeproj`;
1068
+ const pbxprojKnownLocations = [
1069
+ path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName),
1070
+ path.join(iOSDirectory, pbxprojFileName),
1071
+ ];
1072
+ resolvedPbxprojFile = pbxprojKnownLocations.find(fileExists);
1073
+
1074
+ if (!resolvedPbxprojFile) {
1075
+ throw new Error(
1076
+ `Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join(
1077
+ '", "'
1078
+ )}".`
1079
+ );
1080
+ }
1081
+ }
1082
+
1083
+ const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync();
1084
+ const marketingVersion = xcodeProj.getBuildProperty(
1085
+ "MARKETING_VERSION",
1086
+ command.buildConfigurationName,
1087
+ command.xcodeTargetName
1088
+ );
1089
+ if (!isValidVersion(marketingVersion)) {
1090
+ throw new Error(
1091
+ `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).`
1092
+ );
1093
+ }
1094
+ console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`);
1095
+
1096
+ return marketingVersion;
1097
+ }
1098
+
1099
+ function printJson(object: any): void {
1100
+ log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2));
1101
+ }
1102
+
1103
+ function printAccessKeys(format: string, keys: AccessKey[]): void {
1104
+ if (format === "json") {
1105
+ printJson(keys);
1106
+ } else if (format === "table") {
1107
+ printTable(["Name", "Created", "Expires"], (dataSource: any[]): void => {
1108
+ const now = new Date().getTime();
1109
+
1110
+ function isExpired(key: AccessKey): boolean {
1111
+ return now >= key.expires;
1112
+ }
1113
+
1114
+ function keyToTableRow(key: AccessKey, dim: boolean): string[] {
1115
+ const row: string[] = [key.name, key.createdTime ? formatDate(key.createdTime) : "", formatDate(key.expires)];
1116
+
1117
+ if (dim) {
1118
+ row.forEach((col: string, index: number) => {
1119
+ row[index] = (<any>chalk).dim(col);
1120
+ });
1121
+ }
1122
+
1123
+ return row;
1124
+ }
1125
+
1126
+ keys.forEach((key: AccessKey) => !isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ false)));
1127
+ keys.forEach((key: AccessKey) => isExpired(key) && dataSource.push(keyToTableRow(key, /*dim*/ true)));
1128
+ });
1129
+ }
1130
+ }
1131
+
1132
+ function printSessions(format: string, sessions: Session[]): void {
1133
+ if (format === "json") {
1134
+ printJson(sessions);
1135
+ } else if (format === "table") {
1136
+ printTable(["Machine", "Logged in"], (dataSource: any[]): void => {
1137
+ sessions.forEach((session: Session) => dataSource.push([session.machineName, formatDate(session.loggedInTime)]));
1138
+ });
1139
+ }
1140
+ }
1141
+
1142
+ function printTable(columnNames: string[], readData: (dataSource: any[]) => void): void {
1143
+ const table = new Table({
1144
+ head: columnNames,
1145
+ style: { head: ["cyan"] },
1146
+ });
1147
+
1148
+ readData(table);
1149
+
1150
+ log(table.toString());
1151
+ }
1152
+
1153
+ function register(command: cli.IRegisterCommand): Promise<void> {
1154
+ return loginWithExternalAuthentication("register", command.serverUrl);
1155
+ }
1156
+
1157
+ function promote(command: cli.IPromoteCommand): Promise<void> {
1158
+ const packageInfo: PackageInfo = {
1159
+ appVersion: command.appStoreVersion,
1160
+ description: command.description,
1161
+ label: command.label,
1162
+ isDisabled: command.disabled,
1163
+ isMandatory: command.mandatory,
1164
+ rollout: command.rollout,
1165
+ };
1166
+
1167
+ return sdk
1168
+ .promote(command.appName, command.sourceDeploymentName, command.destDeploymentName, packageInfo)
1169
+ .then((): void => {
1170
+ log(
1171
+ "Successfully promoted " +
1172
+ (command.label !== null ? '"' + command.label + '" of ' : "") +
1173
+ 'the "' +
1174
+ command.sourceDeploymentName +
1175
+ '" deployment of the "' +
1176
+ command.appName +
1177
+ '" app to the "' +
1178
+ command.destDeploymentName +
1179
+ '" deployment.'
1180
+ );
1181
+ })
1182
+ .catch((err: CodePushError) => releaseErrorHandler(err, command));
1183
+ }
1184
+
1185
+ function patch(command: cli.IPatchCommand): Promise<void> {
1186
+ const packageInfo: PackageInfo = {
1187
+ appVersion: command.appStoreVersion,
1188
+ description: command.description,
1189
+ isMandatory: command.mandatory,
1190
+ isDisabled: command.disabled,
1191
+ rollout: command.rollout,
1192
+ };
1193
+
1194
+ for (const updateProperty in packageInfo) {
1195
+ if ((<any>packageInfo)[updateProperty] !== null) {
1196
+ return sdk.patchRelease(command.appName, command.deploymentName, command.label, packageInfo).then((): void => {
1197
+ log(
1198
+ `Successfully updated the "${command.label ? command.label : `latest`}" release of "${command.appName}" app's "${
1199
+ command.deploymentName
1200
+ }" deployment.`
1201
+ );
1202
+ });
1203
+ }
1204
+ }
1205
+
1206
+ throw new Error("At least one property must be specified to patch a release.");
1207
+ }
1208
+
1209
+ export const release = (command: cli.IReleaseCommand): Promise<void> => {
1210
+ if (isBinaryOrZip(command.package)) {
1211
+ throw new Error(
1212
+ "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)."
1213
+ );
1214
+ }
1215
+
1216
+ throwForInvalidSemverRange(command.appStoreVersion);
1217
+ const filePath: string = command.package;
1218
+ let isSingleFilePackage: boolean = true;
1219
+
1220
+ if (fs.lstatSync(filePath).isDirectory()) {
1221
+ isSingleFilePackage = false;
1222
+ }
1223
+
1224
+ let lastTotalProgress = 0;
1225
+ const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
1226
+ complete: "=",
1227
+ incomplete: " ",
1228
+ width: 50,
1229
+ total: 100,
1230
+ });
1231
+
1232
+ const uploadProgress = (currentProgress: number): void => {
1233
+ progressBar.tick(currentProgress - lastTotalProgress);
1234
+ lastTotalProgress = currentProgress;
1235
+ };
1236
+
1237
+ const updateMetadata: PackageInfo = {
1238
+ description: command.description,
1239
+ isDisabled: command.disabled,
1240
+ isMandatory: command.mandatory,
1241
+ rollout: command.rollout,
1242
+ };
1243
+
1244
+ return sdk
1245
+ .isAuthenticated(true)
1246
+ .then((isAuth: boolean): Promise<void> => {
1247
+ return sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress);
1248
+ })
1249
+ .then((): void => {
1250
+ log(
1251
+ 'Successfully released an update containing the "' +
1252
+ command.package +
1253
+ '" ' +
1254
+ (isSingleFilePackage ? "file" : "directory") +
1255
+ ' to the "' +
1256
+ command.deploymentName +
1257
+ '" deployment of the "' +
1258
+ command.appName +
1259
+ '" app.'
1260
+ );
1261
+ })
1262
+ .catch((err: CodePushError) => releaseErrorHandler(err, command));
1263
+ };
1264
+
1265
+ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> => {
1266
+ let bundleName: string = command.bundleName;
1267
+ let entryFile: string = command.entryFile;
1268
+ const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
1269
+ const platform: string = (command.platform = command.platform.toLowerCase());
1270
+ const releaseCommand: cli.IReleaseCommand = <any>command;
1271
+ // Check for app and deployment exist before releasing an update.
1272
+ // This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name.
1273
+ return (
1274
+ sdk
1275
+ .getDeployment(command.appName, command.deploymentName)
1276
+ .then((): any => {
1277
+ releaseCommand.package = outputFolder;
1278
+
1279
+ switch (platform) {
1280
+ case "android":
1281
+ case "ios":
1282
+ case "windows":
1283
+ if (!bundleName) {
1284
+ bundleName = platform === "ios" ? "main.jsbundle" : `index.${platform}.bundle`;
1285
+ }
1286
+
1287
+ break;
1288
+ default:
1289
+ throw new Error('Platform must be either "android", "ios" or "windows".');
1290
+ }
1291
+
1292
+ let projectName: string;
1293
+
1294
+ try {
1295
+ const projectPackageJson: any = require(path.join(process.cwd(), "package.json"));
1296
+ projectName = projectPackageJson.name;
1297
+ if (!projectName) {
1298
+ throw new Error('The "package.json" file in the CWD does not have the "name" field set.');
1299
+ }
1300
+
1301
+ if (!projectPackageJson.dependencies["react-native"]) {
1302
+ throw new Error("The project in the CWD is not a React Native project.");
1303
+ }
1304
+ } catch (error) {
1305
+ throw new Error(
1306
+ 'Unable to find or read "package.json" in the CWD. The "release-react" command must be executed in a React Native project folder.'
1307
+ );
1308
+ }
1309
+
1310
+ if (!entryFile) {
1311
+ entryFile = `index.${platform}.js`;
1312
+ if (fileDoesNotExistOrIsDirectory(entryFile)) {
1313
+ entryFile = "index.js";
1314
+ }
1315
+
1316
+ if (fileDoesNotExistOrIsDirectory(entryFile)) {
1317
+ throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`);
1318
+ }
1319
+ } else {
1320
+ if (fileDoesNotExistOrIsDirectory(entryFile)) {
1321
+ throw new Error(`Entry file "${entryFile}" does not exist.`);
1322
+ }
1323
+ }
1324
+
1325
+ const appVersionPromise: Promise<string> = command.appStoreVersion
1326
+ ? Q(command.appStoreVersion)
1327
+ : getReactNativeProjectAppVersion(command, projectName);
1328
+
1329
+ if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) {
1330
+ command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map");
1331
+ }
1332
+
1333
+ return appVersionPromise;
1334
+ })
1335
+ .then((appVersion: string) => {
1336
+ throwForInvalidSemverRange(appVersion);
1337
+ releaseCommand.appStoreVersion = appVersion;
1338
+
1339
+ return createEmptyTempReleaseFolder(outputFolder);
1340
+ })
1341
+ // This is needed to clear the react native bundler cache:
1342
+ // https://github.com/facebook/react-native/issues/4289
1343
+ .then(() => deleteFolder(`${os.tmpdir()}/react-*`))
1344
+ .then(() =>
1345
+ runReactNativeBundleCommand(
1346
+ bundleName,
1347
+ command.development || false,
1348
+ entryFile,
1349
+ outputFolder,
1350
+ platform,
1351
+ command.sourcemapOutput
1352
+ )
1353
+ )
1354
+ .then(async () => {
1355
+ const isHermesEnabled =
1356
+ command.useHermes ||
1357
+ (platform === "android" && (await 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
1358
+ (platform === "ios" && (await 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
1359
+
1360
+ if (isHermesEnabled) {
1361
+ log(chalk.cyan("\nRunning hermes compiler...\n"));
1362
+ await runHermesEmitBinaryCommand(
1363
+ bundleName,
1364
+ outputFolder,
1365
+ command.sourcemapOutput,
1366
+ command.extraHermesFlags,
1367
+ command.gradleFile
1368
+ );
1369
+ }
1370
+ })
1371
+ .then(async () => {
1372
+ if (command.privateKeyPath) {
1373
+ log(chalk.cyan("\nSigning the bundle:\n"));
1374
+ await sign(command.privateKeyPath, outputFolder);
1375
+ } else {
1376
+ console.log("private key was not provided");
1377
+ }
1378
+ })
1379
+ .then(() => {
1380
+ log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
1381
+ return release(releaseCommand);
1382
+ })
1383
+ .then(() => {
1384
+ if (!command.outputDir) {
1385
+ deleteFolder(outputFolder);
1386
+ }
1387
+ })
1388
+ .catch((err: Error) => {
1389
+ deleteFolder(outputFolder);
1390
+ throw err;
1391
+ })
1392
+ );
1393
+ };
1394
+
1395
+ function rollback(command: cli.IRollbackCommand): Promise<void> {
1396
+ return confirm().then((wasConfirmed: boolean) => {
1397
+ if (!wasConfirmed) {
1398
+ log("Rollback cancelled.");
1399
+ return;
1400
+ }
1401
+
1402
+ return sdk.rollback(command.appName, command.deploymentName, command.targetRelease || undefined).then((): void => {
1403
+ log(
1404
+ 'Successfully performed a rollback on the "' + command.deploymentName + '" deployment of the "' + command.appName + '" app.'
1405
+ );
1406
+ });
1407
+ });
1408
+ }
1409
+
1410
+ function requestAccessKey(): Promise<string> {
1411
+ return Promise<string>((resolve, reject, notify): void => {
1412
+ prompt.message = "";
1413
+ prompt.delimiter = "";
1414
+
1415
+ prompt.start();
1416
+
1417
+ prompt.get(
1418
+ {
1419
+ properties: {
1420
+ response: {
1421
+ description: chalk.cyan("Enter your access key: "),
1422
+ },
1423
+ },
1424
+ },
1425
+ (err: any, result: any): void => {
1426
+ if (err) {
1427
+ resolve(null);
1428
+ } else {
1429
+ resolve(result.response.trim());
1430
+ }
1431
+ }
1432
+ );
1433
+ });
1434
+ }
1435
+
1436
+ export const runReactNativeBundleCommand = (
1437
+ bundleName: string,
1438
+ development: boolean,
1439
+ entryFile: string,
1440
+ outputFolder: string,
1441
+ platform: string,
1442
+ sourcemapOutput: string
1443
+ ): Promise<void> => {
1444
+ const reactNativeBundleArgs: string[] = [];
1445
+ const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
1446
+
1447
+ if (typeof envNodeArgs !== "undefined") {
1448
+ Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/));
1449
+ }
1450
+
1451
+ const isOldCLI = fs.existsSync(path.join("node_modules", "react-native", "local-cli", "cli.js"));
1452
+
1453
+ Array.prototype.push.apply(reactNativeBundleArgs, [
1454
+ isOldCLI ? path.join("node_modules", "react-native", "local-cli", "cli.js") : path.join("node_modules", "react-native", "cli.js"),
1455
+ "bundle",
1456
+ "--assets-dest",
1457
+ outputFolder,
1458
+ "--bundle-output",
1459
+ path.join(outputFolder, bundleName),
1460
+ "--dev",
1461
+ development,
1462
+ "--entry-file",
1463
+ entryFile,
1464
+ "--platform",
1465
+ platform,
1466
+ ]);
1467
+
1468
+ if (sourcemapOutput) {
1469
+ reactNativeBundleArgs.push("--sourcemap-output", sourcemapOutput);
1470
+ }
1471
+
1472
+ log(chalk.cyan('Running "react-native bundle" command:\n'));
1473
+ const reactNativeBundleProcess = spawn("node", reactNativeBundleArgs);
1474
+ log(`node ${reactNativeBundleArgs.join(" ")}`);
1475
+
1476
+ return Promise<void>((resolve, reject, notify) => {
1477
+ reactNativeBundleProcess.stdout.on("data", (data: Buffer) => {
1478
+ log(data.toString().trim());
1479
+ });
1480
+
1481
+ reactNativeBundleProcess.stderr.on("data", (data: Buffer) => {
1482
+ console.error(data.toString().trim());
1483
+ });
1484
+
1485
+ reactNativeBundleProcess.on("close", (exitCode: number) => {
1486
+ if (exitCode) {
1487
+ reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
1488
+ }
1489
+
1490
+ resolve(<void>null);
1491
+ });
1492
+ });
1493
+ };
1494
+
1495
+ function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: boolean, customServerUrl?: string, customAuthUrl?: string): void {
1496
+ const connectionInfo: ILoginConnectionInfo = {
1497
+ accessKey: accessKey,
1498
+ preserveAccessKeyOnLogout: preserveAccessKeyOnLogout,
1499
+ };
1500
+ if (customServerUrl) {
1501
+ connectionInfo.customServerUrl = customServerUrl;
1502
+ }
1503
+
1504
+ if(customAuthUrl){
1505
+ connectionInfo.customAuthUrl = customAuthUrl;
1506
+ }
1507
+
1508
+ const json: string = JSON.stringify(connectionInfo);
1509
+ fs.writeFileSync(configFilePath, json, { encoding: "utf8" });
1510
+
1511
+ log(
1512
+ `\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan(
1513
+ "code-push logout"
1514
+ )} command at any time to delete this file and terminate your session.\r\n`
1515
+ );
1516
+ }
1517
+
1518
+ function sessionList(command: cli.ISessionListCommand): Promise<void> {
1519
+ throwForInvalidOutputFormat(command.format);
1520
+
1521
+ return sdk.getSessions().then((sessions: Session[]): void => {
1522
+ printSessions(command.format, sessions);
1523
+ });
1524
+ }
1525
+
1526
+ function sessionRemove(command: cli.ISessionRemoveCommand): Promise<void> {
1527
+ if (os.hostname() === command.machineName) {
1528
+ throw new Error("Cannot remove the current login session via this command. Please run 'appcircle-code-push logout' instead.");
1529
+ } else {
1530
+ return confirm().then((wasConfirmed: boolean): Promise<void> => {
1531
+ if (wasConfirmed) {
1532
+ return sdk.removeSession(command.machineName).then((): void => {
1533
+ log(`Successfully removed the login session for "${command.machineName}".`);
1534
+ });
1535
+ }
1536
+
1537
+ log("Session removal cancelled.");
1538
+ });
1539
+ }
1540
+ }
1541
+
1542
+ function releaseErrorHandler(error: CodePushError, command: cli.ICommand): void {
1543
+ if ((<any>command).noDuplicateReleaseError && error.statusCode === AccountManager.ERROR_CONFLICT) {
1544
+ console.warn(chalk.yellow("[Warning] " + error.message));
1545
+ } else {
1546
+ throw error;
1547
+ }
1548
+ }
1549
+
1550
+ function throwForInvalidEmail(email: string): void {
1551
+ if (!emailValidator.validate(email)) {
1552
+ throw new Error('"' + email + '" is an invalid e-mail address.');
1553
+ }
1554
+ }
1555
+
1556
+ function throwForInvalidSemverRange(semverRange: string): void {
1557
+ if (semver.validRange(semverRange) === null) {
1558
+ throw new Error('Please use a semver-compliant target binary version range, for example "1.0.0", "*" or "^1.2.3".');
1559
+ }
1560
+ }
1561
+
1562
+ function throwForInvalidOutputFormat(format: string): void {
1563
+ switch (format) {
1564
+ case "json":
1565
+ case "table":
1566
+ break;
1567
+
1568
+ default:
1569
+ throw new Error("Invalid format: " + format + ".");
1570
+ }
1571
+ }
1572
+
1573
+ function whoami(command: cli.ICommand): Promise<void> {
1574
+ return sdk.getAccountInfo().then((account): void => {
1575
+ const accountInfo = `${account.email} (${account.linkedProviders.join(", ")})`;
1576
+
1577
+ log(accountInfo);
1578
+ });
1579
+ }
1580
+
1581
+ function isCommandOptionSpecified(option: any): boolean {
1582
+ return option !== undefined && option !== null;
1583
+ }
1584
+
1585
+ function getSdk(accessKey: string, pat:string, headers: Headers, customServerUrl: string, customAuthUrl: string): AccountManager {
1586
+ const sdk: any = new AccountManager(accessKey, pat, CLI_HEADERS, customServerUrl, customAuthUrl);
1587
+ /*
1588
+ * If the server returns `Unauthorized`, it must be due to an invalid
1589
+ * (or expired) access key. For convenience, we patch every SDK call
1590
+ * to delete the cached connection so the user can simply
1591
+ * login again instead of having to log out first.
1592
+ */
1593
+ Object.getOwnPropertyNames(AccountManager.prototype).forEach((functionName: any) => {
1594
+ if (typeof sdk[functionName] === "function") {
1595
+ const originalFunction = sdk[functionName];
1596
+ sdk[functionName] = function () {
1597
+ let maybePromise: Promise<any> = originalFunction.apply(sdk, arguments);
1598
+ if (maybePromise && maybePromise.then !== undefined) {
1599
+ maybePromise = maybePromise.catch((error: any) => {
1600
+ if (error.statusCode && error.statusCode === AccountManager.ERROR_UNAUTHORIZED) {
1601
+ deleteConnectionInfoCache(/* printMessage */ false);
1602
+ }
1603
+
1604
+ throw error;
1605
+ });
1606
+ }
1607
+
1608
+ return maybePromise;
1609
+ };
1610
+ }
1611
+ });
1612
+
1613
+ return sdk;
1614
+ }