avo 2.0.2 → 3.0.0-beta.2

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 (4) hide show
  1. package/Avo.js +771 -1038
  2. package/README.md +7 -7
  3. package/cli.js +1676 -2087
  4. package/package.json +40 -27
package/cli.js CHANGED
@@ -1,2321 +1,1910 @@
1
1
  #!/usr/bin/env node
2
2
  import ora from 'ora';
3
- import chalk from 'chalk'
3
+ import chalk from 'chalk';
4
4
  import minimatch from 'minimatch';
5
- import _ from 'lodash';
6
5
  import dateFns from 'date-fns';
7
6
  import fs from 'fs';
8
7
  import http from 'http';
9
8
  import inquirer from 'inquirer';
10
9
  import jwt from 'jsonwebtoken';
11
- import loadJsonFile from 'load-json-file';
10
+ import { loadJsonFile } from 'load-json-file';
12
11
  import logSymbols from 'log-symbols';
13
- import opn from 'opn';
12
+ import open from 'open';
14
13
  import path from 'path';
15
14
  import pify from 'pify';
16
15
  import portfinder from 'portfinder';
17
16
  import querystring from 'querystring';
18
- import request from 'request';
17
+ import got from 'got'; // eslint-disable-line import/no-unresolved
19
18
  import report from 'yurnalist';
20
19
  import semver from 'semver';
21
20
  import updateNotifier from 'update-notifier';
22
21
  import url from 'url';
23
22
  import util from 'util';
24
- import {v4 as uuidv4} from 'uuid';
23
+ import { v4 as uuidv4 } from 'uuid';
25
24
  import walk from 'ignore-walk';
26
25
  import writeFile from 'write';
27
- import writeJsonFile from 'write-json-file';
26
+ import { writeJsonFile } from 'write-json-file';
28
27
  import Configstore from 'configstore';
29
- import Inspector from 'node-avo-inspector';
28
+ import { AvoInspector, AvoInspectorEnv } from 'node-avo-inspector';
30
29
  import yargs from 'yargs';
31
- import { hideBin } from 'yargs/helpers'
30
+ import { hideBin } from 'yargs/helpers';
32
31
  import httpShutdown from 'http-shutdown';
33
32
  import fuzzypath from 'inquirer-fuzzy-path';
34
-
35
33
  import Avo from './Avo.js';
