@hubspot/cli 4.1.7 → 4.1.8-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,337 @@
1
+ const Spinnies = require('spinnies');
2
+ const {
3
+ sandboxNamePrompt,
4
+ sandboxTypePrompt,
5
+ } = require('./prompts/sandboxesPrompt');
6
+ const {
7
+ sandboxTypeMap,
8
+ getSandboxLimit,
9
+ getHasSandboxesByType,
10
+ saveSandboxToConfig,
11
+ sandboxApiTypeMap,
12
+ getSandboxTypeAsString,
13
+ getAccountName,
14
+ STANDARD_SANDBOX,
15
+ DEVELOPER_SANDBOX,
16
+ } = require('./sandboxes');
17
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
18
+ const { logger } = require('@hubspot/cli-lib/logger');
19
+ const {
20
+ debugErrorAndContext,
21
+ logErrorInstance,
22
+ } = require('@hubspot/cli-lib/errorHandlers/standardErrors');
23
+ const { trackCommandUsage } = require('./usageTracking');
24
+ const {
25
+ isMissingScopeError,
26
+ isSpecifiedError,
27
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
28
+ const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
29
+ const {
30
+ getEnv,
31
+ getAccountConfig,
32
+ getConfig,
33
+ getAccountId,
34
+ } = require('@hubspot/cli-lib');
35
+ const { createSandbox } = require('@hubspot/cli-lib/sandboxes');
36
+ const { promptUser } = require('./prompts/promptUtils');
37
+ const { syncSandbox } = require('./sandbox-sync');
38
+ const {
39
+ setAsDefaultAccountPrompt,
40
+ } = require('./prompts/setAsDefaultAccountPrompt');
41
+ const { updateDefaultAccount } = require('@hubspot/cli-lib/lib/config');
42
+ const { getValidEnv } = require('@hubspot/cli-lib/lib/environment');
43
+
44
+ const i18nKey = 'cli.lib.sandbox.create';
45
+
46
+ /**
47
+ * @param {String} name - Name of sandbox
48
+ * @param {String} type - Sandbox type to be created (standard/developer)
49
+ * @param {Object} accountConfig - Account config of parent portal
50
+ * @param {String} env - Environment (QA/Prod)
51
+ * @param {Boolean} allowEarlyTermination - Option to allow a keypress to terminate early
52
+ * @param {Boolean} allowSyncAssets - Option to allow user to sync assets after creation
53
+ * @param {Boolean} skipDefaultAccountPrompt - Option to skip default account prompt and auto set new sandbox account as default
54
+ * @returns {Object} Object containing sandboxConfigName string and sandbox instance from API
55
+ */
56
+ const buildSandbox = async ({
57
+ name,
58
+ type,
59
+ accountConfig,
60
+ env,
61
+ allowEarlyTermination = true,
62
+ allowSyncAssets = true,
63
+ skipDefaultAccountPrompt = false,
64
+ force = false,
65
+ }) => {
66
+ const spinnies = new Spinnies({
67
+ succeedColor: 'white',
68
+ });
69
+ const accountId = getAccountId(accountConfig.portalId);
70
+
71
+ trackCommandUsage('sandbox-create', null, accountId);
72
+
73
+ // Default account is not a production portal
74
+ if (
75
+ accountConfig.sandboxAccountType &&
76
+ accountConfig.sandboxAccountType !== null
77
+ ) {
78
+ trackCommandUsage('sandbox-create', { successful: false }, accountId);
79
+ logger.error(
80
+ i18n(`${i18nKey}.failure.creatingWithinSandbox`, {
81
+ sandboxType: getSandboxTypeAsString(accountConfig.sandboxAccountType),
82
+ sandboxName: accountConfig.name,
83
+ })
84
+ );
85
+ throw new Error(
86
+ i18n(`${i18nKey}.failure.creatingWithinSandbox`, {
87
+ sandboxType: getSandboxTypeAsString(accountConfig.sandboxAccountType),
88
+ sandboxName: accountConfig.name,
89
+ })
90
+ );
91
+ }
92
+
93
+ let typePrompt;
94
+ let namePrompt;
95
+
96
+ if ((type && !sandboxTypeMap[type]) || !type) {
97
+ if (!force) {
98
+ typePrompt = await sandboxTypePrompt();
99
+ } else {
100
+ logger.error(i18n(`${i18nKey}.failure.optionMissing.type`));
101
+ throw new Error(i18n(`${i18nKey}.failure.optionMissing.type`));
102
+ }
103
+ }
104
+ if (!name) {
105
+ if (!force) {
106
+ namePrompt = await sandboxNamePrompt();
107
+ } else {
108
+ logger.error(i18n(`${i18nKey}.failure.optionMissing.name`));
109
+ throw new Error(i18n(`${i18nKey}.failure.optionMissing.name`));
110
+ }
111
+ }
112
+
113
+ const sandboxName = name || namePrompt.name;
114
+ const sandboxType = sandboxTypeMap[type] || sandboxTypeMap[typePrompt.type];
115
+
116
+ let result;
117
+ const spinniesI18nKey = `${i18nKey}.loading.${sandboxType}`;
118
+
119
+ try {
120
+ spinnies.add('sandboxCreate', {
121
+ text: i18n(`${spinniesI18nKey}.add`, {
122
+ sandboxName,
123
+ }),
124
+ });
125
+
126
+ const sandboxApiType = sandboxApiTypeMap[sandboxType]; // API expects sandbox type as 1 or 2
127
+ result = await createSandbox(accountId, sandboxName, sandboxApiType);
128
+
129
+ spinnies.succeed('sandboxCreate', {
130
+ text: i18n(`${spinniesI18nKey}.succeed`, {
131
+ name: result.name,
132
+ sandboxHubId: result.sandboxHubId,
133
+ }),
134
+ });
135
+ } catch (err) {
136
+ debugErrorAndContext(err);
137
+
138
+ trackCommandUsage('sandbox-create', { successful: false }, accountId);
139
+
140
+ spinnies.fail('sandboxCreate', {
141
+ text: i18n(`${spinniesI18nKey}.fail`, {
142
+ sandboxName,
143
+ }),
144
+ });
145
+
146
+ if (isMissingScopeError(err)) {
147
+ logger.error(
148
+ i18n(`${i18nKey}.failure.scopes.message`, {
149
+ accountName: accountConfig.name || accountId,
150
+ })
151
+ );
152
+ const websiteOrigin = getHubSpotWebsiteOrigin(env);
153
+ const url = `${websiteOrigin}/personal-access-key/${accountId}`;
154
+ logger.info(
155
+ i18n(`${i18nKey}.failure.scopes.instructions`, {
156
+ accountName: accountConfig.name || accountId,
157
+ url,
158
+ })
159
+ );
160
+ } else if (
161
+ isSpecifiedError(
162
+ err,
163
+ 400,
164
+ 'VALIDATION_ERROR',
165
+ 'SandboxErrors.NUM_DEVELOPMENT_SANDBOXES_LIMIT_EXCEEDED_ERROR'
166
+ ) &&
167
+ err.error &&
168
+ err.error.message
169
+ ) {
170
+ logger.log('');
171
+ const devSandboxLimit = getSandboxLimit(err.error);
172
+ const plural = devSandboxLimit !== 1;
173
+ const hasDevelopmentSandboxes = getHasSandboxesByType(
174
+ accountConfig,
175
+ DEVELOPER_SANDBOX
176
+ );
177
+ if (hasDevelopmentSandboxes) {
178
+ logger.error(
179
+ i18n(
180
+ `${i18nKey}.failure.alreadyInConfig.developer.${
181
+ plural ? 'other' : 'one'
182
+ }`,
183
+ {
184
+ accountName: accountConfig.name || accountId,
185
+ limit: devSandboxLimit,
186
+ }
187
+ )
188
+ );
189
+ } else {
190
+ const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId)));
191
+ logger.error(
192
+ i18n(
193
+ `${i18nKey}.failure.limit.developer.${plural ? 'other' : 'one'}`,
194
+ {
195
+ accountName: accountConfig.name || accountId,
196
+ limit: devSandboxLimit,
197
+ link: `${baseUrl}/sandboxes-developer/${accountId}/development`,
198
+ }
199
+ )
200
+ );
201
+ }
202
+ logger.log('');
203
+ } else if (
204
+ isSpecifiedError(
205
+ err,
206
+ 400,
207
+ 'VALIDATION_ERROR',
208
+ 'SandboxErrors.NUM_STANDARD_SANDBOXES_LIMIT_EXCEEDED_ERROR'
209
+ ) &&
210
+ err.error &&
211
+ err.error.message
212
+ ) {
213
+ logger.log('');
214
+ const standardSandboxLimit = getSandboxLimit(err.error);
215
+ const plural = standardSandboxLimit !== 1;
216
+ const hasStandardSandboxes = getHasSandboxesByType(
217
+ accountConfig,
218
+ STANDARD_SANDBOX
219
+ );
220
+ if (hasStandardSandboxes) {
221
+ logger.error(
222
+ i18n(
223
+ `${i18nKey}.failure.alreadyInConfig.standard.${
224
+ plural ? 'other' : 'one'
225
+ }`,
226
+ {
227
+ accountName: accountConfig.name || accountId,
228
+ limit: standardSandboxLimit,
229
+ }
230
+ )
231
+ );
232
+ } else {
233
+ const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId)));
234
+ logger.error(
235
+ i18n(
236
+ `${i18nKey}.failure.limit.standard.${plural ? 'other' : 'one'}`,
237
+ {
238
+ accountName: accountConfig.name || accountId,
239
+ limit: standardSandboxLimit,
240
+ link: `${baseUrl}/sandboxes-developer/${accountId}/standard`,
241
+ }
242
+ )
243
+ );
244
+ }
245
+ logger.log('');
246
+ } else {
247
+ logErrorInstance(err);
248
+ }
249
+ throw err;
250
+ }
251
+
252
+ let sandboxConfigName;
253
+
254
+ try {
255
+ // Response contains PAK, save to config here
256
+ sandboxConfigName = await saveSandboxToConfig(env, result, force);
257
+ } catch (err) {
258
+ logErrorInstance(err);
259
+ throw err;
260
+ }
261
+
262
+ if (skipDefaultAccountPrompt || force) {
263
+ updateDefaultAccount(sandboxConfigName);
264
+ } else {
265
+ const setAsDefault = await setAsDefaultAccountPrompt(sandboxConfigName);
266
+ if (setAsDefault) {
267
+ logger.success(
268
+ i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccount`, {
269
+ accountName: sandboxConfigName,
270
+ })
271
+ );
272
+ } else {
273
+ const config = getConfig();
274
+ logger.info(
275
+ i18n(
276
+ `cli.lib.prompts.setAsDefaultAccountPrompt.keepingCurrentDefault`,
277
+ {
278
+ accountName: config.defaultPortal,
279
+ }
280
+ )
281
+ );
282
+ }
283
+ }
284
+
285
+ // If creating standard sandbox, prompt user to sync assets
286
+ if (allowSyncAssets) {
287
+ if (sandboxType === STANDARD_SANDBOX) {
288
+ const syncI18nKey = 'cli.lib.sandbox.sync';
289
+ const sandboxAccountConfig = getAccountConfig(
290
+ result.sandbox.sandboxHubId
291
+ );
292
+ const handleSyncSandbox = async () => {
293
+ await syncSandbox({
294
+ accountConfig: sandboxAccountConfig,
295
+ parentAccountConfig: accountConfig,
296
+ env,
297
+ allowEarlyTermination,
298
+ });
299
+ };
300
+ try {
301
+ logger.log('');
302
+ if (!force) {
303
+ const { sandboxSyncPrompt } = await promptUser([
304
+ {
305
+ name: 'sandboxSyncPrompt',
306
+ type: 'confirm',
307
+ message: i18n(
308
+ `${syncI18nKey}.confirm.standardSandboxCreateFlow`,
309
+ {
310
+ parentAccountName: getAccountName(accountConfig),
311
+ sandboxName: getAccountName(sandboxAccountConfig),
312
+ }
313
+ ),
314
+ },
315
+ ]);
316
+ if (sandboxSyncPrompt) {
317
+ await handleSyncSandbox();
318
+ }
319
+ } else {
320
+ await handleSyncSandbox();
321
+ }
322
+ } catch (err) {
323
+ logErrorInstance(err);
324
+ throw err;
325
+ }
326
+ }
327
+ }
328
+
329
+ return {
330
+ sandboxConfigName,
331
+ result,
332
+ };
333
+ };
334
+
335
+ module.exports = {
336
+ buildSandbox,
337
+ };
@@ -0,0 +1,174 @@
1
+ const Spinnies = require('spinnies');
2
+ const { getHubSpotWebsiteOrigin } = require('@hubspot/cli-lib/lib/urls');
3
+ const { logger } = require('@hubspot/cli-lib/logger');
4
+ const { i18n } = require('@hubspot/cli-lib/lib/lang');
5
+ const {
6
+ getAvailableSyncTypes,
7
+ pollSyncTaskStatus,
8
+ getAccountName,
9
+ } = require('./sandboxes');
10
+ const { initiateSync } = require('@hubspot/cli-lib/sandboxes');
11
+ const { logErrorInstance } = require('@hubspot/cli-lib/errorHandlers');
12
+ const {
13
+ isSpecifiedError,
14
+ isMissingScopeError,
15
+ } = require('@hubspot/cli-lib/errorHandlers/apiErrors');
16
+ const { getSandboxTypeAsString } = require('./sandboxes');
17
+ const { getAccountId } = require('@hubspot/cli-lib');
18
+
19
+ const i18nKey = 'cli.lib.sandbox.sync';
20
+
21
+ /**
22
+ * @param {Object} accountConfig - Account config of sandbox portal
23
+ * @param {Object} parentAccountConfig - Account config of parent portal
24
+ * @param {String} env - Environment (QA/Prod)
25
+ * @param {Boolean} allowEarlyTermination - Option to allow a keypress to terminate early
26
+ * @returns
27
+ */
28
+ const syncSandbox = async ({
29
+ accountConfig,
30
+ parentAccountConfig,
31
+ env,
32
+ allowEarlyTermination = true,
33
+ }) => {
34
+ const accountId = getAccountId(accountConfig.portalId);
35
+ const parentAccountId = getAccountId(parentAccountConfig.portalId);
36
+ const spinnies = new Spinnies({
37
+ succeedColor: 'white',
38
+ });
39
+ let initiateSyncResponse;
40
+
41
+ const baseUrl = getHubSpotWebsiteOrigin(env);
42
+ const syncStatusUrl = `${baseUrl}/sandboxes-developer/${parentAccountId}/${getSandboxTypeAsString(
43
+ accountConfig.sandboxAccountType
44
+ )}`;
45
+
46
+ try {
47
+ logger.log('');
48
+ spinnies.add('sandboxSync', {
49
+ text: i18n(`${i18nKey}.loading.startSync`),
50
+ });
51
+
52
+ // Fetches sync types based on default account. Parent account required for fetch
53
+ const tasks = await getAvailableSyncTypes(
54
+ parentAccountConfig,
55
+ accountConfig
56
+ );
57
+
58
+ initiateSyncResponse = await initiateSync(
59
+ parentAccountId,
60
+ accountId,
61
+ tasks,
62
+ accountId
63
+ );
64
+
65
+ if (allowEarlyTermination) {
66
+ logger.log(i18n(`${i18nKey}.info.earlyExit`));
67
+ }
68
+ logger.log('');
69
+ spinnies.succeed('sandboxSync', {
70
+ text: i18n(`${i18nKey}.loading.succeed`),
71
+ });
72
+ } catch (err) {
73
+ spinnies.fail('sandboxSync', {
74
+ text: i18n(`${i18nKey}.loading.fail`),
75
+ });
76
+
77
+ logger.log('');
78
+ if (isMissingScopeError(err)) {
79
+ logger.error(
80
+ i18n(`${i18nKey}.failure.missingScopes`, {
81
+ accountName: getAccountName(parentAccountConfig),
82
+ })
83
+ );
84
+ } else if (
85
+ isSpecifiedError(
86
+ err,
87
+ 429,
88
+ 'RATE_LIMITS',
89
+ 'sandboxes-sync-api.SYNC_IN_PROGRESS'
90
+ )
91
+ ) {
92
+ logger.error(
93
+ i18n(`${i18nKey}.failure.syncInProgress`, {
94
+ url: `${baseUrl}/sandboxes-developer/${parentAccountId}/syncactivitylog`,
95
+ })
96
+ );
97
+ } else if (
98
+ isSpecifiedError(
99
+ err,
100
+ 403,
101
+ 'BANNED',
102
+ 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USERID'
103
+ )
104
+ ) {
105
+ // This will only trigger if a user is not a super admin of the target account.
106
+ logger.error(
107
+ i18n(`${i18nKey}.failure.notSuperAdmin`, {
108
+ account: getAccountName(accountConfig),
109
+ })
110
+ );
111
+ } else if (
112
+ isSpecifiedError(
113
+ err,
114
+ 404,
115
+ 'OBJECT_NOT_FOUND',
116
+ 'SandboxErrors.SANDBOX_NOT_FOUND'
117
+ )
118
+ ) {
119
+ logger.error(
120
+ i18n(`${i18nKey}.failure.objectNotFound`, {
121
+ account: getAccountName(accountConfig),
122
+ })
123
+ );
124
+ } else {
125
+ logErrorInstance(err);
126
+ }
127
+ logger.log('');
128
+ throw err;
129
+ }
130
+
131
+ try {
132
+ logger.log('');
133
+ logger.log('Sync progress:');
134
+ // Poll sync task status to show progress bars
135
+ await pollSyncTaskStatus(
136
+ parentAccountId,
137
+ initiateSyncResponse.id,
138
+ syncStatusUrl,
139
+ allowEarlyTermination
140
+ );
141
+
142
+ logger.log('');
143
+ spinnies.add('syncComplete', {
144
+ text: i18n(`${i18nKey}.polling.syncing`),
145
+ });
146
+ spinnies.succeed('syncComplete', {
147
+ text: i18n(`${i18nKey}.polling.succeed`),
148
+ });
149
+ logger.log('');
150
+ logger.log(
151
+ i18n(`${i18nKey}.info.syncStatus`, {
152
+ url: syncStatusUrl,
153
+ })
154
+ );
155
+ } catch (err) {
156
+ // If polling fails at this point, we do not track a failed sync since it is running in the background.
157
+ logErrorInstance(err);
158
+
159
+ spinnies.add('syncComplete', {
160
+ text: i18n(`${i18nKey}.polling.syncing`),
161
+ });
162
+ spinnies.fail('syncComplete', {
163
+ text: i18n(`${i18nKey}.polling.fail`, {
164
+ url: syncStatusUrl,
165
+ }),
166
+ });
167
+
168
+ throw err;
169
+ }
170
+ };
171
+
172
+ module.exports = {
173
+ syncSandbox,
174
+ };