@hubspot/cli 5.2.0 → 5.2.1-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.
@@ -0,0 +1,401 @@
1
+ const { logger } = require('@hubspot/local-dev-lib/logger');
2
+ const {
3
+ HUBSPOT_ACCOUNT_TYPES,
4
+ HUBSPOT_ACCOUNT_TYPE_STRINGS,
5
+ } = require('@hubspot/local-dev-lib/constants/config');
6
+ const {
7
+ isMissingScopeError,
8
+ isSpecifiedError,
9
+ } = require('@hubspot/local-dev-lib/errors/apiErrors');
10
+ const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls');
11
+ const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
12
+ const { createProject } = require('@hubspot/local-dev-lib/api/projects');
13
+
14
+ const {
15
+ confirmDefaultAccountPrompt,
16
+ selectSandboxTargetAccountPrompt,
17
+ selectDeveloperTestTargetAccountPrompt,
18
+ } = require('./prompts/projectDevTargetAccountPrompt');
19
+ const { sandboxNamePrompt } = require('./prompts/sandboxesPrompt');
20
+ const {
21
+ developerTestAccountNamePrompt,
22
+ } = require('./prompts/developerTestAccountNamePrompt');
23
+ const { confirmPrompt } = require('./prompts/promptUtils');
24
+ const {
25
+ validateSandboxUsageLimits,
26
+ getSandboxTypeAsString,
27
+ getAvailableSyncTypes,
28
+ } = require('./sandboxes');
29
+ const { buildSandbox } = require('./sandboxCreate');
30
+ const { syncSandbox } = require('./sandboxSync');
31
+ const {
32
+ validateDevTestAccountUsageLimits,
33
+ } = require('./developerTestAccounts');
34
+ const { buildDeveloperTestAccount } = require('./developerTestAccountCreate');
35
+ const { logErrorInstance } = require('./errorHandlers/standardErrors');
36
+ const { uiCommandReference, uiLine, uiAccountDescription } = require('./ui');
37
+ const SpinniesManager = require('./ui/SpinniesManager');
38
+ const { i18n } = require('./lang');
39
+ const { EXIT_CODES } = require('./enums/exitCodes');
40
+ const { trackCommandMetadataUsage } = require('./usageTracking');
41
+ const {
42
+ isAppDeveloperAccount,
43
+ isDeveloperTestAccount,
44
+ } = require('./accountTypes');
45
+ const {
46
+ handleProjectUpload,
47
+ pollProjectBuildAndDeploy,
48
+ } = require('./projects');
49
+ const {
50
+ PROJECT_ERROR_TYPES,
51
+ PROJECT_BUILD_TEXT,
52
+ PROJECT_DEPLOY_TEXT,
53
+ } = require('./constants');
54
+ const {
55
+ logApiErrorInstance,
56
+ ApiErrorContext,
57
+ } = require('./errorHandlers/apiErrors');
58
+
59
+ const i18nKey = 'cli.lib.localDev';
60
+
61
+ // If the user passed in the --account flag, confirm they want to use that account as
62
+ // their target account, otherwise exit
63
+ const confirmDefaultAccountIsTarget = async accountConfig => {
64
+ logger.log();
65
+ const useDefaultAccount = await confirmDefaultAccountPrompt(
66
+ accountConfig.name,
67
+ isDeveloperTestAccount(accountConfig)
68
+ ? HUBSPOT_ACCOUNT_TYPE_STRINGS[HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST]
69
+ : `${getSandboxTypeAsString(accountConfig.accountType)} sandbox`
70
+ );
71
+
72
+ if (!useDefaultAccount) {
73
+ logger.log(
74
+ i18n(
75
+ `${i18nKey}.confirmDefaultAccountIsTarget.declineDefaultAccountExplanation`,
76
+ {
77
+ useCommand: uiCommandReference('hs accounts use'),
78
+ devCommand: uiCommandReference('hs project dev'),
79
+ }
80
+ )
81
+ );
82
+ process.exit(EXIT_CODES.SUCCESS);
83
+ }
84
+ };
85
+
86
+ // Confirm the default account is a developer account if developing public apps
87
+ const checkIfAppDeveloperAccount = accountConfig => {
88
+ if (!isAppDeveloperAccount(accountConfig)) {
89
+ logger.error(
90
+ i18n(
91
+ `${i18nKey}.checkCorrectParentAccountType.standardAccountNotSupported`
92
+ )
93
+ );
94
+ process.exit(EXIT_CODES.SUCCESS);
95
+ }
96
+ };
97
+
98
+ // If the user isn't using the recommended account type, prompt them to use or create one
99
+ const suggestRecommendedNestedAccount = async (
100
+ accounts,
101
+ accountConfig,
102
+ hasPublicApps
103
+ ) => {
104
+ logger.log();
105
+ uiLine();
106
+ if (hasPublicApps) {
107
+ logger.warn(
108
+ i18n(
109
+ `${i18nKey}.suggestRecommendedNestedAccount.publicAppNonDeveloperTestAccountWarning`
110
+ )
111
+ );
112
+ } else if (isAppDeveloperAccount(accountConfig)) {
113
+ logger.warn(
114
+ i18n(
115
+ `${i18nKey}.suggestRecommendedNestedAccount.publicAppNonDeveloperTestAccountWarning`
116
+ )
117
+ );
118
+ } else {
119
+ logger.warn(
120
+ i18n(`${i18nKey}.suggestRecommendedNestedAccount.nonSandboxWarning`)
121
+ );
122
+ }
123
+ uiLine();
124
+ logger.log();
125
+
126
+ const targetAccountPrompt = isAppDeveloperAccount(accountConfig)
127
+ ? selectDeveloperTestTargetAccountPrompt
128
+ : selectSandboxTargetAccountPrompt;
129
+
130
+ return targetAccountPrompt(accounts, accountConfig, hasPublicApps);
131
+ };
132
+
133
+ // Create a new sandbox and return its accountId
134
+ const createSandboxForLocalDev = async (accountId, accountConfig, env) => {
135
+ try {
136
+ await validateSandboxUsageLimits(
137
+ accountConfig,
138
+ HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
139
+ env
140
+ );
141
+ } catch (err) {
142
+ if (isMissingScopeError(err)) {
143
+ logger.error(
144
+ i18n('cli.lib.sandbox.create.failure.scopes.message', {
145
+ accountName: accountConfig.name || accountId,
146
+ })
147
+ );
148
+ const websiteOrigin = getHubSpotWebsiteOrigin(env);
149
+ const url = `${websiteOrigin}/personal-access-key/${accountId}`;
150
+ logger.info(
151
+ i18n('cli.lib.sandbox.create.failure.scopes.instructions', {
152
+ accountName: accountConfig.name || accountId,
153
+ url,
154
+ })
155
+ );
156
+ } else {
157
+ logErrorInstance(err);
158
+ }
159
+ process.exit(EXIT_CODES.ERROR);
160
+ }
161
+ try {
162
+ const { name } = await sandboxNamePrompt(
163
+ HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX
164
+ );
165
+
166
+ trackCommandMetadataUsage(
167
+ 'sandbox-create',
168
+ { step: 'project-dev' },
169
+ accountId
170
+ );
171
+
172
+ const { result } = await buildSandbox({
173
+ name,
174
+ type: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
175
+ accountConfig,
176
+ env,
177
+ });
178
+
179
+ const targetAccountId = result.sandbox.sandboxHubId;
180
+
181
+ const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId);
182
+ const syncTasks = await getAvailableSyncTypes(
183
+ accountConfig,
184
+ sandboxAccountConfig
185
+ );
186
+ await syncSandbox({
187
+ accountConfig: sandboxAccountConfig,
188
+ parentAccountConfig: accountConfig,
189
+ env,
190
+ syncTasks,
191
+ allowEarlyTermination: false, // Don't let user terminate early in this flow
192
+ skipPolling: true, // Skip polling, sync will run and complete in the background
193
+ });
194
+ return targetAccountId;
195
+ } catch (err) {
196
+ logErrorInstance(err);
197
+ process.exit(EXIT_CODES.ERROR);
198
+ }
199
+ };
200
+
201
+ // Create a developer test account and return its accountId
202
+ const createDeveloperTestAccountForLocalDev = async (
203
+ accountId,
204
+ accountConfig,
205
+ env
206
+ ) => {
207
+ let currentPortalCount = 0;
208
+ let maxTestPortals = 10;
209
+ try {
210
+ const validateResult = await validateDevTestAccountUsageLimits(
211
+ accountConfig
212
+ );
213
+ if (validateResult) {
214
+ currentPortalCount = validateResult.results
215
+ ? validateResult.results.length
216
+ : 0;
217
+ maxTestPortals = validateResult.maxTestPortals;
218
+ }
219
+ } catch (err) {
220
+ if (isMissingScopeError(err)) {
221
+ logger.error(
222
+ i18n('cli.lib.developerTestAccount.create.failure.scopes.message', {
223
+ accountName: accountConfig.name || accountId,
224
+ })
225
+ );
226
+ const websiteOrigin = getHubSpotWebsiteOrigin(env);
227
+ const url = `${websiteOrigin}/personal-access-key/${accountId}`;
228
+ logger.info(
229
+ i18n(
230
+ 'cli.lib.developerTestAccount.create.failure.scopes.instructions',
231
+ {
232
+ accountName: accountConfig.name || accountId,
233
+ url,
234
+ }
235
+ )
236
+ );
237
+ } else {
238
+ logErrorInstance(err);
239
+ }
240
+ process.exit(EXIT_CODES.ERROR);
241
+ }
242
+
243
+ try {
244
+ const { name } = await developerTestAccountNamePrompt(currentPortalCount);
245
+ trackCommandMetadataUsage(
246
+ 'developer-test-account-create',
247
+ { step: 'project-dev' },
248
+ accountId
249
+ );
250
+
251
+ const { result } = await buildDeveloperTestAccount({
252
+ name,
253
+ accountConfig,
254
+ env,
255
+ maxTestPortals,
256
+ });
257
+
258
+ return result.id;
259
+ } catch (err) {
260
+ logErrorInstance(err);
261
+ process.exit(EXIT_CODES.ERROR);
262
+ }
263
+ };
264
+
265
+ // Prompt the user to create a new project if one doesn't exist on their target account
266
+ const createNewProjectForLocalDev = async (
267
+ projectConfig,
268
+ targetAccountId,
269
+ shouldCreateWithoutConfirmation
270
+ ) => {
271
+ // Create the project without prompting if this is a newly created sandbox
272
+ let shouldCreateProject = shouldCreateWithoutConfirmation;
273
+
274
+ if (!shouldCreateProject) {
275
+ logger.log();
276
+ uiLine();
277
+ logger.warn(
278
+ i18n(
279
+ `${i18nKey}.createNewProjectForLocalDev.projectMustExistExplanation`,
280
+ {
281
+ accountIdentifier: uiAccountDescription(targetAccountId),
282
+ projectName: projectConfig.name,
283
+ }
284
+ )
285
+ );
286
+ uiLine();
287
+
288
+ shouldCreateProject = await confirmPrompt(
289
+ i18n(`${i18nKey}.createNewProjectForLocalDev.createProject`, {
290
+ accountIdentifier: uiAccountDescription(targetAccountId),
291
+ projectName: projectConfig.name,
292
+ })
293
+ );
294
+ }
295
+
296
+ if (shouldCreateProject) {
297
+ SpinniesManager.add('createProject', {
298
+ text: i18n(`${i18nKey}.createNewProjectForLocalDev.creatingProject`, {
299
+ accountIdentifier: uiAccountDescription(targetAccountId),
300
+ projectName: projectConfig.name,
301
+ }),
302
+ });
303
+
304
+ try {
305
+ await createProject(targetAccountId, projectConfig.name);
306
+ SpinniesManager.succeed('createProject', {
307
+ text: i18n(`${i18nKey}.createNewProjectForLocalDev.createdProject`, {
308
+ accountIdentifier: uiAccountDescription(targetAccountId),
309
+ projectName: projectConfig.name,
310
+ }),
311
+ succeedColor: 'white',
312
+ });
313
+ } catch (err) {
314
+ SpinniesManager.fail('createProject');
315
+ logger.log(
316
+ i18n(`${i18nKey}.createNewProjectForLocalDev.failedToCreateProject`)
317
+ );
318
+ process.exit(EXIT_CODES.ERROR);
319
+ }
320
+ } else {
321
+ // We cannot continue if the project does not exist in the target account
322
+ logger.log();
323
+ logger.log(
324
+ i18n(`${i18nKey}.createNewProjectForLocalDev.choseNotToCreateProject`)
325
+ );
326
+ process.exit(EXIT_CODES.SUCCESS);
327
+ }
328
+ };
329
+
330
+ // Create an initial build if the project was newly created in the account
331
+ // Return the newly deployed build
332
+ const createInitialBuildForNewProject = async (
333
+ projectConfig,
334
+ projectDir,
335
+ targetAccountId
336
+ ) => {
337
+ const initialUploadResult = await handleProjectUpload(
338
+ targetAccountId,
339
+ projectConfig,
340
+ projectDir,
341
+ (...args) => pollProjectBuildAndDeploy(...args, true),
342
+ i18n(`${i18nKey}.createInitialBuildForNewProject.initialUploadMessage`)
343
+ );
344
+
345
+ if (initialUploadResult.uploadError) {
346
+ if (
347
+ isSpecifiedError(initialUploadResult.uploadError, {
348
+ subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED,
349
+ })
350
+ ) {
351
+ logger.log();
352
+ logger.error(
353
+ i18n(`${i18nKey}.createInitialBuildForNewProject.projectLockedError`)
354
+ );
355
+ logger.log();
356
+ } else {
357
+ logApiErrorInstance(
358
+ initialUploadResult.uploadError,
359
+ new ApiErrorContext({
360
+ accountId: targetAccountId,
361
+ projectName: projectConfig.name,
362
+ })
363
+ );
364
+ }
365
+ process.exit(EXIT_CODES.ERROR);
366
+ }
367
+
368
+ if (!initialUploadResult.succeeded) {
369
+ let subTasks = [];
370
+
371
+ if (initialUploadResult.buildResult.status === 'FAILURE') {
372
+ subTasks =
373
+ initialUploadResult.buildResult[PROJECT_BUILD_TEXT.SUBTASK_KEY];
374
+ } else if (initialUploadResult.deployResult.status === 'FAILURE') {
375
+ subTasks =
376
+ initialUploadResult.deployResult[PROJECT_DEPLOY_TEXT.SUBTASK_KEY];
377
+ }
378
+
379
+ const failedSubTasks = subTasks.filter(task => task.status === 'FAILURE');
380
+
381
+ logger.log();
382
+ failedSubTasks.forEach(failedSubTask => {
383
+ console.error(failedSubTask.errorMessage);
384
+ });
385
+ logger.log();
386
+
387
+ process.exit(EXIT_CODES.ERROR);
388
+ }
389
+
390
+ return initialUploadResult.buildResult;
391
+ };
392
+
393
+ module.exports = {
394
+ confirmDefaultAccountIsTarget,
395
+ checkIfAppDeveloperAccount,
396
+ suggestRecommendedNestedAccount,
397
+ createSandboxForLocalDev,
398
+ createDeveloperTestAccountForLocalDev,
399
+ createNewProjectForLocalDev,
400
+ createInitialBuildForNewProject,
401
+ };
@@ -118,9 +118,16 @@ async function findProjectComponents(projectSourceDir) {
118
118
  return components;
119
119
  }
