@hubspot/cli 4.1.6 → 4.1.7-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.
@@ -12,7 +12,7 @@ const {
12
12
  } = require('../../lib/commonOpts');
13
13
  const { trackCommandUsage } = require('../../lib/usageTracking');
14
14
  const { loadAndValidateOptions } = require('../../lib/validation');
15
- const { getSandboxType } = require('../../lib/prompts/sandboxesPrompt');
15
+ const { getSandboxType } = require('../../lib/sandboxes');
16
16
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
17
17
 
18
18
  const i18nKey = 'cli.commands.accounts.subcommands.list';
@@ -11,101 +11,26 @@ const Spinnies = require('spinnies');
11
11
  const { createSandbox } = require('@hubspot/cli-lib/sandboxes');
12
12
  const { loadAndValidateOptions } = require('../../lib/validation');
13
13
  const { createSandboxPrompt } = require('../../lib/prompts/sandboxesPrompt');
14
+ const {
15
+ getSandboxType,
16
+ sandboxCreatePersonalAccessKeyFlow,
17
+ } = require('../../lib/sandboxes');
14
18
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
15
19
  const { logErrorInstance } = require('@hubspot/cli-lib/errorHandlers');
16
20
  const {
17
21
  debugErrorAndContext,
18
22
  } = require('@hubspot/cli-lib/errorHandlers/standardErrors');
19
- const {
20
- ENVIRONMENTS,
21
- PERSONAL_ACCESS_KEY_AUTH_METHOD,
22
- DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
23
- } = require('@hubspot/cli-lib/lib/constants');
24
- const {
25
- personalAccessKeyPrompt,
26
- } = require('../../lib/prompts/personalAccessKeyPrompt');
27
- const {
28
- updateConfigWithPersonalAccessKey,
29
- } = require('@hubspot/cli-lib/personalAccessKey');
23
+ const { ENVIRONMENTS } = require('@hubspot/cli-lib/lib/constants');
30
24
  const { EXIT_CODES } = require('../../lib/enums/exitCodes');
31
- const {
32
- getConfig,
33
- writeConfig,
34
- updateAccountConfig,
35
- getAccountConfig,
36
- } = require('@hubspot/cli-lib');
37
- const {
38
- enterAccountNamePrompt,
39
- } = require('../../lib/prompts/enterAccountNamePrompt');
40
- const {
41
- setAsDefaultAccountPrompt,
42
- } = require('../../lib/prompts/setAsDefaultAccountPrompt');
25
+ const { getAccountConfig } = require('@hubspot/cli-lib');
43
26
  const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
44
27
  const {
45
28
  isMissingScopeError,
29
+ isSpecifiedError,
46
30
  } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
47
- const { uiFeatureHighlight } = require('../../lib/ui');
48
31
 
49
32
  const i18nKey = 'cli.commands.sandbox.subcommands.create';
50
33
 
51
- const personalAccessKeyFlow = async (env, account, name) => {
52
- const configData = await personalAccessKeyPrompt({ env, account });
53
- const updatedConfig = await updateConfigWithPersonalAccessKey(configData);
54
-
55
- if (!updatedConfig) {
56
- process.exit(EXIT_CODES.SUCCESS);
57
- }
58
-
59
- let validName = updatedConfig.name;
60
-
61
- if (!updatedConfig.name) {
62
- const nameForConfig = name
63
- .toLowerCase()
64
- .split(' ')
65
- .join('-');
66
- const { name: promptName } = await enterAccountNamePrompt(nameForConfig);
67
- validName = promptName;
68
- }
69
-
70
- updateAccountConfig({
71
- ...updatedConfig,
72
- environment: updatedConfig.env,
73
- tokenInfo: updatedConfig.auth.tokenInfo,
74
- name: validName,
75
- });
76
- writeConfig();
77
-
78
- const setAsDefault = await setAsDefaultAccountPrompt(validName);
79
-
80
- logger.log('');
81
- if (setAsDefault) {
82
- logger.success(
83
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccount`, {
84
- accountName: validName,
85
- })
86
- );
87
- } else {
88
- const config = getConfig();
89
- logger.info(
90
- i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.keepingCurrentDefault`, {
91
- accountName: config.defaultPortal,
92
- })
93
- );
94
- }
95
- logger.success(
96
- i18n(`${i18nKey}.success.configFileUpdated`, {
97
- configFilename: DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
98
- authMethod: PERSONAL_ACCESS_KEY_AUTH_METHOD.name,
99
- account: validName,
100
- })
101
- );
102
- uiFeatureHighlight([
103
- 'accountsUseCommand',
104
- 'accountOption',
105
- 'accountsListCommand',
106
- ]);
107
- };
108
-
109
34
  exports.command = 'create [--name]';
110
35
  exports.describe = i18n(`${i18nKey}.describe`);
111
36
 
