@hubspot/cli 4.1.8-beta.0 → 4.1.8-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/sandboxes.js CHANGED
@@ -1,13 +1,9 @@
1
- const cliProgress = require('cli-progress');
2
1
  const {
3
2
  getConfig,
4
3
  writeConfig,
5
4
  updateAccountConfig,
5
+ getAccountId,
6
6
  } = require('@hubspot/cli-lib');
7
- const {
8
- DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
9
- PERSONAL_ACCESS_KEY_AUTH_METHOD,
10
- } = require('@hubspot/cli-lib/lib/constants');
11
7
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
12
8
  const { logger } = require('@hubspot/cli-lib/logger');
13
9
  const {
@@ -15,36 +11,55 @@ const {
15
11
  } = require('@hubspot/cli-lib/personalAccessKey');
16
12
  const { EXIT_CODES } = require('./enums/exitCodes');
17
13
  const { enterAccountNamePrompt } = require('./prompts/enterAccountNamePrompt');
14
+ const { fetchTaskStatus, fetchTypes } = require('@hubspot/cli-lib/sandboxes');
15
+ const { handleExit, handleKeypress } = require('@hubspot/cli-lib/lib/process');
16
+ const { accountNameExistsInConfig } = require('@hubspot/cli-lib/lib/config');
18
17
  const {
19
18
  personalAccessKeyPrompt,
20
19
  } = require('./prompts/personalAccessKeyPrompt');
21
- const {
22
- setAsDefaultAccountPrompt,
23
- } = require('./prompts/setAsDefaultAccountPrompt');
24
- const { uiFeatureHighlight } = require('./ui');
25
- const { fetchTaskStatus, fetchTypes } = require('@hubspot/cli-lib/sandboxes');
26
- const { handleExit, handleKeypress } = require('@hubspot/cli-lib/lib/process');
20
+ const CliProgressMultibarManager = require('./CliProgressMultibarManager');
21
+
22
+ const STANDARD_SANDBOX = 'standard';
23
+ const DEVELOPER_SANDBOX = 'developer';
24
+
25
+ const sandboxTypeMap = {
26
+ DEV: DEVELOPER_SANDBOX,
27
+ dev: DEVELOPER_SANDBOX,
28
+ DEVELOPER: DEVELOPER_SANDBOX,
29
+ developer: DEVELOPER_SANDBOX,
30
+ DEVELOPMENT: DEVELOPER_SANDBOX,
31
+ development: DEVELOPER_SANDBOX,
32
+ STANDARD: STANDARD_SANDBOX,
33
+ standard: STANDARD_SANDBOX,
34
+ };
27
35
 
28
- const getSandboxType = type =>
36
+ const sandboxApiTypeMap = {
37
+ standard: 1,
38
+ developer: 2,
39
+ };
40
+
41
+ const getSandboxTypeAsString = type =>
29
42
  type === 'DEVELOPER' ? 'development' : 'standard';
30
43
 
31
44
  function getAccountName(config) {
32
45
  const isSandbox =
33
46
  config.sandboxAccountType && config.sandboxAccountType !== null;
34
- const sandboxName = `[${getSandboxType(config.sandboxAccountType)} sandbox] `;
47
+ const sandboxName = `[${getSandboxTypeAsString(
48
+ config.sandboxAccountType
49
+ )} sandbox] `;
35
50
  return `${config.name} ${isSandbox ? sandboxName : ''}(${config.portalId})`;
36
51
  }
37
52
 
38
- function getHasDevelopmentSandboxes(parentAccountConfig) {
53
+ function getHasSandboxesByType(parentAccountConfig, type) {
39
54
  const config = getConfig();
40
- const parentPortalId = parentAccountConfig.portalId;
55
+ const parentPortalId = getAccountId(parentAccountConfig.portalId);
41
56
  for (const portal of config.portals) {
42
57
  if (
43
58
  (portal.parentAccountId !== null ||
44
59
  portal.parentAccountId !== undefined) &&
45
60
  portal.parentAccountId === parentPortalId &&
46
61
  portal.sandboxAccountType &&
47
- portal.sandboxAccountType === 'DEVELOPER'
62
+ sandboxTypeMap[portal.sandboxAccountType] === type
48
63
  ) {
49
64
  return true;
50
65
  }
@@ -52,7 +67,7 @@ function getHasDevelopmentSandboxes(parentAccountConfig) {
52
67
  return false;
53
68
  }
54
69
 
55
- function getDevSandboxLimit(error) {
70
+ function getSandboxLimit(error) {
56
71
  // Error context should contain a limit property with a list of one number. That number is the current limit
57
72
  const limit = error.context && error.context.limit && error.context.limit[0];
58
73
  return limit ? parseInt(limit, 10) : 1; // Default to 1
@@ -60,31 +75,57 @@ function getDevSandboxLimit(error) {
60
75
 
61
76
  // Fetches available sync types for a given sandbox portal
62
77
  async function getAvailableSyncTypes(parentAccountConfig, config) {
63
- const parentPortalId = parentAccountConfig.portalId;
64
- const portalId = config.portalId;
78
+ const parentPortalId = getAccountId(parentAccountConfig.portalId);
79
+ const portalId = getAccountId(config.portalId);
65
80
  const syncTypes = await fetchTypes(parentPortalId, portalId);
66
81
  return syncTypes.map(t => ({ type: t.name }));
67
82
  }
68
83
 
69
- const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
70
- const configData = await personalAccessKeyPrompt({ env, account });
84
+ /**
85
+ * @param {String} env - Environment (QA/Prod)
86
+ * @param {Object} result - Sandbox instance returned from API
87
+ * @param {Boolean} force - Force flag to skip prompt
88
+ * @returns {String} validName saved into config
89
+ */
90
+ const saveSandboxToConfig = async (env, result, force = false) => {
91
+ // const configData = { env, personalAccessKey: result.personalAccessKey };
92
+ // TODO: Temporary, remove
93
+ const configData = await personalAccessKeyPrompt({
94
+ env,
95
+ account: result.sandbox.sandboxHubId,
96
+ });
97
+ // End temporary section
71
98
  const updatedConfig = await updateConfigWithPersonalAccessKey(configData);
72
-
73
99
  if (!updatedConfig) {
74
- process.exit(EXIT_CODES.SUCCESS);
100
+ throw new Error('Failed to update config with personal access key.');
75
101
  }
76
102
 
77
103
  let validName = updatedConfig.name;
78
-
79
104
  if (!updatedConfig.name) {
80
- const nameForConfig = name
105
+ const nameForConfig = result.sandbox.name
81
106
  .toLowerCase()
82
107
  .split(' ')
83
108
  .join('-');
84
- const { name: promptName } = await enterAccountNamePrompt(nameForConfig);
85
- validName = promptName;
109
+ validName = nameForConfig;
110
+ const invalidAccountName = accountNameExistsInConfig(nameForConfig);
111
+ if (invalidAccountName) {
112
+ if (!force) {
113
+ logger.log(
114
+ i18n(
115
+ `cli.lib.prompts.enterAccountNamePrompt.errors.accountNameExists`,
116
+ { name: nameForConfig }
117
+ )
118
+ );
119
+ const { name: promptName } = await enterAccountNamePrompt(
120
+ nameForConfig
121
+ );
122
+ validName = promptName;
123
+ } else {
124
+ // Basic invalid name handling when force flag is passed
125
+ validName = nameForConfig + `_${Date.now()}`;
126
+ }
127
+ }
86
128
  }
87
-
88
129
  updateAccountConfig({
89
130
  ...updatedConfig,
90
131
  environment: updatedConfig.env,
@@ -93,35 +134,8 @@ const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
93
134
  });
94
135
  writeConfig();
95
136
 
96
- const setAsDefault = await setAsDefaultAccountPrompt(validName);
97
-
98
137
  logger.log('');
99
- if (setAsDefault) {
100
- logger.success(
101
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccount`, {
102
- accountName: validName,
103
- })
104
- );
105
- } else {
106
- const config = getConfig();
107
- logger.info(
108
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.keepingCurrentDefault`, {
109
- accountName: config.defaultPortal,
110
- })
111
- );
112
- }
113
- logger.success(
114
- i18n('cli.commands.sandbox.subcommands.create.success.configFileUpdated', {
115
- configFilename: DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
116
- authMethod: PERSONAL_ACCESS_KEY_AUTH_METHOD.name,
117
- account: validName,
118
- })
119
- );
120
- uiFeatureHighlight([
121
- 'accountsUseCommand',
122
- 'accountOption',
123
- 'accountsListCommand',
124
- ]);
138
+ return validName;
125
139
  };
126
140
 
127
141
  const ACTIVE_TASK_POLL_INTERVAL = 1000;
@@ -133,26 +147,34 @@ const isTaskComplete = task => {
133
147
  return task.status === 'COMPLETE';
134
148
  };
135
149
 
136
- // Returns a promise to poll a sync task with taskId. Interval runs until sync task status is equal to 'COMPLETE'
137
- function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
138
- const i18nKey = 'cli.commands.sandbox.subcommands.sync.types';
139
- const multibar = new cliProgress.MultiBar(
140
- {
141
- hideCursor: true,
142
- format: '[{bar}] {percentage}% | {taskType}',
143
- gracefulExit: true,
144
- },
145
- cliProgress.Presets.rect
146
- );
150
+ const incrementBy = (value, multiplier = 3) => {
151
+ return Math.min(value + Math.floor(Math.random() * multiplier), 99);
152
+ };
153
+
154
+ /**
155
+ * @param {Number} accountId - Parent portal ID (needs sandbox scopes)
156
+ * @param {String} taksId - Task ID to poll
157
+ * @param {String} syncStatusUrl - Link to UI to check polling status
158
+ * @param {Boolean} allowEarlyTermination - Option to allow a keypress to terminate early
159
+ * @returns {Promise} Interval runs until sync task status is equal to 'COMPLETE'
160
+ */
161
+ function pollSyncTaskStatus(
162
+ accountId,
163
+ taskId,
164
+ syncStatusUrl,
165
+ allowEarlyTermination = true
166
+ ) {
167
+ const i18nKey = 'cli.lib.sandbox.sync.types';
168
+ const progressBar = CliProgressMultibarManager.init();
147
169
  const mergeTasks = {
148
170
  'lead-flows': 'forms', // lead-flows are a subset of forms. We combine these in the UI as a single item, so we want to merge here for consistency.
149
171
  };
150
- const barInstances = {};
172
+ let progressCounter = {};
151
173
  let pollInterval;
152
174
  // Handle manual exit for return key and ctrl+c
153
175
  const onTerminate = () => {
154
176
  clearInterval(pollInterval);
155
- multibar.stop();
177
+ progressBar.stop();
156
178
  logger.log('');
157
179
  logger.log('Exiting, sync will continue in the background.');
158
180
  logger.log('');
@@ -163,16 +185,18 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
163
185
  );
164
186
  process.exit(EXIT_CODES.SUCCESS);
165
187
  };
166
- handleExit(onTerminate);
167
- handleKeypress(key => {
168
- if (
169
- (key && key.ctrl && key.name == 'c') ||
170
- key.name === 'enter' ||
171
- key.name === 'return'
172
- ) {
173
- onTerminate();
174
- }
175
- });
188
+ if (allowEarlyTermination) {
189
+ handleExit(onTerminate);
190
+ handleKeypress(key => {
191
+ if (
192
+ (key && key.ctrl && key.name == 'c') ||
193
+ key.name === 'enter' ||
194
+ key.name === 'return'
195
+ ) {
196
+ onTerminate();
197
+ }
198
+ });
199
+ }
176
200
  return new Promise((resolve, reject) => {
177
201
  pollInterval = setInterval(async () => {
178
202
  const taskResult = await fetchTaskStatus(accountId, taskId).catch(reject);
@@ -181,13 +205,17 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
181
205
  for (const task of taskResult.tasks) {
182
206
  // For each sync task, show a progress bar and increment bar each time we run this interval until status is 'COMPLETE'
183
207
  const taskType = task.type;
184
- if (!barInstances[taskType] && !mergeTasks[taskType]) {
185
- // skip creation of lead-flows bar because we're combining lead-flows into the forms bar
186
- barInstances[taskType] = multibar.create(100, 0, {
187
- taskType: i18n(`${i18nKey}.${taskType}.label`),
208
+ if (!progressBar.get(taskType) && !mergeTasks[taskType]) {
209
+ // skip creation of lead-flows bar because we're combining lead-flows into the forms bar, otherwise create a bar instance for the type
210
+ progressCounter[taskType] = 0;
211
+ progressBar.create(taskType, 100, 0, {
212
+ label: i18n(`${i18nKey}.${taskType}.label`),
188
213
  });
189
214
  } else if (mergeTasks[taskType]) {
190
- // If its a lead-flow, merge status into the forms progress bar
215
+ // It's a lead-flow here, merge status into the forms progress bar
216
+ if (!progressCounter[mergeTasks[taskType]]) {
217
+ progressCounter[mergeTasks[taskType]] = 0;
218
+ }
191
219
  const formsTask = taskResult.tasks.filter(
192
220
  t => t.type === mergeTasks[taskType]
193
221
  )[0];
@@ -197,46 +225,62 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
197
225
  formsTaskStatus !== 'COMPLETE' ||
198
226
  leadFlowsTaskStatus !== 'COMPLETE'
199
227
  ) {
200
- barInstances[mergeTasks[taskType]].increment(
201
- Math.floor(Math.random() * 3),
228
+ // Randomly increment bar while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
229
+ progressCounter[mergeTasks[taskType]] = incrementBy(
230
+ progressCounter[mergeTasks[taskType]]
231
+ );
232
+ progressBar.update(
233
+ mergeTasks[taskType],
234
+ progressCounter[mergeTasks[taskType]],
202
235
  {
203
- taskType: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
236
+ label: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
204
237
  }
205
238
  );
206
239
  }
207
240
  }
208
- if (barInstances[taskType] && task.status === 'COMPLETE') {
209
- barInstances[taskType].update(100, {
210
- taskType: i18n(`${i18nKey}.${taskType}.label`),
241
+ if (progressBar.get(taskType) && task.status === 'COMPLETE') {
242
+ progressBar.update(taskType, 100, {
243
+ label: i18n(`${i18nKey}.${taskType}.label`),
211
244
  });
212
- } else if (barInstances[taskType] && task.status === 'PROCESSING') {
213
- // Do not increment for tasks still in PENDING state
214
- barInstances[taskType].increment(Math.floor(Math.random() * 3), {
215
- // Randomly increment bar by 0 - 2 while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
216
- taskType: i18n(`${i18nKey}.${taskType}.label`),
245
+ } else if (
246
+ // Do not start incrementing for tasks still in PENDING state
247
+ progressBar.get(taskType) &&
248
+ task.status === 'PROCESSING'
249
+ ) {
250
+ // Randomly increment bar while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
251
+ progressCounter[taskType] = incrementBy(
252
+ progressCounter[taskType],
253
+ taskType === 'object-records' ? 2 : 3 // slower progress for object-records, sync can take up to a few minutes
254
+ );
255
+ progressBar.update(taskType, progressCounter[taskType], {
256
+ label: i18n(`${i18nKey}.${taskType}.label`),
217
257
  });
218
258
  }
219
259
  }
220
260
  } else {
221
261
  clearInterval(pollInterval);
222
262
  reject();
223
- multibar.stop();
263
+ progressBar.stop();
224
264
  }
225
265
  if (isTaskComplete(taskResult)) {
226
266
  clearInterval(pollInterval);
227
267
  resolve(taskResult);
228
- multibar.stop();
268
+ progressBar.stop();
229
269
  }
230
270
  }, ACTIVE_TASK_POLL_INTERVAL);
231
271
  });
232
272
  }
233
273
 
234
274
  module.exports = {
235
- getSandboxType,
275
+ STANDARD_SANDBOX,
276
+ DEVELOPER_SANDBOX,
277
+ sandboxTypeMap,
278
+ sandboxApiTypeMap,
279
+ getSandboxTypeAsString,
236
280
  getAccountName,
237
- getHasDevelopmentSandboxes,
238
- getDevSandboxLimit,
281
+ saveSandboxToConfig,
282
+ getHasSandboxesByType,
283
+ getSandboxLimit,
239
284
  getAvailableSyncTypes,
240
- sandboxCreatePersonalAccessKeyFlow,
241
285
  pollSyncTaskStatus,
242
286
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.1.8-beta.0",
3
+ "version": "4.1.8-beta.1",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,8 +8,8 @@
8
8
  "url": "https://github.com/HubSpot/hubspot-cms-tools"
9
9
  },
10
10
  "dependencies": {
11
- "@hubspot/cli-lib": "4.1.8-beta.0",
12
- "@hubspot/serverless-dev-runtime": "4.1.8-beta.0",
11
+ "@hubspot/cli-lib": "4.1.8-beta.1",
12
+ "@hubspot/serverless-dev-runtime": "4.1.8-beta.1",
13
13
  "archiver": "^5.3.0",
14
14
  "chalk": "^4.1.2",
15
15
  "cli-progress": "^3.11.2",
@@ -38,5 +38,5 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "af16182b2c7abd401771d5e0eed3de21e7b0477c"
41
+ "gitHead": "8aafada9270602e912d89fc6a513816f5f7232bb"
42
42
  }