120
120
 
121
+ function getProjectComponentTypes(components) {
122
+ const projectContents = {};
123
+ components.forEach(({ type }) => (projectContents[type] = true));
124
+ return projectContents;
125
+ }
126
+
121
127
  module.exports = {
122
128
  CONFIG_FILES,
123
129
  COMPONENT_TYPES,
124
130
  findProjectComponents,
125
131
  getAppCardConfigs,
132
+ getProjectComponentTypes,
126
133
  };
package/lib/projects.js CHANGED
@@ -26,8 +26,13 @@ const {
26
26
  getDeployStructure,
27
27
  fetchProject,
28
28
  uploadProject,
29
+ fetchBuildWarnLogs,
30
+ fetchDeployWarnLogs,
29
31
  } = require('@hubspot/local-dev-lib/api/projects');
30
- const { isSpecifiedError } = require('@hubspot/local-dev-lib/errors/apiErrors');
32
+ const {
33
+ isSpecifiedError,
34
+ isSpecifiedHubSpotAuthError,
35
+ } = require('@hubspot/local-dev-lib/errors/apiErrors');
31
36
  const { shouldIgnoreFile } = require('@hubspot/local-dev-lib/ignoreRules');
32
37
  const { getCwd, getAbsoluteFilePath } = require('@hubspot/local-dev-lib/path');