@@ -122,6 +47,21 @@ exports.handler = async options => {
122
47
 
123
48
  trackCommandUsage('sandbox-create', null, accountId);
124
49
 
50
+ if (
51
+ accountConfig.sandboxAccountType &&
52
+ accountConfig.sandboxAccountType !== null
53
+ ) {
54
+ trackCommandUsage('sandbox-create', { successful: false }, accountId);
55
+
56
+ logger.error(
57
+ i18n(`${i18nKey}.failure.creatingWithinSandbox`, {
58
+ sandboxType: getSandboxType(accountConfig.sandboxAccountType),
59
+ })
60
+ );
61
+
62
+ process.exit(EXIT_CODES.ERROR);
63
+ }
64
+
125
65
  let namePrompt;
126
66
 
127
67
  logger.log(i18n(`${i18nKey}.sandboxLimitation`));
@@ -176,13 +116,29 @@ exports.handler = async options => {
176
116
  url,
177
117
  })
178
118
  );
179
- } else {
119
+ } else if (
120
+ isSpecifiedError(
121
+ err,
122
+ 400,
123
+ 'VALIDATION_ERROR',
124
+ 'SandboxErrors.NUM_DEVELOPMENT_SANDBOXES_LIMIT_EXCEEDED_ERROR'
125
+ ) &&
126
+ err.error &&
127
+ err.error.message
128
+ ) {
129
+ logger.log('');
180
130
  logger.error(err.error.message);
131
+ } else {
132
+ logErrorInstance(err);
181
133
  }
182
134
  process.exit(EXIT_CODES.ERROR);
183
135
  }
