@hubspot/cli 4.1.7 → 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,53 +11,121 @@ 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
+ };
35
+
36
+ const sandboxApiTypeMap = {
37
+ standard: 1,
38
+ developer: 2,
39
+ };
27
40
 
28
- const getSandboxType = type =>
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
 
53
+ function getHasSandboxesByType(parentAccountConfig, type) {
54
+ const config = getConfig();
55
+ const parentPortalId = getAccountId(parentAccountConfig.portalId);
56
+ for (const portal of config.portals) {
57
+ if (
58
+ (portal.parentAccountId !== null ||
59
+ portal.parentAccountId !== undefined) &&
60
+ portal.parentAccountId === parentPortalId &&
61
+ portal.sandboxAccountType &&
62
+ sandboxTypeMap[portal.sandboxAccountType] === type
63
+ ) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ function getSandboxLimit(error) {
71
+ // Error context should contain a limit property with a list of one number. That number is the current limit
72
+ const limit = error.context && error.context.limit && error.context.limit[0];
73
+ return limit ? parseInt(limit, 10) : 1; // Default to 1
74
+ }
75
+
38
76
  // Fetches available sync types for a given sandbox portal
39
77
  async function getAvailableSyncTypes(parentAccountConfig, config) {
40
- const parentPortalId = parentAccountConfig.portalId;
41
- const portalId = config.portalId;
78
+ const parentPortalId = getAccountId(parentAccountConfig.portalId);
79
+ const portalId = getAccountId(config.portalId);
42
80
  const syncTypes = await fetchTypes(parentPortalId, portalId);
43
81
  return syncTypes.map(t => ({ type: t.name }));
44
82
  }
45
83
 
46
- const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
47
- 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
48
98
  const updatedConfig = await updateConfigWithPersonalAccessKey(configData);
49
-
50
99
  if (!updatedConfig) {
51
- process.exit(EXIT_CODES.SUCCESS);
100
+ throw new Error('Failed to update config with personal access key.');
52
101
  }
53
102
 
54
103
  let validName = updatedConfig.name;
55
-
56
104
  if (!updatedConfig.name) {
57
- const nameForConfig = name
105
+ const nameForConfig = result.sandbox.name
58
106
  .toLowerCase()
59
107
  .split(' ')
60
108
  .join('-');
61
- const { name: promptName } = await enterAccountNamePrompt(nameForConfig);
62
- 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
+ }
63
128
  }
64
-
65
129
  updateAccountConfig({
66
130
  ...updatedConfig,
67
131
  environment: updatedConfig.env,
@@ -70,35 +134,8 @@ const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
70
134
  });
71
135
  writeConfig();
72
136
 
73
- const setAsDefault = await setAsDefaultAccountPrompt(validName);
74
-
75
137
  logger.log('');
76
- if (setAsDefault) {
77
- logger.success(
78
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccount`, {
79
- accountName: validName,
80
- })
81
- );
82
- } else {
83
- const config = getConfig();
84
- logger.info(
85
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.keepingCurrentDefault`, {
86
- accountName: config.defaultPortal,
87
- })
88
- );
89
- }
90
- logger.success(
91
- i18n('cli.commands.sandbox.subcommands.create.success.configFileUpdated', {
92
- configFilename: DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
93
- authMethod: PERSONAL_ACCESS_KEY_AUTH_METHOD.name,
94
- account: validName,
95
- })
96
- );
97
- uiFeatureHighlight([
98
- 'accountsUseCommand',
99
- 'accountOption',
100
- 'accountsListCommand',
101
- ]);
138
+ return validName;
102
139
  };
103
140
 
104
141
  const ACTIVE_TASK_POLL_INTERVAL = 1000;
@@ -110,26 +147,34 @@ const isTaskComplete = task => {
110
147
  return task.status === 'COMPLETE';
111
148
  };
112
149
 
113
- // Returns a promise to poll a sync task with taskId. Interval runs until sync task status is equal to 'COMPLETE'
114
- function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
115
- const i18nKey = 'cli.commands.sandbox.subcommands.sync.types';
116
- const multibar = new cliProgress.MultiBar(
117
- {
118
- hideCursor: true,
119
- format: '[{bar}] {percentage}% | {taskType}',
120
- gracefulExit: true,
121
- },
122
- cliProgress.Presets.rect
123
- );
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();
124
169
  const mergeTasks = {
125
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.
126
171
  };