33
38
  const { downloadGithubRepoContents } = require('@hubspot/local-dev-lib/github');
@@ -39,7 +44,6 @@ const SpinniesManager = require('./ui/SpinniesManager');
39
44
  const {
40
45
  logApiErrorInstance,
41
46
  ApiErrorContext,
42
- isSpecifiedHubSpotAuthError,
43
47
  } = require('./errorHandlers/apiErrors');
44
48
  const { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH } = require('./constants');
45
49
 
@@ -443,7 +447,10 @@ const pollProjectBuildAndDeploy = async (
443
447
  }
444
448
  )
445
449
  );
450
+
451
+ displayWarnLogs(accountId, projectConfig.name, buildId);
446
452
  }
453
+
447
454
  const deployStatus = await pollDeployStatus(
448
455
  accountId,
449
456
  projectConfig.name,
@@ -471,6 +478,14 @@ const pollProjectBuildAndDeploy = async (
471
478
  logger.error(e);
472
479
  }
473
480
 
481
+ if (result && result.deployResult) {
482
+ displayWarnLogs(
483
+ accountId,
484
+ projectConfig.name,
485
+ result.deployResult.deployId,
486
+ true
487
+ );
488
+ }
474
489
  return result;
475
490
  };
476
491
 
@@ -869,6 +884,28 @@ const createProjectComponent = async (
869
884
  );
