@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.
- package/commands/accounts/list.js +2 -2
- package/commands/accounts/remove.js +84 -0
- package/commands/accounts.js +2 -0
- package/commands/functions/deploy.js +0 -1
- package/commands/functions/list.js +1 -1
- package/commands/list.js +1 -5
- package/commands/sandbox/create.js +34 -120
- package/commands/sandbox/delete.js +28 -11
- package/commands/sandbox/sync.js +22 -148
- package/lib/CliProgressMultibarManager.js +66 -0
- package/lib/prompts/accountsPrompt.js +6 -4
- package/lib/prompts/projectsLogsPrompt.js +1 -1
- package/lib/prompts/sandboxesPrompt.js +41 -10
- package/lib/sandbox-create.js +337 -0
- package/lib/sandbox-sync.js +174 -0
- package/lib/sandboxes.js +164 -95
- package/package.json +4 -4
package/lib/sandboxes.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
const cliProgress = require('cli-progress');
|
|
2
1
|
const {
|
|
3
2
|
getConfig,
|
|
4
3
|
writeConfig,
|
|
5
4
|
updateAccountConfig,
|
|
5
|
+
getAccountId,
|
|
6
6
|
} = require('@hubspot/cli-lib');
|
|
7
|
-
const {
|
|
8
|
-
DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
|
|
9
|
-
PERSONAL_ACCESS_KEY_AUTH_METHOD,
|
|
10
|
-
} = require('@hubspot/cli-lib/lib/constants');
|
|
11
7
|
const { i18n } = require('@hubspot/cli-lib/lib/lang');
|
|
12
8
|
const { logger } = require('@hubspot/cli-lib/logger');
|
|
13
9
|
const {
|
|
@@ -15,53 +11,121 @@ const {
|
|
|
15
11
|
} = require('@hubspot/cli-lib/personalAccessKey');
|
|
16
12
|
const { EXIT_CODES } = require('./enums/exitCodes');
|
|
17
13
|
const { enterAccountNamePrompt } = require('./prompts/enterAccountNamePrompt');
|
|
14
|
+
const { fetchTaskStatus, fetchTypes } = require('@hubspot/cli-lib/sandboxes');
|
|
15
|
+
const { handleExit, handleKeypress } = require('@hubspot/cli-lib/lib/process');
|
|
16
|
+
const { accountNameExistsInConfig } = require('@hubspot/cli-lib/lib/config');
|
|
18
17
|
const {
|
|
19
18
|
personalAccessKeyPrompt,
|
|
20
19
|
} = require('./prompts/personalAccessKeyPrompt');
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
20
|
+
const CliProgressMultibarManager = require('./CliProgressMultibarManager');
|
|
21
|
+
|
|
22
|
+
const STANDARD_SANDBOX = 'standard';
|
|
23
|
+
const DEVELOPER_SANDBOX = 'developer';
|
|
24
|
+
|
|
25
|
+
const sandboxTypeMap = {
|
|
26
|
+
DEV: DEVELOPER_SANDBOX,
|
|
27
|
+
dev: DEVELOPER_SANDBOX,
|
|
28
|
+
DEVELOPER: DEVELOPER_SANDBOX,
|
|
29
|
+
developer: DEVELOPER_SANDBOX,
|
|
30
|
+
DEVELOPMENT: DEVELOPER_SANDBOX,
|
|
31
|
+
development: DEVELOPER_SANDBOX,
|
|
32
|
+
STANDARD: STANDARD_SANDBOX,
|
|
33
|
+
standard: STANDARD_SANDBOX,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const sandboxApiTypeMap = {
|
|
37
|
+
standard: 1,
|
|
38
|
+
developer: 2,
|
|
39
|
+
};
|
|
27
40
|
|
|
28
|
-
const
|
|
41
|
+
const getSandboxTypeAsString = type =>
|
|
29
42
|
type === 'DEVELOPER' ? 'development' : 'standard';
|
|
30
43
|
|
|
31
44
|
function getAccountName(config) {
|
|
32
45
|
const isSandbox =
|
|
33
46
|
config.sandboxAccountType && config.sandboxAccountType !== null;
|
|
34
|
-
const sandboxName = `[${
|
|
47
|
+
const sandboxName = `[${getSandboxTypeAsString(
|
|
48
|
+
config.sandboxAccountType
|
|
49
|
+
)} sandbox] `;
|
|
35
50
|
return `${config.name} ${isSandbox ? sandboxName : ''}(${config.portalId})`;
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
function getHasSandboxesByType(parentAccountConfig, type) {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
const parentPortalId = getAccountId(parentAccountConfig.portalId);
|
|
56
|
+
for (const portal of config.portals) {
|
|
57
|
+
if (
|
|
58
|
+
(portal.parentAccountId !== null ||
|
|
59
|
+
portal.parentAccountId !== undefined) &&
|
|
60
|
+
portal.parentAccountId === parentPortalId &&
|
|
61
|
+
portal.sandboxAccountType &&
|
|
62
|
+
sandboxTypeMap[portal.sandboxAccountType] === type
|
|
63
|
+
) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSandboxLimit(error) {
|
|
71
|
+
// Error context should contain a limit property with a list of one number. That number is the current limit
|
|
72
|
+
const limit = error.context && error.context.limit && error.context.limit[0];
|
|
73
|
+
return limit ? parseInt(limit, 10) : 1; // Default to 1
|
|
74
|
+
}
|
|
75
|
+
|
|
38
76
|
// Fetches available sync types for a given sandbox portal
|
|
39
77
|
async function getAvailableSyncTypes(parentAccountConfig, config) {
|
|
40
|
-
const parentPortalId = parentAccountConfig.portalId;
|
|
41
|
-
const portalId = config.portalId;
|
|
78
|
+
const parentPortalId = getAccountId(parentAccountConfig.portalId);
|
|
79
|
+
const portalId = getAccountId(config.portalId);
|
|
42
80
|
const syncTypes = await fetchTypes(parentPortalId, portalId);
|
|
43
81
|
return syncTypes.map(t => ({ type: t.name }));
|
|
44
82
|
}
|
|
45
83
|
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @param {String} env - Environment (QA/Prod)
|
|
86
|
+
* @param {Object} result - Sandbox instance returned from API
|
|
87
|
+
* @param {Boolean} force - Force flag to skip prompt
|
|
88
|
+
* @returns {String} validName saved into config
|
|
89
|
+
*/
|
|
90
|
+
const saveSandboxToConfig = async (env, result, force = false) => {
|
|
91
|
+
// const configData = { env, personalAccessKey: result.personalAccessKey };
|
|
92
|
+
// TODO: Temporary, remove
|
|
93
|
+
const configData = await personalAccessKeyPrompt({
|
|
94
|
+
env,
|
|
95
|
+
account: result.sandbox.sandboxHubId,
|
|
96
|
+
});
|
|
97
|
+
// End temporary section
|
|
48
98
|
const updatedConfig = await updateConfigWithPersonalAccessKey(configData);
|
|
49
|
-
|
|
50
99
|
if (!updatedConfig) {
|
|
51
|
-
|
|
100
|
+
throw new Error('Failed to update config with personal access key.');
|
|
52
101
|
}
|
|
53
102
|
|
|
54
103
|
let validName = updatedConfig.name;
|
|
55
|
-
|
|
56
104
|
if (!updatedConfig.name) {
|
|
57
|
-
const nameForConfig = name
|
|
105
|
+
const nameForConfig = result.sandbox.name
|
|
58
106
|
.toLowerCase()
|
|
59
107
|
.split(' ')
|
|
60
108
|
.join('-');
|
|
61
|
-
|
|
62
|
-
|
|
109
|
+
validName = nameForConfig;
|
|
110
|
+
const invalidAccountName = accountNameExistsInConfig(nameForConfig);
|
|
111
|
+
if (invalidAccountName) {
|
|
112
|
+
if (!force) {
|
|
113
|
+
logger.log(
|
|
114
|
+
i18n(
|
|
115
|
+
`cli.lib.prompts.enterAccountNamePrompt.errors.accountNameExists`,
|
|
116
|
+
{ name: nameForConfig }
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
const { name: promptName } = await enterAccountNamePrompt(
|
|
120
|
+
nameForConfig
|
|
121
|
+
);
|
|
122
|
+
validName = promptName;
|
|
123
|
+
} else {
|
|
124
|
+
// Basic invalid name handling when force flag is passed
|
|
125
|
+
validName = nameForConfig + `_${Date.now()}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
63
128
|
}
|
|
64
|
-
|
|
65
129
|
updateAccountConfig({
|
|
66
130
|
...updatedConfig,
|
|
67
131
|
environment: updatedConfig.env,
|
|
@@ -70,35 +134,8 @@ const sandboxCreatePersonalAccessKeyFlow = async (env, account, name) => {
|
|
|
70
134
|
});
|
|
71
135
|
writeConfig();
|
|
72
136
|
|
|
73
|
-
const setAsDefault = await setAsDefaultAccountPrompt(validName);
|
|
74
|
-
|
|
75
137
|
logger.log('');
|
|
76
|
-
|
|
77
|
-
logger.success(
|
|
78
|
-
i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccount`, {
|
|
79
|
-
accountName: validName,
|
|
80
|
-
})
|
|
81
|
-
);
|
|
82
|
-
} else {
|
|
83
|
-
const config = getConfig();
|
|
84
|
-
logger.info(
|
|
85
|
-
i18n(`cli.lib.prompts.setAsDefaultAccountPrompt.keepingCurrentDefault`, {
|
|
86
|
-
accountName: config.defaultPortal,
|
|
87
|
-
})
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
logger.success(
|
|
91
|
-
i18n('cli.commands.sandbox.subcommands.create.success.configFileUpdated', {
|
|
92
|
-
configFilename: DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME,
|
|
93
|
-
authMethod: PERSONAL_ACCESS_KEY_AUTH_METHOD.name,
|
|
94
|
-
account: validName,
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
uiFeatureHighlight([
|
|
98
|
-
'accountsUseCommand',
|
|
99
|
-
'accountOption',
|
|
100
|
-
'accountsListCommand',
|
|
101
|
-
]);
|
|
138
|
+
return validName;
|
|
102
139
|
};
|
|
103
140
|
|
|
104
141
|
const ACTIVE_TASK_POLL_INTERVAL = 1000;
|
|
@@ -110,26 +147,34 @@ const isTaskComplete = task => {
|
|
|
110
147
|
return task.status === 'COMPLETE';
|
|
111
148
|
};
|
|
112
149
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
const incrementBy = (value, multiplier = 3) => {
|
|
151
|
+
return Math.min(value + Math.floor(Math.random() * multiplier), 99);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Number} accountId - Parent portal ID (needs sandbox scopes)
|
|
156
|
+
* @param {String} taksId - Task ID to poll
|
|
157
|
+
* @param {String} syncStatusUrl - Link to UI to check polling status
|
|
158
|
+
* @param {Boolean} allowEarlyTermination - Option to allow a keypress to terminate early
|
|
159
|
+
* @returns {Promise} Interval runs until sync task status is equal to 'COMPLETE'
|
|
160
|
+
*/
|
|
161
|
+
function pollSyncTaskStatus(
|
|
162
|
+
accountId,
|
|
163
|
+
taskId,
|
|
164
|
+
syncStatusUrl,
|
|
165
|
+
allowEarlyTermination = true
|
|
166
|
+
) {
|
|
167
|
+
const i18nKey = 'cli.lib.sandbox.sync.types';
|
|
168
|
+
const progressBar = CliProgressMultibarManager.init();
|
|
124
169
|
const mergeTasks = {
|
|
125
170
|
'lead-flows': 'forms', // lead-flows are a subset of forms. We combine these in the UI as a single item, so we want to merge here for consistency.
|
|
126
171
|
};
|
|
127
|
-
|
|
172
|
+
let progressCounter = {};
|
|
128
173
|
let pollInterval;
|
|
129
174
|
// Handle manual exit for return key and ctrl+c
|
|
130
175
|
const onTerminate = () => {
|
|
131
176
|
clearInterval(pollInterval);
|
|
132
|
-
|
|
177
|
+
progressBar.stop();
|
|
133
178
|
logger.log('');
|
|
134
179
|
logger.log('Exiting, sync will continue in the background.');
|
|
135
180
|
logger.log('');
|
|
@@ -140,16 +185,18 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
|
|
|
140
185
|
);
|
|
141
186
|
process.exit(EXIT_CODES.SUCCESS);
|
|
142
187
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
188
|
+
if (allowEarlyTermination) {
|
|
189
|
+
handleExit(onTerminate);
|
|
190
|
+
handleKeypress(key => {
|
|
191
|
+
if (
|
|
192
|
+
(key && key.ctrl && key.name == 'c') ||
|
|
193
|
+
key.name === 'enter' ||
|
|
194
|
+
key.name === 'return'
|
|
195
|
+
) {
|
|
196
|
+
onTerminate();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
153
200
|
return new Promise((resolve, reject) => {
|
|
154
201
|
pollInterval = setInterval(async () => {
|
|
155
202
|
const taskResult = await fetchTaskStatus(accountId, taskId).catch(reject);
|
|
@@ -158,13 +205,17 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
|
|
|
158
205
|
for (const task of taskResult.tasks) {
|
|
159
206
|
// For each sync task, show a progress bar and increment bar each time we run this interval until status is 'COMPLETE'
|
|
160
207
|
const taskType = task.type;
|
|
161
|
-
if (!
|
|
162
|
-
// skip creation of lead-flows bar because we're combining lead-flows into the forms bar
|
|
163
|
-
|
|
164
|
-
|
|
208
|
+
if (!progressBar.get(taskType) && !mergeTasks[taskType]) {
|
|
209
|
+
// skip creation of lead-flows bar because we're combining lead-flows into the forms bar, otherwise create a bar instance for the type
|
|
210
|
+
progressCounter[taskType] = 0;
|
|
211
|
+
progressBar.create(taskType, 100, 0, {
|
|
212
|
+
label: i18n(`${i18nKey}.${taskType}.label`),
|
|
165
213
|
});
|
|
166
214
|
} else if (mergeTasks[taskType]) {
|
|
167
|
-
//
|
|
215
|
+
// It's a lead-flow here, merge status into the forms progress bar
|
|
216
|
+
if (!progressCounter[mergeTasks[taskType]]) {
|
|
217
|
+
progressCounter[mergeTasks[taskType]] = 0;
|
|
218
|
+
}
|
|
168
219
|
const formsTask = taskResult.tasks.filter(
|
|
169
220
|
t => t.type === mergeTasks[taskType]
|
|
170
221
|
)[0];
|
|
@@ -174,44 +225,62 @@ function pollSyncTaskStatus(accountId, taskId, syncStatusUrl) {
|
|
|
174
225
|
formsTaskStatus !== 'COMPLETE' ||
|
|
175
226
|
leadFlowsTaskStatus !== 'COMPLETE'
|
|
176
227
|
) {
|
|
177
|
-
|
|
178
|
-
|
|
228
|
+
// Randomly increment bar while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
|
|
229
|
+
progressCounter[mergeTasks[taskType]] = incrementBy(
|
|
230
|
+
progressCounter[mergeTasks[taskType]]
|
|
231
|
+
);
|
|
232
|
+
progressBar.update(
|
|
233
|
+
mergeTasks[taskType],
|
|
234
|
+
progressCounter[mergeTasks[taskType]],
|
|
179
235
|
{
|
|
180
|
-
|
|
236
|
+
label: i18n(`${i18nKey}.${mergeTasks[taskType]}.label`),
|
|
181
237
|
}
|
|
182
238
|
);
|
|
183
239
|
}
|
|
184
240
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
241
|
+
if (progressBar.get(taskType) && task.status === 'COMPLETE') {
|
|
242
|
+
progressBar.update(taskType, 100, {
|
|
243
|
+
label: i18n(`${i18nKey}.${taskType}.label`),
|
|
188
244
|
});
|
|
189
|
-
} else if (
|
|
190
|
-
// Do not
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
245
|
+
} else if (
|
|
246
|
+
// Do not start incrementing for tasks still in PENDING state
|
|
247
|
+
progressBar.get(taskType) &&
|
|
248
|
+
task.status === 'PROCESSING'
|
|
249
|
+
) {
|
|
250
|
+
// Randomly increment bar while sync is in progress. Sandboxes currently does not have an accurate measurement for progress.
|
|
251
|
+
progressCounter[taskType] = incrementBy(
|
|
252
|
+
progressCounter[taskType],
|
|
253
|
+
taskType === 'object-records' ? 2 : 3 // slower progress for object-records, sync can take up to a few minutes
|
|
254
|
+
);
|
|
255
|
+
progressBar.update(taskType, progressCounter[taskType], {
|
|
256
|
+
label: i18n(`${i18nKey}.${taskType}.label`),
|
|
194
257
|
});
|
|
195
258
|
}
|
|
196
259
|
}
|
|
197
260
|
} else {
|
|
198
261
|
clearInterval(pollInterval);
|
|
199
262
|
reject();
|
|
200
|
-
|
|
263
|
+
progressBar.stop();
|
|
201
264
|
}
|
|
202
265
|
if (isTaskComplete(taskResult)) {
|
|
203
266
|
clearInterval(pollInterval);
|
|
204
267
|
resolve(taskResult);
|
|
205
|
-
|
|
268
|
+
progressBar.stop();
|
|
206
269
|
}
|
|
207
270
|
}, ACTIVE_TASK_POLL_INTERVAL);
|
|
208
271
|
});
|
|
209
272
|
}
|
|
210
273
|
|
|
211
274
|
module.exports = {
|
|
212
|
-
|
|
275
|
+
STANDARD_SANDBOX,
|
|
276
|
+
DEVELOPER_SANDBOX,
|
|
277
|
+
sandboxTypeMap,
|
|
278
|
+
sandboxApiTypeMap,
|
|
279
|
+
getSandboxTypeAsString,
|
|
213
280
|
getAccountName,
|
|
281
|
+
saveSandboxToConfig,
|
|
282
|
+
getHasSandboxesByType,
|
|
283
|
+
getSandboxLimit,
|
|
214
284
|
getAvailableSyncTypes,
|
|
215
|
-
sandboxCreatePersonalAccessKeyFlow,
|
|
216
285
|
pollSyncTaskStatus,
|
|
217
286
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.8-beta.1",
|
|
4
4
|
"description": "CLI for working with HubSpot",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"url": "https://github.com/HubSpot/hubspot-cms-tools"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@hubspot/cli-lib": "4.1.
|
|
12
|
-
"@hubspot/serverless-dev-runtime": "4.1.
|
|
11
|
+
"@hubspot/cli-lib": "4.1.8-beta.1",
|
|
12
|
+
"@hubspot/serverless-dev-runtime": "4.1.8-beta.1",
|
|
13
13
|
"archiver": "^5.3.0",
|
|
14
14
|
"chalk": "^4.1.2",
|
|
15
15
|
"cli-progress": "^3.11.2",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "8aafada9270602e912d89fc6a513816f5f7232bb"
|
|
42
42
|
}
|