184
136
  try {
185
- await personalAccessKeyFlow(env, result.sandboxHubId, result.name);
137
+ await sandboxCreatePersonalAccessKeyFlow(
138
+ env,
139
+ result.sandboxHubId,
140
+ result.name
141
+ );
186
142
  process.exit(EXIT_CODES.SUCCESS);
187
143
  } catch (err) {
188
144
  logErrorInstance(err);
@@ -11,7 +11,7 @@ const { loadAndValidateOptions } = require('../../lib/validation');
11
11
  const {
12
12
  debugErrorAndContext,
13
13
  } = require('@hubspot/cli-lib/errorHandlers/standardErrors');
14
-
14
+ const { logErrorInstance } = require('@hubspot/cli-lib/errorHandlers');
15
15
  const { deleteSandbox } = require('@hubspot/cli-lib/sandboxes');
16
16
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
17
17
  const { getConfig, getEnv } = require('@hubspot/cli-lib');
@@ -26,12 +26,12 @@ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
26
26
  const { promptUser } = require('../../lib/prompts/promptUtils');
27
27
  const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
28
28
  const { ENVIRONMENTS } = require('@hubspot/cli-lib/lib/constants');
29
+ const {
30
+ isSpecifiedError,
31
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
29
32
 
30
33
  const i18nKey = 'cli.commands.sandbox.subcommands.delete';
31
34
 
32
- const SANDBOX_NOT_FOUND = 'SandboxErrors.SANDBOX_NOT_FOUND';
33
- const OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND';
34
-
35
35
  exports.command = 'delete [--account]';
36
36
  exports.describe = i18n(`${i18nKey}.describe`);
37
37
 
@@ -147,9 +147,12 @@ exports.handler = async options => {
147
147
  );
148
148
 
149
149
  if (
150
- err.error &&
151
- err.error.category === OBJECT_NOT_FOUND &&
152
- err.error.subCategory === SANDBOX_NOT_FOUND
150
+ isSpecifiedError(
151
+ err,
152
+ 404,
153
+ 'OBJECT_NOT_FOUND',
154
+ 'SandboxErrors.SANDBOX_NOT_FOUND'
155
+ )
153
156
  ) {
154
157
  logger.log('');
155
158
  logger.warn(
@@ -167,7 +170,7 @@ exports.handler = async options => {
167
170
  }
168
171
  process.exit(EXIT_CODES.SUCCESS);
169
172
  } else {
170
- logger.error(err.error.message);
173
+ logErrorInstance(err);
171
174
  }
172
175
  process.exit(EXIT_CODES.ERROR);
173
176
  }
@@ -0,0 +1,300 @@
1
+ const {
2
+ addAccountOptions,
3
+ addConfigOptions,
4
+ getAccountId,
5
+ addUseEnvironmentOptions,
6
+ addTestingOptions,
7
+ } = require('../../lib/commonOpts');
8
+ const { trackCommandUsage } = require('../../lib/usageTracking');
9
+ const { logger } = require('@hubspot/cli-lib/logger');
10
+ const Spinnies = require('spinnies');
11
+ const { initiateSync } = require('@hubspot/cli-lib/sandboxes');
12
+ const { loadAndValidateOptions } = require('../../lib/validation');
13
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
14
+ const { logErrorInstance } = require('@hubspot/cli-lib/errorHandlers');
15
+ const { ENVIRONMENTS } = require('@hubspot/cli-lib/lib/constants');
16
+ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
17
+ const { getAccountConfig, getEnv } = require('@hubspot/cli-lib');
18
+ const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
19
+ const { promptUser } = require('../../lib/prompts/promptUtils');
20
+ const { uiLine } = require('../../lib/ui');
21
+ const {
22
+ getAccountName,
23
+ getAvailableSyncTypes,
24
+ pollSyncTaskStatus,
25
+ } = require('../../lib/sandboxes');
26
+ const {
27
+ isMissingScopeError,
28
+ isSpecifiedError,
29
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
30
+
31
+ const i18nKey = 'cli.commands.sandbox.subcommands.sync';
32
+
33
+ exports.command = 'sync';
34
+ exports.describe = i18n(`${i18nKey}.describe`);
35
+
36
+ exports.handler = async options => {
37
+ await loadAndValidateOptions(options);
38
+
39
+ const { force } = options; // For scripting purposes
40
+ const accountId = getAccountId(options);
41
+ const accountConfig = getAccountConfig(accountId);
42
+ const spinnies = new Spinnies({
43
+ succeedColor: 'white',
44
+ });
45
+
46
+ trackCommandUsage('sandbox-sync', null, accountId);
47
+
48
+ if (
49
+ // Check if default account is a sandbox, otherwise exit
50
+ // sandboxAccountType is null for non-sandbox portals, and one of 'DEVELOPER' or 'STANDARD' for sandbox portals. Undefined is to handle older config entries.
51
+ accountConfig.sandboxAccountType === undefined ||
52
+ accountConfig.sandboxAccountType === null
53
+ ) {
54
+ trackCommandUsage('sandbox-sync', { successful: false }, accountId);
55
+
56
+ logger.error(i18n(`${i18nKey}.failure.notSandbox`));
57
+
58
+ process.exit(EXIT_CODES.ERROR);
59
+ }
60
+
61
+ // Verify parent account exists in the config
62
+ let parentAccountId = accountConfig.parentAccountId || undefined;
63
+ if (!parentAccountId || !getAccountId({ account: parentAccountId })) {
64
+ logger.log('');
65
+ logger.error(
66
+ i18n(`${i18nKey}.failure.missingParentPortal`, {
67
+ sandboxName: getAccountName(accountConfig),
68
+ })
69
+ );
70
+ process.exit(EXIT_CODES.ERROR);
71
+ }
72
+
73
+ const parentAccountConfig = getAccountConfig(parentAccountId);
74
+ const isDevelopmentSandbox = accountConfig.sandboxAccountType === 'DEVELOPER';
75
+ const isStandardSandbox = accountConfig.sandboxAccountType === 'STANDARD';
76
+
77
+ if (isDevelopmentSandbox) {
78
+ logger.log(i18n(`${i18nKey}.info.developmentSandbox`));
79
+ logger.log(
80
+ i18n(`${i18nKey}.info.sync`, {
81
+ parentAccountName: getAccountName(parentAccountConfig),
82
+ sandboxName: getAccountName(accountConfig),
83
+ })
84
+ );
85
+ uiLine();
86
+ logger.warn(i18n(`${i18nKey}.warning.developmentSandbox`));
87
+ uiLine();
88
+ logger.log('');
89
+
90
+ if (!force) {
91
+ // Skip confirmation if force flag is present.
92
+ const { confirmSandboxSyncPrompt: confirmed } = await promptUser([
93
+ {
94
+ name: 'confirmSandboxSyncPrompt',
95
+ type: 'confirm',
96
+ message: i18n(`${i18nKey}.confirm.developmentSandbox`, {
97
+ parentAccountName: getAccountName(parentAccountConfig),
98
+ sandboxName: getAccountName(accountConfig),
99
+ }),
100
+ },
101
+ ]);
102
+ if (!confirmed) {
103
+ process.exit(EXIT_CODES.SUCCESS);
104
+ }
105
+ }
106
+ } else if (isStandardSandbox) {
107
+ const standardSyncUrl = `${getHubSpotWebsiteOrigin(
108
+ getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD
109
+ )}/sandboxes-developer/${parentAccountId}/sync?step=select_sync_path&id=${parentAccountId}_${accountId}`;
110
+
111
+ logger.log(
112
+ i18n(`${i18nKey}.info.standardSandbox`, {
113
+ url: standardSyncUrl,
114
+ })
115
+ );
116
+ logger.log(
117
+ i18n(`${i18nKey}.info.sync`, {
118
+ parentAccountName: getAccountName(parentAccountConfig),
119
+ sandboxName: getAccountName(accountConfig),
120
+ })
121
+ );
122
+ uiLine();
123
+ logger.warn(i18n(`${i18nKey}.warning.standardSandbox`));
124
+ uiLine();
125
+ logger.log('');
126
+
127
+ if (!force) {
128
+ // Skip confirmation if force flag is present.
129
+ const { confirmSandboxSyncPrompt: confirmed } = await promptUser([
130
+ {
131
+ name: 'confirmSandboxSyncPrompt',
132
+ type: 'confirm',
133
+ message: i18n(`${i18nKey}.confirm.standardSandbox`, {
134
+ parentAccountName: getAccountName(parentAccountConfig),
135
+ sandboxName: getAccountName(accountConfig),
136
+ }),
137
+ },
138
+ ]);
139
+ if (!confirmed) {
140
+ process.exit(EXIT_CODES.SUCCESS);
141
+ }
142
+ }
143
+ } else {
144
+ logger.error('Sync must be run in a sandbox account.');
145
+ process.exit(EXIT_CODES.ERROR);
146
+ }
147
+
148
+ let initiateSyncResponse;
149
+
150
+ const baseUrl = getHubSpotWebsiteOrigin(
151
+ getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD
152
+ );
153
+ const syncStatusUrl = `${baseUrl}/sandboxes-developer/${parentAccountId}/${
154
+ isDevelopmentSandbox ? 'development' : 'standard'
155
+ }`;
156
+
157
+ try {
158
+ logger.log('');
159
+ spinnies.add('sandboxSync', {
160
+ text: i18n(`${i18nKey}.loading.startSync`),
161
+ });
162
+
163
+ // Fetches sync types based on default account. Parent account required for fetch
164
+ const tasks = await getAvailableSyncTypes(
165
+ parentAccountConfig,
166
+ accountConfig
167
+ );
168
+
169
+ initiateSyncResponse = await initiateSync(
170
+ parentAccountId,
171
+ accountId,
172
+ tasks,
173
+ accountId
174
+ );
175
+
176
+ logger.log(i18n(`${i18nKey}.info.earlyExit`));
177
+ logger.log('');
178
+ spinnies.succeed('sandboxSync', {
179
+ text: i18n(`${i18nKey}.loading.succeed`),
180
+ });
181
+ } catch (err) {
182
+ trackCommandUsage('sandbox-sync', { successful: false }, accountId);
183
+
184
+ spinnies.fail('sandboxSync', {
185
+ text: i18n(`${i18nKey}.loading.fail`),
186
+ });
187
+
188
+ logger.log('');
189
+ if (isMissingScopeError(err)) {
190
+ logger.error(
191
+ i18n(`${i18nKey}.failure.missingScopes`, {
192
+ accountName: getAccountName(parentAccountConfig),
193
+ })
194
+ );
195
+ } else if (
196
+ isSpecifiedError(
197
+ err,
198
+ 429,
199
+ 'RATE_LIMITS',
200
+ 'sandboxes-sync-api.SYNC_IN_PROGRESS'
201
+ )
202
+ ) {
203
+ logger.error(
204
+ i18n(`${i18nKey}.failure.syncInProgress`, {
205
+ url: `${baseUrl}/sandboxes-developer/${parentAccountId}/syncactivitylog`,
206
+ })
207
+ );
208
+ } else if (
209
+ isSpecifiedError(
210
+ err,
211
+ 403,
212
+ 'BANNED',
213
+ 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USERID'
214
+ )
215
+ ) {
216
+ // This will only trigger if a user is not a super admin of the target account.
217
+ logger.error(
218
+ i18n(`${i18nKey}.failure.notSuperAdmin`, {
219
+ account: getAccountName(accountConfig),
220
+ })
221
+ );
222
+ } else if (
223
+ isSpecifiedError(
224
+ err,
225
+ 404,
226
+ 'OBJECT_NOT_FOUND',
227
+ 'SandboxErrors.SANDBOX_NOT_FOUND'
228
+ )
229
+ ) {
230
+ logger.error(
231
+ i18n(`${i18nKey}.failure.objectNotFound`, {
232
+ account: getAccountName(accountConfig),
233
+ })
234
+ );
235
+ } else {
236
+ logErrorInstance(err);
237
+ }
238
+ logger.log('');
239
+
240
+ process.exit(EXIT_CODES.ERROR);
241
+ }
242
+
243
+ try {
244
+ logger.log('');
245
+ logger.log('Sync progress:');
246
+ // Poll sync task status to show progress bars
247
+ await pollSyncTaskStatus(
248
+ parentAccountId,
249
+ initiateSyncResponse.id,
250
+ syncStatusUrl
251
+ );
252
+
253
+ logger.log('');
254
+ spinnies.add('syncComplete', {
255
+ text: i18n(`${i18nKey}.polling.syncing`),
256
+ });
257
+ spinnies.succeed('syncComplete', {
258
+ text: i18n(`${i18nKey}.polling.succeed`),
259
+ });
260
+ logger.log('');
261
+ logger.log(
262
+ i18n(`${i18nKey}.info.syncStatus`, {
263
+ url: syncStatusUrl,
264
+ })
265
+ );
266
+
267
+ process.exit(EXIT_CODES.SUCCESS);
268
+ } catch (err) {
269
+ // If polling fails at this point, we do not track a failed sync since it is running in the background.
270
+ logErrorInstance(err);
271
+
272
+ spinnies.add('syncComplete', {
273
+ text: i18n(`${i18nKey}.polling.syncing`),
274
+ });
275
+ spinnies.fail('syncComplete', {
276
+ text: i18n(`${i18nKey}.polling.fail`, {
277
+ url: syncStatusUrl,
278
+ }),
279
+ });
280
+
281
+ process.exit(EXIT_CODES.ERROR);
282
+ }
283
+ };
284
+
285
+ exports.builder = yargs => {
286
+ yargs.option('f', {
287
+ type: 'boolean',
288
+ alias: 'force',
289
+ describe: i18n(`${i18nKey}.examples.force`),
290
+ });
291
+
292
+ yargs.example([['$0 sandbox sync', i18n(`${i18nKey}.examples.default`)]]);
293
+
294
+ addConfigOptions(yargs, true);
295
+ addAccountOptions(yargs, true);
296
+ addUseEnvironmentOptions(yargs, true);
297
+ addTestingOptions(yargs, true);
298
+
299
+ return yargs;
300
+ };
@@ -1,6 +1,7 @@
1
1
  const { addConfigOptions, addAccountOptions } = require('../lib/commonOpts');
2
2
  const create = require('./sandbox/create');
3
3
  const del = require('./sandbox/delete');
4
+ const sync = require('./sandbox/sync');
4
5
 
5
6
  // const i18nKey = 'cli.commands.sandbox';
6
7
 
@@ -14,6 +15,7 @@ exports.builder = yargs => {
14
15
  yargs
15
16
  .command(create)
16
17
  .command(del)
18
+ .command(sync)
17
19
  .demandCommand(1, '');
18
20
 
19
21
  return yargs;
package/lib/projects.js CHANGED
@@ -18,6 +18,8 @@ const {
18
18
  PROJECT_BUILD_TEXT,
19
19
  PROJECT_DEPLOY_TEXT,
20
20
  PROJECT_CONFIG_FILE,
21
+ PROJECT_TASK_TYPES,
22
+ SPINNER_STATUS,
21
23
  } = require('@hubspot/cli-lib/lib/constants');
22
24
  const {
23
25
  createProject,
@@ -361,7 +363,7 @@ const handleProjectUpload = async (
361
363
  archive.pipe(output);
362
364
 
363
365
  archive.directory(srcDir, false, file =>
364
- shouldIgnoreFile(file.name) ? false : file
366
+ shouldIgnoreFile(file.name, true) ? false : file
365
367
  );
366
368
 
367
369
  archive.finalize();
@@ -374,6 +376,8 @@ const makePollTaskStatusFunc = ({
374
376
  statusStrings,
375
377
  linkToHubSpot,
376
378
  }) => {
379
+ const i18nKey = 'cli.commands.project.lib.makePollTaskStatusFunc';
380
+
377
381
  const isTaskComplete = task => {
378
382
  if (
379
383
  !task[statusText.SUBTASK_KEY].length ||
@@ -410,29 +414,60 @@ const makePollTaskStatusFunc = ({
410
414
  structureFn(accountId, taskName, taskId),
411
415
  ]);
412
416
 
413
- const topLevelSubtasks = initialTaskStatus[statusText.SUBTASK_KEY].filter(
414
- ({ id }) => !!taskStructure[id]
417
+ const tasksById = initialTaskStatus[statusText.SUBTASK_KEY].reduce(
418
+ (acc, task) => {
419
+ const type = task[statusText.TYPE_KEY];
420
+ if (type !== 'APP_ID' && type !== 'SERVERLESS_PKG') {
421
+ acc[task.id] = task;
422
+ }
423
+ return acc;
424
+ },
425
+ {}
415
426
  );
416
427
 
417
- const numOfComponents = topLevelSubtasks.length;
418
- const componentCountText = `\nFound ${numOfComponents} component${
419
- numOfComponents !== 1 ? 's' : ''
420
- } in this project ...\n`;
428
+ const structuredTasks = Object.keys(taskStructure).map(key => {
429
+ return {
430
+ ...tasksById[key],
431
+ subtasks: taskStructure[key]
432
+ .filter(taskId => Boolean(tasksById[taskId]))
433
+ .map(taskId => tasksById[taskId]),
434
+ };
435
+ });
436
+
437
+ const numComponents = structuredTasks.length;
438
+ const componentCountText = i18n(
439
+ numComponents === 1
440
+ ? `${i18nKey}.componentCountSingular`
441
+ : `${i18nKey}.componentCount`,
442
+ { numComponents }
443
+ );
421
444
 
422
445
  spinnies.update('overallTaskStatus', {
423
- text: `${statusStrings.INITIALIZE(taskName)}${componentCountText}`,
446
+ text: `${statusStrings.INITIALIZE(taskName)}\n${componentCountText}\n`,
424
447
  });
425
448
 
426
- for (let subTask of topLevelSubtasks) {
427
- const subTaskName = subTask[statusText.SUBTASK_NAME_KEY];
428
-
429
- spinnies.add(subTaskName, {
430
- text: `${chalk.bold(subTaskName)} ${
431
- statusText.STATUS_TEXT[statusText.STATES.ENQUEUED]
432
- }\n`,
433
- indent: 2,
449
+ const addTaskSpinner = (task, indent, newline) => {
450
+ const taskName = task[statusText.SUBTASK_NAME_KEY];
451
+ const taskType = task[statusText.TYPE_KEY];
452
+ const formattedTaskType = PROJECT_TASK_TYPES[taskType]
453
+ ? `[${PROJECT_TASK_TYPES[taskType]}]`
454
+ : '';
455
+ const text = `${statusText.STATUS_TEXT} ${chalk.bold(
456
+ taskName
457
+ )} ${formattedTaskType} ...${newline ? '\n' : ''}`;
458
+
459
+ spinnies.add(task.id, {
460
+ text,
461
+ indent,
434
462
  });
435
- }
463
+ };
464
+
465
+ structuredTasks.forEach(task => {
466
+ addTaskSpinner(task, 2, !task.subtasks || task.subtasks.length === 0);
467
+ task.subtasks.forEach((subtask, i) =>
468
+ addTaskSpinner(subtask, 4, i === task.subtasks.length - 1)
469
+ );
470
+ });
436
471
 
437
472
  return new Promise((resolve, reject) => {
438
473
  const pollInterval = setInterval(async () => {
@@ -444,34 +479,43 @@ const makePollTaskStatusFunc = ({
444
479
 
445
480
  if (spinnies.hasActiveSpinners()) {
446
481
  subTaskStatus.forEach(subTask => {
447
- const subTaskName = subTask[statusText.SUBTASK_NAME_KEY];
482
+ const { id, status } = subTask;
483
+ const spinner = spinnies.pick(id);
448
484
 
449
- if (!spinnies.pick(subTaskName)) {
485
+ if (!spinner || spinner.status !== SPINNER_STATUS.SPINNING) {
450
486
  return;
451
487
  }
452
488
 
453
- const updatedText = `${chalk.bold(subTaskName)} ${
454
- statusText.STATUS_TEXT[subTask.status]
455
- }\n`;
456
-
457
- switch (subTask.status) {
458
- case statusText.STATES.SUCCESS:
459
- spinnies.succeed(subTaskName, { text: updatedText });
460
- break;
461
- case statusText.STATES.FAILURE:
462
- spinnies.fail(subTaskName, { text: updatedText });
463
- break;
464
- default:
465
- spinnies.update(subTaskName, { text: updatedText });
466
- break;
489
+ const topLevelTask = structuredTasks.find(t => t.id == id);
490
+
491
+ if (
492
+ status === statusText.STATES.SUCCESS ||
493
+ status === statusText.STATES.FAILURE
494
+ ) {
495
+ const taskStatusText =
496
+ subTask.status === statusText.STATES.SUCCESS
497
+ ? i18n(`${i18nKey}.successStatusText`)
498
+ : i18n(`${i18nKey}.failedStatusText`);
499
+ const hasNewline =
500
+ spinner.text.includes('\n') || Boolean(topLevelTask);
501
+ const updatedText = `${spinner.text.replace(
502
+ '\n',
503
+ ''
504
+ )} ${taskStatusText}${hasNewline ? '\n' : ''}`;
505
+
506
+ status === statusText.STATES.SUCCESS
507
+ ? spinnies.succeed(id, { text: updatedText })
508
+ : spinnies.fail(id, { text: updatedText });
509
+
510
+ if (topLevelTask) {
511
+ topLevelTask.subtasks.forEach(currentSubtask =>
512
+ spinnies.remove(currentSubtask.id)
513
+ );
514
+ }
467
515
  }
468
516
  });
469
517
 
470
518
  if (isTaskComplete(taskStatus)) {
471
- // subTaskStatus.forEach(subTask => {
472
- // spinnies.remove(subTask[statusText.SUBTASK_NAME_KEY]);
473
- // });
474
-
475
519
  if (status === statusText.STATES.SUCCESS) {
476
520
  spinnies.succeed('overallTaskStatus', {
477
521
  text: statusStrings.SUCCESS(taskName),
@@ -481,7 +525,7 @@ const makePollTaskStatusFunc = ({
481
525
  text: statusStrings.FAIL(taskName),
482
526
  });
483
527
 
484
- const failedSubtask = subTaskStatus.filter(
528
+ const failedSubtasks = subTaskStatus.filter(
485
529
  subtask => subtask.status === 'FAILURE'
486
530
  );
487
531
 
@@ -489,19 +533,19 @@ const makePollTaskStatusFunc = ({
489
533
  logger.log(
490
534
  `${statusStrings.SUBTASK_FAIL(
491
535
  displayId,
492
- failedSubtask.length === 1
493
- ? failedSubtask[0][statusText.SUBTASK_NAME_KEY]
494
- : failedSubtask.length + ' components'
536
+ failedSubtasks.length === 1
537
+ ? failedSubtasks[0][statusText.SUBTASK_NAME_KEY]
538
+ : failedSubtasks.length + ' components'
495
539
  )}\n`
496
540
  );
497
541
  logger.log('See below for a summary of errors.');
498
542
  uiLine();
499
543
 
500
- failedSubtask.forEach(subTask => {
544
+ failedSubtasks.forEach(subTask => {
501
545
  logger.log(
502
- `\n--- ${chalk.bold(subTask[statusText.SUBTASK_NAME_KEY])} ${
503
- statusText.STATUS_TEXT[subTask.status]
504
- } with the following error ---`
546
+ `\n--- ${chalk.bold(
547
+ subTask[statusText.SUBTASK_NAME_KEY]
548
+ )} failed with the following error ---`
505
549
  );
506
550
  logger.error(subTask.errorMessage);
507
551
 
@@ -1,7 +1,17 @@
1
1
  const { updateDefaultAccount } = require('@hubspot/cli-lib/lib/config');
2
2
  const { promptUser } = require('./promptUtils');
3
3
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
4
- const { mapAccountChoices } = require('./sandboxesPrompt');
4
+ const { getSandboxType } = require('../sandboxes');
5
+
6
+ const mapAccountChoices = portals =>
7
+ portals.map(p => {
8
+ const isSandbox = p.sandboxAccountType && p.sandboxAccountType !== null;
9
+ const sandboxName = `[${getSandboxType(p.sandboxAccountType)} sandbox] `;
10
+ return {
11
+ name: `${p.name} ${isSandbox ? sandboxName : ''}(${p.portalId})`,
12
+ value: p.name || p.portalId,
13
+ };
14
+ });
5
15
 
6
16
  const i18nKey = 'cli.commands.accounts.subcommands.use';
7
17
 
@@ -1,20 +1,19 @@
1
1
  const { promptUser } = require('./promptUtils');
2
2
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
3
+ const { getSandboxType } = require('../sandboxes');
3
4
 
4
5
  const i18nKey = 'cli.lib.prompts.sandboxesPrompt';
5
6
 
6
- const getSandboxType = type =>
7
- type === 'DEVELOPER' ? 'development' : 'standard';
8
-
9
- const mapAccountChoices = portals =>
10
- portals.map(p => {
11
- const isSandbox = p.sandboxAccountType !== null;
12
- const sandboxName = `[${getSandboxType(p.sandboxAccountType)} sandbox] `;
13
- return {
14
- name: `${p.name} ${isSandbox ? sandboxName : ''}(${p.portalId})`,
15
- value: p.name || p.portalId,
16
- };
17
- });
7
+ const mapSandboxAccountChoices = portals =>
8
+ portals
9
+ .filter(p => p.sandboxAccountType && p.sandboxAccountType !== null)
10
+ .map(p => {
11
+ const sandboxName = `[${getSandboxType(p.sandboxAccountType)} sandbox] `;
12
+ return {
13
+ name: `${p.name} ${sandboxName}(${p.portalId})`,
14
+ value: p.name || p.portalId,
15
+ };
16
+ });
18
17
 
19
18
  const createSandboxPrompt = () => {
20
19
  return promptUser([
@@ -44,7 +43,7 @@ const deleteSandboxPrompt = (config, promptParentAccount = false) => {
44
43
  type: 'list',
45
44
  look: false,
46
45
  pageSize: 20,
47
- choices: mapAccountChoices(config.portals),
46
+ choices: mapSandboxAccountChoices(config.portals),
48
47
  default: config.defaultPortal,
49
48
  },
50
49
  ]);
@@ -54,5 +53,4 @@ module.exports = {
54
53
  createSandboxPrompt,
55
54
  deleteSandboxPrompt,
56
55
  getSandboxType,
57
- mapAccountChoices,
58
56
  };
@@ -0,0 +1,217 @@
1
+ const cliProgress = require('cli-progress');
2
+ const {
3
+ getConfig,
4
+ writeConfig,
5
+ updateAccountConfig,
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
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
12
+ const { logger } = require('@hubspot/cli-lib/logger');
13
+ const {
14
+ updateConfigWithPersonalAccessKey,
15
+ } = require('@hubspot/cli-lib/personalAccessKey');
16
+ const { EXIT_CODES } = require('./enums/exitCodes');
17
+ const { enterAccountNamePrompt } = require('./prompts/enterAccountNamePrompt');
18
+ const {
19
+ personalAccessKeyPrompt,
20
+ } = 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');
27
+
28
+ const getSandboxType = type =>
29
+ type === 'DEVELOPER' ? 'development' : 'standard';
30
+
31
+ function getAccountName(config) {
32
+ const isSandbox =
33
+ config.sandboxAccountType && config.sandboxAccountType !== null;
34
+ const sandboxName = `[${getSandboxType(config.sandboxAccountType)} sandbox] `;
35
+ return `${config.name} ${isSandbox ? sandboxName : ''}(${config.portalId})`;
36
+ }
37
+
38
+ // Fetches available sync types for a given sandbox portal
39
+ async function getAvailableSyncTypes(parentAccountConfig, config) {
40
+ const parentPortalId = parentAccountConfig.portalId;
41
+ const portalId = config.portalId;
42
+ const syncTypes = await fetchTypes(parentPortalId, portalId);
43
+ return syncTypes.map(t => ({ type: t.name }));
44
+ }
45
+
46
+ const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
47
+ const configData = await personalAccessKeyPrompt({ env, account });
48
+ const updatedConfig = await updateConfigWithPersonalAccessKey(configData);
49
+
50
+ if (!updatedConfig) {
51
+ process.exit(EXIT_CODES.SUCCESS);
52
+ }
53
+
54
+ let validName = updatedConfig.name;
55
+
56
+ if (!updatedConfig.name) {
57
+ const nameForConfig = name
58
+ .toLowerCase()
59
+ .split(' ')
60
+ .join('-');
61
+ const { name: promptName } = await enterAccountNamePrompt(nameForConfig);
62
+ validName = promptName;
63
+ }
64
+
65
+ updateAccountConfig({
66
+ ...updatedConfig,
67
+ environment: updatedConfig.env,
68
+ tokenInfo: updatedConfig.auth.tokenInfo,
69
+ name: validName,
70
+ });
71
+ writeConfig();
72
+
73
+ const setAsDefault = await setAsDefaultAccountPrompt(validName);
74
+
75
+ 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
+ ]);
102
+ };
103
+
104
+ const ACTIVE_TASK_POLL_INTERVAL = 1000;
105
+
106
+ const isTaskComplete = task => {
107
+ if (!task) {
108
+ return false;
109
+ }
110
+ return task.status === 'COMPLETE';
111
+ };
112
+
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
+ );
124
+ const mergeTasks = {
125
+ '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
+ };
127
+ const barInstances = {};
128
+ let pollInterval;
129
+ // Handle manual exit for return key and ctrl+c
130
+ const onTerminate = () => {
131
+ clearInterval(pollInterval);
132
+ multibar.stop();
133
+ logger.log('');
134
+ logger.log('Exiting, sync will continue in the background.');
135
+ logger.log('');
136
+ logger.log(
137
+ i18n('cli.commands.sandbox.subcommands.sync.info.syncStatus', {
138
+ url: syncStatusUrl,
139
+ })
140
+ );
141
+ process.exit(EXIT_CODES.SUCCESS);
142
+ };
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
+ });
153
+ return new Promise((resolve, reject) => {
154
+ pollInterval = setInterval(async () => {
155
+ const taskResult = await fetchTaskStatus(accountId, taskId).catch(reject);
156
+ if (taskResult.tasks) {
157
+ // Array of sync tasks, eg: workflows, pipelines, object-schemas, etc. with each task containing a status of 'PENDING', 'IN_PROGRESS', 'COMPLETE', and 'FAILURE'
158
+ for (const task of taskResult.tasks) {
159
+ // For each sync task, show a progress bar and increment bar each time we run this interval until status is 'COMPLETE'
160
+ 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`),
165
+ });
166
+ } else if (mergeTasks[taskType]) {
167
+ // If its a lead-flow, merge status into the forms progress bar
168
+ const formsTask = taskResult.tasks.filter(
169
+ t => t.type === mergeTasks[taskType]
170
+ )[0];
171
+ const formsTaskStatus = formsTask.status;
172
+ const leadFlowsTaskStatus = task.status;
173
+ if (
174
+ formsTaskStatus !== 'COMPLETE' ||
175
+ leadFlowsTaskStatus !== 'COMPLETE'
176
+ ) {
177
+ barInstances[mergeTasks[taskType]].increment(
178
+ Math.floor(Math.random() * 3),
179
+ {
180
+ taskType: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
181
+ }
182
+ );
183
+ }
184
+ }
185
+ if (barInstances[taskType] && task.status === 'COMPLETE') {
186
+ barInstances[taskType].update(100, {
187
+ taskType: i18n(`${i18nKey}.${taskType}.label`),
188
+ });
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`),
194
+ });
195
+ }
196
+ }
197
+ } else {
198
+ clearInterval(pollInterval);
199
+ reject();
200
+ multibar.stop();
201
+ }
202
+ if (isTaskComplete(taskResult)) {
203
+ clearInterval(pollInterval);
204
+ resolve(taskResult);
205
+ multibar.stop();
206
+ }
207
+ }, ACTIVE_TASK_POLL_INTERVAL);
208
+ });
209
+ }
210
+
211
+ module.exports = {
212
+ getSandboxType,
213
+ getAccountName,
214
+ getAvailableSyncTypes,
215
+ sandboxCreatePersonalAccessKeyFlow,
216
+ pollSyncTaskStatus,
217
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.1.6",
3
+ "version": "4.1.7-beta.1",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,10 +8,11 @@
8
8
  "url": "https://github.com/HubSpot/hubspot-cms-tools"
9
9
  },
10
10
  "dependencies": {
11
- "@hubspot/cli-lib": "4.1.6",
12
- "@hubspot/serverless-dev-runtime": "4.1.6",
11
+ "@hubspot/cli-lib": "4.1.7-beta.1",
12
+ "@hubspot/serverless-dev-runtime": "4.1.7-beta.1",
13
13
  "archiver": "^5.3.0",
14
14
  "chalk": "^4.1.2",
15
+ "cli-progress": "^3.11.2",
15
16
  "express": "^4.17.1",
16
17
  "findup-sync": "^4.0.0",
17
18
  "fs-extra": "^8.1.0",
@@ -37,5 +38,5 @@
37
38
  "publishConfig": {
38
39
  "access": "public"
39
40
  },
40
- "gitHead": "cc2745e695099e23886958d130bed6ae22685215"
41
+ "gitHead": "f97135fb4b7a43bbe9915191a70f38d509ba1b26"
41
42
  }