870
885
  };
871
886
 
887
+ const displayWarnLogs = async (
888
+ accountId,
889
+ projectName,
890
+ taskId,
891
+ isDeploy = false
892
+ ) => {
893
+ let result;
894
+
895
+ if (isDeploy) {
896
+ result = await fetchDeployWarnLogs(accountId, projectName, taskId);
897
+ } else {
898
+ result = await fetchBuildWarnLogs(accountId, projectName, taskId);
899
+ }
900
+
901
+ if (result && result.logs.length) {
902
+ result.logs.forEach(log => {
903
+ logger.warn(log.message);
904
+ logger.log('');
905
+ });
906
+ }
907
+ };
908
+
872
909
  module.exports = {
873
910
  writeProjectConfig,
874
911
  getProjectConfig,
@@ -885,4 +922,5 @@ module.exports = {
885
922
  ensureProjectExists,
886
923
  logFeedbackMessage,
887
924
  createProjectComponent,
925
+ displayWarnLogs,
888
926
  };
@@ -0,0 +1,29 @@
1
+ const { promptUser } = require('./promptUtils');
2
+ const { i18n } = require('../lang');
3
+ const { accountNameExistsInConfig } = require('@hubspot/local-dev-lib/config');
4
+
5
+ const i18nKey = 'cli.lib.prompts.developerTestAccountPrompt';
6
+
7
+ const developerTestAccountNamePrompt = currentPortalCount => {
8
+ return promptUser([
9
+ {
10
+ name: 'name',
11
+ message: i18n(`${i18nKey}.name.message`),
12
+ validate(val) {
13
+ if (typeof val !== 'string') {
14
+ return i18n(`${i18nKey}.name.errors.invalidName`);
15
+ } else if (!val.length) {
16
+ return i18n(`${i18nKey}.name.errors.nameRequired`);
17
+ }
18
+ return accountNameExistsInConfig(val)
19
+ ? i18n(`${i18nKey}.name.errors.accountNameExists`, { name: val })
20
+ : true;
21
+ },
22
+ default: `Developer test account ${currentPortalCount + 1}`,
23
+ },
24
+ ]);
25
+ };
26
+
27
+ module.exports = {
28
+ developerTestAccountNamePrompt,
29
+ };
@@ -1,28 +1,36 @@
1
1
  const { promptUser } = require('./promptUtils');
2
2
  const { i18n } = require('../lang');
3
3
  const { uiAccountDescription, uiCommandReference } = require('../ui');
4
- const { isSandbox } = require('../sandboxes');
4
+ const { isSandbox, isDeveloperTestAccount } = require('../accountTypes');
5
5
  const { getAccountId } = require('@hubspot/local-dev-lib/config');
6
6
  const { getSandboxUsageLimits } = require('@hubspot/local-dev-lib/sandboxes');
7
7
  const {
8
8
  HUBSPOT_ACCOUNT_TYPES,
9
+ HUBSPOT_ACCOUNT_TYPE_STRINGS,
9
10
  } = require('@hubspot/local-dev-lib/constants/config');
10
11
  const { logger } = require('@hubspot/local-dev-lib/logger');
12
+ const {
13
+ fetchDeveloperTestAccounts,
14
+ } = require('@hubspot/local-dev-lib/developerTestAccounts');
11
15
 
12
16
  const i18nKey = 'cli.lib.prompts.projectDevTargetAccountPrompt';
13
17
 
14
- const mapSandboxAccount = accountConfig => ({
18
+ const mapNestedAccount = accountConfig => ({
15
19
  name: uiAccountDescription(accountConfig.portalId, false),
16
20
  value: {
17
21
  targetAccountId: getAccountId(accountConfig.name),
18
- createNewSandbox: false,
22
+ createNestedAccount: false,
23
+ parentAccountId: accountConfig.parentAccountId,
19
24
  },
20
25
  });
21
26
 
22
- const selectTargetAccountPrompt = async (accounts, defaultAccountConfig) => {
23
- let sandboxUsage = {};
27
+ const selectSandboxTargetAccountPrompt = async (
28
+ accounts,
29
+ defaultAccountConfig
30
+ ) => {
24
31
  const defaultAccountId = getAccountId(defaultAccountConfig.name);
25
-
32
+ let choices = [];
33
+ let sandboxUsage = {};
26
34
  try {
27
35
  sandboxUsage = await getSandboxUsageLimits(defaultAccountId);
28
36
  } catch (err) {
@@ -48,20 +56,19 @@ const selectTargetAccountPrompt = async (accounts, defaultAccountConfig) => {
48
56
  });
49
57
  }
50
58
  }
51
-
52
59
  // Order choices by Developer Sandbox -> Standard Sandbox
53
- const choices = [
60
+ choices = [
54
61
  ...sandboxAccounts
55
62
  .filter(a => a.accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX)
56
- .map(mapSandboxAccount),
63
+ .map(mapNestedAccount),
57
64
  ...sandboxAccounts
58
65
  .filter(a => a.accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX)
59
- .map(mapSandboxAccount),
66
+ .map(mapNestedAccount),
60
67
  {
61
68
  name: i18n(`${i18nKey}.createNewSandboxOption`),
62
69
  value: {
63
70
  targetAccountId: null,
64
- createNewSandbox: true,
71
+ createNestedAccount: true,
65
72
  },
66
73
  disabled: disabledMessage,
67
74
  },
@@ -69,17 +76,80 @@ const selectTargetAccountPrompt = async (accounts, defaultAccountConfig) => {
69
76
  name: i18n(`${i18nKey}.chooseDefaultAccountOption`),
70
77
  value: {
71
78
  targetAccountId: defaultAccountId,
72
- createNewSandbox: false,
79
+ createNestedAccount: false,
73
80
  },
74
81
  },
75
82
  ];
76
83
 
84
+ return selectTargetAccountPrompt(
85
+ defaultAccountId,
86
+ 'sandbox account',
87
+ choices
88
+ );
89
+ };
90
+
91
+ const selectDeveloperTestTargetAccountPrompt = async (
92
+ accounts,
93
+ defaultAccountConfig
94
+ ) => {
95
+ const defaultAccountId = getAccountId(defaultAccountConfig.name);
96
+ let choices = [];
97
+ let devTestAccountUsage = undefined;
98
+ try {
99
+ devTestAccountUsage = await fetchDeveloperTestAccounts(defaultAccountId);
100
+ } catch (err) {
101
+ logger.debug('Unable to fetch developer test account usage limits: ', err);
102
+ }
103
+
104
+ const devTestAccounts = accounts
105
+ .reverse()
106
+ .filter(
107
+ config =>
108
+ isDeveloperTestAccount(config) &&
109
+ config.parentAccountId === defaultAccountId
110
+ );
111
+ let disabledMessage = false;
112
+ if (
113
+ devTestAccountUsage &&
114
+ devTestAccountUsage.results.length >= devTestAccountUsage.maxTestPortals
115
+ ) {
116
+ disabledMessage = i18n(`${i18nKey}.developerTestAccountLimit`, {
117
+ authCommand: uiCommandReference('hs auth'),
118
+ limit: devTestAccountUsage.maxTestPortals,
119
+ });
120
+ }
121
+
122
+ choices = [
123
+ ...devTestAccounts.map(mapNestedAccount),
124
+ {
125
+ name: i18n(`${i18nKey}.createNewDeveloperTestAccountOption`),
126
+ value: {
127
+ targetAccountId: null,
128
+ createNestedAccount: true,
129
+ },
130
+ disabled: disabledMessage,
131
+ },
132
+ ];
133
+
134
+ return selectTargetAccountPrompt(
135
+ defaultAccountId,
136
+ HUBSPOT_ACCOUNT_TYPE_STRINGS[HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST],
137
+ choices
138
+ );
139
+ };
140
+
141
+ const selectTargetAccountPrompt = async (
142
+ defaultAccountId,
143
+ accountType,
144
+ choices
145
+ ) => {
77
146
  const { targetAccountInfo } = await promptUser([
78
147
  {
79
148
  name: 'targetAccountInfo',
80
149
  type: 'list',
81
150
  message: i18n(`${i18nKey}.promptMessage`, {
82
151
  accountIdentifier: uiAccountDescription(defaultAccountId),
152
+ accountType,
83
153
  }),
84
154
  choices,
85
155
  },
@@ -103,6 +173,7 @@ const confirmDefaultAccountPrompt = async (accountName, accountType) => {
103
173
  };
104
174
 
105
175
  module.exports = {
106
- selectTargetAccountPrompt,
176
+ selectSandboxTargetAccountPrompt,
177
+ selectDeveloperTestTargetAccountPrompt,
107
178
  confirmDefaultAccountPrompt,
108
179
  };
@@ -5,7 +5,7 @@ const { uiAccountDescription } = require('../ui');
5
5
  const {
6
6
  HUBSPOT_ACCOUNT_TYPES,
7
7
  } = require('@hubspot/local-dev-lib/constants/config');
8
- const { isSandbox } = require('../sandboxes');
8
+ const { isSandbox } = require('../accountTypes');
9
9
 
10
10
  const i18nKey = 'cli.lib.prompts.sandboxesPrompt';
11
11
 
@@ -2,11 +2,11 @@ const SpinniesManager = require('./ui/SpinniesManager');
2
2
  const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls');
3
3
  const { logger } = require('@hubspot/local-dev-lib/logger');
4
4
  const { i18n } = require('./lang');
5
+ const { isDevelopmentSandbox } = require('./accountTypes');
5
6
  const {
6
7
  getAvailableSyncTypes,
7
8
  pollSyncTaskStatus,
8
9
  syncTypes,
9
- isDevelopmentSandbox,
10
10
  } = require('./sandboxes');
11
11
  const { initiateSync } = require('@hubspot/local-dev-lib/sandboxes');
12
12
  const {