36
- const pkg = JSON.parse(fs.readFileSync(new URL('package.json', import.meta.url)));
37
-
38
- const Minimatch = minimatch.Minimatch;
39
- const {cyan, gray, red, bold, underline} = chalk;
40
-
41
- const customAnalyticsDestination = {
42
- make: function make(production) {
43
- this.production = production;
44
- },
45
-
46
- logEvent: function logEvent(userId, eventName, eventProperties) {
47
- api
48
- .request('POST', '/c/v1/track', {
49
- origin: api.apiOrigin,
50
- data: {
51
- userId: userId,
52
- eventName: eventName,
53
- eventProperties: eventProperties
54
- }
55
- })
56
- .catch(function() {
57
- // don't crash on tracking errors
58
- });
59
- }
34
+ const pkg = JSON.parse(fs.readFileSync(new URL('package.json', import.meta.url), 'utf-8'));
35
+ const { Minimatch } = minimatch;
36
+ /// //////////////////////////////////////////////////////////////////////
37
+ // LOGGING
38
+ const { cyan, gray, red, bold, underline } = chalk;
39
+ function cmd(command) {
40
+ return `${gray('`')}${cyan(command)}${gray('`')}`;
41
+ }
42
+ function link(text) {
43
+ return underline(text);
44
+ }
45
+ function file(text) {
46
+ return underline(text);
47
+ }
48
+ function email(text) {
49
+ return underline(text);
50
+ }
51
+ // to cancel spinners globally
52
+ let _cancel = null;
53
+ let cancelWait = () => {
54
+ if (_cancel !== null) {
55
+ _cancel();
56
+ _cancel = null;
57
+ }
60
58
  };
61
-
62
- let inspector = new Inspector.AvoInspector({
63
- apiKey: "3UWtteG9HenZ825cYoYr", env: Inspector.AvoInspectorEnv.Prod, version: "1.0.0", appName: "Avo CLI"
64
- });
65
-
66
- // setup Avo analytics
67
- Avo.initAvo(
68
- {env: 'prod', inspector: inspector},
69
- {client: Avo.Client.CLI, version: pkg.version},
70
- {},
71
- customAnalyticsDestination
72
- );
73
-
59
+ function wait(message, timeOut = 300) {
60
+ cancelWait();
61
+ let running = false;
62
+ let spinner;
63
+ let stopped = false;
64
+ setTimeout(() => {
65
+ if (stopped)
66
+ return;
67
+ spinner = ora(gray(message));
68
+ spinner.color = 'gray';
69
+ spinner.start();
70
+ running = true;
71
+ }, timeOut);
72
+ const cancel = () => {
73
+ stopped = true;
74
+ if (running) {
75
+ spinner.stop();
76
+ running = false;
77
+ }
78
+ process.removeListener('nowExit', cancel);
79
+ };
80
+ process.on('nowExit', cancel);
81
+ cancelWait = cancel;
82
+ }
74
83
  // register inquirer-file-path
75
84
  inquirer.registerPrompt('fuzzypath', fuzzypath);
76
-
77
- updateNotifier({pkg: pkg}).notify();
78
-
85
+ updateNotifier({ pkg }).notify();
79
86
  const conf = new Configstore(pkg.name);
80
-
81
87
  if (!conf.has('avo_install_id')) {
82
- conf.set('avo_install_id', uuidv4());
88
+ conf.set('avo_install_id', uuidv4());
83
89
  }
84
-
85
90
  const FIFTEEN_MINUTES_IN_MS = 15 * 60 * 1000;
86
-
87
- // to cancel spinners globally
88
- let _cancel = null;
89
-
90
- var nonce = _.random(1, 2 << 29).toString();
91
-
91
+ const nonce = (1 + Math.random() * (2 << 29)).toString();
92
+ function isString(str) {
93
+ if (str != null && typeof str.valueOf() === 'string') {
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ const sum = (base, value) => base + value;
92
99
  portfinder.basePort = 9005;
93
- var _getPort = portfinder.getPortPromise;
94
-
95
- function AvoError(message, options) {
96
- options = options || {};
97
-
98
- this.name = 'AvoError';
99
- this.message = message;
100
- this.children = options.children || [];
101
- this.status = options.status || 500;
102
- this.exit = options.exit || 1;
103
- this.stack = new Error().stack;
104
- this.original = options.original;
105
- this.context = options.context;
100
+ const _getPort = portfinder.getPortPromise;
101
+ function AvoError(message, options = {}) {
102
+ this.name = 'AvoError';
103
+ this.message = message;
104
+ this.status = options.status ?? 500;
105
+ this.exit = options.exit ?? 1;
106
+ this.stack = new Error().stack;
107
+ this.original = options.original;
108
+ this.context = options.context;
106
109
  }
107
110
  AvoError.prototype = Object.create(Error.prototype);
108
-
109
- var INVALID_CREDENTIAL_ERROR = new AvoError(
110
- `Authentication Error: Your credentials are no longer valid. Please run ${cmd(
111
- 'avo logout; avo login'
112
- )}`,
113
- {exit: 1}
114
- );
115
-
116
- // in-memory cache, so we have it for successive calls
117
- var lastAccessToken = {};
118
- var accessToken;
119
- var refreshToken;
120
- var commandScopes;
121
-
122
- /////////////////////////////////////////////////////////////////////////
111
+ const INVALID_CREDENTIAL_ERROR = new AvoError(`Authentication Error: Your credentials are no longer valid. Please run ${cmd('avo logout; avo login')}`, { exit: 1 });
112
+ let lastAccessToken = {};
113
+ let accessToken;
114
+ let refreshToken;
115
+ /// //////////////////////////////////////////////////////////////////////
123
116
  // REQUEST HANDLING
124
-
125
- function _request(options, logOptions) {
126
- logOptions = logOptions || {};
127
-
128
- if (options.qs && !logOptions.skipQueryParams) {
129
- qsLog = JSON.stringify(options.qs);
130
- }
131
-
132
- return new Promise(function(resolve, reject) {
133
- var req = request(options, function(err, response, body) {
134
- if (err) {
135
- return reject(
136
- new AvoError('Server Error. ' + err.message, {
137
- original: err,
138
- exit: 2
139
- })
140
- );
141
- }
142
-
143
- if (response.statusCode >= 400 && !logOptions.skipResponseBody) {
144
- if (!options.resolveOnHTTPError) {
145
- return reject(responseToError(response, body, options));
117
+ function responseToError(response) {
118
+ let { body } = response;
119
+ if (typeof body === 'string' && response.statusCode === 404) {
120
+ body = {
121
+ error: {
122
+ message: 'Not Found',
123
+ },
124
+ };
125
+ }
126
+ if (response.statusCode < 400) {
127
+ return null;
128
+ }
129
+ if (typeof body !== 'object') {
130
+ try {
131
+ body = JSON.parse(body);
132
+ }
133
+ catch (e) {
134
+ body = {};
146
135
  }
147
- }
148
- return resolve({
149
- status: response.statusCode,
150
- response: response,
151
- body: body
152
- });
153
- });
154
-
155
- if (_.size(options.files) > 0) {
156
- var form = req.form();
157
- _.forEach(options.files, function(details, param) {
158
- form.append(param, details.stream, {
159
- knownLength: details.knownLength,
160
- filename: details.filename,
161
- contentType: details.contentType
162
- });
163
- });
164
136
  }
165
- });
166
- }
167
-
168
- var _appendQueryData = function(path, data) {
169
- if (data && _.size(data) > 0) {
170
- path += _.includes(path, '?') ? '&' : '?';
171
- path += querystring.stringify(data);
172
- }
173
- return path;
174
- };
175
-
176
- var api = {
177
- authOrigin: 'https://www.avo.app',
178
-
179
- apiOrigin: 'https://api.avo.app',
180
-
181
- setRefreshToken: function(token) {
182
- refreshToken = token;
183
- },
184
- setAccessToken: function(token) {
185
- accessToken = token;
186
- },
187
- getAccessToken: function() {
188
- return accessToken
189
- ? Promise.resolve({idToken: accessToken})
190
- : getAccessToken(refreshToken, commandScopes);
191
- },
192
- addRequestHeaders: function(reqOptions) {
193
- // Runtime fetch of Auth singleton to prevent circular module dependencies
194
- _.set(reqOptions, ['headers', 'User-Agent'], 'AvoCLI/' + pkg.version);
195
- _.set(reqOptions, ['headers', 'X-Client-Version'], 'AvoCLI/' + pkg.version);
196
- return api.getAccessToken().then(function(result) {
197
- _.set(reqOptions, 'headers.authorization', 'Bearer ' + result.idToken);
198
- return reqOptions;
199
- });
200
- },
201
- request: function(method, resource, options) {
202
- options = _.extend(
203
- {
204
- data: {},
205
- origin: undefined, // origin must be set
206
- resolveOnHTTPError: false, // by default, status codes >= 400 leads to reject
207
- json: true,
208
- gzip: true
209
- },
210
- options
211
- );
212
-
213
- var validMethods = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'];
214
-
215
- if (validMethods.indexOf(method) < 0) {
216
- method = 'GET';
137
+ if (!body.error) {
138
+ body.error = {
139
+ message: response.statusCode === 404 ? 'Not Found' : 'Unknown Error',
140
+ };
217
141
  }
218
-
219
- var reqOptions = {
220
- method: method
221
- };
222
-
223
- if (options.query) {
224
- resource = _appendQueryData(resource, options.query);
142
+ const message = `HTTP Error: ${response.statusCode}, ${body.error.message ?? body.error}`;
143
+ let exitCode;
144
+ if (response.statusCode >= 500) {
145
+ // 5xx errors are unexpected
146
+ exitCode = 2;
225
147
  }
226
-
227
- if (method === 'GET') {
228
- resource = _appendQueryData(resource, options.data);
229
- } else {
230
- if (_.size(options.data) > 0) {
231
- reqOptions.body = options.data;
232
- } else if (_.size(options.form) > 0) {
233
- reqOptions.form = options.form;
234
- }
148
+ else {
149
+ // 4xx errors happen sometimes
150
+ exitCode = 1;
235
151
  }
236
-
237
- reqOptions.url = options.origin + resource;
238
- reqOptions.files = options.files;
239
- reqOptions.resolveOnHTTPError = options.resolveOnHTTPError;
240
- reqOptions.json = options.json;
241
- reqOptions.gzip = options.gzip;
242
- reqOptions.qs = options.qs;
243
- reqOptions.headers = options.headers;
244
- reqOptions.timeout = options.timeout;
245
-
246
- var requestFunction = function() {
247
- return _request(reqOptions, options.logOptions);
248
- };
249
- if (options.auth === true) {
250
- requestFunction = function() {
251
- return api
252
- .addRequestHeaders(reqOptions)
253
- .then(function(reqOptionsWithToken) {
254
- return _request(reqOptionsWithToken, options.logOptions);
255
- });
256
- };
152
+ delete response.request.headers;
153
+ return new AvoError(message, {
154
+ context: {
155
+ body,
156
+ response,
157
+ },
158
+ exit: exitCode,
159
+ });
160
+ }
161
+ function _request(options) {
162
+ return new Promise((resolve, reject) => {
163
+ got(options)
164
+ .then((response) => {
165
+ if (response.statusCode >= 400) {
166
+ return reject(responseToError(response));
167
+ }
168
+ return resolve(JSON.parse(response.body));
169
+ })
170
+ .catch((err) => reject(new AvoError(`Server Error. ${err.message}`, {
171
+ original: err,
172
+ exit: 2,
173
+ })));
174
+ });
175
+ }
176
+ const _appendQueryData = (urlPath, data) => {
177
+ let returnPath = urlPath;
178
+ if (data && Object.keys(data).length > 0) {
179
+ returnPath += returnPath.includes('?') ? '&' : '?';
180
+ returnPath += querystring.stringify(data);
257
181
  }
258
-
259
- return requestFunction().catch(function(err) {
260
- if (
261
- options.retryCodes &&
262
- _.includes(
263
- options.retryCodes,
264
- _.get(err, 'context.response.statusCode')
265
- )
266
- ) {
267
- return new Promise(function(resolve) {
268
- setTimeout(resolve, 1000);
269
- }).then(requestFunction);
270
- }
271
- return Promise.reject(err);
182
+ return returnPath;
183
+ };
184
+ function _refreshAccessToken(refreshToken) {
185
+ return api // eslint-disable-line
186
+ .request('POST', '/auth/refresh', {
187
+ origin: api.apiOrigin,
188
+ json: {
189
+ token: refreshToken,
190
+ },
191
+ })
192
+ .then((data) => {
193
+ if (!isString(data.idToken)) {
194
+ throw INVALID_CREDENTIAL_ERROR;
195
+ }
196
+ lastAccessToken = {
197
+ expiresAt: Date.now() + data.expiresIn * 1000,
198
+ refreshToken,
199
+ ...data,
200
+ };
201
+ const currentRefreshToken = conf.get('tokens').refreshToken;
202
+ if (refreshToken === currentRefreshToken) {
203
+ conf.set('tokens', lastAccessToken);
204
+ }
205
+ return lastAccessToken;
206
+ }, () => {
207
+ throw INVALID_CREDENTIAL_ERROR;
272
208
  });
273
- }
209
+ }
210
+ function _haveValidAccessToken(refreshToken) {
211
+ if (Object.keys(lastAccessToken).length === 0) {
212
+ const tokens = conf.get('tokens');
213
+ if (refreshToken === tokens.refreshToken) {
214
+ lastAccessToken = tokens;
215
+ }
216
+ }
217
+ return (lastAccessToken.idToken &&
218
+ lastAccessToken.refreshToken === refreshToken &&
219
+ lastAccessToken.expiresAt &&
220
+ lastAccessToken.expiresAt > Date.now() + FIFTEEN_MINUTES_IN_MS);
221
+ }
222
+ function getAccessToken(refreshToken) {
223
+ if (_haveValidAccessToken(refreshToken)) {
224
+ return Promise.resolve(lastAccessToken);
225
+ }
226
+ return _refreshAccessToken(refreshToken);
227
+ }
228
+ const api = {
229
+ authOrigin: 'https://www.avo.app',
230
+ apiOrigin: 'https://api.avo.app',
231
+ setRefreshToken(token) {
232
+ refreshToken = token;
233
+ },
234
+ setAccessToken(token) {
235
+ accessToken = token;
236
+ },
237
+ getAccessToken() {
238
+ return accessToken
239
+ ? Promise.resolve({ idToken: accessToken })
240
+ : getAccessToken(refreshToken);
241
+ },
242
+ addRequestHeaders(reqOptions) {
243
+ // Runtime fetch of Auth singleton to prevent circular module dependencies
244
+ return api.getAccessToken().then((result) => ({
245
+ ...reqOptions,
246
+ headers: {
247
+ ...reqOptions.headers,
248
+ 'User-Agent': `AvoCLI/${pkg.version}`,
249
+ 'X-Client-Version': `AvoCLI/${pkg.version}`,
250
+ authorization: `Bearer ${result.idToken}`,
251
+ },
252
+ }));
253
+ },
254
+ request(method, resource, options) {
255
+ const validMethods = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'];
256
+ const reqOptions = {
257
+ method: validMethods.includes(method) ? method : 'GET',
258
+ decompress: true,
259
+ headers: options.headers ?? {},
260
+ };
261
+ let urlPath = resource;
262
+ if (options.query) {
263
+ urlPath = _appendQueryData(urlPath, options.query);
264
+ }
265
+ if (reqOptions.method === 'GET') {
266
+ urlPath = _appendQueryData(urlPath, options.json);
267
+ }
268
+ else if (Object.keys(options.json).length > 0) {
269
+ reqOptions.json = options.json;
270
+ }
271
+ else if (Object.keys(options.form).length > 0) {
272
+ reqOptions.form = options.form;
273
+ }
274
+ reqOptions.url = options.origin + urlPath;
275
+ let requestFunction = () => _request(reqOptions);
276
+ if (options.auth === true) {
277
+ requestFunction = () => api
278
+ .addRequestHeaders(reqOptions)
279
+ .then((reqOptionsWithToken) => _request(reqOptionsWithToken));
280
+ }
281
+ return requestFunction().catch((err) => {
282
+ if (options.retryCodes &&
283
+ options.retryCodes.includes(err.context.response.statusCode)) {
284
+ return new Promise((resolve) => {
285
+ setTimeout(resolve, 1000);
286
+ }).then(requestFunction);
287
+ }
288
+ return Promise.reject(err);
289
+ });
290
+ },
291
+ };
292
+ const customAnalyticsDestination = {
293
+ make: function make(production) {
294
+ this.production = production;
295
+ },
296
+ logEvent: (userId, eventName, eventProperties) => {
297
+ api
298
+ .request('POST', '/c/v1/track', {
299
+ origin: api.apiOrigin,
300
+ json: {
301
+ userId,
302
+ eventName,
303
+ eventProperties,
304
+ },
305
+ })
306
+ .catch(() => {
307
+ // don't crash on tracking errors
308
+ });
309
+ return undefined;
310
+ },
311
+ setUserProperties: () => undefined, // noop
274
312
  };
275
-
313
+ const inspector = new AvoInspector({
314
+ apiKey: '3UWtteG9HenZ825cYoYr',
315
+ env: AvoInspectorEnv.Prod,
316
+ version: pkg.version,
317
+ appName: 'Avo CLI',
318
+ });
319
+ // setup Avo analytics
320
+ Avo.initAvo({ env: Avo.AvoEnv.Prod, inspector }, { client: Avo.Client.CLI, version: pkg.version }, {}, customAnalyticsDestination);
276
321
  function isLegacyAvoJson(json) {
277
- // check if legacy avo.json or un-initialized project
278
- return json.types || !json.schema;
322
+ // check if legacy avo.json or un-initialized project
323
+ return json.types ?? !json.schema;
279
324
  }
280
-
281
325
  function avoNeedsUpdate(json) {
282
- // if avo.json has version, and this binary has lower version number it needs updating
283
- return (
284
- json.avo && json.avo.version && semver.major(pkg.version) < json.avo.version
285
- );
326
+ // if avo.json has version, and this binary has lower version number it needs updating
327
+ return (json.avo && json.avo.version && semver.major(pkg.version) < json.avo.version);
286
328
  }
287
-
288
329
  const MERGE_CONFLICT_ANCESTOR = '|||||||';
289
330
  const MERGE_CONFLICT_END = '>>>>>>>';
290
331
  const MERGE_CONFLICT_SEP = '=======';
291
332
  const MERGE_CONFLICT_START = '<<<<<<<';
292
333
  function hasMergeConflicts(str) {
293
- return (
294
- str.includes(MERGE_CONFLICT_START) &&
295
- str.includes(MERGE_CONFLICT_SEP) &&
296
- str.includes(MERGE_CONFLICT_END)
297
- );
334
+ return (str.includes(MERGE_CONFLICT_START) &&
335
+ str.includes(MERGE_CONFLICT_SEP) &&
336
+ str.includes(MERGE_CONFLICT_END));
298
337
  }
299
-
300
338
  function extractConflictingFiles(str) {
301
- const files = [[], []];
302
- const lines = str.split(/\r?\n/g);
303
- let skip = false;
304
-
305
- while (lines.length) {
306
- const line = lines.shift();
307
- if (line.startsWith(MERGE_CONFLICT_START)) {
308
- while (lines.length) {
309
- const conflictLine = lines.shift();
310
- if (conflictLine === MERGE_CONFLICT_SEP) {
311
- skip = false;
312
- break;
313
- } else if (skip || conflictLine.startsWith(MERGE_CONFLICT_ANCESTOR)) {
314
- skip = true;
315
- continue;
316
- } else {
317
- files[0].push(conflictLine);
339
+ const files = [[], []];
340
+ const lines = str.split(/\r?\n/g);
341
+ let skip = false;
342
+ while (lines.length) {
343
+ const line = lines.shift();
344
+ if (line.startsWith(MERGE_CONFLICT_START)) {
345
+ while (lines.length) {
346
+ const conflictLine = lines.shift();
347
+ if (conflictLine === MERGE_CONFLICT_SEP) {
348
+ skip = false;
349
+ break;
350
+ }
351
+ else if (skip || conflictLine.startsWith(MERGE_CONFLICT_ANCESTOR)) {
352
+ skip = true;
353
+ }
354
+ else {
355
+ files[0].push(conflictLine);
356
+ }
357
+ }
358
+ while (lines.length) {
359
+ const conflictLine = lines.shift();
360
+ if (conflictLine.startsWith(MERGE_CONFLICT_END)) {
361
+ break;
362
+ }
363
+ else {
364
+ files[1].push(conflictLine);
365
+ }
366
+ }
318
367
  }
319
- }
320
-
321
- while (lines.length) {
322
- const conflictLine = lines.shift();
323
- if (conflictLine.startsWith(MERGE_CONFLICT_END)) {
324
- break;
325
- } else {
326
- files[1].push(conflictLine);
368
+ else {
369
+ files[0].push(line);
370
+ files[1].push(line);
327
371
  }
328
- }
329
- } else {
330
- files[0].push(line);
331
- files[1].push(line);
332
372
  }
333
- }
334
-
335
- return [files[0].join('\n'), files[1].join('\n')];
336
- }
337
-
338
- function validateAvoJson(json) {
339
- if (avoNeedsUpdate(json)) {
340
- throw new AvoError(`Your avo CLI is outdated, please update`);
341
- }
342
-
343
- if (isLegacyAvoJson(json)) {
344
- return init();
345
- }
346
-
347
- // augment the latest major version into avo.json
348
- json.avo = Object.assign({}, json.avo || {}, {
349
- version: semver.major(pkg.version)
350
- });
351
-
352
- return json;
373
+ return [files[0].join('\n'), files[1].join('\n')];
353
374
  }
354
-
355
375
  const BRANCH_UP_TO_DATE = 'branch-up-to-date';
356
376
  const BRANCH_NOT_UP_TO_DATE = 'branch-not-up-to-date';
357
-
358
377
  function getMasterStatus(json) {
359
- if (json.branch.id == 'master') {
360
- return Promise.resolve(BRANCH_UP_TO_DATE);
361
- } else {
378
+ if (json.branch.id === 'master') {
379
+ return Promise.resolve(BRANCH_UP_TO_DATE);
380
+ }
362
381
  return api
363
- .request('POST', '/c/v1/master', {
382
+ .request('POST', '/c/v1/master', {
364
383
  origin: api.apiOrigin,
365
384
  auth: true,
366
- data: {
367
- schemaId: json.schema.id,
368
- branchId: json.branch.id
369
- }
370
- })
371
- .then(res => {
372
- return res.body.pullRequired
373
- ? BRANCH_NOT_UP_TO_DATE
374
- : BRANCH_UP_TO_DATE;
375
- });
376
- }
385
+ json: {
386
+ schemaId: json.schema.id,
387
+ branchId: json.branch.id,
388
+ },
389
+ })
390
+ .then(({ pullRequired }) => pullRequired ? BRANCH_NOT_UP_TO_DATE : BRANCH_UP_TO_DATE);
377
391
  }
378
-
379
392
  function pullMaster(json) {
380
- if (json.branch.name == 'main') {
381
- report.info('Your current branch is main');
382
- return Promise.resolve(json);
383
- } else {
393
+ if (json.branch.name === 'main') {
394
+ report.info('Your current branch is main');
395
+ return Promise.resolve(json);
396
+ }
384
397
  wait(json.force ? 'Force pulling main into branch' : 'Pulling main into branch');
385
398
  return api
386
- .request('POST', '/c/v1/master/pull', {
399
+ .request('POST', '/c/v1/master/pull', {
387
400
  origin: api.apiOrigin,
388
401
  auth: true,
389
- data: {
390
- schemaId: json.schema.id,
391
- branchId: json.branch.id,
392
- force: json.force
393
- }
394
- })
395
- .then(() => {
402
+ json: {
403
+ schemaId: json.schema.id,
404
+ branchId: json.branch.id,
405
+ force: json.force,
406
+ },
407
+ })
408
+ .then(() => {
396
409
  cancelWait();
397
410
  report.success('Branch is up to date with main');
398
411
  return json;
399
- });
400
- }
412
+ });
401
413
  }
402
-
403
414
  function promptPullMaster(json) {
404
- wait('Check if branch is up to date with main');
405
- return getMasterStatus(json)
406
- .then(branchStatus => {
407
- cancelWait();
408
- if (branchStatus == BRANCH_UP_TO_DATE) {
415
+ wait('Check if branch is up to date with main');
416
+ return getMasterStatus(json)
417
+ .then((branchStatus) => {
418
+ cancelWait();
419
+ if (branchStatus === BRANCH_NOT_UP_TO_DATE) {
420
+ return inquirer
421
+ .prompt([
422
+ {
423
+ type: 'confirm',
424
+ name: 'pull',
425
+ default: true,
426
+ message: `Your branch '${bold(json.branch.name)}' is not up to date with the Avo main branch. Would you like to pull main into your branch?`,
427
+ },
428
+ ])
429
+ .then((answer) => Promise.resolve([branchStatus, answer]));
430
+ }
431
+ // We're expecting branchStatus === BRANCH_UP_TO_DATE
409
432
  return Promise.resolve([branchStatus]);
410
- } else if (branchStatus == BRANCH_NOT_UP_TO_DATE) {
411
- return inquirer
412
- .prompt([
413
- {
414
- type: 'confirm',
415
- name: 'pull',
416
- default: true,
417
- message: `Your branch '${bold(
418
- json.branch.name
419
- )}' is not up to date with the Avo main branch. Would you like to pull main into your branch?`
420
- }
421
- ])
422
- .then(answer => Promise.resolve([branchStatus, answer]));
423
- }
424
433
  })
425
- .then(([branchStatus, answer]) => {
426
- if (branchStatus == BRANCH_UP_TO_DATE) {
427
- report.success('Branch is up to date with main');
428
- return Promise.resolve(json);
429
- } else if (answer.pull) {
430
- return pullMaster(json);
431
- } else {
432
- report.info(`Did not pull main into branch`);
434
+ .then(([branchStatus, answer]) => {
435
+ if (branchStatus === BRANCH_UP_TO_DATE) {
436
+ report.success('Branch is up to date with main');
437
+ return Promise.resolve(json);
438
+ }
439
+ if (answer.pull) {
440
+ return pullMaster(json);
441
+ }
442
+ report.info('Did not pull main into branch');
433
443
  return Promise.resolve(json);
434
- }
435
444
  });
436
445
  }
437
-
438
- function resolveAvoJsonConflicts(file, {argv, skipPullMaster}) {
439
- report.info('Resolving Avo merge conflicts');
440
- let files = extractConflictingFiles(file);
441
- const head = JSON.parse(files[0]);
442
- const incoming = JSON.parse(files[1]);
443
-
444
- Avo.cliConflictResolveAttempted({
445
- userId_: installIdOrUserId(),
446
- cliInvokedByCi: invokedByCi(),
447
- schemaId: head.schema.id,
448
- schemaName: head.schema.name,
449
- branchId: head.branch.id,
450
- branchName: head.branch.name
451
- });
452
-
453
- if (
454
- head.avo.version != incoming.avo.version ||
455
- head.schema.id != incoming.schema.id
456
- ) {
457
- Avo.cliConflictResolveFailed({
458
- userId_: installIdOrUserId(),
459
- cliInvokedByCi: invokedByCi(),
460
- schemaId: head.schema.id,
461
- schemaName: head.schema.name,
462
- branchId: head.branch.id,
463
- branchName: head.branch.name
446
+ function installIdOrUserId() {
447
+ const installId = conf.get('avo_install_id');
448
+ const user = conf.get('user');
449
+ if (user && user.user_id) {
450
+ return user.user_id;
451
+ }
452
+ return installId;
453
+ }
454
+ function invokedByCi() {
455
+ return process.env.CI !== undefined;
456
+ }
457
+ function requireAuth(argv, cb) {
458
+ const tokens = conf.get('tokens');
459
+ const user = conf.get('user');
460
+ const tokenOpt = argv.token ?? process.env.AVO_TOKEN;
461
+ if (tokenOpt) {
462
+ api.setRefreshToken(tokenOpt);
463
+ return cb();
464
+ }
465
+ if (!user || !tokens) {
466
+ report.error(`Command requires authentication. Run ${cmd('avo login')}`);
467
+ process.exit(1);
468
+ }
469
+ argv.user = user; // eslint-disable-line no-param-reassign
470
+ argv.tokens = tokens; // eslint-disable-line no-param-reassign
471
+ api.setRefreshToken(tokens.refreshToken);
472
+ return cb();
473
+ }
474
+ function init() {
475
+ const makeAvoJson = (schema) => {
476
+ report.success(`Initialized for workspace ${cyan(schema.name)}`);
477
+ return {
478
+ avo: {
479
+ version: semver.major(pkg.version),
480
+ },
481
+ schema: {
482
+ id: schema.id,
483
+ name: schema.name,
484
+ },
485
+ branch: {
486
+ id: 'master',
487
+ name: 'main',
488
+ },
489
+ };
490
+ };
491
+ wait('Initializing');
492
+ return api
493
+ .request('GET', '/c/v1/workspaces', {
494
+ origin: api.apiOrigin,
495
+ auth: true,
496
+ })
497
+ .then(({ workspaces }) => {
498
+ cancelWait();
499
+ const schemas = [...workspaces].sort((a, b) => a.lastUsedAt - b.lastUsedAt);
500
+ if (schemas.length > 1) {
501
+ const choices = schemas.map((schema) => ({
502
+ value: schema,
503
+ name: schema.name,
504
+ }));
505
+ return inquirer
506
+ .prompt([
507
+ {
508
+ type: 'list',
509
+ name: 'schema',
510
+ message: 'Select a workspace to initialize',
511
+ choices,
512
+ },
513
+ ])
514
+ .then((answer) => makeAvoJson(answer.schema));
515
+ }
516
+ if (schemas.length === 0) {
517
+ throw new AvoError(`No workspaces to initialize. Go to ${link('wwww.avo.app')} to create one`);
518
+ }
519
+ else {
520
+ const schema = schemas[0];
521
+ return makeAvoJson(schema);
522
+ }
523
+ });
524
+ }
525
+ function validateAvoJson(json) {
526
+ if (avoNeedsUpdate(json)) {
527
+ throw new AvoError('Your avo CLI is outdated, please update');
528
+ }
529
+ if (isLegacyAvoJson(json)) {
530
+ return init();
531
+ }
532
+ // augment the latest major version into avo.json
533
+ return { ...json, avo: { ...json.avo, version: semver.major(pkg.version) } };
534
+ }
535
+ function fetchBranches(json) {
536
+ wait('Fetching open branches');
537
+ const payload = {
538
+ origin: api.apiOrigin,
539
+ auth: true,
540
+ json: {
541
+ schemaId: json.schema.id,
542
+ },
543
+ };
544
+ return api
545
+ .request('POST', '/c/v1/branches', payload)
546
+ .then((data) => {
547
+ cancelWait();
548
+ const branches = [...data.branches].sort((a, b) => {
549
+ if (a.name < b.name)
550
+ return -1;
551
+ if (a.name > b.name)
552
+ return 1;
553
+ return 0;
554
+ });
555
+ // The api still returns master for backwards comparability so we manually
556
+ // update the branch name to main
557
+ return branches.map((branch) => branch.name === 'master' ? { ...branch, name: 'main' } : branch);
558
+ });
559
+ }
560
+ function checkout(branchToCheckout, json) {
561
+ return fetchBranches(json).then((branches) => {
562
+ if (!branchToCheckout) {
563
+ const choices = branches.map((branch) => ({
564
+ value: branch,
565
+ name: branch.name,
566
+ }));
567
+ const currentBranch = branches.find(({ id }) => id === json.branch.id);
568
+ return inquirer
569
+ .prompt([
570
+ {
571
+ type: 'list',
572
+ name: 'branch',
573
+ message: 'Select a branch',
574
+ default: currentBranch ?? branches.find(({ id }) => id === 'master'),
575
+ choices,
576
+ pageSize: 15,
577
+ },
578
+ ])
579
+ .then((answer) => {
580
+ if (answer.branch === currentBranch) {
581
+ report.info(`Already on '${currentBranch.name}'`);
582
+ return json;
583
+ }
584
+ const { branch } = answer;
585
+ report.success(`Switched to branch '${branch.name}'`);
586
+ return {
587
+ ...json,
588
+ branch: {
589
+ id: branch.id,
590
+ name: branch.name,
591
+ },
592
+ };
593
+ });
594
+ }
595
+ if (branchToCheckout === 'master') {
596
+ report.info("The master branch has been renamed to main. Continuing checkout with main branch...'");
597
+ }
598
+ const adjustedBranchToCheckout = branchToCheckout === 'master' ? 'main' : branchToCheckout;
599
+ if (adjustedBranchToCheckout === json.branch.name) {
600
+ // XXX should check here if json.branch.id === branch.id from server
601
+ // if not, it indicates branch delete, same branch re-created and client is out of sync
602
+ report.info(`Already on '${adjustedBranchToCheckout}'`);
603
+ return json;
604
+ }
605
+ const branch = branches.find(({ name }) => name === adjustedBranchToCheckout);
606
+ if (!branch) {
607
+ report.error(`Branch '${adjustedBranchToCheckout}' does not exist. Run ${cmd('avo checkout')} to list available branches`);
608
+ }
609
+ report.success(`Switched to branch '${branch.name}'`);
610
+ return {
611
+ ...json,
612
+ branch: {
613
+ id: branch.id,
614
+ name: branch.name,
615
+ },
616
+ };
464
617
  });
465
- throw new Error(
466
- "Could not automatically resolve merge conflicts in avo.json. Resolve merge conflicts in avo.json before running 'avo pull' again."
467
- );
468
- }
469
-
470
- if (
471
- !_.isEqual(head.sources.map(s => s.id), incoming.sources.map(s => s.id))
472
- ) {
473
- Avo.cliConflictResolveFailed({
474
- userId_: installIdOrUserId(),
475
- cliInvokedByCi: invokedByCi(),
476
- schemaId: head.schema.id,
477
- schemaName: head.schema.name,
478
- branchId: head.branch.id,
479
- branchName: head.branch.name
618
+ }
619
+ function resolveAvoJsonConflicts(avoFile, { argv, skipPullMaster }) {
620
+ report.info('Resolving Avo merge conflicts');
621
+ const files = extractConflictingFiles(avoFile);
622
+ const head = JSON.parse(files[0]);
623
+ const incoming = JSON.parse(files[1]);
624
+ Avo.cliConflictResolveAttempted({
625
+ userId_: installIdOrUserId(),
626
+ cliInvokedByCi: invokedByCi(),
627
+ schemaId: head.schema.id,
628
+ schemaName: head.schema.name,
629
+ branchId: head.branch.id,
630
+ branchName: head.branch.name,
480
631
  });
481
- throw new Error(
482
- "Could not automatically resolve merge conflicts in avo.json. Resolve merge conflicts in sources list in avo.json before running 'avo pull' again."
483
- );
484
- }
485
-
486
- const nextAvoJson = {
487
- avo: head.avo,
488
- schema: head.schema,
489
- branch: head.branch,
490
- sources: head.sources
491
- };
492
- return requireAuth(argv, () => {
493
- return fetchBranches(nextAvoJson).then(branches => {
494
- const isHeadBranchOpen = branches.find(branch => {
495
- return branch.id == nextAvoJson.branch.id;
496
- });
497
-
498
- const isIncomingBranchOpen = branches.find(branch => {
499
- return branch.id == incoming.branch.id;
500
- });
501
-
502
- function switchBranchIfRequired(json) {
503
- if (isHeadBranchOpen) {
504
- return Promise.resolve(json);
505
- } else {
506
- report.info(
507
- `Your current branch '${json.branch.name}' has been closed or merged. Go to another branch:`
508
- );
509
- return checkout(null, json);
632
+ if (head.avo.version !== incoming.avo.version ||
633
+ head.schema.id !== incoming.schema.id) {
634
+ Avo.cliConflictResolveFailed({
635
+ userId_: installIdOrUserId(),
636
+ cliInvokedByCi: invokedByCi(),
637
+ schemaId: head.schema.id,
638
+ schemaName: head.schema.name,
639
+ branchId: head.branch.id,
640
+ branchName: head.branch.name,
641
+ });
642
+ throw new Error("Could not automatically resolve merge conflicts in avo.json. Resolve merge conflicts in avo.json before running 'avo pull' again.");
643
+ }
644
+ if (JSON.stringify(head.sources.map((s) => s.id)) !==
645
+ JSON.stringify(incoming.sources.map((s) => s.id))) {
646
+ Avo.cliConflictResolveFailed({
647
+ userId_: installIdOrUserId(),
648
+ cliInvokedByCi: invokedByCi(),
649
+ schemaId: head.schema.id,
650
+ schemaName: head.schema.name,
651
+ branchId: head.branch.id,
652
+ branchName: head.branch.name,
653
+ });
654
+ throw new Error("Could not automatically resolve merge conflicts in avo.json. Resolve merge conflicts in sources list in avo.json before running 'avo pull' again.");
655
+ }
656
+ const nextAvoJson = {
657
+ avo: head.avo,
658
+ schema: head.schema,
659
+ branch: head.branch,
660
+ sources: head.sources,
661
+ };
662
+ return requireAuth(argv, () => fetchBranches(nextAvoJson).then((branches) => {
663
+ const isHeadBranchOpen = branches.find((branch) => branch.id === nextAvoJson.branch.id);
664
+ const isIncomingBranchOpen = branches.find((branch) => branch.id === incoming.branch.id);
665
+ function switchBranchIfRequired(json) {
666
+ if (isHeadBranchOpen) {
667
+ return Promise.resolve(json);
668
+ }
669
+ report.info(`Your current branch '${json.branch.name}' has been closed or merged. Go to another branch:`);
670
+ return checkout(null, json);
510
671
  }
511
- }
512
-
513
- return switchBranchIfRequired(nextAvoJson)
514
- .then(json => {
515
- if (
516
- head.branch.id == incoming.branch.id ||
517
- incoming.branch.id == 'master'
518
- ) {
519
- return Promise.resolve([true, json]);
520
- } else {
672
+ return switchBranchIfRequired(nextAvoJson)
673
+ .then((json) => {
674
+ if (head.branch.id === incoming.branch.id ||
675
+ incoming.branch.id === 'master') {
676
+ return Promise.resolve([true, json]);
677
+ }
521
678
  return Promise.resolve([false, json]);
522
- }
523
679
  })
524
- .then(([isDone, json]) => {
525
- if (!isDone && isIncomingBranchOpen && argv.force) {
526
- report.warn(
527
- `Incoming branch, ${
528
- incoming.branch.name
529
- }, has not been merged to Avo main. To review and merge go to: ${link(
530
- `https://www.avo.app/schemas/${nextAvoJson.schema.id}/branches/${incoming.branch.id}/diff`
531
- )}`
532
- );
533
- return Promise.resolve(json);
534
- } else if (!isDone && isIncomingBranchOpen) {
535
- Avo.cliConflictResolveFailed({
536
- userId_: installIdOrUserId(),
537
- cliInvokedByCi: invokedByCi(),
538
- schemaId: head.schema.id,
539
- schemaName: head.schema.name,
540
- branchId: head.branch.id,
541
- branchName: head.branch.name
542
- });
543
- throw new Error(
544
- `Incoming branch, ${
545
- incoming.branch.name
546
- }, has not been merged to Avo main.\n\nTo review and merge go to:\n${link(
547
- `https://www.avo.app/schemas/${nextAvoJson.schema.id}/branches/${incoming.branch.id}/diff`
548
- )}\n\nOnce merged, run 'avo pull'. To skip this check use the --force flag.`
549
- );
550
- } else {
551
- return Promise.resolve(json);
552
- }
680
+ .then(([isDone, json]) => {
681
+ if (!isDone && isIncomingBranchOpen && argv.force) {
682
+ report.warn(`Incoming branch, ${incoming.branch.name}, has not been merged to Avo main. To review and merge go to: ${link(`https://www.avo.app/schemas/${nextAvoJson.schema.id}/branches/${incoming.branch.id}/diff`)}`);
683
+ return Promise.resolve(json);
684
+ }
685
+ if (!isDone && isIncomingBranchOpen) {
686
+ Avo.cliConflictResolveFailed({
687
+ userId_: installIdOrUserId(),
688
+ cliInvokedByCi: invokedByCi(),
689
+ schemaId: head.schema.id,
690
+ schemaName: head.schema.name,
691
+ branchId: head.branch.id,
692
+ branchName: head.branch.name,
693
+ });
694
+ throw new Error(`Incoming branch, ${incoming.branch.name}, has not been merged to Avo main.\n\nTo review and merge go to:\n${link(`https://www.avo.app/schemas/${nextAvoJson.schema.id}/branches/${incoming.branch.id}/diff`)}\n\nOnce merged, run 'avo pull'. To skip this check use the --force flag.`);
695
+ }
696
+ else {
697
+ return Promise.resolve(json);
698
+ }
553
699
  })
554
- .then(json => {
555
- if (skipPullMaster) {
556
- return Promise.resolve(json);
557
- } else {
700
+ .then((json) => {
701
+ if (skipPullMaster) {
702
+ return Promise.resolve(json);
703
+ }
558
704
  return promptPullMaster(json);
559
- }
560
705
  })
561
- .then(json => {
562
- Avo.cliConflictResolveSucceeded({
563
- userId_: installIdOrUserId(),
564
- cliInvokedByCi: invokedByCi(),
565
- schemaId: head.schema.id,
566
- schemaName: head.schema.name,
567
- branchId: head.branch.id,
568
- branchName: head.branch.name
569
- });
570
- report.success('Successfully resolved Avo merge conflicts');
571
- return validateAvoJson(json);
706
+ .then((json) => {
707
+ Avo.cliConflictResolveSucceeded({
708
+ userId_: installIdOrUserId(),
709
+ cliInvokedByCi: invokedByCi(),
710
+ schemaId: head.schema.id,
711
+ schemaName: head.schema.name,
712
+ branchId: head.branch.id,
713
+ branchName: head.branch.name,
714
+ });
715
+ report.success('Successfully resolved Avo merge conflicts');
716
+ return validateAvoJson(json);
572
717
  });
573
- });
574
- });
718
+ }));
575
719
  }
576
-
577
720
  function loadAvoJson() {
578
- return loadJsonFile('avo.json')
579
- .then(validateAvoJson)
580
- .catch(err => {
581
- if (err.code === 'ENOENT') {
582
- throw new AvoError(
583
- `File ${file('avo.json')} does not exist. Run ${cmd('avo init')}`
584
- );
585
- } else {
586
- throw err;
587
- }
721
+ return loadJsonFile('avo.json')
722
+ .then(validateAvoJson)
723
+ .catch((err) => {
724
+ if (err.code === 'ENOENT') {
725
+ throw new AvoError(`File ${file('avo.json')} does not exist. Run ${cmd('avo init')}`);
726
+ }
727
+ else {
728
+ throw err;
729
+ }
588
730
  });
589
731
  }
590
-
591
- function loadAvoJsonOrInit({argv, skipPullMaster, skipInit}) {
592
- return pify(fs.readFile)('avo.json', 'utf8')
593
- .then(file => {
594
- if (hasMergeConflicts(file)) {
595
- return resolveAvoJsonConflicts(file, {
596
- argv,
597
- skipPullMaster
598
- });
599
- } else {
600
- return Promise.resolve(JSON.parse(file));
601
- }
602
- })
603
- .then(json => {
604
- json.force = argv.f === true;
605
- return Promise.resolve(json);
732
+ function loadAvoJsonOrInit({ argv, skipPullMaster, skipInit }) {
733
+ return pify(fs.readFile)('avo.json', 'utf8')
734
+ .then((avoFile) => {
735
+ if (hasMergeConflicts(avoFile)) {
736
+ return resolveAvoJsonConflicts(avoFile, {
737
+ argv,
738
+ skipPullMaster,
739
+ });
740
+ }
741
+ return Promise.resolve(JSON.parse(avoFile));
606
742
  })
607
- .then(validateAvoJson)
608
- .catch(error => {
609
- if (error.code === 'ENOENT' && skipInit) {
610
- return;
611
- } else if (error.code === 'ENOENT') {
612
- report.info('Avo not initialized');
613
- return requireAuth(argv, init);
614
- } else {
743
+ .then((json) => Promise.resolve({ ...json, force: argv.f === true }))
744
+ .then(validateAvoJson)
745
+ .catch((error) => {
746
+ if (error.code === 'ENOENT' && skipInit) {
747
+ return Promise.resolve();
748
+ }
749
+ if (error.code === 'ENOENT') {
750
+ report.info('Avo not initialized');
751
+ return requireAuth(argv, init);
752
+ }
615
753
  throw error;
616
- }
617
754
  });
618
755
  }
619
-
620
756
  function writeAvoJson(json) {
621
- return writeJsonFile('avo.json', json, {
622
- indent: 2
623
- }).then(() => json);
624
- }
625
-
626
- function init() {
627
- let makeAvoJson = schema => {
628
- report.success(`Initialized for workspace ${cyan(schema.name)}`);
629
-
630
- return {
631
- avo: {
632
- version: semver.major(pkg.version)
633
- },
634
- schema: {
635
- id: schema.id,
636
- name: schema.name
637
- },
638
- branch: {
639
- id: 'master',
640
- name: 'main'
641
- }
642
- };
643
- };
644
- wait('Initializing');
645
- return api
646
- .request('GET', '/c/v1/workspaces', {
647
- origin: api.apiOrigin,
648
- auth: true
649
- })
650
- .then(res => {
651
- cancelWait();
652
- let result = res.body;
653
- let schemas = _.orderBy(result.workspaces, 'lastUsedAt', 'desc');
654
- if (schemas.length > 1) {
655
- let choices = schemas.map(schema => ({
656
- value: schema,
657
- name: schema.name
658
- }));
659
- return inquirer
660
- .prompt([
661
- {
662
- type: 'list',
663
- name: 'schema',
664
- message: 'Select a workspace to initialize',
665
- choices: choices
666
- }
667
- ])
668
- .then(answer => {
669
- return makeAvoJson(answer.schema);
670
- });
671
- } else if (schemas.length === 0) {
672
- throw new AvoError(
673
- `No workspaces to initialize. Go to ${link(
674
- 'wwww.avo.app'
675
- )} to create one`
676
- );
677
- } else {
678
- let schema = schemas[0];
679
- return makeAvoJson(schema);
680
- }
681
- });
757
+ return writeJsonFile('avo.json', json, {
758
+ indent: 2,
759
+ }).then(() => json);
682
760
  }
683
-
684
- function codegen(json, result) {
685
- let schema = result.schema;
686
- let targets = result.sources;
687
- let newJson = Object.assign({}, _.cloneDeep(json), {schema: schema});
688
- let warnings = result.warnings;
689
- let errors = result.errors;
690
-
691
- newJson.sources = newJson.sources.map(source => {
692
- let target = _.find(targets, target => target.id === source.id);
693
- if (target) {
694
- return Object.assign({}, source, {
695
- actionId: target.actionId,
696
- name: target.name,
697
- id: target.id,
698
- path: source.path,
699
- branchId: target.branchId,
700
- updatedAt: target.updatedAt
701
- });
702
- } else {
703
- return source;
704
- }
705
- });
706
-
707
- let sourceTasks = targets.map(target => {
708
- return Promise.all(
709
- target.code.map(code => writeFile(code.path, code.content))
710
- );
711
- });
712
-
713
- let avoJsonTask = writeAvoJson(newJson);
714
-
715
- Promise.all(_.concat([avoJsonTask], sourceTasks)).then(() => {
716
- if (errors !== undefined && errors !== null && errors !== "") {
717
- report.warn(errors + "\n");
718
- }
719
-
720
- if (warnings !== undefined && warnings !== null && Array.isArray(warnings)) {
721
- warnings.forEach(warning => {
722
- report.warn(warning);
723
- });
724
- }
725
- report.success(
726
- `Analytics ${
727
- targets.length > 1 ? 'wrappers' : 'wrapper'
728
- } successfully updated`
729
- );
730
- targets.forEach(target => {
731
- let source = _.find(newJson.sources, source => source.id === target.id);
732
- report.tree('sources', [
733
- {
734
- name: source.name,
735
- children: target.code.map(code => {
736
- return {name: code.path};
737
- })
761
+ function codegen(json, { schema, sources: targets, warnings, errors }) {
762
+ const newJson = { ...JSON.parse(JSON.stringify(json)), schema };
763
+ newJson.sources = newJson.sources.map((source) => {
764
+ const target = targets.find(({ id }) => id === source.id);
765
+ if (target) {
766
+ return {
767
+ ...source,
768
+ actionId: target.actionId,
769
+ name: target.name,
770
+ id: target.id,
771
+ path: source.path,
772
+ branchId: target.branchId,
773
+ updatedAt: target.updatedAt,
774
+ };
738
775
  }
739
- ]);
776
+ return source;
740
777
  });
741
- });
742
- }
743
-
744
- function selectSource(sourceToAdd, json) {
745
- wait('Fetching sources');
746
- return api
747
- .request('POST', '/c/v1/sources', {
748
- origin: api.apiOrigin,
749
- auth: true,
750
- data: {
751
- schemaId: json.schema.id,
752
- branchId: json.branch.id
753
- }
754
- })
755
- .then(res => {
756
- cancelWait();
757
- let result = res.body;
758
- let existingSources = json.sources || [];
759
- let sources = _.sortBy(
760
- _.filter(
761
- result.sources,
762
- source =>
763
- _.find(
764
- existingSources,
765
- existingSource => source.id === existingSource.id
766
- ) === undefined
767
- ),
768
- 'name'
769
- );
770
-
771
- let prompts = [
772
- {
773
- type: 'fuzzypath',
774
- name: 'folder',
775
- excludePath: path =>
776
- path.startsWith('node_modules') || path.startsWith('.git'),
777
- itemType: 'directory',
778
- rootPath: '.',
779
- message: 'Select a folder to save the analytics wrapper in',
780
- default: '.',
781
- suggestOnly: false,
782
- depthLimit: 10
778
+ const sourceTasks = targets.map((target) => Promise.all(target.code.map((code) => writeFile(code.path, code.content))));
779
+ const avoJsonTask = writeAvoJson(newJson);
780
+ Promise.all([avoJsonTask].concat(sourceTasks)).then(() => {
781
+ if (errors !== undefined && errors !== null && errors !== '') {
782
+ report.warn(`${errors}\n`);
783
783
  }
784
- ];
785
-
786
- if (!sourceToAdd) {
787
- let choices = sources.map(source => {
788
- return {value: source, name: source.name};
789
- });
790
-
791
- prompts.unshift({
792
- type: 'list',
793
- name: 'source',
794
- message: 'Select a source to set up',
795
- choices: choices,
796
- pageSize: 15
797
- });
798
- prompts.push({
799
- type: 'input',
800
- name: 'filename',
801
- message: 'Select a filename for the analytics wrapper',
802
- default: function(answers) {
803
- return answers.source.filenameHint;
804
- }
805
- });
806
- } else {
807
- let source = _.find(sources, source =>
808
- matchesSource(source, sourceToAdd)
809
- );
810
- if (!source) {
811
- throw new AvoError(`Source ${sourceToAdd} does not exist`);
784
+ if (warnings !== undefined &&
785
+ warnings !== null &&
786
+ Array.isArray(warnings)) {
787
+ warnings.forEach((warning) => {
788
+ report.warn(warning);
789
+ });
812
790
  }
813
- prompts.push({
814
- type: 'input',
815
- name: 'filename',
816
- message: 'Select a filename for the library',
817
- default: function(answers) {
818
- return source.filenameHint;
819
- }
791
+ report.success(`Analytics ${targets.length > 1 ? 'wrappers' : 'wrapper'} successfully updated`);
792
+ targets.forEach((target) => {
793
+ const source = newJson.sources.find(({ id }) => id === target.id);
794
+ report.tree('sources', [
795
+ {
796
+ name: source.name,
797
+ children: target.code.map((code) => ({ name: code.path })),
798
+ },
799
+ ]);
820
800
  });
821
- }
822
-
823
- return inquirer.prompt(prompts).then(answer => {
824
- let relativePath = path.relative(
825
- process.cwd(),
826
- path.join(path.resolve(answer.folder), answer.filename)
827
- );
828
- let source;
829
- if (sourceToAdd) {
830
- source = _.find(sources, source =>
831
- matchesSource(source, sourceToAdd)
832
- );
833
- source = {id: source.id, name: source.name, path: relativePath};
834
- } else {
835
- source = {
836
- id: answer.source.id,
837
- name: answer.source.name,
838
- path: relativePath
839
- };
840
- }
841
- sources = _.concat(json.sources || [], [source]);
842
- let newJson = Object.assign({}, json, {sources: sources});
843
- report.info(`Added source ${source.name} to the project`);
844
- report.info(
845
- `Run 'avo pull "${source.name}"' to pull the latest analytics wrapper for this source`
846
- );
847
- return newJson;
848
- });
849
801
  });
850
802
  }
851
-
852
- function fetchBranches(json) {
853
- const schemaId = json.schema.id;
854
- wait('Fetching open branches');
855
- const payload = {
856
- origin: api.apiOrigin,
857
- auth: true,
858
- data: {
859
- schemaId: json.schema.id
860
- }
861
- };
862
- return api.request('POST', '/c/v1/branches', payload).then(res => {
863
- cancelWait();
864
- let result = res.body;
865
- let branches = _.sortBy(result.branches, 'name');
866
- // The api still returns master for backwards comparability so we manually
867
- // update the branch name to main
868
- return branches.map(
869
- branch => branch.name === "master" ? {...branch, name: "main"} : branch
870
- );
871
- return branches;
872
- });
873
- }
874
-
875
- function checkout(branchToCheckout, json) {
876
- return fetchBranches(json).then(branches => {
877
- if (!branchToCheckout) {
878
- let choices = branches.map(branch => {
879
- return {value: branch, name: branch.name};
880
- });
881
- let currentBranch = _.find(
882
- branches,
883
- branch => branch.id == json.branch.id
884
- );
885
- return inquirer
886
- .prompt([
887
- {
888
- type: 'list',
889
- name: 'branch',
890
- message: 'Select a branch',
891
- default:
892
- currentBranch ||
893
- _.find(branches, branch => branch.id == 'master'),
894
- choices: choices,
895
- pageSize: 15
896
- }
897
- ])
898
- .then(answer => {
899
- if (answer.branch === currentBranch) {
900
- report.info(`Already on '${currentBranch.name}'`);
901
- return json;
902
- } else {
903
- let branch = answer.branch;
904
- json = Object.assign({}, json, {
905
- branch: {
906
- id: branch.id,
907
- name: branch.name
908
- }
909
- });
910
- report.success(`Switched to branch '${branch.name}'`);
911
- return json;
912
- }
913
- });
914
- } else {
915
- if (branchToCheckout == "master") {
916
- report.info(
917
- `The master branch has been renamed to main. Continuing checkout with main branch...'`
918
- );
919
- }
920
- let adjustedBranchToCheckout =
921
- branchToCheckout == "master" ? "main" : branchToCheckout;
922
- if (adjustedBranchToCheckout == json.branch.name) {
923
- // XXX should check here if json.branch.id === branch.id from server
924
- // if not, it indicates branch delete, same branch re-created and client is out of sync
925
- report.info(`Already on '${adjustedBranchToCheckout}'`);
926
- return json;
927
- }
928
- let branch = _.find(branches, branch => branch.name == adjustedBranchToCheckout);
929
- if (!branch) {
930
- report.error(
931
- `Branch '${adjustedBranchToCheckout}' does not exist. Run ${cmd(
932
- 'avo checkout'
933
- )} to list available branches`
934
- );
935
- } else {
936
- json = Object.assign({}, json, {
937
- branch: {
938
- id: branch.id,
939
- name: branch.name
940
- }
941
- });
942
- report.success(`Switched to branch '${branch.name}'`);
943
- return json;
944
- }
945
- }
946
- });
947
- }
948
-
949
803
  function matchesSource(source, filter) {
950
- return source.name.toLowerCase() === filter.toLowerCase();
804
+ return source.name.toLowerCase() === filter.toLowerCase();
951
805
  }
952
-
953
- function pull(sourceFilter, json) {
954
- let sources = sourceFilter
955
- ? [_.find(json.sources, source => matchesSource(source, sourceFilter))]
956
- : json.sources;
957
- let sourceNames = _.map(sources, source => source.name);
958
- wait(`Pulling ${sourceNames.join(', ')}`);
959
-
960
- return getMasterStatus(json)
961
- .then(status => {
962
- if (status == BRANCH_NOT_UP_TO_DATE) {
963
- report.warn(
964
- `Your branch '${json.branch.name}' is not up to date with Avo main. To merge latest Avo main into the branch, run 'avo merge main'.`
965
- );
966
- }
967
- return Promise.resolve();
968
- })
969
- .then(() => {
970
- return api.request('POST', '/c/v1/pull', {
806
+ function selectSource(sourceToAdd, json) {
807
+ wait('Fetching sources');
808
+ return api
809
+ .request('POST', '/c/v1/sources', {
971
810
  origin: api.apiOrigin,
972
811
  auth: true,
973
- data: {
974
- schemaId: json.schema.id,
975
- branchId: json.branch.id,
976
- sources: _.map(sources, source => {
977
- return {id: source.id, path: source.path};
978
- }),
979
- force: json.force || false
980
- }
981
- });
812
+ json: {
813
+ schemaId: json.schema.id,
814
+ branchId: json.branch.id,
815
+ },
982
816
  })
983
- .then(res => {
984
-
985
- cancelWait();
986
- let result = res.body;
987
- if (result.ok) {
988
- codegen(json, result);
989
- } else {
990
- report.error(
991
- `Branch ${result.branchName} was ${
992
- result.reason
993
- } ${dateFns.distanceInWords(
994
- new Date(result.closedAt),
995
- new Date()
996
- )} ago. Pick another branch.`
997
- );
998
- checkout(null, json).then(json => {
999
- return pull(sourceFilter, json);
817
+ .then((data) => {
818
+ cancelWait();
819
+ const existingSources = json.sources ?? [];
820
+ let sources = data.sources
821
+ .filter((source) => !existingSources.find(({ id }) => source.id === id))
822
+ .sort((a, b) => {
823
+ if (a.name < b.name)
824
+ return -1;
825
+ if (a.name > b.name)
826
+ return 1;
827
+ return 0;
828
+ });
829
+ const prompts = [
830
+ {
831
+ type: 'fuzzypath',
832
+ name: 'folder',
833
+ excludePath: (maybeExcludePath) => maybeExcludePath.startsWith('node_modules') ||
834
+ maybeExcludePath.startsWith('.git'),
835
+ itemType: 'directory',
836
+ rootPath: '.',
837
+ message: 'Select a folder to save the analytics wrapper in',
838
+ default: '.',
839
+ suggestOnly: false,
840
+ depthLimit: 10,
841
+ },
842
+ ];
843
+ if (!sourceToAdd) {
844
+ const choices = sources.map((source) => ({
845
+ value: source,
846
+ name: source.name,
847
+ }));
848
+ prompts.unshift({
849
+ type: 'list',
850
+ name: 'source',
851
+ message: 'Select a source to set up',
852
+ // @ts-ignore
853
+ choices,
854
+ pageSize: 15,
855
+ });
856
+ prompts.push({
857
+ type: 'input',
858
+ name: 'filename',
859
+ message: 'Select a filename for the analytics wrapper',
860
+ // @ts-ignore
861
+ default(answers) {
862
+ return answers.source.filenameHint;
863
+ },
864
+ });
865
+ }
866
+ else {
867
+ const source = sources.find((soruceToFind) => matchesSource(soruceToFind, sourceToAdd));
868
+ if (!source) {
869
+ throw new AvoError(`Source ${sourceToAdd} does not exist`);
870
+ }
871
+ prompts.push({
872
+ type: 'input',
873
+ name: 'filename',
874
+ message: 'Select a filename for the library',
875
+ // @ts-ignore
876
+ default() {
877
+ return source.filenameHint;
878
+ },
879
+ });
880
+ }
881
+ return inquirer.prompt(prompts).then((answer) => {
882
+ const relativePath = path.relative(process.cwd(), path.join(path.resolve(answer.folder), answer.filename));
883
+ let source;
884
+ if (sourceToAdd) {
885
+ source = sources.find((sourceToFind) => matchesSource(sourceToFind, sourceToAdd));
886
+ source = { id: source.id, name: source.name, path: relativePath };
887
+ }
888
+ else {
889
+ source = {
890
+ id: answer.source.id,
891
+ name: answer.source.name,
892
+ path: relativePath,
893
+ };
894
+ }
895
+ sources = (json.sources ?? []).concat([source]);
896
+ const newJson = { ...json, sources };
897
+ report.info(`Added source ${source.name} to the project`);
898
+ report.info(`Run 'avo pull "${source.name}"' to pull the latest analytics wrapper for this source`);
899
+ return newJson;
1000
900
  });
1001
- }
1002
901
  });
1003
902
  }
1004
-
1005
- function installIdOrUserId() {
1006
- let installId = conf.get('avo_install_id');
1007
- let user = conf.get('user');
1008
- if (user && user.user_id) {
1009
- return user.user_id;
1010
- } else {
1011
- return installId;
1012
- }
1013
- }
1014
-
1015
- function invokedByCi() {
1016
- return process.env.CI !== undefined;
903
+ function pull(sourceFilter, json) {
904
+ const sources = sourceFilter
905
+ ? [json.sources.find((source) => matchesSource(source, sourceFilter))]
906
+ : json.sources;
907
+ const sourceNames = sources.map((source) => source.name);
908
+ wait(`Pulling ${sourceNames.join(', ')}`);
909
+ return getMasterStatus(json)
910
+ .then((masterStatus) => {
911
+ if (masterStatus === BRANCH_NOT_UP_TO_DATE) {
912
+ report.warn(`Your branch '${json.branch.name}' is not up to date with Avo main. To merge latest Avo main into the branch, run 'avo merge main'.`);
913
+ }
914
+ return Promise.resolve();
915
+ })
916
+ .then(() => api.request('POST', '/c/v1/pull', {
917
+ origin: api.apiOrigin,
918
+ auth: true,
919
+ json: {
920
+ schemaId: json.schema.id,
921
+ branchId: json.branch.id,
922
+ sources: sources.map((source) => ({
923
+ id: source.id,
924
+ path: source.path,
925
+ })),
926
+ force: json.force ?? false,
927
+ },
928
+ }))
929
+ .then((result) => {
930
+ cancelWait();
931
+ if (result.ok) {
932
+ codegen(json, result);
933
+ }
934
+ else {
935
+ report.error(`Branch ${result.branchName} was ${result.reason} ${dateFns.formatDistance(new Date(), new Date(result.closedAt))} ago. Pick another branch.`);
936
+ checkout(null, json).then((data) => pull(sourceFilter, data));
937
+ }
938
+ });
1017
939
  }
1018
-
1019
940
  function findMatches(data, regex) {
1020
- let isGlobal = regex.global;
1021
- let lines = data.split('\n');
1022
- let fileMatches = [];
1023
- let lastIndex = 0;
1024
-
1025
- for (let index = 0; index < lines.length; index++) {
1026
- let lineContents = lines[index];
1027
- let line = lastIndex + index;
1028
- let match;
1029
-
1030
- while (true) {
1031
- match = regex.exec(lineContents);
1032
- if (!match) break;
1033
-
1034
- let start = match.index;
1035
- let end = match.index + match[0].length;
1036
-
1037
- fileMatches.push({
1038
- line,
1039
- start,
1040
- end,
1041
- lineContents
1042
- });
1043
-
1044
- if (!isGlobal) break;
941
+ const isGlobal = regex.global;
942
+ const lines = data.split('\n');
943
+ const fileMatches = [];
944
+ let lastIndex = 0;
945
+ for (let index = 0; index < lines.length; index += 1) {
946
+ const lineContents = lines[index];
947
+ const line = lastIndex + index;
948
+ let match;
949
+ while (true) {
950
+ match = regex.exec(lineContents);
951
+ if (!match)
952
+ break;
953
+ const start = match.index;
954
+ const end = match.index + match[0].length;
955
+ fileMatches.push({
956
+ line,
957
+ start,
958
+ end,
959
+ lineContents,
960
+ });
961
+ if (!isGlobal)
962
+ break;
963
+ }
1045
964
  }
1046
- }
1047
-
1048
- lastIndex += lines.length;
1049
-
1050
- return fileMatches;
965
+ lastIndex += lines.length;
966
+ return fileMatches;
1051
967
  }
1052
-
1053
968
  function getEventMap(data) {
1054
- let searchFor = 'AVOEVENTMAP:';
1055
- let lines = data.split('\n').filter(line => line.indexOf(searchFor) > -1);
1056
- if (lines.length === 1) {
1057
- let line = lines[0].substring(
1058
- lines[0].indexOf(searchFor) + searchFor.length
1059
- );
1060
- line = line.substring(line.indexOf('['), line.indexOf(']') + 1);
1061
- let eventMap = JSON.parse(line);
1062
- return eventMap;
1063
- } else {
969
+ const searchFor = 'AVOEVENTMAP:';
970
+ const lines = data.split('\n').filter((line) => line.indexOf(searchFor) > -1);
971
+ if (lines.length === 1) {
972
+ let line = lines[0].substring(lines[0].indexOf(searchFor) + searchFor.length);
973
+ line = line.substring(line.indexOf('['), line.indexOf(']') + 1);
974
+ const eventMap = JSON.parse(line);
975
+ return eventMap;
976
+ }
1064
977
  return null;
1065
- }
1066
978
  }
1067
-
1068
979
  function getModuleMap(data) {
1069
- let searchFor = 'AVOMODULEMAP:';
1070
- let lines = data.split('\n').filter(line => line.indexOf(searchFor) > -1);
1071
- if (lines.length === 1) {
1072
- let line = lines[0].substring(
1073
- lines[0].indexOf(searchFor) + searchFor.length
1074
- );
1075
- line = line.substring(line.indexOf('"'), line.lastIndexOf('"') + 1);
1076
- let moduleMap = JSON.parse(line);
1077
- return moduleMap;
1078
- } else {
980
+ const searchFor = 'AVOMODULEMAP:';
981
+ const lines = data.split('\n').filter((line) => line.indexOf(searchFor) > -1);
982
+ if (lines.length === 1) {
983
+ let line = lines[0].substring(lines[0].indexOf(searchFor) + searchFor.length);
984
+ line = line.substring(line.indexOf('"'), line.lastIndexOf('"') + 1);
985
+ const moduleMap = JSON.parse(line);
986
+ return moduleMap;
987
+ }
1079
988
  return null;
1080
- }
1081
989
  }
1082
-
1083
990
  function getSource(argv, json) {
1084
- if (!json.sources || !json.sources.length) {
1085
- report.info(`No sources configured.`);
1086
- return requireAuth(argv, () => {
1087
- if (argv.source) {
1088
- report.info(`Setting up source "${argv.source}"`);
1089
- }
1090
- return selectSource(argv.source, json).then(json => [argv.source, json]);
1091
- });
1092
- } else if (
1093
- argv.source &&
1094
- !_.find(json.sources, source => matchesSource(source, argv.source))
1095
- ) {
1096
- report.error(`Source ${argv.source} not found`);
1097
- return requireAuth(argv, () =>
1098
- selectSource(argv.source, json).then(json => [argv.source, json])
1099
- );
1100
- } else {
991
+ if (!json.sources || !json.sources.length) {
992
+ report.info('No sources configured.');
993
+ return requireAuth(argv, () => {
994
+ if (argv.source) {
995
+ report.info(`Setting up source "${argv.source}"`);
996
+ }
997
+ return selectSource(argv.source, json).then((sourceJson) => [
998
+ argv.source,
999
+ sourceJson,
1000
+ ]);
1001
+ });
1002
+ }
1003
+ if (argv.source &&
1004
+ !json.sources.find((source) => matchesSource(source, argv.source))) {
1005
+ report.error(`Source ${argv.source} not found`);
1006
+ return requireAuth(argv, () => selectSource(argv.source, json).then((sourceJson) => [
1007
+ argv.source,
1008
+ sourceJson,
1009
+ ]));
1010
+ }
1101
1011
  return Promise.resolve([argv.source, json]);
1102
- }
1103
1012
  }
1104
-
1105
1013
  function status(source, json, argv) {
1106
- let sources = source
1107
- ? _.filter(json.sources, source => matchesSource(source, source))
1108
- : json.sources;
1109
-
1110
- sources = sources.filter(source => source.analysis !== false);
1111
- let fileCache = walk({
1112
- ignoreFiles: ['.gitignore'],
1113
- follow: false
1114
- }).then(results => {
1115
- results = results.filter(path => !path.startsWith('.git'));
1116
- return Promise.all(
1117
- results.map(path => {
1118
- return pify(fs.lstat)(path).then(stats => {
1119
- if (stats.isSymbolicLink()) {
1014
+ let sources = source
1015
+ ? json.sources.filter((s) => matchesSource(s, source))
1016
+ : json.sources;
1017
+ sources = sources.filter(({ analysis }) => analysis !== false);
1018
+ const fileCache = walk({
1019
+ ignoreFiles: ['.gitignore'],
1020
+ follow: false,
1021
+ }).then((results) => Promise.all(results
1022
+ .filter((result) => !result.startsWith('.git'))
1023
+ .map((resultPath) => pify(fs.lstat)(resultPath).then((stats) => {
1024
+ if (stats.isSymbolicLink()) {
1120
1025
  return [];
1121
- } else {
1122
- return pify(fs.readFile)(path, 'utf8').then(data => {
1123
- return [path, data];
1124
- });
1125
- }
1126
- });
1127
- })
1128
- ).then(cachePairs => _.fromPairs(cachePairs));
1129
- });
1130
-
1131
- fileCache
1132
- .then(cache => {
1133
- sources = Promise.all(
1134
- sources.map(source => {
1135
- return pify(fs.readFile)(source.path, 'utf8').then(data => {
1136
- let eventMap = getEventMap(data);
1026
+ }
1027
+ return pify(fs.readFile)(resultPath, 'utf8').then((data) => [
1028
+ resultPath,
1029
+ data,
1030
+ ]);
1031
+ }))).then((cachePairs) => Object.fromEntries(cachePairs)));
1032
+ fileCache
1033
+ .then((cache) => {
1034
+ sources = Promise.all(sources.map((source) => pify(fs.readFile)(source.path, 'utf8').then((data) => {
1035
+ const eventMap = getEventMap(data);
1137
1036
  if (eventMap !== null) {
1138
- let moduleMap = getModuleMap(data);
1139
- let sourcePath = path.parse(source.path);
1140
- let moduleName = _.get(
1141
- source,
1142
- 'analysis.module',
1143
- moduleMap || sourcePath.name || 'Avo'
1144
- );
1145
-
1146
- let sourcePathExts = [];
1147
-
1148
- if (sourcePath.ext === ".js" || sourcePath.ext === ".ts") {
1149
- sourcePathExts.push("js");
1150
- sourcePathExts.push("jsx");
1151
- sourcePathExts.push("ts");
1152
- sourcePathExts.push("tsx");
1153
- } else if (sourcePath.ext === ".java" || sourcePath.ext === ".kt") {
1154
- sourcePathExts.push("java");
1155
- sourcePathExts.push("kt");
1156
- } else if (sourcePath.ext === ".m" || sourcePath.ext === ".swift") {
1157
- sourcePathExts.push("m");
1158
- sourcePathExts.push("swift");
1159
- } else {
1160
- sourcePathExts.push(sourcePath.ext.substring(1));
1161
- }
1162
-
1163
- if (argv.verbose) {
1164
- console.log("Looking in files with extensions:" , sourcePathExts);
1165
- }
1166
-
1167
- let globs = [
1168
- new Minimatch(
1169
- _.get(source, 'analysis.glob', '**/*.+(' + sourcePathExts.join("|") + ")"),
1170
- {}
1171
- ),
1172
- new Minimatch('!' + source.path, {})
1173
- ];
1174
-
1175
- let lookup = _.pickBy(cache, (value, path) =>
1176
- _.every(globs, mm => mm.match(path))
1177
- );
1178
-
1179
- return Promise.all(
1180
- eventMap.map(eventName => {
1181
- let re = new RegExp('(' + moduleName + '\\.' + eventName + '|\\[' + moduleName + " " + eventName +')');
1182
- let results = _.flatMap(lookup, (data, path) => {
1183
- if (argv.verbose) {
1184
- report.info(`Looking for events in ${path}`);
1037
+ const moduleMap = getModuleMap(data);
1038
+ const sourcePath = path.parse(source.path);
1039
+ const moduleName = source.analysis?.module ??
1040
+ moduleMap ??
1041
+ sourcePath.name ??
1042
+ 'Avo';
1043
+ const sourcePathExts = [];
1044
+ if (sourcePath.ext === '.js' || sourcePath.ext === '.ts') {
1045
+ sourcePathExts.push('js');
1046
+ sourcePathExts.push('jsx');
1047
+ sourcePathExts.push('ts');
1048
+ sourcePathExts.push('tsx');
1049
+ }
1050
+ else if (sourcePath.ext === '.java' ||
1051
+ sourcePath.ext === '.kt') {
1052
+ sourcePathExts.push('java');
1053
+ sourcePathExts.push('kt');
1054
+ }
1055
+ else if (sourcePath.ext === '.m' ||
1056
+ sourcePath.ext === '.swift') {
1057
+ sourcePathExts.push('m');
1058
+ sourcePathExts.push('swift');
1059
+ }
1060
+ else if (sourcePath.ext === '.re') {
1061
+ sourcePathExts.push('re');
1062
+ sourcePathExts.push('res');
1063
+ }
1064
+ else {
1065
+ sourcePathExts.push(sourcePath.ext.substring(1));
1066
+ }
1067
+ if (argv.verbose) {
1068
+ console.log('Looking in files with extensions:', sourcePathExts);
1069
+ }
1070
+ const globs = [
1071
+ new Minimatch(source.analysis?.glob ??
1072
+ `**/*.+(${sourcePathExts.join('|')})`, {}),
1073
+ new Minimatch(`!${source.path}`, {}),
1074
+ ];
1075
+ const lookup = {};
1076
+ Object.entries(cache).forEach(([cachePath, value]) => {
1077
+ if (globs.every((mm) => mm.match(cachePath))) {
1078
+ lookup[cachePath] = value;
1185
1079
  }
1186
- let results = findMatches(data, re);
1187
- return results.length ? [[path, results]] : [];
1188
- });
1189
- return [eventName, _.fromPairs(results)];
1190
- })
1191
- ).then(results => {
1192
- return Object.assign({}, source, {
1193
- results: _.fromPairs(results)
1194
1080
  });
1195
- });
1196
- } else {
1197
- return source;
1081
+ return Promise.all(eventMap.map((eventName) => {
1082
+ const re = new RegExp(`(${moduleName}\\.${eventName}|\\[${moduleName} ${eventName})`);
1083
+ const results = Object.entries(lookup)
1084
+ .map(([path, data]) => {
1085
+ if (argv.verbose) {
1086
+ report.info(`Looking for events in ${path}`);
1087
+ }
1088
+ const results = findMatches(data, re);
1089
+ return results.length ? [[path, results]] : [];
1090
+ })
1091
+ .flat();
1092
+ return [eventName, Object.fromEntries(results)];
1093
+ })).then((results) => ({
1094
+ ...source,
1095
+ results: Object.fromEntries(results),
1096
+ }));
1198
1097
  }
1199
- });
1200
- })
1201
- );
1202
-
1203
- return sources.then(sources => {
1204
- report.tree(
1205
- 'sources',
1206
- sources.map(source => {
1207
- return {
1208
- name: source.name + ' (' + source.path + ')',
1209
- children:
1210
- _.map(source.results, (results, eventName) => {
1211
- return {
1212
- name: eventName,
1213
- children:
1214
- _.size(results) > 0
1215
- ? _.map(results, (result, matchFile) => {
1216
- return {
1217
- name:
1218
- 'used in ' +
1219
- matchFile +
1220
- ': ' +
1221
- result.length +
1222
- (result.length === 1 ? ' time' : ' times')
1223
- };
1224
- })
1225
- : [
1226
- {
1227
- name: `${logSymbols.error} no usage found`
1228
- }
1229
- ]
1230
- };
1231
- })
1232
- };
1233
- })
1234
- );
1235
-
1236
- let totalEvents = _.sumBy(sources, source => _.size(source.results));
1237
- let missingEvents = _.sumBy(sources, source =>
1238
- _.sum(
1239
- _.map(source.results, (results, eventName) =>
1240
- _.size(results) > 0 ? 0 : 1
1241
- )
1242
- )
1243
- );
1244
- if (missingEvents === 0) {
1245
- if (totalEvents === 0) {
1246
- report.error('no events found in the avo file - please run avo pull');
1247
- } else {
1248
- report.info(`${totalEvents} events seen in code`);
1249
- }
1250
- } else {
1251
- report.info(
1252
- `${totalEvents -
1253
- missingEvents} of ${totalEvents} events seen in code`
1254
- );
1098
+ return source;
1099
+ })));
1100
+ return sources.then((sources) => {
1101
+ report.tree('sources', sources.map((source) => ({
1102
+ name: `${source.name} (${source.path})`,
1103
+ children: Object.entries(source.results).map(([eventName, results]) => ({
1104
+ name: eventName,
1105
+ children: Object.keys(results).length > 0
1106
+ ? Object.entries(results).map(([matchFile, result]) => ({
1107
+ name: `used in ${matchFile}: ${result.length}${result.length === 1 ? ' time' : ' times'}`,
1108
+ }))
1109
+ : [
1110
+ {
1111
+ name: `${logSymbols.error} no usage found`,
1112
+ },
1113
+ ],
1114
+ })),
1115
+ })));
1116
+ const totalEvents = sources
1117
+ .map(({ results }) => Object.keys(results).length)
1118
+ .reduce(sum, 0);
1119
+ const missingEvents = sources
1120
+ .map(({ results }) => Object.values(results).filter((missing) => Object.keys(missing).length === 0).length)
1121
+ .reduce(sum, 0);
1122
+ if (missingEvents === 0) {
1123
+ if (totalEvents === 0) {
1124
+ report.error('no events found in the avo file - please run avo pull');
1125
+ }
1126
+ else {
1127
+ report.info(`${totalEvents} events seen in code`);
1128
+ }
1129
+ }
1130
+ else {
1131
+ report.info(`${totalEvents - missingEvents} of ${totalEvents} events seen in code`);
1132
+ }
1133
+ if (missingEvents > 0) {
1134
+ report.error(`${missingEvents} missing ${missingEvents > 1 ? 'events' : 'event'}`);
1135
+ report.tree('missingEvents', sources.map((source) => ({
1136
+ name: `${source.name} (${source.path})`,
1137
+ children: Object.entries(source.results)
1138
+ .map(([eventName, results]) => Object.keys(results).length === 0
1139
+ ? [
1140
+ {
1141
+ name: `${red(eventName)}: no usage found`,
1142
+ },
1143
+ ]
1144
+ : [])
1145
+ .flat(),
1146
+ })));
1147
+ process.exit(1);
1148
+ }
1149
+ });
1150
+ })
1151
+ .catch((error) => {
1152
+ if (error.code === 'ENOENT') {
1153
+ report.error("Avo file not found. Run 'avo pull' to pull latest Avo files.");
1255
1154
  }
1256
- if (missingEvents > 0) {
1257
- report.error(
1258
- `${missingEvents} missing ${missingEvents > 1 ? 'events' : 'event'}`
1259
- );
1260
- report.tree(
1261
- 'missingEvents',
1262
- sources.map(source => {
1263
- return {
1264
- name: source.name + ' (' + source.path + ')',
1265
- children:
1266
- _.flatMap(source.results, (results, eventName) => {
1267
- return _.size(results) === 0
1268
- ? [
1269
- {
1270
- name: `${red(eventName)}: no usage found`
1271
- }
1272
- ]
1273
- : [];
1274
- })
1275
- };
1276
- })
1277
- );
1278
- process.exit(1);
1155
+ else {
1156
+ throw error;
1279
1157
  }
1280
- });
1158
+ });
1159
+ }
1160
+ /// //////////////////////////////////////////////////////////////////////
1161
+ // AUTH
1162
+ function _getLoginUrl(callbackUrl) {
1163
+ return `${api.authOrigin}/auth/cli?state=${encodeURIComponent(nonce)}&redirect_uri=${encodeURIComponent(callbackUrl)}`;
1164
+ }
1165
+ function _getCallbackUrl(port) {
1166
+ if (port === undefined) {
1167
+ return 'urn:ietf:wg:oauth:2.0:oob';
1168
+ }
1169
+ return `http://localhost:${port}`;
1170
+ }
1171
+ function _getTokensFromAuthorizationCode(code, callbackUrl) {
1172
+ return api
1173
+ .request('POST', '/auth/token', {
1174
+ origin: api.apiOrigin,
1175
+ json: {
1176
+ token: code,
1177
+ redirect_uri: callbackUrl,
1178
+ },
1281
1179
  })
1282
- .catch(error => {
1283
- if (error.code == 'ENOENT') {
1284
- report.error(
1285
- "Avo file not found. Run 'avo pull' to pull latest Avo files."
1286
- );
1287
- } else {
1288
- throw error;
1289
- }
1180
+ .then((data) => {
1181
+ if (!data.idToken && !data.refreshToken) {
1182
+ throw INVALID_CREDENTIAL_ERROR;
1183
+ }
1184
+ lastAccessToken = {
1185
+ expiresAt: Date.now() + data.expiresIn * 1000,
1186
+ ...data,
1187
+ };
1188
+ return lastAccessToken;
1189
+ }, () => {
1190
+ throw INVALID_CREDENTIAL_ERROR;
1191
+ });
1192
+ }
1193
+ function _respondWithRedirect(req, res, Location) {
1194
+ return new Promise((resolve) => {
1195
+ res.writeHead(302, { Location });
1196
+ res.end();
1197
+ req.socket.destroy();
1198
+ resolve();
1199
+ });
1200
+ }
1201
+ function _loginWithoutLocalhost() {
1202
+ const callbackUrl = _getCallbackUrl();
1203
+ const authUrl = _getLoginUrl(callbackUrl);
1204
+ report.info(`Visit this URL on any device to login: ${new URL(authUrl)}`);
1205
+ return open(authUrl);
1206
+ }
1207
+ function _loginWithLocalhost(port) {
1208
+ return new Promise((resolve, reject) => {
1209
+ const callbackUrl = _getCallbackUrl(port);
1210
+ const authUrl = _getLoginUrl(callbackUrl);
1211
+ let server = http.createServer((req, res) => {
1212
+ let tokens;
1213
+ const query = url.parse(req.url, true).query ?? {};
1214
+ if (query.state === nonce && isString(query.code)) {
1215
+ return _getTokensFromAuthorizationCode(query.code, callbackUrl)
1216
+ .then((result) => {
1217
+ tokens = result;
1218
+ return _respondWithRedirect(req, res, `${api.authOrigin}/auth/cli/success`);
1219
+ })
1220
+ .then(() => {
1221
+ cancelWait();
1222
+ server.close();
1223
+ return resolve({
1224
+ user: jwt.decode(tokens.idToken),
1225
+ tokens,
1226
+ });
1227
+ })
1228
+ .catch(() => _respondWithRedirect(req, res, `${api.authOrigin}/auth/cli/error`));
1229
+ }
1230
+ return _respondWithRedirect(req, res, `${api.authOrigin}/auth/cli/error`);
1231
+ });
1232
+ server = httpShutdown(server);
1233
+ server.listen(port, () => {
1234
+ report.info(`Visit this URL on any device to login: ${link(authUrl)}`);
1235
+ wait('Waiting for authentication...');
1236
+ open(authUrl);
1237
+ });
1238
+ server.on('error', () => {
1239
+ _loginWithoutLocalhost().then(resolve, reject);
1240
+ });
1290
1241
  });
1291
1242
  }
1292
-
1293
- yargs(hideBin(process.argv))
1294
- .usage('$0 command')
1295
- .scriptName('avo')
1296
- .version(pkg.version)
1297
- .option('v', {
1243
+ function login() {
1244
+ return _getPort().then(_loginWithLocalhost, _loginWithoutLocalhost);
1245
+ }
1246
+ function logout(refreshToken) {
1247
+ if (lastAccessToken.refreshToken === refreshToken) {
1248
+ lastAccessToken = {};
1249
+ }
1250
+ const tokens = conf.get('tokens');
1251
+ const currentToken = tokens.refreshToken;
1252
+ if (refreshToken === currentToken) {
1253
+ conf.delete('user');
1254
+ conf.delete('tokens');
1255
+ }
1256
+ }
1257
+ yargs(hideBin(process.argv)) // eslint-disable-line no-unused-expressions
1258
+ .usage('$0 command')
1259
+ .scriptName('avo')
1260
+ .version(pkg.version)
1261
+ .option('v', {
1298
1262
  alias: 'verbose',
1299
1263
  default: false,
1300
1264
  describe: 'make output more verbose',
1301
- type: 'boolean'
1302
- })
1303
- .option('f', {
1265
+ type: 'boolean',
1266
+ })
1267
+ .option('f', {
1304
1268
  alias: 'force',
1305
1269
  describe: 'Proceed with merge when incoming branch is open',
1306
1270
  default: false,
1307
- type: 'boolean'
1308
- })
1309
- .command({
1271
+ type: 'boolean',
1272
+ })
1273
+ .command({
1310
1274
  command: 'track-install',
1311
1275
  desc: false,
1312
1276
  handler: () => {
1313
- Avo.cliInstalled({
1314
- userId_: installIdOrUserId(),
1315
- cliInvokedByCi: invokedByCi()
1316
- });
1317
- }
1318
- })
1319
- .command({
1277
+ Avo.cliInstalled({
1278
+ userId_: installIdOrUserId(),
1279
+ cliInvokedByCi: invokedByCi(),
1280
+ });
1281
+ },
1282
+ })
1283
+ .command({
1320
1284
  command: 'init',
1321
1285
  desc: 'Initialize an Avo workspace in the current folder',
1322
- handler: argv => {
1323
- loadAvoJsonOrInit({argv, skipInit: true})
1324
- .then(json => {
1325
- if (json) {
1326
- Avo.cliInvoked({
1327
- schemaId: json.schema.id,
1328
- schemaName: json.schema.name,
1329
- branchId: json.branch.id,
1330
- branchName: json.branch.name,
1331
- userId_: installIdOrUserId(),
1332
- cliAction: Avo.CliAction.INIT,
1333
- cliInvokedByCi: invokedByCi()
1334
- });
1335
- report.info(
1336
- `Avo is already initialized for workspace ${cyan(
1337
- json.schema.name
1338
- )} (${file('avo.json')} exists)`
1339
- );
1340
- } else {
1286
+ handler: (argv) => {
1287
+ loadAvoJsonOrInit({ argv, skipPullMaster: false, skipInit: true })
1288
+ .then((json) => {
1289
+ if (json) {
1290
+ Avo.cliInvoked({
1291
+ schemaId: json.schema.id,
1292
+ schemaName: json.schema.name,
1293
+ branchId: json.branch.id,
1294
+ branchName: json.branch.name,
1295
+ userId_: installIdOrUserId(),
1296
+ cliAction: Avo.CliAction.INIT,
1297
+ cliInvokedByCi: invokedByCi(),
1298
+ force: undefined,
1299
+ });
1300
+ report.info(`Avo is already initialized for workspace ${cyan(json.schema.name)} (${file('avo.json')} exists)`);
1301
+ return Promise.resolve();
1302
+ }
1341
1303
  Avo.cliInvoked({
1342
- schemaId: 'N/A',
1343
- schemaName: 'N/A',
1344
- branchId: 'N/A',
1345
- branchName: 'N/A',
1346
- userId_: installIdOrUserId(),
1347
- cliAction: Avo.CliAction.INIT,
1348
- cliInvokedByCi: invokedByCi()
1304
+ schemaId: 'N/A',
1305
+ schemaName: 'N/A',
1306
+ branchId: 'N/A',
1307
+ branchName: 'N/A',
1308
+ userId_: installIdOrUserId(),
1309
+ cliAction: Avo.CliAction.INIT,
1310
+ cliInvokedByCi: invokedByCi(),
1311
+ force: undefined,
1349
1312
  });
1350
- return requireAuth(argv, () => {
1351
- return init()
1313
+ return requireAuth(argv, () => init()
1352
1314
  .then(writeAvoJson)
1353
1315
  .then(() => {
1354
- report.info(
1355
- "Run 'avo pull' to pull analytics wrappers from Avo"
1356
- );
1357
- });
1358
- });
1359
- }
1316
+ report.info("Run 'avo pull' to pull analytics wrappers from Avo");
1317
+ }));
1360
1318
  })
1361
- .catch(() => {
1362
- Avo.cliInvoked({
1363
- schemaId: 'N/A',
1364
- schemaName: 'N/A',
1365
- branchId: 'N/A',
1366
- branchName: 'N/A',
1367
- userId_: installIdOrUserId(),
1368
- cliAction: Avo.CliAction.INIT,
1369
- cliInvokedByCi: invokedByCi()
1370
- });
1319
+ .catch(() => {
1320
+ Avo.cliInvoked({
1321
+ schemaId: 'N/A',
1322
+ schemaName: 'N/A',
1323
+ branchId: 'N/A',
1324
+ branchName: 'N/A',
1325
+ userId_: installIdOrUserId(),
1326
+ cliAction: Avo.CliAction.INIT,
1327
+ cliInvokedByCi: invokedByCi(),
1328
+ force: undefined,
1329
+ });
1371
1330
  });
1372
- }
1373
- })
1374
- .command({
1331
+ },
1332
+ })
1333
+ .command({
1375
1334
  command: 'pull [source]',
1376
1335
  desc: 'Pull analytics wrappers from Avo workspace',
1377
- builder: yargs => {
1378
- return yargs.option('branch', {
1336
+ builder: (yargs) => yargs.option('branch', {
1379
1337
  describe: 'Name of Avo branch to pull from',
1380
- type: 'string'
1381
- });
1382
- },
1383
- handler: argv => {
1384
- loadAvoJsonOrInit({argv})
1385
- .then(json => {
1386
- Avo.cliInvoked({
1387
- schemaId: json.schema.id,
1388
- schemaName: json.schema.name,
1389
- branchId: json.branch.id,
1390
- branchName: json.branch.name,
1391
- userId_: installIdOrUserId(),
1392
- cliAction: Avo.CliAction.PULL,
1393
- cliInvokedByCi: invokedByCi()
1394
- });
1395
- requireAuth(argv, () => {
1396
- if (argv.branch && json.branch.name !== argv.branch) {
1397
- return checkout(argv.branch, json)
1398
- .then(json => getSource(argv, json))
1399
- .then(([source, json]) => pull(source, json));
1400
- } else {
1401
- report.info(`Pulling from branch '${json.branch.name}'`);
1402
- return getSource(argv, json).then(([source, json]) => {
1403
- return pull(source, json);
1404
- });
1405
- }
1406
- });
1338
+ type: 'string',
1339
+ }),
1340
+ handler: (argv) => {
1341
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1342
+ .then((json) => {
1343
+ Avo.cliInvoked({
1344
+ schemaId: json.schema.id,
1345
+ schemaName: json.schema.name,
1346
+ branchId: json.branch.id,
1347
+ branchName: json.branch.name,
1348
+ userId_: installIdOrUserId(),
1349
+ cliAction: Avo.CliAction.PULL,
1350
+ cliInvokedByCi: invokedByCi(),
1351
+ force: undefined,
1352
+ });
1353
+ requireAuth(argv, () => {
1354
+ if (argv.branch && json.branch.name !== argv.branch) {
1355
+ return checkout(argv.branch, json)
1356
+ .then((data) => getSource(argv, data))
1357
+ .then(([source, data]) => pull(source, data));
1358
+ }
1359
+ report.info(`Pulling from branch '${json.branch.name}'`);
1360
+ return getSource(argv, json).then(([source, data]) => pull(source, data));
1361
+ });
1407
1362
  })
1408
- .catch(error => {
1409
- Avo.cliInvoked({
1410
- schemaId: 'N/A',
1411
- schemaName: 'N/A',
1412
- branchId: 'N/A',
1413
- branchName: 'N/A',
1414
- userId_: installIdOrUserId(),
1415
- cliAction: Avo.CliAction.PULL,
1416
- cliInvokedByCi: invokedByCi()
1417
- });
1418
- throw error;
1363
+ .catch((error) => {
1364
+ Avo.cliInvoked({
1365
+ schemaId: 'N/A',
1366
+ schemaName: 'N/A',
1367
+ branchId: 'N/A',
1368
+ branchName: 'N/A',
1369
+ userId_: installIdOrUserId(),
1370
+ cliAction: Avo.CliAction.PULL,
1371
+ cliInvokedByCi: invokedByCi(),
1372
+ force: undefined,
1373
+ });
1374
+ throw error;
1419
1375
  });
1420
- }
1421
- })
1422
- .command({
1376
+ },
1377
+ })
1378
+ .command({
1423
1379
  command: 'checkout [branch]',
1424
1380
  aliases: ['branch'],
1425
1381
  desc: 'Switch branches',
1426
- handler: argv => {
1427
- return loadAvoJsonOrInit({argv})
1428
- .then(json => {
1429
- Avo.cliInvoked({
1382
+ handler: (argv) => loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1383
+ .then((json) => {
1384
+ Avo.cliInvoked({
1430
1385
  schemaId: json.schema.id,
1431
1386
  schemaName: json.schema.name,
1432
1387
  branchId: json.branch.id,
1433
1388
  branchName: json.branch.name,
1434
1389
  userId_: installIdOrUserId(),
1435
1390
  cliAction: Avo.CliAction.CHECKOUT,
1436
- cliInvokedByCi: invokedByCi()
1437
- });
1438
- report.info(`Currently on branch '${json.branch.name}'`);
1439
- requireAuth(argv, () => {
1440
- return checkout(argv.branch, json).then(writeAvoJson);
1441
- });
1442
- })
1443
- .catch(error => {
1444
- Avo.cliInvoked({
1391
+ cliInvokedByCi: invokedByCi(),
1392
+ force: undefined,
1393
+ });
1394
+ report.info(`Currently on branch '${json.branch.name}'`);
1395
+ requireAuth(argv, () => checkout(argv.branch, json).then(writeAvoJson));
1396
+ })
1397
+ .catch((error) => {
1398
+ Avo.cliInvoked({
1445
1399
  schemaId: 'N/A',
1446
1400
  schemaName: 'N/A',
1447
1401
  branchId: 'N/A',
1448
1402
  branchName: 'N/A',
1449
1403
  userId_: installIdOrUserId(),
1450
1404
  cliAction: Avo.CliAction.CHECKOUT,
1451
- cliInvokedByCi: invokedByCi()
1452
- });
1453
- throw error;
1405
+ cliInvokedByCi: invokedByCi(),
1406
+ force: undefined,
1454
1407
  });
1455
- }
1456
- })
1457
- .command({
1408
+ throw error;
1409
+ }),
1410
+ })
1411
+ .command({
1458
1412
  command: 'source <command>',
1459
1413
  desc: 'Manage sources for the current project',
1460
- builder: yargs => {
1461
- yargs
1462
- .command({
1463
- command: '$0',
1464
- desc: 'List sources in this project',
1465
- handler: argv => {
1466
- loadAvoJsonOrInit({argv})
1467
- .then(json => {
1468
- Avo.cliInvoked({
1469
- schemaId: json.schema.id,
1470
- schemaName: json.schema.name,
1471
- branchId: json.branch.id,
1472
- branchName: json.branch.name,
1473
- userId_: installIdOrUserId(),
1474
- cliAction: Avo.CliAction.SOURCE,
1475
- cliInvokedByCi: invokedByCi()
1476
- });
1477
-
1478
- if (!json.sources || !json.sources.length) {
1479
- report.info(
1480
- `No sources defined in ${file('avo.json')}. Run ${cmd(
1481
- 'avo source add'
1482
- )} to add sources`
1483
- );
1484
- return;
1485
- }
1486
-
1487
- report.info(`Sources in this project:`);
1488
- report.tree(
1489
- 'sources',
1490
- json.sources.map(source => {
1491
- return {
1492
- name: source.name,
1493
- children: [{name: source.path}]
1494
- };
1495
- })
1496
- );
1497
- })
1498
- .catch(error => {
1499
- Avo.cliInvoked({
1500
- schemaId: 'N/A',
1501
- schemaName: 'N/A',
1502
- branchId: 'N/A',
1503
- branchName: 'N/A',
1504
- userId_: installIdOrUserId(),
1505
- cliAction: Avo.CliAction.SOURCE,
1506
- cliInvokedByCi: invokedByCi()
1414
+ builder: (yargs) => {
1415
+ yargs
1416
+ .command({
1417
+ command: '$0',
1418
+ desc: 'List sources in this project',
1419
+ handler: (argv) => {
1420
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1421
+ .then((json) => {
1422
+ Avo.cliInvoked({
1423
+ schemaId: json.schema.id,
1424
+ schemaName: json.schema.name,
1425
+ branchId: json.branch.id,
1426
+ branchName: json.branch.name,
1427
+ userId_: installIdOrUserId(),
1428
+ cliAction: Avo.CliAction.SOURCE,
1429
+ cliInvokedByCi: invokedByCi(),
1430
+ force: undefined,
1431
+ });
1432
+ if (!json.sources || !json.sources.length) {
1433
+ report.info(`No sources defined in ${file('avo.json')}. Run ${cmd('avo source add')} to add sources`);
1434
+ return;
1435
+ }
1436
+ report.info('Sources in this project:');
1437
+ report.tree('sources', json.sources.map((source) => ({
1438
+ name: source.name,
1439
+ children: [{ name: source.path }],
1440
+ })));
1441
+ })
1442
+ .catch((error) => {
1443
+ Avo.cliInvoked({
1444
+ schemaId: 'N/A',
1445
+ schemaName: 'N/A',
1446
+ branchId: 'N/A',
1447
+ branchName: 'N/A',
1448
+ userId_: installIdOrUserId(),
1449
+ cliAction: Avo.CliAction.SOURCE,
1450
+ cliInvokedByCi: invokedByCi(),
1451
+ force: undefined,
1452
+ });
1453
+ throw error;
1507
1454
  });
1508
- throw error;
1509
- });
1510
- }
1455
+ },
1511
1456
  })
1512
- .command({
1513
- command: 'add [source]',
1514
- desc: 'Add a source to this project',
1515
- handler: argv => {
1516
- loadAvoJsonOrInit({argv})
1517
- .then(json => {
1518
- Avo.cliInvoked({
1519
- schemaId: json.schema.id,
1520
- schemaName: json.schema.name,
1521
- branchId: json.branch.id,
1522
- branchName: json.branch.name,
1523
- userId_: installIdOrUserId(),
1524
- cliAction: Avo.CliAction.SOURCE_ADD,
1525
- cliInvokedByCi: invokedByCi()
1526
- });
1527
-
1528
- requireAuth(argv, () => {
1529
- selectSource(argv.source, json).then(writeAvoJson);
1530
- });
1531
- })
1532
- .catch(error => {
1533
- Avo.cliInvoked({
1534
- schemaId: 'N/A',
1535
- schemaName: 'N/A',
1536
- branchId: 'N/A',
1537
- branchName: 'N/A',
1538
- userId_: installIdOrUserId(),
1539
- cliAction: Avo.CliAction.SOURCE_ADD,
1540
- cliInvokedByCi: invokedByCi()
1457
+ .command({
1458
+ command: 'add [source]',
1459
+ desc: 'Add a source to this project',
1460
+ handler: (argv) => {
1461
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1462
+ .then((json) => {
1463
+ Avo.cliInvoked({
1464
+ schemaId: json.schema.id,
1465
+ schemaName: json.schema.name,
1466
+ branchId: json.branch.id,
1467
+ branchName: json.branch.name,
1468
+ userId_: installIdOrUserId(),
1469
+ cliAction: Avo.CliAction.SOURCE_ADD,
1470
+ cliInvokedByCi: invokedByCi(),
1471
+ force: undefined,
1472
+ });
1473
+ requireAuth(argv, () => {
1474
+ selectSource(argv.source, json).then(writeAvoJson);
1475
+ });
1476
+ })
1477
+ .catch((error) => {
1478
+ Avo.cliInvoked({
1479
+ schemaId: 'N/A',
1480
+ schemaName: 'N/A',
1481
+ branchId: 'N/A',
1482
+ branchName: 'N/A',
1483
+ userId_: installIdOrUserId(),
1484
+ cliAction: Avo.CliAction.SOURCE_ADD,
1485
+ cliInvokedByCi: invokedByCi(),
1486
+ force: undefined,
1487
+ });
1488
+ throw error;
1541
1489
  });
1542
- throw error;
1543
- });
1544
- }
1490
+ },
1545
1491
  })
1546
- .command({
1547
- command: 'remove [source]',
1548
- aliases: ['rm'],
1549
- desc: 'Remove a source from this project',
1550
- handler: argv => {
1551
- loadAvoJsonOrInit({argv})
1552
- .then(json => {
1553
- Avo.cliInvoked({
1554
- schemaId: json.schema.id,
1555
- schemaName: json.schema.name,
1556
- branchId: json.branch.id,
1557
- branchName: json.branch.name,
1558
- userId_: installIdOrUserId(),
1559
- cliAction: Avo.CliAction.SOURCE_REMOVE,
1560
- cliInvokedByCi: invokedByCi()
1561
- });
1562
-
1563
- if (!json.sources || !json.sources.length) {
1564
- report.warn(
1565
- `No sources defined in ${file('avo.json')}. Run ${cmd(
1566
- 'avo source add'
1567
- )} to add sources`
1568
- );
1569
- return;
1570
- }
1571
-
1572
- const getSource = () => {
1573
- if (argv.source) {
1574
- return Promise.resolve(
1575
- _.find(json.sources, source =>
1576
- matchesSource(source, argv.source)
1577
- )
1578
- );
1579
- } else {
1580
- let choices = json.sources.map(source => ({
1581
- value: source,
1582
- name: source.name
1583
- }));
1584
- return inquirer
1585
- .prompt({
1586
- type: 'list',
1587
- name: 'source',
1588
- message: 'Select a source to remove',
1589
- choices: choices,
1590
- pageSize: 15
1591
- })
1592
- .then(answer => answer.source);
1593
- }
1594
- };
1595
- getSource(argv, json).then(targetSource => {
1596
- if (!targetSource) {
1597
- report.error(`Source ${argv.source} not found in project.`);
1598
- return;
1599
- }
1600
-
1601
- return inquirer
1602
- .prompt([
1603
- {
1604
- type: 'confirm',
1605
- name: 'remove',
1606
- default: true,
1607
- message: `Are you sure you want to remove source ${targetSource.name} from project`
1608
- }
1609
- ])
1610
- .then(answer => {
1611
- if (answer.remove) {
1612
- let sources = _.filter(
1613
- json.sources || [],
1614
- source => source.id !== targetSource.id
1615
- );
1616
- let newJson = Object.assign({}, json, {
1617
- sources: sources
1618
- });
1619
- return writeAvoJson(newJson).then(() => {
1620
- // XXX ask to remove file as well?
1621
- report.info(
1622
- `Removed source ${targetSource.name} from project`
1623
- );
1492
+ .command({
1493
+ command: 'remove [source]',
1494
+ aliases: ['rm'],
1495
+ desc: 'Remove a source from this project',
1496
+ handler: (argv) => {
1497
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1498
+ .then((json) => {
1499
+ Avo.cliInvoked({
1500
+ schemaId: json.schema.id,
1501
+ schemaName: json.schema.name,
1502
+ branchId: json.branch.id,
1503
+ branchName: json.branch.name,
1504
+ userId_: installIdOrUserId(),
1505
+ cliAction: Avo.CliAction.SOURCE_REMOVE,
1506
+ cliInvokedByCi: invokedByCi(),
1507
+ force: undefined,
1508
+ });
1509
+ if (!json.sources || !json.sources.length) {
1510
+ report.warn(`No sources defined in ${file('avo.json')}. Run ${cmd('avo source add')} to add sources`);
1511
+ return;
1512
+ }
1513
+ const getSourceToRemove = (argv, json) => {
1514
+ if (argv.source) {
1515
+ return Promise.resolve(json.sources.find((source) => matchesSource(source, argv.source)));
1516
+ }
1517
+ const choices = json.sources.map((source) => ({
1518
+ value: source,
1519
+ name: source.name,
1520
+ }));
1521
+ return inquirer
1522
+ .prompt({
1523
+ type: 'list',
1524
+ name: 'source',
1525
+ message: 'Select a source to remove',
1526
+ choices,
1527
+ pageSize: 15,
1528
+ })
1529
+ .then((answer) => answer.source);
1530
+ };
1531
+ getSourceToRemove(argv, json).then((targetSource) => {
1532
+ if (!targetSource) {
1533
+ report.error(`Source ${argv.source} not found in project.`);
1534
+ return Promise.resolve();
1535
+ }
1536
+ return inquirer
1537
+ .prompt([
1538
+ {
1539
+ type: 'confirm',
1540
+ name: 'remove',
1541
+ default: true,
1542
+ message: `Are you sure you want to remove source ${targetSource.name} from project`,
1543
+ },
1544
+ ])
1545
+ .then((answer) => {
1546
+ if (answer.remove) {
1547
+ const sources = (json.sources ?? []).filter((source) => source.id !== targetSource.id);
1548
+ const newJson = { ...json, sources };
1549
+ return writeAvoJson(newJson).then(() => {
1550
+ // XXX ask to remove file as well?
1551
+ report.info(`Removed source ${targetSource.name} from project`);
1552
+ });
1553
+ }
1554
+ report.info(`Did not remove source ${targetSource.name} from project`);
1555
+ return Promise.resolve();
1624
1556
  });
1625
- } else {
1626
- report.info(
1627
- `Did not remove source ${targetSource.name} from project`
1628
- );
1629
- }
1630
1557
  });
1558
+ })
1559
+ .catch((error) => {
1560
+ Avo.cliInvoked({
1561
+ schemaId: 'N/A',
1562
+ schemaName: 'N/A',
1563
+ branchId: 'N/A',
1564
+ branchName: 'N/A',
1565
+ userId_: installIdOrUserId(),
1566
+ cliAction: Avo.CliAction.SOURCE_REMOVE,
1567
+ cliInvokedByCi: invokedByCi(),
1568
+ force: undefined,
1569
+ });
1570
+ throw error;
1631
1571
  });
1632
- })
1633
- .catch(error => {
1634
- Avo.cliInvoked({
1635
- schemaId: 'N/A',
1636
- schemaName: 'N/A',
1637
- branchId: 'N/A',
1638
- branchName: 'N/A',
1639
- userId_: installIdOrUserId(),
1640
- cliAction: Avo.CliAction.SOURCE_REMOVE,
1641
- cliInvokedByCi: invokedByCi()
1642
- });
1643
- throw error;
1644
- });
1645
- }
1572
+ },
1646
1573
  });
1647
- }
1648
- })
1649
- .command({
1574
+ },
1575
+ })
1576
+ .command({
1650
1577
  command: 'status [source]',
1651
1578
  desc: 'Show the status of the Avo implementation',
1652
- handler: argv => {
1653
- loadAvoJsonOrInit({argv})
1654
- .then(json => {
1655
- Avo.cliInvoked({
1656
- schemaId: json.schema.id,
1657
- schemaName: json.schema.name,
1658
- branchId: json.branch.id,
1659
- branchName: json.branch.name,
1660
- userId_: installIdOrUserId(),
1661
- cliAction: Avo.CliAction.STATUS,
1662
- cliInvokedByCi: invokedByCi()
1663
- });
1664
- report.info(`Currently on branch '${json.branch.name}'`);
1665
- return getSource(argv, json);
1666
- })
1667
- .then(([source, json]) => {
1668
- return status(source, json, argv);
1579
+ handler: (argv) => {
1580
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1581
+ .then((json) => {
1582
+ Avo.cliInvoked({
1583
+ schemaId: json.schema.id,
1584
+ schemaName: json.schema.name,
1585
+ branchId: json.branch.id,
1586
+ branchName: json.branch.name,
1587
+ userId_: installIdOrUserId(),
1588
+ cliAction: Avo.CliAction.STATUS,
1589
+ cliInvokedByCi: invokedByCi(),
1590
+ force: undefined,
1591
+ });
1592
+ report.info(`Currently on branch '${json.branch.name}'`);
1593
+ return getSource(argv, json);
1669
1594
  })
1670
- .catch(error => {
1671
- Avo.cliInvoked({
1672
- schemaId: 'N/A',
1673
- schemaName: 'N/A',
1674
- branchId: 'N/A',
1675
- branchName: 'N/A',
1676
- userId_: installIdOrUserId(),
1677
- cliAction: Avo.CliAction.STATUS,
1678
- cliInvokedByCi: invokedByCi()
1679
- });
1680
- throw error;
1595
+ .then(([source, json]) => status(source, json, argv))
1596
+ .catch((error) => {
1597
+ Avo.cliInvoked({
1598
+ schemaId: 'N/A',
1599
+ schemaName: 'N/A',
1600
+ branchId: 'N/A',
1601
+ branchName: 'N/A',
1602
+ userId_: installIdOrUserId(),
1603
+ cliAction: Avo.CliAction.STATUS,
1604
+ cliInvokedByCi: invokedByCi(),
1605
+ force: undefined,
1606
+ });
1607
+ throw error;
1681
1608
  });
1682
- }
1683
- })
1684
-
1685
- .command({
1609
+ },
1610
+ })
1611
+ .command({
1686
1612
  command: 'merge main',
1687
1613
  aliases: ['merge master'],
1688
1614
  desc: 'Pull the Avo main branch into your current branch',
1689
- handler: argv => {
1690
- loadAvoJsonOrInit({argv, skipPullMaster: true})
1691
- .then(json => {
1692
- Avo.cliInvoked({
1693
- schemaId: json.schema.id,
1694
- schemaName: json.schema.name,
1695
- branchId: json.branch.id,
1696
- branchName: json.branch.name,
1697
- userId_: installIdOrUserId(),
1698
- cliAction: Avo.CliAction.MERGE,
1699
- cliInvokedByCi: invokedByCi(),
1700
- force: json.force
1701
- });
1702
-
1703
- return requireAuth(argv, () => {
1704
- return pullMaster(json).then(writeAvoJson);
1705
- });
1615
+ handler: (argv) => {
1616
+ loadAvoJsonOrInit({ argv, skipPullMaster: true, skipInit: false })
1617
+ .then((json) => {
1618
+ Avo.cliInvoked({
1619
+ schemaId: json.schema.id,
1620
+ schemaName: json.schema.name,
1621
+ branchId: json.branch.id,
1622
+ branchName: json.branch.name,
1623
+ userId_: installIdOrUserId(),
1624
+ cliAction: Avo.CliAction.MERGE,
1625
+ cliInvokedByCi: invokedByCi(),
1626
+ force: json.force,
1627
+ });
1628
+ return requireAuth(argv, () => pullMaster(json).then(writeAvoJson));
1706
1629
  })
1707
- .catch(error => {
1708
- Avo.cliInvoked({
1709
- schemaId: 'N/A',
1710
- schemaName: 'N/A',
1711
- branchId: 'N/A',
1712
- branchName: 'N/A',
1713
- userId_: installIdOrUserId(),
1714
- cliAction: Avo.CliAction.MERGE,
1715
- cliInvokedByCi: invokedByCi()
1716
- });
1717
- throw error;
1630
+ .catch((error) => {
1631
+ Avo.cliInvoked({
1632
+ schemaId: 'N/A',
1633
+ schemaName: 'N/A',
1634
+ branchId: 'N/A',
1635
+ branchName: 'N/A',
1636
+ userId_: installIdOrUserId(),
1637
+ cliAction: Avo.CliAction.MERGE,
1638
+ cliInvokedByCi: invokedByCi(),
1639
+ force: undefined,
1640
+ });
1641
+ throw error;
1718
1642
  });
1719
- }
1720
- })
1721
- .command({
1643
+ },
1644
+ })
1645
+ .command({
1722
1646
  command: 'conflict',
1723
1647
  aliases: ['resolve', 'conflicts'],
1724
1648
  desc: 'Resolve git conflicts in Avo files',
1725
- handler: argv => {
1726
- return pify(fs.readFile)('avo.json', 'utf8')
1727
- .then(file => {
1728
- if (hasMergeConflicts(file)) {
1729
- return requireAuth(argv, () => {
1730
- return resolveAvoJsonConflicts(file, {
1731
- argv
1732
- }).then(json => {
1649
+ handler: (argv) => pify(fs.readFile)('avo.json', 'utf8')
1650
+ .then((avoFile) => {
1651
+ if (hasMergeConflicts(avoFile)) {
1652
+ return requireAuth(argv, () => resolveAvoJsonConflicts(avoFile, {
1653
+ argv,
1654
+ skipPullMaster: false,
1655
+ }).then((json) => {
1733
1656
  Avo.cliInvoked({
1734
- schemaId: json.schema.id,
1735
- schemaName: json.schema.name,
1736
- branchId: json.branch.id,
1737
- branchName: json.branch.name,
1738
- userId_: installIdOrUserId(),
1739
- cliAction: Avo.CliAction.CONFLICT,
1740
- cliInvokedByCi: invokedByCi()
1657
+ schemaId: json.schema.id,
1658
+ schemaName: json.schema.name,
1659
+ branchId: json.branch.id,
1660
+ branchName: json.branch.name,
1661
+ userId_: installIdOrUserId(),
1662
+ cliAction: Avo.CliAction.CONFLICT,
1663
+ cliInvokedByCi: invokedByCi(),
1664
+ force: undefined,
1741
1665
  });
1742
1666
  pull(null, json);
1743
- });
1744
- });
1745
- } else {
1746
- report.info(
1747
- "No git conflicts found in avo.json. Run 'avo pull' to resolve git conflicts in other Avo files."
1748
- );
1749
- const json = JSON.parse(file);
1750
- Avo.cliInvoked({
1751
- schemaId: json.schema.id,
1752
- schemaName: json.schema.name,
1753
- branchId: json.branch.id,
1754
- branchName: json.branch.name,
1755
- userId_: installIdOrUserId(),
1756
- cliAction: Avo.CliAction.CONFLICT,
1757
- cliInvokedByCi: invokedByCi()
1758
- });
1759
- return Promise.resolve(json);
1760
- }
1761
- })
1762
- .catch(error => {
1763
- Avo.cliInvoked({
1764
- schemaId: 'N/A',
1765
- schemaName: 'N/A',
1766
- branchId: 'N/A',
1767
- branchName: 'N/A',
1768
- userId_: installIdOrUserId(),
1769
- cliAction: Avo.CliAction.CONFLICT,
1770
- cliInvokedByCi: invokedByCi()
1771
- });
1772
- throw error;
1773
- });
1774
- }
1775
- })
1776
- .command({
1777
- command: 'edit',
1778
- desc: 'Open the Avo workspace in your browser',
1779
- handler: argv => {
1780
- loadAvoJsonOrInit({argv})
1781
- .then(json => {
1782
- Avo.cliInvoked({
1667
+ }));
1668
+ }
1669
+ report.info("No git conflicts found in avo.json. Run 'avo pull' to resolve git conflicts in other Avo files.");
1670
+ const json = JSON.parse(avoFile);
1671
+ Avo.cliInvoked({
1783
1672
  schemaId: json.schema.id,
1784
1673
  schemaName: json.schema.name,
1785
1674
  branchId: json.branch.id,
1786
1675
  branchName: json.branch.name,
1787
1676
  userId_: installIdOrUserId(),
1788
- cliAction: Avo.CliAction.EDIT,
1789
- cliInvokedByCi: invokedByCi()
1790
- });
1791
-
1792
- const schema = json.schema;
1793
- const url = `https://www.avo.app/schemas/${schema.id}`;
1794
- report.info(
1795
- `Opening ${cyan(schema.name)} workspace in Avo: ${link(url)}`
1796
- );
1797
- opn(url, {wait: false});
1798
- })
1799
- .catch(error => {
1800
- Avo.cliInvoked({
1677
+ cliAction: Avo.CliAction.CONFLICT,
1678
+ cliInvokedByCi: invokedByCi(),
1679
+ force: undefined,
1680
+ });
1681
+ return Promise.resolve(json);
1682
+ })
1683
+ .catch((error) => {
1684
+ Avo.cliInvoked({
1801
1685
  schemaId: 'N/A',
1802
1686
  schemaName: 'N/A',
1803
1687
  branchId: 'N/A',
1804
1688
  branchName: 'N/A',
1805
1689
  userId_: installIdOrUserId(),
1806
- cliAction: Avo.CliAction.EDIT,
1807
- cliInvokedByCi: invokedByCi()
1808
- });
1809
- throw error;
1690
+ cliAction: Avo.CliAction.CONFLICT,
1691
+ cliInvokedByCi: invokedByCi(),
1692
+ force: undefined,
1810
1693
  });
1811
- }
1812
- })
1813
- .command({
1694
+ throw error;
1695
+ }),
1696
+ })
1697
+ .command({
1698
+ command: 'edit',
1699
+ desc: 'Open the Avo workspace in your browser',
1700
+ handler: (argv) => {
1701
+ loadAvoJsonOrInit({ argv, skipInit: false, skipPullMaster: false })
1702
+ .then((json) => {
1703
+ Avo.cliInvoked({
1704
+ schemaId: json.schema.id,
1705
+ schemaName: json.schema.name,
1706
+ branchId: json.branch.id,
1707
+ branchName: json.branch.name,
1708
+ userId_: installIdOrUserId(),
1709
+ cliAction: Avo.CliAction.EDIT,
1710
+ cliInvokedByCi: invokedByCi(),
1711
+ force: undefined,
1712
+ });
1713
+ const { schema } = json;
1714
+ const schemaUrl = `https://www.avo.app/schemas/${schema.id}`;
1715
+ report.info(`Opening ${cyan(schema.name)} workspace in Avo: ${link(schemaUrl)}`);
1716
+ open(schemaUrl);
1717
+ })
1718
+ .catch((error) => {
1719
+ Avo.cliInvoked({
1720
+ schemaId: 'N/A',
1721
+ schemaName: 'N/A',
1722
+ branchId: 'N/A',
1723
+ branchName: 'N/A',
1724
+ userId_: installIdOrUserId(),
1725
+ cliAction: Avo.CliAction.EDIT,
1726
+ cliInvokedByCi: invokedByCi(),
1727
+ force: undefined,
1728
+ });
1729
+ throw error;
1730
+ });
1731
+ },
1732
+ })
1733
+ .command({
1814
1734
  command: 'login',
1815
1735
  desc: 'Log into the Avo platform',
1816
1736
  handler: () => {
1817
- let command = () => {
1818
- let user = conf.get('user');
1819
- if (user) {
1820
- report.info(`Already logged in as ${email(user.email)}`);
1821
- return;
1822
- }
1823
- login()
1824
- .then(function(result) {
1825
- conf.set('user', result.user);
1826
- conf.set('tokens', result.tokens);
1827
-
1828
- Avo.signedIn({
1829
- userId_: result.user.user_id,
1830
- email: result.user.email,
1831
- cliInvokedByCi: invokedByCi()
1737
+ const command = () => {
1738
+ const user = conf.get('user');
1739
+ if (user) {
1740
+ report.info(`Already logged in as ${email(user.email)}`);
1741
+ return;
1742
+ }
1743
+ login()
1744
+ .then((result) => {
1745
+ conf.set('user', result.user);
1746
+ conf.set('tokens', result.tokens);
1747
+ Avo.signedIn({
1748
+ userId_: result.user.user_id,
1749
+ email: result.user.email,
1750
+ authenticationMethod: Avo.AuthenticationMethod.CLI,
1751
+ });
1752
+ report.success(`Logged in as ${email(result.user.email)}`);
1753
+ })
1754
+ .catch(() => {
1755
+ Avo.signInFailed({
1756
+ userId_: conf.get('avo_install_id'),
1757
+ emailInput: '',
1758
+ signInError: Avo.SignInError.UNKNOWN,
1759
+ });
1832
1760
  });
1833
-
1834
- report.success(`Logged in as ${email(result.user.email)}`);
1835
- })
1836
- .catch(() => {
1837
- Avo.signInFailed({
1838
- userId_: conf.get('avo_install_id'),
1839
- emailInput: '', // XXX this is not passed back here
1840
- signInError: Avo.SignInError.UNKNOWN,
1841
- cliInvokedByCi: invokedByCi()
1761
+ };
1762
+ loadAvoJson()
1763
+ .then((json) => {
1764
+ Avo.cliInvoked({
1765
+ schemaId: json.schema.id,
1766
+ schemaName: json.schema.name,
1767
+ branchId: json.branch.id,
1768
+ branchName: json.branch.name,
1769
+ userId_: installIdOrUserId(),
1770
+ cliAction: Avo.CliAction.LOGIN,
1771
+ cliInvokedByCi: invokedByCi(),
1772
+ force: undefined,
1842
1773
  });
1843
- });
1844
- };
1845
-
1846
- loadAvoJson()
1847
- .then(json => {
1848
- Avo.cliInvoked({
1849
- schemaId: json.schema.id,
1850
- schemaName: json.schema.name,
1851
- branchId: json.branch.id,
1852
- branchName: json.branch.name,
1853
- userId_: installIdOrUserId(),
1854
- cliAction: Avo.CliAction.LOGIN,
1855
- cliInvokedByCi: invokedByCi()
1856
- });
1857
- command();
1774
+ command();
1858
1775
  })
1859
- .catch(() => {
1860
- Avo.cliInvoked({
1861
- schemaId: 'N/A',
1862
- schemaName: 'N/A',
1863
- branchId: 'N/A',
1864
- branchName: 'N/A',
1865
- userId_: installIdOrUserId(),
1866
- cliAction: Avo.CliAction.LOGIN,
1867
- cliInvokedByCi: invokedByCi()
1868
- });
1869
- command();
1776
+ .catch(() => {
1777
+ Avo.cliInvoked({
1778
+ schemaId: 'N/A',
1779
+ schemaName: 'N/A',
1780
+ branchId: 'N/A',
1781
+ branchName: 'N/A',
1782
+ userId_: installIdOrUserId(),
1783
+ cliAction: Avo.CliAction.LOGIN,
1784
+ cliInvokedByCi: invokedByCi(),
1785
+ force: undefined,
1786
+ });
1787
+ command();
1870
1788
  });
1871
- }
1872
- })
1873
- .command({
1789
+ },
1790
+ })
1791
+ .command({
1874
1792
  command: 'logout',
1875
1793
  desc: 'Log out from the Avo platform',
1876
1794
  handler: () => {
1877
- let command = () => {
1878
- let user = conf.get('user');
1879
- let tokens = conf.get('tokens');
1880
- let currentToken = _.get(tokens, 'refreshToken');
1881
- let token = currentToken;
1882
- api.setRefreshToken(token);
1883
- if (token) {
1884
- logout(token);
1885
- }
1886
- if (token || user || tokens) {
1887
- var msg = 'Logged out';
1888
- if (token === currentToken) {
1889
- if (user) {
1890
- msg += ' from ' + bold(user.email);
1795
+ const command = () => {
1796
+ const user = conf.get('user');
1797
+ const tokens = conf.get('tokens');
1798
+ const currentToken = tokens.refreshToken;
1799
+ const token = currentToken;
1800
+ api.setRefreshToken(token);
1801
+ if (token) {
1802
+ logout(token);
1891
1803
  }
1892
- } else {
1893
- msg += ' token "' + bold(token) + '"';
1894
- }
1895
- report.log(msg);
1896
- } else {
1897
- report.log(`No need to logout, you're not logged in`);
1898
- }
1899
- };
1900
-
1901
- loadAvoJson()
1902
- .then(json => {
1903
- Avo.cliInvoked({
1904
- schemaId: json.schema.id,
1905
- schemaName: json.schema.name,
1906
- branchId: json.branch.id,
1907
- branchName: json.branch.name,
1908
- userId_: installIdOrUserId(),
1909
- cliAction: Avo.CliAction.LOGOUT,
1910
- cliInvokedByCi: invokedByCi()
1911
- });
1912
- command();
1804
+ if (token || user || tokens) {
1805
+ let msg = 'Logged out';
1806
+ if (token === currentToken) {
1807
+ if (user) {
1808
+ msg += ` from ${bold(user.email)}`;
1809
+ }
1810
+ }
1811
+ else {
1812
+ msg += ` token "${bold(token)}"`;
1813
+ }
1814
+ report.log(msg);
1815
+ }
1816
+ else {
1817
+ report.log("No need to logout, you're not logged in");
1818
+ }
1819
+ };
1820
+ loadAvoJson()
1821
+ .then((json) => {
1822
+ Avo.cliInvoked({
1823
+ schemaId: json.schema.id,
1824
+ schemaName: json.schema.name,
1825
+ branchId: json.branch.id,
1826
+ branchName: json.branch.name,
1827
+ userId_: installIdOrUserId(),
1828
+ cliAction: Avo.CliAction.LOGOUT,
1829
+ cliInvokedByCi: invokedByCi(),
1830
+ force: undefined,
1831
+ });
1832
+ command();
1913
1833
  })
1914
- .catch(() => {
1915
- Avo.cliInvoked({
1916
- schemaId: 'N/A',
1917
- schemaName: 'N/A',
1918
- branchId: 'N/A',
1919
- branchName: 'N/A',
1920
- userId_: installIdOrUserId(),
1921
- cliAction: Avo.CliAction.LOGOUT,
1922
- cliInvokedByCi: invokedByCi()
1923
- });
1924
- command();
1834
+ .catch(() => {
1835
+ Avo.cliInvoked({
1836
+ schemaId: 'N/A',
1837
+ schemaName: 'N/A',
1838
+ branchId: 'N/A',
1839
+ branchName: 'N/A',
1840
+ userId_: installIdOrUserId(),
1841
+ cliAction: Avo.CliAction.LOGOUT,
1842
+ cliInvokedByCi: invokedByCi(),
1843
+ force: undefined,
1844
+ });
1845
+ command();
1925
1846
  });
1926
- }
1927
- })
1928
- .command({
1847
+ },
1848
+ })
1849
+ .command({
1929
1850
  command: 'whoami',
1930
1851
  desc: 'Shows the currently logged in username',
1931
- handler: argv => {
1932
- let command = () => {
1933
- requireAuth(argv, () => {
1934
- if (conf.has('user')) {
1935
- let user = conf.get('user');
1936
- report.info(`Logged in as ${email(user.email)}`);
1937
- } else {
1938
- report.warn(`Not logged in`);
1939
- }
1940
- });
1941
- };
1942
-
1943
- loadAvoJson()
1944
- .then(json => {
1945
- Avo.cliInvoked({
1946
- schemaId: json.schema.id,
1947
- schemaName: json.schema.name,
1948
- branchId: json.branch.id,
1949
- branchName: json.branch.name,
1950
- userId_: installIdOrUserId(),
1951
- cliAction: Avo.CliAction.WHOAMI,
1952
- cliInvokedByCi: invokedByCi()
1953
- });
1954
- command();
1852
+ handler: (argv) => {
1853
+ const command = () => {
1854
+ requireAuth(argv, () => {
1855
+ if (conf.has('user')) {
1856
+ const user = conf.get('user');
1857
+ report.info(`Logged in as ${email(user.email)}`);
1858
+ }
1859
+ else {
1860
+ report.warn('Not logged in');
1861
+ }
1862
+ });
1863
+ };
1864
+ loadAvoJson()
1865
+ .then((json) => {
1866
+ Avo.cliInvoked({
1867
+ schemaId: json.schema.id,
1868
+ schemaName: json.schema.name,
1869
+ branchId: json.branch.id,
1870
+ branchName: json.branch.name,
1871
+ userId_: installIdOrUserId(),
1872
+ cliAction: Avo.CliAction.WHOAMI,
1873
+ cliInvokedByCi: invokedByCi(),
1874
+ force: undefined,
1875
+ });
1876
+ command();
1955
1877
  })
1956
- .catch(() => {
1957
- Avo.cliInvoked({
1958
- schemaId: 'N/A',
1959
- schemaName: 'N/A',
1960
- branchId: 'N/A',
1961
- branchName: 'N/A',
1962
- userId_: installIdOrUserId(),
1963
- cliAction: Avo.CliAction.WHOAMI,
1964
- cliInvokedByCi: invokedByCi()
1965
- });
1966
- command();
1878
+ .catch(() => {
1879
+ Avo.cliInvoked({
1880
+ schemaId: 'N/A',
1881
+ schemaName: 'N/A',
1882
+ branchId: 'N/A',
1883
+ branchName: 'N/A',
1884
+ userId_: installIdOrUserId(),
1885
+ cliAction: Avo.CliAction.WHOAMI,
1886
+ cliInvokedByCi: invokedByCi(),
1887
+ force: undefined,
1888
+ });
1889
+ command();
1967
1890
  });
1891
+ },
1892
+ })
1893
+ .demandCommand(1, 'must provide a valid command')
1894
+ .recommendCommands()
1895
+ .help().argv;
1896
+ /// ///////////////// ////////
1897
+ // catch unhandled promises
1898
+ process.on('unhandledRejection', (err) => {
1899
+ cancelWait();
1900
+ if (!(err instanceof Error) && !(err instanceof AvoError)) {
1901
+ report.error(new AvoError(`Promise rejected with value: ${util.inspect(err)}`));
1968
1902
  }
1969
- })
1970
-
1971
- .demandCommand(1, 'must provide a valid command')
1972
- .recommendCommands()
1973
- .help().argv;
1974
-
1975
- /////////////////////////////////////////////////////////////////////////
1976
- // LOGGING
1977
-
1978
- function cmd(command) {
1979
- return `${gray('`')}${cyan(command)}${gray('`')}`;
1980
- }
1981
-
1982
- function link(url) {
1983
- return underline(url);
1984
- }
1985
-
1986
- function file(url) {
1987
- return underline(url);
1988
- }
1989
-
1990
- function email(email) {
1991
- return underline(email);
1992
- }
1993
-
1994
- function cancelWait() {
1995
- if (_cancel !== null) {
1996
- _cancel();
1997
- _cancel = null;
1998
- }
1999
- }
2000
- function wait(message, timeOut) {
2001
- cancelWait();
2002
- timeOut = timeOut || 300;
2003
- let running = false;
2004
- let spinner;
2005
- let stopped = false;
2006
-
2007
- setTimeout(() => {
2008
- if (stopped) return;
2009
-
2010
- spinner = ora(gray(message));
2011
- spinner.color = 'gray';
2012
- spinner.start();
2013
-
2014
- running = true;
2015
- }, timeOut);
2016
-
2017
- const cancel = () => {
2018
- stopped = true;
2019
- if (running) {
2020
- spinner.stop();
2021
- running = false;
2022
- }
2023
- process.removeListener('nowExit', cancel);
2024
- };
2025
-
2026
- process.on('nowExit', cancel);
2027
- cancelWait = cancel;
2028
- }
2029
-
2030
- /////////////////////////////////////////////////////////////////////////
2031
- // AUTH
2032
-
2033
- function _haveValidAccessToken(refreshToken) {
2034
- if (_.isEmpty(lastAccessToken)) {
2035
- var tokens = conf.get('tokens');
2036
- if (refreshToken === _.get(tokens, 'refreshToken')) {
2037
- lastAccessToken = tokens;
2038
- }
2039
- }
2040
-
2041
- return (
2042
- _.has(lastAccessToken, 'idToken') &&
2043
- lastAccessToken.refreshToken === refreshToken &&
2044
- _.has(lastAccessToken, 'expiresAt') &&
2045
- lastAccessToken.expiresAt > Date.now() + FIFTEEN_MINUTES_IN_MS
2046
- );
2047
- }
2048
-
2049
- function getAccessToken(refreshToken) {
2050
- if (_haveValidAccessToken(refreshToken)) {
2051
- return Promise.resolve(lastAccessToken);
2052
- }
2053
-
2054
- return _refreshAccessToken(refreshToken);
2055
- }
2056
-
2057
- function _refreshAccessToken(refreshToken) {
2058
- return api
2059
- .request('POST', '/auth/refresh', {
2060
- origin: api.apiOrigin,
2061
- data: {
2062
- token: refreshToken
2063
- }
2064
- })
2065
- .then(
2066
- function(res) {
2067
- if (res.status === 401 || res.status === 400) {
2068
- return {idToken: refreshToken};
2069
- }
2070
-
2071
- if (!_.isString(res.body.idToken)) {
2072
- throw INVALID_CREDENTIAL_ERROR;
2073
- }
2074
- lastAccessToken = _.assign(
2075
- {
2076
- expiresAt: Date.now() + res.body.expiresIn * 1000,
2077
- refreshToken: refreshToken
2078
- },
2079
- res.body
2080
- );
2081
-
2082
- var currentRefreshToken = _.get(conf.get('tokens'), 'refreshToken');
2083
- if (refreshToken === currentRefreshToken) {
2084
- conf.set('tokens', lastAccessToken);
2085
- }
2086
-
2087
- return lastAccessToken;
2088
- },
2089
- function(err) {
2090
- throw INVALID_CREDENTIAL_ERROR;
2091
- }
2092
- );
2093
- }
2094
-
2095
- function _getLoginUrl(callbackUrl) {
2096
- return (
2097
- api.authOrigin +
2098
- '/auth/cli?' +
2099
- _.map(
2100
- {
2101
- state: nonce,
2102
- redirect_uri: callbackUrl
2103
- },
2104
- function(v, k) {
2105
- return k + '=' + encodeURIComponent(v);
2106
- }
2107
- ).join('&')
2108
- );
2109
- }
2110
-
2111
- function _loginWithLocalhost(port) {
2112
- return new Promise(function(resolve, reject) {
2113
- var callbackUrl = _getCallbackUrl(port);
2114
- var authUrl = _getLoginUrl(callbackUrl);
2115
-
2116
- var server = http.createServer(function(req, res) {
2117
- var tokens;
2118
- var query = _.get(url.parse(req.url, true), 'query', {});
2119
-
2120
- if (query.state === nonce && _.isString(query.code)) {
2121
- return _getTokensFromAuthorizationCode(query.code, callbackUrl)
2122
- .then(function(result) {
2123
- tokens = result;
2124
- return _respondWithRedirect(
2125
- req,
2126
- res,
2127
- api.authOrigin + '/auth/cli/success'
2128
- );
2129
- })
2130
- .then(function() {
2131
- cancelWait();
2132
- server.shutdown();
2133
- return resolve({
2134
- user: jwt.decode(tokens.idToken),
2135
- tokens: tokens
2136
- });
2137
- })
2138
- .catch(function() {
2139
- return _respondWithRedirect(
2140
- req,
2141
- res,
2142
- api.authOrigin + '/auth/cli/error'
2143
- );
2144
- });
2145
- }
2146
- _respondWithRedirect(req, res, api.authOrigin + '/auth/cli/error');
2147
- });
2148
-
2149
- server = httpShutdown(server);
2150
-
2151
- server.listen(port, function() {
2152
- report.info(`Visit this URL on any device to login: ${link(authUrl)}`);
2153
- wait(`Waiting for authentication...`);
2154
-
2155
- opn(authUrl, {wait: false});
2156
- });
2157
-
2158
- server.on('error', function() {
2159
- _loginWithoutLocalhost().then(resolve, reject);
2160
- });
2161
- });
2162
- }
2163
-
2164
- function _loginWithoutLocalhost() {
2165
- var callbackUrl = _getCallbackUrl();
2166
- var authUrl = _getLoginUrl(callbackUrl);
2167
-
2168
- report.info(`Visit this URL on any device to login: ${url(authUrl)}`);
2169
-
2170
- opn(authUrl, {wait: false});
2171
- }
2172
-
2173
- function login() {
2174
- return _getPort().then(_loginWithLocalhost, _loginWithoutLocalhost);
2175
- }
2176
-
2177
- function _respondWithRedirect(req, res, url) {
2178
- return new Promise(function(resolve, reject) {
2179
- res.writeHead(302, {
2180
- Location: url
2181
- });
2182
- res.end();
2183
- req.socket.destroy();
2184
- return resolve();
2185
- });
2186
- }
2187
-
2188
- function _getTokensFromAuthorizationCode(code, callbackUrl) {
2189
- return api
2190
- .request('POST', '/auth/token', {
2191
- origin: api.apiOrigin,
2192
- data: {
2193
- token: code,
2194
- redirect_uri: callbackUrl
2195
- }
2196
- })
2197
- .then(
2198
- function(res) {
2199
- if (!_.has(res, 'body.idToken') && !_.has(res, 'body.refreshToken')) {
2200
- throw INVALID_CREDENTIAL_ERROR;
2201
- }
2202
- lastAccessToken = _.assign(
2203
- {
2204
- expiresAt: Date.now() + res.body.expiresIn * 1000
2205
- },
2206
- res.body
2207
- );
2208
- return lastAccessToken;
2209
- },
2210
- function(err) {
2211
- throw INVALID_CREDENTIAL_ERROR;
2212
- }
2213
- );
2214
- }
2215
-
2216
- function _getCallbackUrl(port) {
2217
- if (_.isUndefined(port)) {
2218
- return 'urn:ietf:wg:oauth:2.0:oob';
2219
- }
2220
- return 'http://localhost:' + port;
2221
- }
2222
-
2223
- function logout(refreshToken) {
2224
- if (lastAccessToken.refreshToken === refreshToken) {
2225
- lastAccessToken = {};
2226
- }
2227
- var tokens = conf.get('tokens');
2228
- var currentToken = _.get(tokens, 'refreshToken');
2229
- if (refreshToken === currentToken) {
2230
- conf.delete('user');
2231
- conf.delete('tokens');
2232
- }
2233
- }
2234
-
2235
- function responseToError(response, body) {
2236
- if (typeof body === 'string' && response.statusCode === 404) {
2237
- body = {
2238
- error: {
2239
- message: 'Not Found'
2240
- }
2241
- };
2242
- }
2243
-
2244
- if (response.statusCode < 400) {
2245
- return null;
2246
- }
2247
-
2248
- if (typeof body !== 'object') {
2249
- try {
2250
- body = JSON.parse(body);
2251
- } catch (e) {
2252
- body = {};
1903
+ else {
1904
+ // @ts-ignore
1905
+ report.error(err.message);
2253
1906
  }
2254
- }
2255
-
2256
- if (!body.error) {
2257
- var message = response.statusCode === 404 ? 'Not Found' : 'Unknown Error';
2258
- body.error = {
2259
- message: message
2260
- };
2261
- }
2262
-
2263
- var message = `HTTP Error: ${response.statusCode}, ${body.error.message ||
2264
- body.error}`;
2265
-
2266
- var exitCode;
2267
- if (response.statusCode >= 500) {
2268
- // 5xx errors are unexpected
2269
- exitCode = 2;
2270
- } else {
2271
- // 4xx errors happen sometimes
2272
- exitCode = 1;
2273
- }
2274
-
2275
- _.unset(response, 'request.headers');
2276
- return new AvoError(message, {
2277
- context: {
2278
- body: body,
2279
- response: response
2280
- },
2281
- exit: exitCode
2282
- });
2283
- }
2284
-
2285
- function requireAuth(argv, cb) {
2286
- let tokens = conf.get('tokens');
2287
- let user = conf.get('user');
2288
-
2289
- let tokenOpt = argv.token || process.env.AVO_TOKEN;
2290
-
2291
- if (tokenOpt) {
2292
- api.setRefreshToken(tokenOpt);
2293
- return cb();
2294
- }
2295
-
2296
- if (!user || !tokens) {
2297
- report.error(`Command requires authentication. Run ${cmd('avo login')}`);
1907
+ // @ts-ignore
1908
+ // console.error(err.stack);
2298
1909
  process.exit(1);
2299
- return;
2300
- }
2301
-
2302
- argv.user = user;
2303
- argv.tokens = tokens;
2304
- api.setRefreshToken(tokens.refreshToken);
2305
- return cb();
2306
- }
2307
-
2308
- //////////////////// ////////
2309
- // catch unhandled promises
2310
-
2311
- process.on('unhandledRejection', err => {
2312
- cancelWait();
2313
-
2314
- if (!(err instanceof Error) && !(err instanceof AvoError)) {
2315
- err = new AvoError(`Promise rejected with value: ${util.inspect(err)}`);
2316
- }
2317
- report.error(err.message);
2318
- // console.error(err.stack);
2319
-
2320
- process.exit(1);
2321
1910
  });