@hubspot/cli 4.1.8-beta.0 → 4.1.8-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.
package/lib/projects.js CHANGED
@@ -4,17 +4,14 @@ const archiver = require('archiver');
4
4
  const tmp = require('tmp');
5
5
  const chalk = require('chalk');
6
6
  const findup = require('findup-sync');
7
- const Spinnies = require('spinnies');
8
7
  const { logger } = require('@hubspot/cli-lib/logger');
9
8
  const { getEnv } = require('@hubspot/cli-lib/lib/config');
10
- const { cloneGitHubRepo } = require('@hubspot/cli-lib/github');
11
9
  const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
12
10
  const {
13
11
  ENVIRONMENTS,
14
12
  FEEDBACK_INTERVAL,
15
13
  ERROR_TYPES,
16
14
  POLLING_DELAY,
17
- PROJECT_TEMPLATES,
18
15
  PROJECT_BUILD_TEXT,
19
16
  PROJECT_DEPLOY_TEXT,
20
17
  PROJECT_CONFIG_FILE,
@@ -36,10 +33,17 @@ const {
36
33
  } = require('@hubspot/cli-lib/errorHandlers');
37
34
  const { shouldIgnoreFile } = require('@hubspot/cli-lib/ignoreRules');
38
35
  const { getCwd, getAbsoluteFilePath } = require('@hubspot/cli-lib/path');
36
+ const { downloadGitHubRepoContents } = require('@hubspot/cli-lib/github');
39
37
  const { promptUser } = require('./prompts/promptUtils');
40
38
  const { EXIT_CODES } = require('./enums/exitCodes');
41
39
  const { uiLine, uiLink, uiAccountDescription } = require('../lib/ui');
42
40
  const { i18n } = require('@hubspot/cli-lib/lib/lang');
41
+ const SpinniesManager = require('./SpinniesManager');
42
+ const {
43
+ isSpecifiedError,
44
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
45
+
46
+ const i18nKey = 'cli.lib.projects';
43
47
 
44
48
  const writeProjectConfig = (configPath, config) => {
45
49
  try {
@@ -85,7 +89,12 @@ const getProjectConfig = async _dir => {
85
89
  }
86
90
  };
87
91
 
88
- const createProjectConfig = async (projectPath, projectName, template) => {
92
+ const createProjectConfig = async (
93
+ projectPath,
94
+ projectName,
95
+ template,
96
+ repoPath
97
+ ) => {
89
98
  const { projectConfig, projectDir } = await getProjectConfig(projectPath);
90
99
 
91
100
  if (projectConfig) {
@@ -121,7 +130,7 @@ const createProjectConfig = async (projectPath, projectName, template) => {
121
130
  }`
122
131
  );
123
132
 
124
- if (template === 'no-template') {
133
+ if (template.name === 'no-template') {
125
134
  fs.ensureDirSync(path.join(projectPath, 'src'));
126
135
 
127
136
  writeProjectConfig(projectConfigPath, {
@@ -129,12 +138,7 @@ const createProjectConfig = async (projectPath, projectName, template) => {
129
138
  srcDir: 'src',
130
139
  });
131
140
  } else {
132
- await cloneGitHubRepo(
133
- projectPath,
134
- 'project',
135
- PROJECT_TEMPLATES.find(t => t.name === template).repo,
136
- ''
137
- );
141
+ await downloadGitHubRepoContents(repoPath, template.path, projectPath);
138
142
  const _config = JSON.parse(fs.readFileSync(projectConfigPath));
139
143
  writeProjectConfig(projectConfigPath, {
140
144
  ..._config,
@@ -168,15 +172,61 @@ const validateProjectConfig = (projectConfig, projectDir) => {
168
172
  }
169
173
  };
170
174
 
175
+ const pollFetchProject = async (accountId, projectName) => {
176
+ // Temporary solution for gating slowness. Retry on 403 statusCode
177
+ return new Promise((resolve, reject) => {
178
+ let pollCount = 0;
179
+ const spinnies = SpinniesManager.init();
180
+ spinnies.add('pollFetchProject', {
181
+ text: 'Fetching project status',
182
+ });
183
+ const pollInterval = setInterval(async () => {
184
+ try {
185
+ const project = await fetchProject(accountId, projectName);
186
+ if (project) {
187
+ spinnies.remove('pollFetchProject');
188
+ clearInterval(pollInterval);
189
+ resolve(project);
190
+ }
191
+ } catch (err) {
192
+ if (
193
+ isSpecifiedError(err, {
194
+ statusCode: 403,
195
+ category: 'GATED',
196
+ subCategory: 'BuildPipelineErrorType.PORTAL_GATED',
197
+ })
198
+ ) {
199
+ pollCount += 1;
200
+ } else if (pollCount >= 15) {
201
+ // Poll up to max 30s
202
+ spinnies.remove('pollFetchProject');
203
+ clearInterval(pollInterval);
204
+ reject(err);
205
+ } else {
206
+ spinnies.remove('pollFetchProject');
207
+ clearInterval(pollInterval);
208
+ reject(err);
209
+ }
210
+ }
211
+ }, POLLING_DELAY);
212
+ });
213
+ };
214
+
171
215
  const ensureProjectExists = async (
172
216
  accountId,
173
217
  projectName,
174
- { forceCreate = false, allowCreate = true, noLogs = false } = {}
218
+ {
219
+ forceCreate = false,
220
+ allowCreate = true,
221
+ noLogs = false,
222
+ withPolling = false,
223
+ } = {}
175
224
  ) => {
176
- const i18nKey = 'cli.commands.project.lib.ensureProjectExists';
177
225
  const accountIdentifier = uiAccountDescription(accountId);
178
226
  try {
179
- const project = await fetchProject(accountId, projectName);
227
+ const project = withPolling
228
+ ? await pollFetchProject(accountId, projectName)
229
+ : await fetchProject(accountId, projectName);
180
230
  return !!project;
181
231
  } catch (err) {
182
232
  if (err.statusCode === 404) {
@@ -186,7 +236,7 @@ const ensureProjectExists = async (
186
236
  const promptResult = await promptUser([
187
237
  {
188
238
  name: 'shouldCreateProject',
189
- message: i18n(`${i18nKey}.createPrompt`, {
239
+ message: i18n(`${i18nKey}.ensureProjectExists.createPrompt`, {
190
240
  projectName,
191
241
  accountIdentifier,
192
242
  }),
@@ -200,7 +250,10 @@ const ensureProjectExists = async (
200
250
  try {
201
251
  await createProject(accountId, projectName);
202
252
  logger.success(
203
- i18n(`${i18nKey}.createSuccess`, { projectName, accountIdentifier })
253
+ i18n(`${i18nKey}.ensureProjectExists.createSuccess`, {
254
+ projectName,
255
+ accountIdentifier,
256
+ })
204
257
  );
205
258
  return true;
206
259
  } catch (err) {
@@ -209,9 +262,10 @@ const ensureProjectExists = async (
209
262
  } else {
210
263
  if (!noLogs) {
211
264
  logger.log(
212
- `Your project ${chalk.bold(
213
- projectName
214
- )} could not be found in ${chalk.bold(accountIdentifier)}.`
265
+ i18n(`${i18nKey}.ensureProjectExists.notFound`, {
266
+ projectName,
267
+ accountIdentifier,
268
+ })
215
269
  );
216
270
  }
217
271
  return false;
@@ -222,14 +276,17 @@ const ensureProjectExists = async (
222
276
  }
223
277
  };
224
278
 
225
- const getProjectDetailUrl = (projectName, accountId) => {
226
- if (!projectName) return;
227
-
279
+ const getProjectHomeUrl = accountId => {
228
280
  const baseUrl = getHubSpotWebsiteOrigin(
229
281
  getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD
230
282
  );
231
283
 
232
- return `${baseUrl}/developer-projects/${accountId}/project/${projectName}`;
284
+ return `${baseUrl}/developer-projects/${accountId}`;
285
+ };
286
+
287
+ const getProjectDetailUrl = (projectName, accountId) => {
288
+ if (!projectName) return;
289
+ return `${getProjectHomeUrl(accountId)}/project/${projectName}`;
233
290
  };
234
291
 
235
292
  const getProjectBuildDetailUrl = (projectName, buildId, accountId) => {
@@ -244,23 +301,22 @@ const getProjectDeployDetailUrl = (projectName, deployId, accountId) => {
244
301
  accountId
245
302
  )}/activity/deploy/${deployId}`;
246
303
  };
304
+
247
305
  const uploadProjectFiles = async (
248
306
  accountId,
249
307
  projectName,
250
308
  filePath,
251
309
  uploadMessage
252
310
  ) => {
253
- const i18nKey = 'cli.commands.project.subcommands.upload';
254
- const spinnies = new Spinnies({
255
- succeedColor: 'white',
256
- });
311
+ const spinnies = SpinniesManager.init({});
257
312
  const accountIdentifier = uiAccountDescription(accountId);
258
313
 
259
314
  spinnies.add('upload', {
260
- text: i18n(`${i18nKey}.loading.upload.add`, {
315
+ text: i18n(`${i18nKey}.uploadProjectFiles.add`, {
261
316
  accountIdentifier,
262
317
  projectName,
263
318
  }),
319
+ succeedColor: 'white',
264
320
  });
265
321
 
266
322
  let buildId;
@@ -276,21 +332,21 @@ const uploadProjectFiles = async (
276
332
  buildId = upload.buildId;
277
333
 
278
334
  spinnies.succeed('upload', {
279
- text: i18n(`${i18nKey}.loading.upload.succeed`, {
335
+ text: i18n(`${i18nKey}.uploadProjectFiles.succeed`, {
280
336
  accountIdentifier,
281
337
  projectName,
282
338
  }),
283
339
  });
284
340
 
285
341
  logger.debug(
286
- i18n(`${i18nKey}.debug.buildCreated`, {
342
+ i18n(`${i18nKey}.uploadProjectFiles.buildCreated`, {
287
343
  buildId,
288
344
  projectName,
289
345
  })
290
346
  );
291
347
  } catch (err) {
292
348
  spinnies.fail('upload', {
293
- text: i18n(`${i18nKey}.loading.upload.fail`, {
349
+ text: i18n(`${i18nKey}.uploadProjectFiles.fail`, {
294
350
  accountIdentifier,
295
351
  projectName,
296
352
  }),
@@ -304,7 +360,7 @@ const uploadProjectFiles = async (
304
360
  })
305
361
  );
306
362
  if (err.error.subCategory === ERROR_TYPES.PROJECT_LOCKED) {
307
- logger.log(i18n(`${i18nKey}.logs.projectLockedError`));
363
+ logger.log(i18n(`${i18nKey}.uploadProjectFiles.projectLockedError`));
308
364
  }
309
365
  process.exit(EXIT_CODES.ERROR);
310
366
  }
@@ -312,6 +368,84 @@ const uploadProjectFiles = async (
312
368
  return { buildId };
313
369
  };
314
370
 
371
+ const pollProjectBuildAndDeploy = async (
372
+ accountId,
373
+ projectConfig,
374
+ tempFile,
375
+ buildId,
376
+ silenceLogs = false
377
+ ) => {
378
+ const {
379
+ autoDeployId,
380
+ isAutoDeployEnabled,
381
+ deployStatusTaskLocator,
382
+ status,
383
+ } = await pollBuildStatus(
384
+ accountId,
385
+ projectConfig.name,
386
+ buildId,
387
+ null,
388
+ silenceLogs
389
+ );
390
+ // autoDeployId of 0 indicates a skipped deploy
391
+ const isDeploying =
392
+ isAutoDeployEnabled && autoDeployId > 0 && deployStatusTaskLocator;
393
+
394
+ if (!silenceLogs) {
395
+ uiLine();
396
+ }
397
+
398
+ const result = {
399
+ succeeded: true,
400
+ buildId,
401
+ buildSucceeded: true,
402
+ autodeployEnabled: isAutoDeployEnabled,
403
+ };
404
+
405
+ if (status === 'FAILURE') {
406
+ result.buildSucceeded = false;
407
+ result.succeeded = false;
408
+ return result;
409
+ } else if (isDeploying) {
410
+ if (!silenceLogs) {
411
+ logger.log(
412
+ i18n(
413
+ `${i18nKey}.pollProjectBuildAndDeploy.buildSucceededAutomaticallyDeploying`,
414
+ {
415
+ accountIdentifier: uiAccountDescription(accountId),
416
+ buildId,
417
+ }
418
+ )
419
+ );
420
+ }
421
+ const { status } = await pollDeployStatus(
422
+ accountId,
423
+ projectConfig.name,
424
+ deployStatusTaskLocator.id,
425
+ buildId,
426
+ silenceLogs
427
+ );
428
+ if (status === 'FAILURE') {
429
+ result.succeeded = false;
430
+ }
431
+ }
432
+
433
+ try {
434
+ if (tempFile) {
435
+ tempFile.removeCallback();
436
+ logger.debug(
437
+ i18n(`${i18nKey}.pollProjectBuildAndDeploy.cleanedUpTempFile`, {
438
+ path: tempFile.name,
439
+ })
440
+ );
441
+ }
442
+ } catch (e) {
443
+ logger.error(e);
444
+ }
445
+
446
+ return result;
447
+ };
448
+
315
449
  const handleProjectUpload = async (
316
450
  accountId,
317
451
  projectConfig,
@@ -319,13 +453,14 @@ const handleProjectUpload = async (
319
453
  callbackFunc,
320
454
  uploadMessage
321
455
  ) => {
322
- const i18nKey = 'cli.commands.project.subcommands.upload';
323
456
  const srcDir = path.resolve(projectDir, projectConfig.srcDir);
324
457
 
325
458
  const filenames = fs.readdirSync(srcDir);
326
459
  if (!filenames || filenames.length === 0) {
327
460
  logger.log(
328
- i18n(`${i18nKey}.logs.emptySource`, { srcDir: projectConfig.srcDir })
461
+ i18n(`${i18nKey}.handleProjectUpload.emptySource`, {
462
+ srcDir: projectConfig.srcDir,
463
+ })
329
464
  );
330
465
  process.exit(EXIT_CODES.SUCCESS);
331
466
  }
@@ -333,7 +468,7 @@ const handleProjectUpload = async (
333
468
  const tempFile = tmp.fileSync({ postfix: '.zip' });
334
469
 
335
470
  logger.debug(
336
- i18n(`${i18nKey}.debug.compressing`, {
471
+ i18n(`${i18nKey}.handleProjectUpload.compressing`, {
337
472
  path: tempFile.name,
338
473
  })
339
474
  );
@@ -341,24 +476,34 @@ const handleProjectUpload = async (
341
476
  const output = fs.createWriteStream(tempFile.name);
342
477
  const archive = archiver('zip');
343
478
 
344
- output.on('close', async function() {
345
- logger.debug(
346
- i18n(`${i18nKey}.debug.compressed`, {
347
- byteCount: archive.pointer(),
348
- })
349
- );
479
+ const result = new Promise(resolve =>
480
+ output.on('close', async function() {
481
+ let result = {};
350
482
 
351
- const { buildId } = await uploadProjectFiles(
352
- accountId,
353
- projectConfig.name,
354
- tempFile.name,
355
- uploadMessage
356
- );
483
+ logger.debug(
484
+ i18n(`${i18nKey}.handleProjectUpload.compressed`, {
485
+ byteCount: archive.pointer(),
486
+ })
487
+ );
357
488
 
358
- if (callbackFunc) {
359
- callbackFunc(tempFile, buildId);
360
- }
361
- });
489
+ const { buildId } = await uploadProjectFiles(
490
+ accountId,
491
+ projectConfig.name,
492
+ tempFile.name,
493
+ uploadMessage
494
+ );
495
+
496
+ if (callbackFunc) {
497
+ result = await callbackFunc(
498
+ accountId,
499
+ projectConfig,
500
+ tempFile,
501
+ buildId
502
+ );
503
+ }
504
+ resolve(result);
505
+ })
506
+ );
362
507
 
363
508
  archive.pipe(output);
364
509
 
@@ -367,6 +512,8 @@ const handleProjectUpload = async (
367
512
  );
368
513
 
369
514
  archive.finalize();
515
+
516
+ return result;
370
517
  };
371
518
 
372
519
  const makePollTaskStatusFunc = ({
@@ -376,8 +523,6 @@ const makePollTaskStatusFunc = ({
376
523
  statusStrings,
377
524
  linkToHubSpot,
378
525
  }) => {
379
- const i18nKey = 'cli.commands.project.lib.makePollTaskStatusFunc';
380
-
381
526
  const isTaskComplete = task => {
382
527
  if (
383
528
  !task[statusText.SUBTASK_KEY].length ||
@@ -389,23 +534,32 @@ const makePollTaskStatusFunc = ({
389
534
  }
390
535
  };
391
536
 
392
- return async (accountId, taskName, taskId, deployedBuildId = null) => {
537
+ return async (
538
+ accountId,
539
+ taskName,
540
+ taskId,
541
+ deployedBuildId = null,
542
+ silenceLogs = false
543
+ ) => {
393
544
  const displayId = deployedBuildId || taskId;
394
545
 
395
- if (linkToHubSpot) {
546
+ if (linkToHubSpot && !silenceLogs) {
396
547
  logger.log(
397
548
  `\n${linkToHubSpot(accountId, taskName, taskId, deployedBuildId)}\n`
398
549
  );
399
550
  }
400
551
 
401
- const spinnies = new Spinnies({
552
+ const spinnies = SpinniesManager.init();
553
+
554
+ const overallTaskSpinniesKey = `overallTaskStatus-${statusText.STATUS_TEXT}`;
555
+
556
+ spinnies.add(overallTaskSpinniesKey, {
557
+ text: 'Beginning',
402
558
  succeedColor: 'white',
403
559
  failColor: 'white',
404
560
  failPrefix: chalk.bold('!'),
405
561
  });
406
562
 
407
- spinnies.add('overallTaskStatus', { text: 'Beginning' });
408
-
409
563
  const [
410
564
  initialTaskStatus,
411
565
  { topLevelComponentsWithChildren: taskStructure },
@@ -435,39 +589,45 @@ const makePollTaskStatusFunc = ({
435
589
  });
436
590
 
437
591
  const numComponents = structuredTasks.length;
438
- const componentCountText = i18n(
439
- numComponents === 1
440
- ? `${i18nKey}.componentCountSingular`
441
- : `${i18nKey}.componentCount`,
442
- { numComponents }
443
- );
444
-
445
- spinnies.update('overallTaskStatus', {
446
- text: `${statusStrings.INITIALIZE(taskName)}\n${componentCountText}\n`,
592
+ const componentCountText = silenceLogs
593
+ ? ''
594
+ : i18n(
595
+ numComponents === 1
596
+ ? `${i18nKey}.makePollTaskStatusFunc.componentCountSingular`
597
+ : `${i18nKey}.makePollTaskStatusFunc.componentCount`,
598
+ { numComponents }
599
+ ) + '\n';
600
+
601
+ spinnies.update(overallTaskSpinniesKey, {
602
+ text: `${statusStrings.INITIALIZE(taskName)}\n${componentCountText}`,
447
603
  });
448
604
 
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,
462
- });
463
- };
605
+ if (!silenceLogs) {
606
+ const addTaskSpinner = (task, indent, newline) => {
607
+ const taskName = task[statusText.SUBTASK_NAME_KEY];
608
+ const taskType = task[statusText.TYPE_KEY];
609
+ const formattedTaskType = PROJECT_TASK_TYPES[taskType]
610
+ ? `[${PROJECT_TASK_TYPES[taskType]}]`
611
+ : '';
612
+ const text = `${statusText.STATUS_TEXT} ${chalk.bold(
613
+ taskName
614
+ )} ${formattedTaskType} ...${newline ? '\n' : ''}`;
615
+
616
+ spinnies.add(task.id, {
617
+ text,
618
+ indent,
619
+ succeedColor: 'white',
620
+ failColor: 'white',
621
+ });
622
+ };
464
623
 
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
- });
624
+ structuredTasks.forEach(task => {
625
+ addTaskSpinner(task, 2, !task.subtasks || task.subtasks.length === 0);
626
+ task.subtasks.forEach((subtask, i) =>
627
+ addTaskSpinner(subtask, 4, i === task.subtasks.length - 1)
628
+ );
629
+ });
630
+ }
471
631
 
472
632
  return new Promise((resolve, reject) => {
473
633
  const pollInterval = setInterval(async () => {
@@ -494,8 +654,8 @@ const makePollTaskStatusFunc = ({
494
654
  ) {
495
655
  const taskStatusText =
496
656
  subTask.status === statusText.STATES.SUCCESS
497
- ? i18n(`${i18nKey}.successStatusText`)
498
- : i18n(`${i18nKey}.failedStatusText`);
657
+ ? i18n(`${i18nKey}.makePollTaskStatusFunc.successStatusText`)
658
+ : i18n(`${i18nKey}.makePollTaskStatusFunc.failedStatusText`);
499
659
  const hasNewline =
500
660
  spinner.text.includes('\n') || Boolean(topLevelTask);
501
661
  const updatedText = `${spinner.text.replace(
@@ -517,48 +677,49 @@ const makePollTaskStatusFunc = ({
517
677
 
518
678
  if (isTaskComplete(taskStatus)) {
519
679
  if (status === statusText.STATES.SUCCESS) {
520
- spinnies.succeed('overallTaskStatus', {
680
+ spinnies.succeed(overallTaskSpinniesKey, {
521
681
  text: statusStrings.SUCCESS(taskName),
522
682
  });
523
683
  } else if (status === statusText.STATES.FAILURE) {
524
- spinnies.fail('overallTaskStatus', {
684
+ spinnies.fail(overallTaskSpinniesKey, {
525
685
  text: statusStrings.FAIL(taskName),
526
686
  });
527
687
 
528
- const failedSubtasks = subTaskStatus.filter(
529
- subtask => subtask.status === 'FAILURE'
530
- );
531
-
532
- uiLine();
533
- logger.log(
534
- `${statusStrings.SUBTASK_FAIL(
535
- displayId,
536
- failedSubtasks.length === 1
537
- ? failedSubtasks[0][statusText.SUBTASK_NAME_KEY]
538
- : failedSubtasks.length + ' components'
539
- )}\n`
540
- );
541
- logger.log('See below for a summary of errors.');
542
- uiLine();
543
-
544
- failedSubtasks.forEach(subTask => {
688
+ if (!silenceLogs) {
689
+ const failedSubtasks = subTaskStatus.filter(
690
+ subtask => subtask.status === 'FAILURE'
691
+ );
692
+
693
+ uiLine();
545
694
  logger.log(
546
- `\n--- ${chalk.bold(
547
- subTask[statusText.SUBTASK_NAME_KEY]
548
- )} failed with the following error ---`
695
+ `${statusStrings.SUBTASK_FAIL(
696
+ displayId,
697
+ failedSubtasks.length === 1
698
+ ? failedSubtasks[0][statusText.SUBTASK_NAME_KEY]
699
+ : failedSubtasks.length + ' components'
700
+ )}\n`
549
701
  );
550
- logger.error(subTask.errorMessage);
551
-
552
- // Log nested errors
553
- if (subTask.standardError && subTask.standardError.errors) {
554
- logger.log();
555
- subTask.standardError.errors.forEach(error => {
556
- logger.log(error.message);
557
- });
558
- }
559
- });
702
+ logger.log('See below for a summary of errors.');
703
+ uiLine();
704
+
705
+ failedSubtasks.forEach(subTask => {
706
+ logger.log(
707
+ `\n--- ${chalk.bold(
708
+ subTask[statusText.SUBTASK_NAME_KEY]
709
+ )} failed with the following error ---`
710
+ );
711
+ logger.error(subTask.errorMessage);
712
+
713
+ // Log nested errors
714
+ if (subTask.standardError && subTask.standardError.errors) {
715
+ logger.log();
716
+ subTask.standardError.errors.forEach(error => {
717
+ logger.log(error.message);
718
+ });
719
+ }
720
+ });
721
+ }
560
722
  }
561
-
562
723
  clearInterval(pollInterval);
563
724
  resolve(taskStatus);
564
725
  }
@@ -609,26 +770,53 @@ const pollDeployStatus = makePollTaskStatusFunc({
609
770
  });
610
771
 
611
772
  const logFeedbackMessage = buildId => {
612
- const i18nKey = 'cli.commands.project.subcommands.upload';
613
773
  if (buildId > 0 && buildId % FEEDBACK_INTERVAL === 0) {
614
774
  uiLine();
615
- logger.log(i18n(`${i18nKey}.logs.feedbackHeader`));
775
+ logger.log(i18n(`${i18nKey}.logFeedbackMessage.feedbackHeader`));
616
776
  uiLine();
617
- logger.log(i18n(`${i18nKey}.logs.feedbackMessage`));
777
+ logger.log(i18n(`${i18nKey}.logFeedbackMessage.feedbackMessage`));
618
778
  }
619
779
  };
620
780
 
781
+ const createProjectComponent = async (component, name) => {
782
+ const i18nKey = 'cli.commands.project.subcommands.add';
783
+ let componentName = name;
784
+
785
+ const configInfo = await getProjectConfig();
786
+
787
+ if (!configInfo.projectDir && !configInfo.projectConfig) {
788
+ logger.error(i18n(`${i18nKey}.error.locationInProject`));
789
+ process.exit(EXIT_CODES.ERROR);
790
+ }
791
+
792
+ const componentPath = path.join(
793
+ configInfo.projectDir,
794
+ configInfo.projectConfig.srcDir,
795
+ component.insertPath,
796
+ componentName
797
+ );
798
+
799
+ await downloadGitHubRepoContents(
800
+ 'HubSpot/hubspot-project-components',
801
+ component.path,
802
+ componentPath
803
+ );
804
+ };
805
+
621
806
  module.exports = {
622
807
  writeProjectConfig,
623
808
  getProjectConfig,
624
809
  getIsInProject,
810
+ pollProjectBuildAndDeploy,
625
811
  handleProjectUpload,
626
812
  createProjectConfig,
627
813
  validateProjectConfig,
814
+ getProjectHomeUrl,
628
815
  getProjectDetailUrl,
629
816
  getProjectBuildDetailUrl,
630
817
  pollBuildStatus,
631
818
  pollDeployStatus,
632
819
  ensureProjectExists,
633
820
  logFeedbackMessage,
821
+ createProjectComponent,
634
822
  };