@hubspot/cli 4.1.8-beta.4 → 4.1.8-beta.5

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.
@@ -29,6 +29,7 @@ const {
29
29
  LocalDevManager,
30
30
  UPLOAD_PERMISSIONS,
31
31
  } = require('../../lib/LocalDevManager');
32
+ const { isSandbox } = require('../../lib/sandboxes');
32
33
  const { getAccountConfig, getEnv } = require('@hubspot/cli-lib');
33
34
  const { sandboxNamePrompt } = require('../../lib/prompts/sandboxesPrompt');
34
35
  const {
@@ -37,12 +38,18 @@ const {
37
38
  getAvailableSyncTypes,
38
39
  } = require('../../lib/sandboxes');
39
40
  const { getValidEnv } = require('@hubspot/cli-lib/lib/environment');
40
- const { logErrorInstance } = require('@hubspot/cli-lib/errorHandlers');
41
+ const { ERROR_TYPES } = require('@hubspot/cli-lib/lib/constants');
42
+ const {
43
+ logErrorInstance,
44
+ logApiErrorInstance,
45
+ ApiErrorContext,
46
+ } = require('@hubspot/cli-lib/errorHandlers');
41
47
  const { buildSandbox } = require('../../lib/sandbox-create');
42
48
  const { syncSandbox } = require('../../lib/sandbox-sync');
43
49
  const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
44
50
  const {
45
51
  isMissingScopeError,
52
+ isSpecifiedError,
46
53
  } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
47
54
 
48
55
  const i18nKey = 'cli.commands.project.subcommands.dev';
@@ -60,13 +67,7 @@ exports.handler = async options => {
60
67
 
61
68
  const { projectConfig, projectDir } = await getProjectConfig();
62
69
 
63
- logger.log(i18n(`${i18nKey}.logs.introHeader`));
64
- uiLine();
65
- logger.log(i18n(`${i18nKey}.logs.introBody1`));
66
- logger.log();
67
- logger.log(i18n(`${i18nKey}.logs.introBody2`));
68
- uiLine();
69
- logger.log();
70
+ logger.log(i18n(`${i18nKey}.logs.betaMessage`));
70
71
 
71
72
  if (!projectConfig) {
72
73
  logger.error(i18n(`${i18nKey}.errors.noProjectConfig`));
@@ -74,47 +75,32 @@ exports.handler = async options => {
74
75
  }
75
76
 
76
77
  const accounts = getConfigAccounts();
77
- let targetAccountId = options.accountId;
78
+ let targetAccountId = options.account ? accountId : null;
78
79
  let createNewSandbox = false;
79
- let chooseNonSandbox = false;
80
+ const defaultAccountIsSandbox = isSandbox(accountConfig);
81
+
82
+ if (!targetAccountId && defaultAccountIsSandbox) {
83
+ targetAccountId = accountId;
84
+ }
80
85
 
81
86
  if (!targetAccountId) {
87
+ //logger.log(i18n(`${i18nKey}.logs.learnMoreLink`));
88
+ logger.log();
89
+ uiLine();
90
+ logger.warn(i18n(`${i18nKey}.logs.nonSandboxWarning`));
91
+ uiLine();
92
+ logger.log();
93
+
82
94
  const {
83
95
  targetAccountId: promptTargetAccountId,
84
- chooseNonSandbox: promptChooseNonSandbox,
85
96
  createNewSandbox: promptCreateNewSandbox,
86
- } = await selectTargetAccountPrompt(accounts, accountConfig, false);
97
+ } = await selectTargetAccountPrompt(accounts, accountConfig);
87
98
 
88
99
  targetAccountId = promptTargetAccountId;
89
- chooseNonSandbox = promptChooseNonSandbox;
90
100
  createNewSandbox = promptCreateNewSandbox;
91
101
  }
92
102
 
93
- logger.log();
94
-
95
- // Show a warning if the user chooses a non-sandbox account (false)
96
- let shouldTargetNonSandboxAccount;
97
- if (chooseNonSandbox) {
98
- uiLine();
99
- logger.warn(i18n(`${i18nKey}.logs.prodAccountWarning`));
100
- uiLine();
101
- logger.log();
102
-
103
- shouldTargetNonSandboxAccount = await confirmPrompt(
104
- i18n(`${i18nKey}.prompt.targetNonSandbox`)
105
- );
106
-
107
- if (shouldTargetNonSandboxAccount) {
108
- const {
109
- targetAccountId: promptNonSandboxTargetAccountId,
110
- } = await selectTargetAccountPrompt(accounts, accountConfig, true);
111
-
112
- targetAccountId = promptNonSandboxTargetAccountId;
113
- logger.log();
114
- } else {
115
- process.exit(EXIT_CODES.SUCCESS);
116
- }
117
- } else if (createNewSandbox) {
103
+ if (createNewSandbox) {
118
104
  try {
119
105
  await validateSandboxUsageLimits(accountConfig, DEVELOPER_SANDBOX, env);
120
106
  } catch (err) {
@@ -179,7 +165,8 @@ exports.handler = async options => {
179
165
  }
180
166
  );
181
167
 
182
- const isNonSandboxAccount = shouldTargetNonSandboxAccount;
168
+ const isNonSandboxAccount =
169
+ !defaultAccountIsSandbox && targetAccountId === accountId;
183
170
 
184
171
  let uploadPermission = isNonSandboxAccount
185
172
  ? UPLOAD_PERMISSIONS.manual
@@ -198,21 +185,26 @@ exports.handler = async options => {
198
185
  const spinnies = SpinniesManager.init();
199
186
 
200
187
  if (!projectExists) {
201
- uiLine();
202
- logger.warn(
203
- i18n(`${i18nKey}.logs.projectMustExistExplanation`, {
204
- accountIdentifier: uiAccountDescription(targetAccountId),
205
- projectName: projectConfig.name,
206
- })
207
- );
208
- uiLine();
188
+ // Create the project without prompting if this is a newly created sandbox
189
+ let shouldCreateProject = createNewSandbox;
190
+
191
+ if (!shouldCreateProject) {
192
+ uiLine();
193
+ logger.warn(
194
+ i18n(`${i18nKey}.logs.projectMustExistExplanation`, {
195
+ accountIdentifier: uiAccountDescription(targetAccountId),
196
+ projectName: projectConfig.name,
197
+ })
198
+ );
199
+ uiLine();
209
200
 
210
- const shouldCreateProject = await confirmPrompt(
211
- i18n(`${i18nKey}.prompt.createProject`, {
212
- accountIdentifier: uiAccountDescription(targetAccountId),
213
- projectName: projectConfig.name,
214
- })
215
- );
201
+ shouldCreateProject = await confirmPrompt(
202
+ i18n(`${i18nKey}.prompt.createProject`, {
203
+ accountIdentifier: uiAccountDescription(targetAccountId),
204
+ projectName: projectConfig.name,
205
+ })
206
+ );
207
+ }
216
208
 
217
209
  if (shouldCreateProject) {
218
210
  try {
@@ -228,9 +220,10 @@ exports.handler = async options => {
228
220
  accountIdentifier: uiAccountDescription(targetAccountId),
229
221
  projectName: projectConfig.name,
230
222
  }),
223
+ succeedColor: 'white',
231
224
  });
232
225
  } catch (err) {
233
- logger.log(i18n(`${i18nKey}.logs.failedToCreateProject`));
226
+ logger.log(i18n(`${i18nKey}.status.failedToCreateProject`));
234
227
  process.exit(EXIT_CODES.ERROR);
235
228
  }
236
229
  } else {
@@ -260,10 +253,24 @@ exports.handler = async options => {
260
253
  );
261
254
  }
262
255
 
263
- if (result && !result.succeeded) {
264
- spinnies.fail('devModeSetup', {
265
- text: i18n(`${i18nKey}.status.startupFailed`),
266
- });
256
+ if (result && result.error) {
257
+ if (
258
+ isSpecifiedError(result.error, {
259
+ subCategory: ERROR_TYPES.PROJECT_LOCKED,
260
+ })
261
+ ) {
262
+ logger.log();
263
+ logger.error(i18n(`${i18nKey}.errors.projectLockedError`));
264
+ logger.log();
265
+ } else {
266
+ logApiErrorInstance(
267
+ result.error,
268
+ new ApiErrorContext({
269
+ accountId,
270
+ projectName: projectConfig.name,
271
+ })
272
+ );
273
+ }
267
274
  process.exit(EXIT_CODES.ERROR);
268
275
  } else {
269
276
  spinnies.remove('devModeSetup');
@@ -19,6 +19,14 @@ const {
19
19
  } = require('../../lib/projects');
20
20
  const { i18n } = require('../../lib/lang');
21
21
  const { getAccountConfig } = require('@hubspot/cli-lib');
22
+ const { ERROR_TYPES } = require('@hubspot/cli-lib/lib/constants');
23
+ const {
24
+ isSpecifiedError,
25
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
26
+ const {
27
+ logApiErrorInstance,
28
+ ApiErrorContext,
29
+ } = require('@hubspot/cli-lib/errorHandlers');
22
30
  const { EXIT_CODES } = require('../../lib/enums/exitCodes');
23
31
 
24
32
  const i18nKey = 'cli.commands.project.subcommands.upload';
@@ -50,6 +58,26 @@ exports.handler = async options => {
50
58
  message
51
59
  );
52
60
 
61
+ if (result.error) {
62
+ if (
63
+ isSpecifiedError(result.error, {
64
+ subCategory: ERROR_TYPES.PROJECT_LOCKED,
65
+ })
66
+ ) {
67
+ logger.log();
68
+ logger.error(i18n(`${i18nKey}.errors.projectLockedError`));
69
+ logger.log();
70
+ } else {
71
+ logApiErrorInstance(
72
+ result.error,
73
+ new ApiErrorContext({
74
+ accountId,
75
+ projectName: projectConfig.name,
76
+ })
77
+ );
78
+ }
79
+ process.exit(EXIT_CODES.ERROR);
80
+ }
53
81
  if (result.buildSucceeded && !result.autodeployEnabled) {
54
82
  uiLine();
55
83
  logger.log(
package/lang/en.lyaml CHANGED
@@ -448,11 +448,10 @@ en:
448
448
  dev:
449
449
  describe: "Start local dev for the current project"
450
450
  logs:
451
- introHeader: "{{#bold}}HubSpot projects local development (beta){{/bold}}"
452
- introBody1: "This command runs a file watcher that works together with a HubSpot account to allow developers to write code locally and test quickly."
453
- introBody2: "All project file changes will be reflected in the target account after a slight delay."
451
+ betaMessage: "{{#yellow}}{{#bold}}[beta]{{/bold}}{{/yellow}} HubSpot projects local development"
452
+ learnMoreLink: "Learn more about the projects local dev server"
453
+ nonSandboxWarning: "Testing in a sandbox is strongly recommended. To switch the target account, select an option below or run {{#bold}}`hs accounts use`{{/bold}} before running the command again."
454
454
  placeholderAccountSelection: "Using default account as target account (for now)"
455
- prodAccountWarning: "Targeting a non-sandbox account is risky because this command will upload changes to the target account. We recommend picking a sandbox account for any work that has not been finalized or is undergoing testing prior to deploying to production."
456
455
  projectMustExistExplanation: "The project {{ projectName }} does not exist in the target account {{ accountIdentifier}}. This command requires the project to exist in the target account."
457
456
  choseNotToCreateProject: "Exiting because this command requires the project to exist in the target account."
458
457
  initialUploadMessage: "HubSpot Local Dev Server Startup"
@@ -461,7 +460,6 @@ en:
461
460
  createdProject: "Created project {{ projectName }} in {{ accountIdentifier }}"
462
461
  failedToCreateProject: "Failed to create project in the target account."
463
462
  startupMessage: "Starting local dev server for {{#bold}}{{ projectName }}{{/bold}} ..."
464
- startupFailed: "Failed to initialize local dev server"
465
463
  prompt:
466
464
  createProject: "Create new project {{ projectName}} in {{#bold}}[{{ accountIdentifier }}]{{/bold}}?"
467
465
  targetNonSandbox: "Continue testing in a non-sandbox account?"
@@ -470,6 +468,7 @@ en:
470
468
  describe: "The port that the running server will listen on"
471
469
  errors:
472
470
  noProjectConfig: "No project detected. Please run this command again from a project directory."
471
+ projectLockedError: "Your project is locked. This may mean that another user is running the {{#bold}}`hs project dev`{{/bold}} command for this project. If this is you, unlock the project in Projects UI."
473
472
  examples:
474
473
  default: "Start local dev for the current project"
475
474
  create:
@@ -561,6 +560,8 @@ en:
561
560
  buildSucceeded: "Build #{{ buildId }} succeeded\n"
562
561
  readyToGoLive: "🚀 Ready to take your project live?"
563
562
  runCommand: "Run `{{ command }}`"
563
+ errors:
564
+ projectLockedError: "Your project is locked. This may mean that another user is running the {{#bold}}`hs project dev`{{/bold}} command for this project. If this is you, unlock the project in Projects UI."
564
565
  options:
565
566
  forceCreate:
566
567
  describe: "Automatically create project if it does not exist"
@@ -569,15 +570,6 @@ en:
569
570
  positionals:
570
571
  path:
571
572
  describe: "Path to a project folder"
572
- unlock:
573
- describe: "Unlock a locked project"
574
- examples:
575
- default: "Unlock a locked project in the myProjectsFolder folder"
576
- logs:
577
- unlockSucceeded: "Successfully unlocked the project"
578
- positionals:
579
- path:
580
- describe: "Path to a project folder"
581
573
  watch:
582
574
  describe: "Watch your local project for changes and automatically upload changed files to a new build in HubSpot"
583
575
  examples:
@@ -844,6 +836,8 @@ en:
844
836
  options:
845
837
  describe: "Options to pass to javascript fields files"
846
838
  lib:
839
+ DevServerManager:
840
+ portConflict: "The port {{ port }} is already in use."
847
841
  LocalDevManager:
848
842
  exitingStart: "Stopping local dev server ..."
849
843
  exitingSucceed: "Successfully exited"
@@ -851,6 +845,8 @@ en:
851
845
  previousStagingBuildCancelled: "Failed to create a staging build because the project was already locked. It is now unlocked. Run the command again."
852
846
  cancelledFromUI: "The dev process has been cancelled from the UI. Any changes made since cancelling have not been uploaded. To resume dev mode, rerun {{#yellow}}`hs project dev`{{/yellow}}."
853
847
  header:
848
+ betaMessage: "{{#yellow}}{{#bold}}[beta]{{/bold}}{{/yellow}} HubSpot projects local development"
849
+ learnMoreLink: "Learn more about the projects local dev server"
854
850
  running: "Running {{ projectName}} locally on {{ accountIdentifier }}, waiting for project file changes ..."
855
851
  quitHelper: "Press {{#bold}}'q'{{/bold}} to stop the local dev server"
856
852
  viewInHubSpotLink: "View in HubSpot"
@@ -863,20 +859,25 @@ en:
863
859
  manualUpload: "{{#bold}}Status:{{/bold}} {{#green}}Manually uploading pending changes{{/green}}"
864
860
  upload:
865
861
  noUploadsAllowed: "The change to {{ filePath }} requires an upload, but the CLI cannot upload to a project that is using a github integration."
866
- manualUploadSkipped: "Manual upload skipped. Some changes may not be visible."
867
- manualUploadRequired: "Project files changed, manual upload and deploy is needed ..."
862
+ manualUploadSkipped: "Manually upload and deploy project: {{#green}}(n){{/green}}"
863
+ manualUploadConfirmed: "Manually upload and deploy project: {{#green}}(Y){{/green}}"
864
+ manualUploadRequired: "Project file changes require a manual upload and deploy ..."
868
865
  manualUploadExplanation1: "{{#yellow}}> Dev server is running on a {{#bold}}non-sandbox account{{/bold}}.{{/yellow}}"
869
866
  manualUploadExplanation2: "{{#yellow}}> Uploading changes may overwrite production data.{{/yellow}}"
870
867
  manualUploadPrompt: "? Manually upload and deploy project? {{#green}}Y/n{{/green}}"
871
- uploadingChange: "[INFO] Uploading {{ filePath }}"
868
+ uploadingAddChange: "[INFO] Uploading {{ filePath }}"
869
+ uploadedAddChange: "[INFO] Uploaded {{ filePath }}"
870
+ uploadingRemoveChange: "[INFO] Removing {{ filePath }}"
871
+ uploadedRemoveChange: "[INFO] Removed {{ filePath }}"
872
872
  uploadingChanges: "{{#bold}}Building and deploying recent changes on {{ accountIdentifier }}{{/bold}}"
873
+ uploadedChangesSucceeded: "{{#bold}}Built and deployed recent changes on {{ accountIdentifier }}{{/bold}}"
874
+ uploadedChangesFailed: "{{#bold}}Failed to build and deploy recent changes on {{ accountIdentifier }}{{/bold}}"
873
875
  projects:
874
876
  uploadProjectFiles:
875
877
  add: "Uploading {{#bold}}{{ projectName }}{{/bold}} project files to {{ accountIdentifier }}"
876
878
  fail: "Failed to upload {{#bold}}{{ projectName }}{{/bold}} project files to {{ accountIdentifier }}"
877
879
  succeed: "Uploaded {{#bold}}{{ projectName }}{{/bold}} project files to {{ accountIdentifier }}"
878
880
  buildCreated: "Project \"{{ projectName }}\" uploaded and build #{{ buildId }} created"
879
- projectLockedError: "\nYour project may be locked by an active `{{#yellow}}hs project watch{{/yellow}}`. Try stopping the `{{#yellow}}watch{{/yellow}}` process before uploading.\nIf that doesn't work, you may need to start and stop a new `{{#yellow}}watch{{/yellow}}` process to unlock the project."
880
881
  handleProjectUpload:
881
882
  emptySource: "Source directory \"{{ srcDir }}\" is empty. Add files to your project and rerun `{{#yellow}}hs project upload{{/yellow}}` to upload them to HubSpot."
882
883
  compressed: "Project files compressed: {{ byteCount }} bytes"
@@ -958,12 +959,10 @@ en:
958
959
  describe: "Use environment variable config"
959
960
  prompts:
960
961
  projectDevTargetAccountPrompt:
961
- createNewSandboxOption: "<< Create a new development sandbox account >>"
962
- chooseNonSandboxOption: "<< Choose a non-sandbox account >>"
963
- chooseNonSandboxAccount: "[--account] Choose a non-sandbox account to test with:"
964
- chooseSandboxAccount: "[--account] Choose a sandbox account to test with:"
965
- defaultAccountNotProd: "Option unavailable because your default account must be a production account."
966
- sandboxLimit: "Option unavailable because you’ve reached the limit of {{ limit }} development sandboxes."
962
+ createNewSandboxOption: "<Test on a new dev sandbox>"
963
+ chooseDefaultAccountOption: "<{{#bold}}!{{/bold}} Test on this production account {{#bold}}!{{/bold}}>"
964
+ promptMessage: "[--account] Choose a sandbox under {{ accountIdentifier }} to test with:"
965
+ sandboxLimit: "You’ve reached the limit of {{ limit }} development sandboxes"
967
966
  projectLogsPrompt:
968
967
  projectName:
969
968
  message: "[--project] Enter the project name:"
@@ -1,7 +1,12 @@
1
1
  const express = require('express');
2
2
  const bodyParser = require('body-parser');
3
3
  const cors = require('cors');
4
+ const { i18n } = require('./lang');
4
5
  const { getProjectDetailUrl } = require('./projects');
6
+ const { EXIT_CODES } = require('./enums/exitCodes');
7
+ const { logger } = require('@hubspot/cli-lib/logger');
8
+
9
+ const i18nKey = 'cli.lib.DevServerManager';
5
10
 
6
11
  const DEFAULT_PORT = 8080;
7
12
 
@@ -41,6 +46,10 @@ class DevServerManager {
41
46
  app.get('/hs/project', (req, res) => {
42
47
  res.redirect(getProjectDetailUrl(projectConfig.name, accountId));
43
48
  });
49
+ app.get('/hs/learnMore', (req, res) => {
50
+ //TODO link to docs
51
+ res.redirect(getProjectDetailUrl(projectConfig.name, accountId));
52
+ });
44
53
 
45
54
  // Initialize component servers
46
55
  await this.iterateDevServers(async (serverInterface, serverKey) => {
@@ -51,9 +60,19 @@ class DevServerManager {
51
60
  });
52
61
 
53
62
  // Start server
54
- this.server = app.listen(port || DEFAULT_PORT);
63
+ this.server = app.listen(port || DEFAULT_PORT).on('error', err => {
64
+ if (err.code === 'EADDRINUSE') {
65
+ logger.error(
66
+ i18n(`${i18nKey}.portConflict`, { port: port || DEFAULT_PORT })
67
+ );
68
+ logger.log();
69
+ process.exit(EXIT_CODES.ERROR);
70
+ }
71
+ });
55
72
 
56
- return `http://localhost:${this.server.address().port}`;
73
+ return this.server.address()
74
+ ? `http://localhost:${this.server.address().port}`
75
+ : null;
57
76
  }
58
77
 
59
78
  async notify() {
@@ -29,7 +29,7 @@ const { uiAccountDescription, uiLink } = require('./ui');
29
29
 
30
30
  const i18nKey = 'cli.lib.LocalDevManager';
31
31
 
32
- const BUILD_DEBOUNCE_TIME = 2000;
32
+ const BUILD_DEBOUNCE_TIME = 3500;
33
33
 
34
34
  const WATCH_EVENTS = {
35
35
  add: 'add',
@@ -146,6 +146,26 @@ class LocalDevManager {
146
146
 
147
147
  logConsoleHeader() {
148
148
  this.spinnies.removeAll();
149
+ this.spinnies.add('betaMessage', {
150
+ text: i18n(`${i18nKey}.header.betaMessage`),
151
+ category: 'header',
152
+ status: 'non-spinnable',
153
+ });
154
+
155
+ // this.spinnies.add('learnMoreLink', {
156
+ // text: uiLink(
157
+ // i18n(`${i18nKey}.header.learnMoreLink`),
158
+ // this.generateLocalURL(`/hs/learnMore`),
159
+ // { inSpinnies: true }
160
+ // ),
161
+ // category: 'header',
162
+ // status: 'non-spinnable',
163
+ // });
164
+ this.spinnies.add('spacer-1', {
165
+ text: ' ',
166
+ status: 'non-spinnable',
167
+ category: 'header',
168
+ });
149
169
  this.spinnies.add('devModeRunning', {
150
170
  text: i18n(`${i18nKey}.header.running`, {
151
171
  accountIdentifier: uiAccountDescription(this.targetAccountId),
@@ -164,15 +184,13 @@ class LocalDevManager {
164
184
  text: uiLink(
165
185
  i18n(`${i18nKey}.header.viewInHubSpotLink`),
166
186
  this.generateLocalURL(`/hs/project`),
167
- {
168
- inSpinnies: true,
169
- }
187
+ { inSpinnies: true }
170
188
  ),
171
189
  status: 'non-spinnable',
172
190
  indent: 1,
173
191
  category: 'header',
174
192
  });
175
- this.spinnies.add('spacer-1', {
193
+ this.spinnies.add('spacer-2', {
176
194
  text: ' ',
177
195
  status: 'non-spinnable',
178
196
  category: 'header',
@@ -199,24 +217,29 @@ class LocalDevManager {
199
217
  handleKeypress(async key => {
200
218
  if ((key.ctrl && key.name === 'c') || key.name === 'q') {
201
219
  this.stop();
202
- } else if (key.name === 'y') {
203
- if (
204
- this.uploadPermission === UPLOAD_PERMISSIONS.manual &&
205
- this.hasAnyUnsupportedStandbyChanges()
206
- ) {
207
- this.clearConsoleContent();
220
+ } else if (
221
+ (key.name === 'y' || key.name === 'n') &&
222
+ this.uploadPermission === UPLOAD_PERMISSIONS.manual &&
223
+ this.hasAnyUnsupportedStandbyChanges()
224
+ ) {
225
+ this.spinnies.remove('manualUploadRequired');
226
+ this.spinnies.remove('manualUploadExplanation1');
227
+ this.spinnies.remove('manualUploadExplanation2');
228
+ this.spinnies.remove('manualUploadPrompt');
229
+
230
+ if (key.name === 'y') {
231
+ this.spinnies.add(null, {
232
+ text: i18n(`${i18nKey}.upload.manualUploadConfirmed`),
233
+ status: 'succeed',
234
+ succeedColor: 'white',
235
+ noIndent: true,
236
+ });
208
237
  this.updateDevModeStatus('manualUpload');
209
238
  await this.createNewStagingBuild();
210
239
  await this.flushStandbyChanges();
211
240
  await this.queueBuild();
212
- }
213
- } else if (key.name === 'n') {
214
- if (
215
- this.uploadPermission === UPLOAD_PERMISSIONS.manual &&
216
- this.hasAnyUnsupportedStandbyChanges()
217
- ) {
218
- this.clearConsoleContent();
219
- this.spinnies.add('manualUploadSkipped', {
241
+ } else if (key.name === 'n') {
242
+ this.spinnies.add(null, {
220
243
  text: i18n(`${i18nKey}.upload.manualUploadSkipped`),
221
244
  status: 'fail',
222
245
  failColor: 'white',
@@ -240,13 +263,6 @@ class LocalDevManager {
240
263
  }
241
264
 
242
265
  async pauseUploadQueue() {
243
- this.spinnies.add('uploading', {
244
- text: i18n(`${i18nKey}.upload.uploadingChanges`, {
245
- accountIdentifier: uiAccountDescription(this.targetAccountId),
246
- }),
247
- noIndent: true,
248
- });
249
-
250
266
  this.uploadQueue.pause();
251
267
  await this.uploadQueue.onIdle();
252
268
  }
@@ -314,13 +330,7 @@ class LocalDevManager {
314
330
  }
315
331
 
316
332
  if (this.uploadQueue.isPaused) {
317
- if (
318
- !this.standbyChanges.find(
319
- changeInfo => changeInfo.filePath === filePath
320
- )
321
- ) {
322
- this.addChangeToStandbyQueue({ ...changeInfo, supported: false });
323
- }
333
+ this.addChangeToStandbyQueue({ ...changeInfo, supported: false });
324
334
  } else {
325
335
  await this.flushStandbyChanges();
326
336
 
@@ -337,7 +347,6 @@ class LocalDevManager {
337
347
  handlePreventedUpload(changeInfo) {
338
348
  const { remotePath } = changeInfo;
339
349
 
340
- this.clearConsoleContent();
341
350
  if (this.uploadPermission === UPLOAD_PERMISSIONS.never) {
342
351
  this.updateDevModeStatus('noUploadsAllowed');
343
352
 
@@ -391,35 +400,63 @@ class LocalDevManager {
391
400
  logger.debug(`File ignored: ${filePath}`);
392
401
  return;
393
402
  }
394
- this.standbyChanges.push(changeInfo);
403
+
404
+ const existingIndex = this.standbyChanges.findIndex(
405
+ standyChangeInfo => standyChangeInfo.filePath === filePath
406
+ );
407
+
408
+ if (existingIndex > -1) {
409
+ // Make sure the most recent event to this file is the one that gets acted on
410
+ this.standbyChanges[existingIndex].event = event;
411
+ } else {
412
+ this.standbyChanges.push(changeInfo);
413
+ }
395
414
  }
396
415
 
397
416
  async sendChanges(changeInfo) {
398
417
  const { event, filePath, remotePath } = changeInfo;
399
418
 
400
- this.spinnies.add(filePath, {
401
- text: i18n(`${i18nKey}.upload.uploadingChange`, {
402
- filePath: remotePath,
403
- }),
404
- status: 'non-spinnable',
405
- });
406
419
  try {
407
420
  if (event === WATCH_EVENTS.add || event === WATCH_EVENTS.change) {
421
+ const spinniesKey = this.spinnies.add(null, {
422
+ text: i18n(`${i18nKey}.upload.uploadingAddChange`, {
423
+ filePath: remotePath,
424
+ }),
425
+ status: 'non-spinnable',
426
+ });
408
427
  await uploadFileToBuild(
409
428
  this.targetAccountId,
410
429
  this.projectConfig.name,
411
430
  filePath,
412
431
  remotePath
413
432
  );
433
+ this.spinnies.update(spinniesKey, {
434
+ text: i18n(`${i18nKey}.upload.uploadedAddChange`, {
435
+ filePath: remotePath,
436
+ }),
437
+ status: 'non-spinnable',
438
+ });
414
439
  } else if (
415
440
  event === WATCH_EVENTS.unlink ||
416
441
  event === WATCH_EVENTS.unlinkDir
417
442
  ) {
443
+ const spinniesKey = this.spinnies.add(null, {
444
+ text: i18n(`${i18nKey}.upload.uploadingRemoveChange`, {
445
+ filePath: remotePath,
446
+ }),
447
+ status: 'non-spinnable',
448
+ });
418
449
  await deleteFileFromBuild(
419
450
  this.targetAccountId,
420
451
  this.projectConfig.name,
421
452
  remotePath
422
453
  );
454
+ this.spinnies.update(spinniesKey, {
455
+ text: i18n(`${i18nKey}.upload.uploadedRemoveChange`, {
456
+ filePath: remotePath,
457
+ }),
458
+ status: 'non-spinnable',
459
+ });
423
460
  }
424
461
  } catch (err) {
425
462
  logger.debug(err);
@@ -442,6 +479,13 @@ class LocalDevManager {
442
479
  }
443
480
 
444
481
  async queueBuild() {
482
+ const spinniesKey = this.spinnies.add(null, {
483
+ text: i18n(`${i18nKey}.upload.uploadingChanges`, {
484
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
485
+ }),
486
+ noIndent: true,
487
+ });
488
+
445
489
  await this.pauseUploadQueue();
446
490
 
447
491
  try {
@@ -467,7 +511,7 @@ class LocalDevManager {
467
511
  return;
468
512
  }
469
513
 
470
- await pollProjectBuildAndDeploy(
514
+ const result = await pollProjectBuildAndDeploy(
471
515
  this.targetAccountId,
472
516
  this.projectConfig,
473
517
  null,
@@ -475,12 +519,31 @@ class LocalDevManager {
475
519
  true
476
520
  );
477
521
 
522
+ if (result && result.succeeded) {
523
+ this.spinnies.succeed(spinniesKey, {
524
+ text: i18n(`${i18nKey}.upload.uploadedChangesSucceeded`, {
525
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
526
+ }),
527
+ succeedColor: 'white',
528
+ noIndent: true,
529
+ });
530
+ } else {
531
+ this.spinnies.fail(spinniesKey, {
532
+ text: i18n(`${i18nKey}.upload.uploadedChangesFailed`, {
533
+ accountIdentifier: uiAccountDescription(this.targetAccountId),
534
+ }),
535
+ failColor: 'white',
536
+ noIndent: true,
537
+ });
538
+ }
539
+
540
+ this.spinnies.removeAll({ targetCategory: 'projectPollStatus' });
541
+
478
542
  if (this.uploadPermission === UPLOAD_PERMISSIONS.always) {
479
543
  await this.createNewStagingBuild();
480
544
  }
481
545
 
482
546
  this.uploadQueue.start();
483
- this.clearConsoleContent();
484
547
 
485
548
  if (this.hasAnyUnsupportedStandbyChanges()) {
486
549
  this.flushStandbyChanges();
@@ -51,18 +51,23 @@ class SpinniesManager {
51
51
  const { category, isParent, noIndent, ...rest } = options;
52
52
  const originalIndent = rest.indent || 0;
53
53
 
54
+ // Support adding generic spinnies lines without specifying a key
55
+ const uniqueKey = key || `${Date.now()}`;
56
+
54
57
  if (category) {
55
- this.addKeyToCategory(key, category);
58
+ this.addKeyToCategory(uniqueKey, category);
56
59
  }
57
60
 
58
- this.spinnies.add(key, {
61
+ this.spinnies.add(uniqueKey, {
59
62
  ...rest,
60
63
  indent: this.parentKey && !noIndent ? originalIndent + 1 : originalIndent,
61
64
  });
62
65
 
63
66
  if (isParent) {
64
- this.parentKey = key;
67
+ this.parentKey = uniqueKey;
65
68
  }
69
+
70
+ return uniqueKey;
66
71
  }
67
72
 
68
73
  remove(key) {
@@ -79,10 +84,14 @@ class SpinniesManager {
79
84
  * Removes all spinnies instances
80
85
  * @param {string} preserveCategory - do not remove spinnies with a matching category
81
86
  */
82
- removeAll({ preserveCategory = null } = {}) {
87
+ removeAll({ preserveCategory = null, targetCategory = null } = {}) {
83
88
  if (this.spinnies) {
84
89
  Object.keys(this.spinnies.spinners).forEach(key => {
85
- if (
90
+ if (targetCategory) {
91
+ if (this.getCategoryForKey(key) === targetCategory) {
92
+ this.remove(key);
93
+ }
94
+ } else if (
86
95
  !preserveCategory ||
87
96
  this.getCategoryForKey(key) !== preserveCategory
88
97
  ) {
package/lib/projects.js CHANGED
@@ -10,7 +10,6 @@ const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
10
10
  const {
11
11
  ENVIRONMENTS,
12
12
  FEEDBACK_INTERVAL,
13
- ERROR_TYPES,
14
13
  POLLING_DELAY,
15
14
  PROJECT_BUILD_TEXT,
16
15
  PROJECT_DEPLOY_TEXT,
@@ -320,6 +319,7 @@ const uploadProjectFiles = async (
320
319
  });
321
320
 
322
321
  let buildId;
322
+ let error;
323
323
 
324
324
  try {
325
325
  const upload = await uploadProject(
@@ -352,20 +352,10 @@ const uploadProjectFiles = async (
352
352
  }),
353
353
  });
354
354
 
355
- logApiErrorInstance(
356
- err,
357
- new ApiErrorContext({
358
- accountId,
359
- projectName,
360
- })
361
- );
362
- if (err.error.subCategory === ERROR_TYPES.PROJECT_LOCKED) {
363
- logger.log(i18n(`${i18nKey}.uploadProjectFiles.projectLockedError`));
364
- }
365
- process.exit(EXIT_CODES.ERROR);
355
+ error = err;
366
356
  }
367
357
 
368
- return { buildId };
358
+ return { buildId, error };
369
359
  };
370
360
 
371
361
  const pollProjectBuildAndDeploy = async (
@@ -478,7 +468,7 @@ const handleProjectUpload = async (
478
468
 
479
469
  const result = new Promise(resolve =>
480
470
  output.on('close', async function() {
481
- let result = {};
471
+ let uploadResult = {};
482
472
 
483
473
  logger.debug(
484
474
  i18n(`${i18nKey}.handleProjectUpload.compressed`, {
@@ -486,22 +476,24 @@ const handleProjectUpload = async (
486
476
  })
487
477
  );
488
478
 
489
- const { buildId } = await uploadProjectFiles(
479
+ const { buildId, error } = await uploadProjectFiles(
490
480
  accountId,
491
481
  projectConfig.name,
492
482
  tempFile.name,
493
483
  uploadMessage
494
484
  );
495
485
 
496
- if (callbackFunc) {
497
- result = await callbackFunc(
486
+ if (error) {
487
+ uploadResult.error = error;
488
+ } else if (callbackFunc) {
489
+ uploadResult = await callbackFunc(
498
490
  accountId,
499
491
  projectConfig,
500
492
  tempFile,
501
493
  buildId
502
494
  );
503
495
  }
504
- resolve(result);
496
+ resolve(uploadResult);
505
497
  })
506
498
  );
507
499
 
@@ -558,6 +550,7 @@ const makePollTaskStatusFunc = ({
558
550
  succeedColor: 'white',
559
551
  failColor: 'white',
560
552
  failPrefix: chalk.bold('!'),
553
+ category: 'projectPollStatus',
561
554
  });
562
555
 
563
556
  const [
@@ -618,6 +611,7 @@ const makePollTaskStatusFunc = ({
618
611
  indent,
619
612
  succeedColor: 'white',
620
613
  failColor: 'white',
614
+ category: 'projectPollStatus',
621
615
  });
622
616
  };
623
617
 
@@ -9,90 +9,64 @@ const { logger } = require('@hubspot/cli-lib/logger');
9
9
  const i18nKey = 'cli.lib.prompts.projectDevTargetAccountPrompt';
10
10
 
11
11
  const mapSandboxAccount = accountConfig => ({
12
- name: getAccountName(accountConfig),
12
+ name: getAccountName(accountConfig, false),
13
13
  value: {
14
14
  targetAccountId: getAccountId(accountConfig.name),
15
- chooseNonSandbox: false,
16
15
  createNewSandbox: false,
17
16
  },
18
17
  });
19
18
 
20
- const selectTargetAccountPrompt = async (
21
- accounts,
22
- defaultAccountConfig,
23
- nonSandbox = false
24
- ) => {
25
- let choices;
19
+ const selectTargetAccountPrompt = async (accounts, defaultAccountConfig) => {
20
+ let sandboxUsage = {};
21
+ const defaultAccountId = getAccountId(defaultAccountConfig.name);
26
22
 
27
- if (nonSandbox) {
28
- choices = accounts
29
- .filter(accountConfig => !isSandbox(accountConfig))
30
- .map(accountConfig => {
31
- const accountId = getAccountId(accountConfig.name);
32
- return {
33
- name: uiAccountDescription(accountId),
34
- value: {
35
- targetAccountId: accountId,
36
- chooseNonSandbox: false,
37
- createNewSandbox: false,
38
- },
39
- };
40
- });
41
- } else {
42
- let sandboxUsage = {};
43
- try {
44
- const accountId = getAccountId(defaultAccountConfig.portalId);
45
- sandboxUsage = await getSandboxUsageLimits(accountId);
46
- } catch (err) {
47
- logger.debug('Unable to fetch sandbox usage limits: ', err);
48
- }
49
- const sandboxAccounts = accounts.reverse().filter(isSandbox);
50
- let disabledMessage = false;
51
- if (isSandbox(defaultAccountConfig)) {
52
- disabledMessage = i18n(`${i18nKey}.defaultAccountNotProd`);
53
- }
54
- if (
55
- sandboxUsage['DEVELOPER'] &&
56
- sandboxUsage['DEVELOPER'].available === 0
57
- ) {
58
- disabledMessage = i18n(`${i18nKey}.sandboxLimit`, {
59
- limit: sandboxUsage['DEVELOPER'].limit,
60
- });
61
- }
62
- // Order choices by Create new -> Developer Sandbox -> Standard Sandbox -> Non sandbox
63
- choices = [
64
- {
65
- name: i18n(`${i18nKey}.createNewSandboxOption`),
66
- value: {
67
- targetAccountId: null,
68
- chooseNonSandbox: false,
69
- createNewSandbox: true,
70
- },
71
- disabled: disabledMessage,
23
+ try {
24
+ sandboxUsage = await getSandboxUsageLimits(defaultAccountId);
25
+ } catch (err) {
26
+ logger.debug('Unable to fetch sandbox usage limits: ', err);
27
+ }
28
+
29
+ const sandboxAccounts = accounts.reverse().filter(isSandbox);
30
+ let disabledMessage = false;
31
+
32
+ if (sandboxUsage['DEVELOPER'] && sandboxUsage['DEVELOPER'].available === 0) {
33
+ disabledMessage = i18n(`${i18nKey}.sandboxLimit`, {
34
+ limit: sandboxUsage['DEVELOPER'].limit,
35
+ });
36
+ }
37
+
38
+ // Order choices by Developer Sandbox -> Standard Sandbox
39
+ const choices = [
40
+ ...sandboxAccounts
41
+ .filter(a => a.sandboxAccountType === 'DEVELOPER')
42
+ .map(mapSandboxAccount),
43
+ ...sandboxAccounts
44
+ .filter(a => a.sandboxAccountType === 'STANDARD')
45
+ .map(mapSandboxAccount),
46
+ {
47
+ name: i18n(`${i18nKey}.createNewSandboxOption`),
48
+ value: {
49
+ targetAccountId: null,
50
+ createNewSandbox: true,
72
51
  },
73
- ...sandboxAccounts
74
- .filter(a => a.sandboxAccountType === 'DEVELOPER')
75
- .map(mapSandboxAccount),
76
- ...sandboxAccounts
77
- .filter(a => a.sandboxAccountType === 'STANDARD')
78
- .map(mapSandboxAccount),
79
- {
80
- name: i18n(`${i18nKey}.chooseNonSandboxOption`),
81
- value: {
82
- targetAccountId: null,
83
- chooseNonSandbox: true,
84
- createNewSandbox: false,
85
- },
52
+ disabled: disabledMessage,
53
+ },
54
+ {
55
+ name: i18n(`${i18nKey}.chooseDefaultAccountOption`),
56
+ value: {
57
+ targetAccountId: defaultAccountId,
58
+ createNewSandbox: false,
86
59
  },
87
- ];
88
- }
60
+ },
61
+ ];
62
+
89
63
  const { targetAccountInfo } = await promptUser([
90
64
  {
91
65
  name: 'targetAccountInfo',
92
66
  type: 'list',
93
- message: nonSandbox
94
- ? i18n(`${i18nKey}.chooseNonSandboxAccount`)
95
- : i18n(`${i18nKey}.chooseSandboxAccount`),
67
+ message: i18n(`${i18nKey}.promptMessage`, {
68
+ accountIdentifier: uiAccountDescription(defaultAccountId),
69
+ }),
96
70
  choices,
97
71
  },
98
72
  ]);
package/lib/sandboxes.js CHANGED
@@ -55,13 +55,16 @@ const getSandboxTypeAsString = type =>
55
55
  const isSandbox = config =>
56
56
  config.sandboxAccountType && config.sandboxAccountType !== null;
57
57
 
58
- function getAccountName(config) {
58
+ function getAccountName(config, bold = true) {
59
59
  const sandboxName = `[${getSandboxTypeAsString(
60
60
  config.sandboxAccountType
61
61
  )} sandbox] `;
62
- return chalk.bold(
63
- `${config.name} ${isSandbox(config) ? sandboxName : ''}(${config.portalId})`
64
- );
62
+
63
+ const message = `${config.name} ${isSandbox(config) ? sandboxName : ''}(${
64
+ config.portalId
65
+ })`;
66
+
67
+ return bold ? chalk.bold(message) : message;
65
68
  }
66
69
 
67
70
  function getHasSandboxesByType(parentAccountConfig, type) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "4.1.8-beta.4",
3
+ "version": "4.1.8-beta.5",
4
4
  "description": "CLI for working with HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,5 +43,5 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
- "gitHead": "650d373d034efb5736b357200fc496d5da605e37"
46
+ "gitHead": "5b5d25bf5e3a4d29b516dbc31195f4cd7051b898"
47
47
  }