127
- const barInstances = {};
172
+ let progressCounter = {};
128
173
  let pollInterval;
129
174
  // Handle manual exit for return key and ctrl+c
130
175
  const onTerminate = () => {
131
176
  clearInterval(pollInterval);
132
- multibar.stop();
177
+ progressBar.stop();
133
178
  logger.log('');
134
179
  logger.log('Exiting, sync will continue in the background.');
135
180
  logger.log('');
@@ -140,16 +185,18 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
140
185
  );
141
186
  process.exit(EXIT_CODES.SUCCESS);
142
187
  };
143
- handleExit(onTerminate);
144
- handleKeypress(key => {
145
- if (
146
- (key && key.ctrl && key.name == 'c') ||
147
- key.name === 'enter' ||
148
- key.name === 'return'
149
- ) {
150
- onTerminate();
151
- }
152
- });
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
+ }
153
200
  return new Promise((resolve, reject) => {
154
201
  pollInterval = setInterval(async () => {
155
202
  const taskResult = await fetchTaskStatus(accountId, taskId).catch(reject);
@@ -158,13 +205,17 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
158
205
  for (const task of taskResult.tasks) {
159
206
  // For each sync task, show a progress bar and increment bar each time we run this interval until status is 'COMPLETE'
160
207
  const taskType = task.type;
161
- if (!barInstances[taskType] && !mergeTasks[taskType]) {
162
- // skip creation of lead-flows bar because we're combining lead-flows into the forms bar
163
- barInstances[taskType] = multibar.create(100, 0, {
164
- 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`),
165
213
  });
166
214
  } else if (mergeTasks[taskType]) {
167
- // 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
+ }
168
219
  const formsTask = taskResult.tasks.filter(
169
220
  t => t.type === mergeTasks[taskType]
170
221
  )[0];
@@ -174,44 +225,62 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
174
225
  formsTaskStatus !== 'COMPLETE' ||
175
226
  leadFlowsTaskStatus !== 'COMPLETE'
176
227
  ) {
177
- barInstances[mergeTasks[taskType]].increment(
178
- 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]],
179
235
  {
180
- taskType: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
236
+ label: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
181
237
  }
182
238
  );
183
239
  }
184
240
  }
185
- if (barInstances[taskType] && task.status === 'COMPLETE') {
186
- barInstances[taskType].update(100, {
187
- taskType: i18n(`${i18nKey}.${taskType}.label`),
241
+ if (progressBar.get(taskType) && task.status === 'COMPLETE') {
242
+ progressBar.update(taskType, 100, {
243
+ label: i18n(`${i18nKey}.${taskType}.label`),
188
244
  });
189
- } else if (barInstances[taskType] && task.status === 'PROCESSING') {
190
- // Do not increment for tasks still in PENDING state
191
- barInstances[taskType].increment(Math.floor(Math.random() * 3), {
192
- // Randomly increment bar by 0 - 2 while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
193
- 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`),
194
257
  });
195
258
  }
196
259
  }
197
260
  } else {
198
261
  clearInterval(pollInterval);
199
262
  reject();
200
- multibar.stop();
263
+ progressBar.stop();
201
264
  }
202
265
  if (isTaskComplete(taskResult)) {
203
266
  clearInterval(pollInterval);
204
267
  resolve(taskResult);
205
- multibar.stop();
268
+ progressBar.stop();
206
269
  }
207
270
  }, ACTIVE_TASK_POLL_INTERVAL);
208
271
  });
209
272
  }
210
273
 
211
274
  module.exports = {
212
- getSandboxType,
275
+ STANDARD_SANDBOX,
276
+ DEVELOPER_SANDBOX,
277
+ sandboxTypeMap,
278
+ sandboxApiTypeMap,
279
+ getSandboxTypeAsString,
213
280
  getAccountName,
281
+ saveSandboxToConfig,
282
+ getHasSandboxesByType,
283
+ getSandboxLimit,
214
284
  getAvailableSyncTypes,
215
- sandboxCreatePersonalAccessKeyFlow,
216
285
  pollSyncTaskStatus,
217
286
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.1.7",
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.7",
12
- "@hubspot/serverless-dev-runtime": "4.1.7",
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": "ef2292568ec8308feb0c5841e4d3dd25a64d1050"
41
+ "gitHead": "8aafada9270602e912d89fc6a513816f5f7232bb"
42
42
  }