@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,454 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT License.
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const Q = require("q");
8
+ const superagent = require("superagent");
9
+ const recursiveFs = require("recursive-fs");
10
+ const yazl = require("yazl");
11
+ const slash = require("slash");
12
+ const qs = require("qs");
13
+ var Promise = Q.Promise;
14
+ const packageJson = require("../../package.json");
15
+ // A template string tag function that URL encodes the substituted values
16
+ function urlEncode(strings, ...values) {
17
+ let result = "";
18
+ for (let i = 0; i < strings.length; i++) {
19
+ result += strings[i];
20
+ if (i < values.length) {
21
+ result += encodeURIComponent(values[i]);
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ class AccountManager {
27
+ static AppPermission = {
28
+ OWNER: "Owner",
29
+ COLLABORATOR: "Collaborator",
30
+ };
31
+ static SERVER_URL = "https://api.appcircle.io/codepush";
32
+ static AUTH_URL = "https://auth.appcircle.io";
33
+ static API_VERSION = 2;
34
+ static ERROR_GATEWAY_TIMEOUT = 504; // Used if there is a network error
35
+ static ERROR_INTERNAL_SERVER = 500;
36
+ static ERROR_NOT_FOUND = 404;
37
+ static ERROR_CONFLICT = 409; // Used if the resource already exists
38
+ static ERROR_UNAUTHORIZED = 401;
39
+ _accessKey;
40
+ _pat;
41
+ _serverUrl;
42
+ _authUrl;
43
+ _customHeaders;
44
+ constructor(accessKey, pat, customHeaders, serverUrl, authUrl) {
45
+ if (!accessKey && !pat)
46
+ throw new Error("An access key or PAT must be specified.");
47
+ this._accessKey = accessKey || "";
48
+ this._pat = pat || "";
49
+ this._customHeaders = customHeaders;
50
+ this._serverUrl = serverUrl || AccountManager.SERVER_URL;
51
+ this._authUrl = authUrl || AccountManager.AUTH_URL;
52
+ }
53
+ get accessKey() {
54
+ return this._accessKey;
55
+ }
56
+ loginToAppcircleWithPAT() {
57
+ return Promise((resolve, reject) => {
58
+ if (!this._accessKey) {
59
+ const request = superagent.post(`${this._authUrl}${urlEncode([`/auth/v1/token`])}`);
60
+ const data = qs.stringify({
61
+ pat: this._pat
62
+ });
63
+ request.set("Accept", 'application/json');
64
+ request.set("Content-Type", 'application/x-www-form-urlencoded');
65
+ request
66
+ .send(data);
67
+ request.end((err, res) => {
68
+ if (err) {
69
+ reject(this.getCodePushError(err, res));
70
+ return;
71
+ }
72
+ resolve(res.body.access_token);
73
+ return;
74
+ });
75
+ }
76
+ else {
77
+ resolve(this._accessKey);
78
+ }
79
+ });
80
+ }
81
+ isAuthenticated(throwIfUnauthorized) {
82
+ const accessTokenPromise = this.loginToAppcircleWithPAT();
83
+ return accessTokenPromise.then(appcircleAccessToken => {
84
+ this._accessKey = appcircleAccessToken;
85
+ return Promise(async (resolve, reject, notify) => {
86
+ const request = superagent.get(`${this._serverUrl}${urlEncode(["/authenticated"])}`);
87
+ this.attachCredentials(request);
88
+ request.end((err, res) => {
89
+ const status = this.getErrorStatus(err, res);
90
+ if (err && status !== AccountManager.ERROR_UNAUTHORIZED) {
91
+ reject(this.getCodePushError(err, res));
92
+ return;
93
+ }
94
+ const authenticated = status === 200;
95
+ if (!authenticated && throwIfUnauthorized) {
96
+ reject(this.getCodePushError(err, res));
97
+ return;
98
+ }
99
+ resolve(authenticated);
100
+ });
101
+ });
102
+ });
103
+ }
104
+ addAccessKey(friendlyName, ttl) {
105
+ if (!friendlyName) {
106
+ throw new Error("A name must be specified when adding an access key.");
107
+ }
108
+ const accessKeyRequest = {
109
+ createdBy: os.hostname(),
110
+ friendlyName,
111
+ ttl,
112
+ };
113
+ return this.post(urlEncode(["/accessKeys"]), JSON.stringify(accessKeyRequest), /*expectResponseBody=*/ true).then((response) => {
114
+ return {
115
+ createdTime: response.body.accessKey.createdTime,
116
+ expires: response.body.accessKey.expires,
117
+ key: response.body.accessKey.name,
118
+ name: response.body.accessKey.friendlyName,
119
+ };
120
+ });
121
+ }
122
+ getAccessKey(accessKeyName) {
123
+ return this.get(urlEncode([`/accessKeys/${accessKeyName}`])).then((res) => {
124
+ return {
125
+ createdTime: res.body.accessKey.createdTime,
126
+ expires: res.body.accessKey.expires,
127
+ name: res.body.accessKey.friendlyName,
128
+ };
129
+ });
130
+ }
131
+ getAccessKeys() {
132
+ return this.get(urlEncode(["/accessKeys"])).then((res) => {
133
+ const accessKeys = [];
134
+ res.body.accessKeys.forEach((serverAccessKey) => {
135
+ !serverAccessKey.isSession &&
136
+ accessKeys.push({
137
+ createdTime: serverAccessKey.createdTime,
138
+ expires: serverAccessKey.expires,
139
+ name: serverAccessKey.friendlyName,
140
+ });
141
+ });
142
+ return accessKeys;
143
+ });
144
+ }
145
+ getSessions() {
146
+ return this.get(urlEncode(["/accessKeys"])).then((res) => {
147
+ // A machine name might be associated with multiple session keys,
148
+ // but we should only return one per machine name.
149
+ const sessionMap = {};
150
+ const now = new Date().getTime();
151
+ res.body.accessKeys.forEach((serverAccessKey) => {
152
+ if (serverAccessKey.isSession && serverAccessKey.expires > now) {
153
+ sessionMap[serverAccessKey.createdBy] = {
154
+ loggedInTime: serverAccessKey.createdTime,
155
+ machineName: serverAccessKey.createdBy,
156
+ };
157
+ }
158
+ });
159
+ const sessions = Object.keys(sessionMap).map((machineName) => sessionMap[machineName]);
160
+ return sessions;
161
+ });
162
+ }
163
+ patchAccessKey(oldName, newName, ttl) {
164
+ const accessKeyRequest = {
165
+ friendlyName: newName,
166
+ ttl,
167
+ };
168
+ return this.patch(urlEncode([`/accessKeys/${oldName}`]), JSON.stringify(accessKeyRequest)).then((res) => {
169
+ return {
170
+ createdTime: res.body.accessKey.createdTime,
171
+ expires: res.body.accessKey.expires,
172
+ name: res.body.accessKey.friendlyName,
173
+ };
174
+ });
175
+ }
176
+ removeAccessKey(name) {
177
+ return this.del(urlEncode([`/accessKeys/${name}`])).then(() => null);
178
+ }
179
+ removeSession(machineName) {
180
+ return this.del(urlEncode([`/sessions/${machineName}`])).then(() => null);
181
+ }
182
+ // Account
183
+ getAccountInfo() {
184
+ return this.get(urlEncode(["/account"])).then((res) => res.body.account);
185
+ }
186
+ // Apps
187
+ getApps() {
188
+ return this.get(urlEncode(["/apps"])).then((res) => res.body.apps);
189
+ }
190
+ getApp(appName) {
191
+ return this.get(urlEncode([`/apps/${appName}`])).then((res) => res.body.app);
192
+ }
193
+ addApp(appName) {
194
+ const app = { name: appName };
195
+ return this.post(urlEncode(["/apps"]), JSON.stringify(app), /*expectResponseBody=*/ false).then(() => app);
196
+ }
197
+ removeApp(appName) {
198
+ return this.del(urlEncode([`/apps/${appName}`])).then(() => null);
199
+ }
200
+ renameApp(oldAppName, newAppName) {
201
+ return this.patch(urlEncode([`/apps/${oldAppName}`]), JSON.stringify({ name: newAppName })).then(() => null);
202
+ }
203
+ transferApp(appName, email) {
204
+ return this.post(urlEncode([`/apps/${appName}/transfer/${email}`]), /*requestBody=*/ null, /*expectResponseBody=*/ false).then(() => null);
205
+ }
206
+ // Collaborators
207
+ getCollaborators(appName) {
208
+ return this.get(urlEncode([`/apps/${appName}/collaborators`])).then((res) => res.body.collaborators);
209
+ }
210
+ addCollaborator(appName, email) {
211
+ return this.post(urlEncode([`/apps/${appName}/collaborators/${email}`]),
212
+ /*requestBody=*/ null,
213
+ /*expectResponseBody=*/ false).then(() => null);
214
+ }
215
+ removeCollaborator(appName, email) {
216
+ return this.del(urlEncode([`/apps/${appName}/collaborators/${email}`])).then(() => null);
217
+ }
218
+ // Deployments
219
+ addDeployment(appName, deploymentName, deploymentKey) {
220
+ const deployment = { name: deploymentName, key: deploymentKey };
221
+ return this.post(urlEncode([`/apps/${appName}/deployments`]), JSON.stringify(deployment), /*expectResponseBody=*/ true).then((res) => res.body.deployment);
222
+ }
223
+ clearDeploymentHistory(appName, deploymentName) {
224
+ return this.del(urlEncode([`/apps/${appName}/deployments/${deploymentName}/history`])).then(() => null);
225
+ }
226
+ getDeployments(appName) {
227
+ return this.get(urlEncode([`/apps/${appName}/deployments`])).then((res) => res.body.deployments);
228
+ }
229
+ getDeployment(appName, deploymentName) {
230
+ return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then((res) => res.body.deployment);
231
+ }
232
+ renameDeployment(appName, oldDeploymentName, newDeploymentName) {
233
+ return this.patch(urlEncode([`/apps/${appName}/deployments/${oldDeploymentName}`]), JSON.stringify({ name: newDeploymentName })).then(() => null);
234
+ }
235
+ removeDeployment(appName, deploymentName) {
236
+ return this.del(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then(() => null);
237
+ }
238
+ getDeploymentMetrics(appName, deploymentName) {
239
+ return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/metrics`])).then((res) => res.body.metrics);
240
+ }
241
+ getDeploymentHistory(appName, deploymentName) {
242
+ return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/history`])).then((res) => res.body.history);
243
+ }
244
+ release(appName, deploymentName, filePath, targetBinaryVersion, updateMetadata, uploadProgressCallback) {
245
+ return Promise((resolve, reject, notify) => {
246
+ updateMetadata.appVersion = targetBinaryVersion;
247
+ const request = superagent.post(this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]));
248
+ this.attachCredentials(request);
249
+ const getPackageFilePromise = Q.Promise((resolve, reject) => {
250
+ this.packageFileFromPath(filePath)
251
+ .then((result) => {
252
+ resolve(result);
253
+ })
254
+ .catch((error) => {
255
+ reject(error);
256
+ });
257
+ });
258
+ getPackageFilePromise.then((packageFile) => {
259
+ const file = fs.createReadStream(packageFile.path);
260
+ request
261
+ .attach("package", file)
262
+ .field("packageInfo", JSON.stringify(updateMetadata))
263
+ .on("progress", (event) => {
264
+ if (uploadProgressCallback && event && event.total > 0) {
265
+ const currentProgress = (event.loaded / event.total) * 100;
266
+ uploadProgressCallback(currentProgress);
267
+ }
268
+ })
269
+ .end((err, res) => {
270
+ if (packageFile.isTemporary) {
271
+ fs.unlinkSync(packageFile.path);
272
+ }
273
+ if (err) {
274
+ reject(this.getCodePushError(err, res));
275
+ return;
276
+ }
277
+ if (res.ok) {
278
+ resolve(null);
279
+ }
280
+ else {
281
+ let body;
282
+ try {
283
+ body = JSON.parse(res.text);
284
+ }
285
+ catch (err) { }
286
+ if (body) {
287
+ reject({
288
+ message: body.message,
289
+ statusCode: res && res.status,
290
+ });
291
+ }
292
+ else {
293
+ reject({
294
+ message: res.text,
295
+ statusCode: res && res.status,
296
+ });
297
+ }
298
+ }
299
+ });
300
+ });
301
+ });
302
+ }
303
+ patchRelease(appName, deploymentName, label, updateMetadata) {
304
+ updateMetadata.label = label;
305
+ const requestBody = JSON.stringify({ packageInfo: updateMetadata });
306
+ return this.patch(urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]), requestBody,
307
+ /*expectResponseBody=*/ false).then(() => null);
308
+ }
309
+ promote(appName, sourceDeploymentName, destinationDeploymentName, updateMetadata) {
310
+ const requestBody = JSON.stringify({ packageInfo: updateMetadata });
311
+ return this.post(urlEncode([`/apps/${appName}/deployments/${sourceDeploymentName}/promote/${destinationDeploymentName}`]), requestBody,
312
+ /*expectResponseBody=*/ false).then(() => null);
313
+ }
314
+ rollback(appName, deploymentName, targetRelease) {
315
+ return this.post(urlEncode([`/apps/${appName}/deployments/${deploymentName}/rollback/${targetRelease || ``}`]),
316
+ /*requestBody=*/ null,
317
+ /*expectResponseBody=*/ false).then(() => null);
318
+ }
319
+ packageFileFromPath(filePath) {
320
+ let getPackageFilePromise;
321
+ if (fs.lstatSync(filePath).isDirectory()) {
322
+ getPackageFilePromise = Promise((resolve, reject) => {
323
+ const directoryPath = filePath;
324
+ recursiveFs.readdirr(directoryPath, (error, directories, files) => {
325
+ if (error) {
326
+ reject(error);
327
+ return;
328
+ }
329
+ const baseDirectoryPath = path.dirname(directoryPath);
330
+ const fileName = this.generateRandomFilename(15) + ".zip";
331
+ const zipFile = new yazl.ZipFile();
332
+ const writeStream = fs.createWriteStream(fileName);
333
+ zipFile.outputStream
334
+ .pipe(writeStream)
335
+ .on("error", (error) => {
336
+ reject(error);
337
+ })
338
+ .on("close", () => {
339
+ filePath = path.join(process.cwd(), fileName);
340
+ resolve({ isTemporary: true, path: filePath });
341
+ });
342
+ for (let i = 0; i < files.length; ++i) {
343
+ const file = files[i];
344
+ // yazl does not like backslash (\) in the metadata path.
345
+ const relativePath = slash(path.relative(baseDirectoryPath, file));
346
+ zipFile.addFile(file, relativePath);
347
+ }
348
+ zipFile.end();
349
+ });
350
+ });
351
+ }
352
+ else {
353
+ getPackageFilePromise = Q({ isTemporary: false, path: filePath });
354
+ }
355
+ return getPackageFilePromise;
356
+ }
357
+ generateRandomFilename(length) {
358
+ let filename = "";
359
+ const validChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
360
+ for (let i = 0; i < length; i++) {
361
+ filename += validChar.charAt(Math.floor(Math.random() * validChar.length));
362
+ }
363
+ return filename;
364
+ }
365
+ get(endpoint, expectResponseBody = true) {
366
+ return this.makeApiRequest("get", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null);
367
+ }
368
+ post(endpoint, requestBody, expectResponseBody, contentType = "application/json;charset=UTF-8") {
369
+ return this.makeApiRequest("post", endpoint, requestBody, expectResponseBody, contentType);
370
+ }
371
+ patch(endpoint, requestBody, expectResponseBody = false, contentType = "application/json;charset=UTF-8") {
372
+ return this.makeApiRequest("patch", endpoint, requestBody, expectResponseBody, contentType);
373
+ }
374
+ del(endpoint, expectResponseBody = false) {
375
+ return this.makeApiRequest("del", endpoint, /*requestBody=*/ null, expectResponseBody, /*contentType=*/ null);
376
+ }
377
+ makeApiRequest(method, endpoint, requestBody, expectResponseBody, contentType) {
378
+ return Promise((resolve, reject, notify) => {
379
+ let request = superagent[method](this._serverUrl + endpoint);
380
+ this.attachCredentials(request);
381
+ if (requestBody) {
382
+ if (contentType) {
383
+ request = request.set("Content-Type", contentType);
384
+ }
385
+ request = request.send(requestBody);
386
+ }
387
+ request.end((err, res) => {
388
+ if (err) {
389
+ reject(this.getCodePushError(err, res));
390
+ return;
391
+ }
392
+ let body;
393
+ try {
394
+ body = JSON.parse(res.text);
395
+ }
396
+ catch (err) { }
397
+ if (res.ok) {
398
+ if (expectResponseBody && !body) {
399
+ reject({
400
+ message: `Could not parse response: ${res.text}`,
401
+ statusCode: AccountManager.ERROR_INTERNAL_SERVER,
402
+ });
403
+ }
404
+ else {
405
+ resolve({
406
+ headers: res.header,
407
+ body: body,
408
+ });
409
+ }
410
+ }
411
+ else {
412
+ if (body) {
413
+ reject({
414
+ message: body.message,
415
+ statusCode: this.getErrorStatus(err, res),
416
+ });
417
+ }
418
+ else {
419
+ reject({
420
+ message: res.text,
421
+ statusCode: this.getErrorStatus(err, res),
422
+ });
423
+ }
424
+ }
425
+ });
426
+ });
427
+ }
428
+ getCodePushError(error, response) {
429
+ if (error.syscall === "getaddrinfo") {
430
+ error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`;
431
+ }
432
+ return {
433
+ message: this.getErrorMessage(error, response),
434
+ statusCode: this.getErrorStatus(error, response),
435
+ };
436
+ }
437
+ getErrorStatus(error, response) {
438
+ return (error && error.status) || (response && response.status) || AccountManager.ERROR_GATEWAY_TIMEOUT;
439
+ }
440
+ getErrorMessage(error, response) {
441
+ return response && response.text ? response.text : error.message;
442
+ }
443
+ attachCredentials(request) {
444
+ if (this._customHeaders) {
445
+ for (const headerName in this._customHeaders) {
446
+ request.set(headerName, this._customHeaders[headerName]);
447
+ }
448
+ }
449
+ request.set("Accept", `application/vnd.code-push.v${AccountManager.API_VERSION}+json`);
450
+ request.set("Authorization", `Bearer ${this._accessKey}`);
451
+ request.set("X-CodePush-SDK-Version", packageJson.version);
452
+ }
453
+ }
454
+ module.exports = AccountManager;