@hubspot/cli 5.2.1-beta.8 → 5.3.0
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/auth.js +2 -4
- package/commands/functions/deploy.js +2 -2
- package/commands/init.js +2 -4
- package/commands/module/marketplace-validate.js +1 -1
- package/commands/project/__tests__/deploy.test.js +432 -0
- package/commands/project/cloneApp.js +208 -0
- package/commands/project/deploy.js +82 -27
- package/commands/project/listBuilds.js +1 -1
- package/commands/project/logs.js +1 -1
- package/commands/project/migrateApp.js +85 -30
- package/commands/project/upload.js +1 -1
- package/commands/project.js +15 -12
- package/commands/sandbox/create.js +17 -48
- package/commands/sandbox/delete.js +5 -2
- package/commands/sandbox/sync.js +12 -2
- package/commands/sandbox.js +2 -1
- package/commands/theme/marketplace-validate.js +1 -1
- package/lang/en.lyaml +77 -76
- package/lib/LocalDevManager.js +59 -11
- package/lib/buildAccount.js +3 -3
- package/lib/constants.js +3 -1
- package/lib/interpolationHelpers.js +3 -0
- package/lib/localDev.js +21 -11
- package/lib/marketplace-validate.js +11 -3
- package/lib/polling.js +16 -10
- package/lib/projects.js +143 -100
- package/lib/prompts/accountNamePrompt.js +78 -0
- package/lib/prompts/activeInstallConfirmationPrompt.js +20 -0
- package/lib/prompts/createProjectPrompt.js +12 -2
- package/lib/prompts/deployBuildIdPrompt.js +22 -0
- package/lib/prompts/installPublicAppPrompt.js +13 -4
- package/lib/prompts/personalAccessKeyPrompt.js +2 -2
- package/lib/prompts/sandboxesPrompt.js +12 -41
- package/lib/prompts/selectPublicAppPrompt.js +41 -22
- package/lib/sandboxSync.js +49 -68
- package/lib/sandboxes.js +8 -149
- package/lib/serverlessLogs.js +2 -2
- package/lib/ui/index.js +74 -0
- package/package.json +5 -4
- package/lib/prompts/buildIdPrompt.js +0 -35
- package/lib/prompts/developerTestAccountNamePrompt.js +0 -29
- package/lib/prompts/enterAccountNamePrompt.js +0 -33
- package/lib/ui/CliProgressMultibarManager.js +0 -66
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const {
|
|
4
|
+
addAccountOptions,
|
|
5
|
+
addConfigOptions,
|
|
6
|
+
getAccountId,
|
|
7
|
+
addUseEnvironmentOptions,
|
|
8
|
+
} = require('../../lib/commonOpts');
|
|
9
|
+
const {
|
|
10
|
+
trackCommandUsage,
|
|
11
|
+
trackCommandMetadataUsage,
|
|
12
|
+
} = require('../../lib/usageTracking');
|
|
13
|
+
const { loadAndValidateOptions } = require('../../lib/validation');
|
|
14
|
+
const { i18n } = require('../../lib/lang');
|
|
15
|
+
const {
|
|
16
|
+
selectPublicAppPrompt,
|
|
17
|
+
} = require('../../lib/prompts/selectPublicAppPrompt');
|
|
18
|
+
const {
|
|
19
|
+
createProjectPrompt,
|
|
20
|
+
} = require('../../lib/prompts/createProjectPrompt');
|
|
21
|
+
const { poll } = require('../../lib/polling');
|
|
22
|
+
const {
|
|
23
|
+
uiLine,
|
|
24
|
+
uiCommandReference,
|
|
25
|
+
uiAccountDescription,
|
|
26
|
+
} = require('../../lib/ui');
|
|
27
|
+
const SpinniesManager = require('../../lib/ui/SpinniesManager');
|
|
28
|
+
const {
|
|
29
|
+
logApiErrorInstance,
|
|
30
|
+
ApiErrorContext,
|
|
31
|
+
} = require('../../lib/errorHandlers/apiErrors');
|
|
32
|
+
const { EXIT_CODES } = require('../../lib/enums/exitCodes');
|
|
33
|
+
const { isAppDeveloperAccount } = require('../../lib/accountTypes');
|
|
34
|
+
const { writeProjectConfig } = require('../../lib/projects');
|
|
35
|
+
const { PROJECT_CONFIG_FILE } = require('../../lib/constants');
|
|
36
|
+
const {
|
|
37
|
+
cloneApp,
|
|
38
|
+
checkCloneStatus,
|
|
39
|
+
downloadClonedProject,
|
|
40
|
+
} = require('@hubspot/local-dev-lib/api/projects');
|
|
41
|
+
const { getCwd } = require('@hubspot/local-dev-lib/path');
|
|
42
|
+
const { logger } = require('@hubspot/local-dev-lib/logger');
|
|
43
|
+
const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
|
|
44
|
+
const { extractZipArchive } = require('@hubspot/local-dev-lib/archive');
|
|
45
|
+
const {
|
|
46
|
+
fetchPublicAppMetadata,
|
|
47
|
+
} = require('@hubspot/local-dev-lib/api/appsDev');
|
|
48
|
+
|
|
49
|
+
const i18nKey = 'commands.project.subcommands.cloneApp';
|
|
50
|
+
|
|
51
|
+
exports.command = 'clone-app';
|
|
52
|
+
exports.describe = null; // uiBetaTag(i18n(`${i18nKey}.describe`), false);
|
|
53
|
+
|
|
54
|
+
exports.handler = async options => {
|
|
55
|
+
await loadAndValidateOptions(options);
|
|
56
|
+
|
|
57
|
+
const accountId = getAccountId(options);
|
|
58
|
+
const accountConfig = getAccountConfig(accountId);
|
|
59
|
+
const accountName = uiAccountDescription(accountId);
|
|
60
|
+
|
|
61
|
+
trackCommandUsage('clone-app', {}, accountId);
|
|
62
|
+
|
|
63
|
+
if (!isAppDeveloperAccount(accountConfig)) {
|
|
64
|
+
uiLine();
|
|
65
|
+
logger.error(i18n(`${i18nKey}.errors.invalidAccountTypeTitle`));
|
|
66
|
+
logger.log(
|
|
67
|
+
i18n(`${i18nKey}.errors.invalidAccountTypeDescription`, {
|
|
68
|
+
useCommand: uiCommandReference('hs accounts use'),
|
|
69
|
+
authCommand: uiCommandReference('hs auth'),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
uiLine();
|
|
73
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let appId;
|
|
77
|
+
let name;
|
|
78
|
+
let location;
|
|
79
|
+
let preventProjectMigrations;
|
|
80
|
+
let listingInfo;
|
|
81
|
+
try {
|
|
82
|
+
appId = options.appId;
|
|
83
|
+
if (!appId) {
|
|
84
|
+
const appIdResponse = await selectPublicAppPrompt({
|
|
85
|
+
accountId,
|
|
86
|
+
accountName,
|
|
87
|
+
options,
|
|
88
|
+
isMigratingApp: false,
|
|
89
|
+
});
|
|
90
|
+
appId = appIdResponse.appId;
|
|
91
|
+
}
|
|
92
|
+
const selectedApp = await fetchPublicAppMetadata(appId, accountId);
|
|
93
|
+
// preventProjectMigrations returns true if we have not added app to allowlist config.
|
|
94
|
+
// listingInfo will only exist for marketplace apps
|
|
95
|
+
preventProjectMigrations = selectedApp.preventProjectMigrations;
|
|
96
|
+
listingInfo = selectedApp.listingInfo;
|
|
97
|
+
|
|
98
|
+
const projectResponse = await createProjectPrompt('', options, true);
|
|
99
|
+
name = projectResponse.name;
|
|
100
|
+
location = projectResponse.location;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logApiErrorInstance(error, new ApiErrorContext({ accountId }));
|
|
103
|
+
process.exit(EXIT_CODES.ERROR);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
SpinniesManager.init();
|
|
107
|
+
|
|
108
|
+
SpinniesManager.add('cloneApp', {
|
|
109
|
+
text: i18n(`${i18nKey}.cloneStatus.inProgress`),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const { exportId } = await cloneApp(accountId, appId);
|
|
113
|
+
const { status } = await poll(checkCloneStatus, accountId, exportId);
|
|
114
|
+
if (status === 'SUCCESS') {
|
|
115
|
+
// Ensure correct project folder structure exists
|
|
116
|
+
const baseDestPath = path.resolve(getCwd(), location);
|
|
117
|
+
const absoluteDestPath = path.resolve(baseDestPath, 'src', 'app');
|
|
118
|
+
fs.mkdirSync(absoluteDestPath, { recursive: true });
|
|
119
|
+
|
|
120
|
+
// Extract zipped app files and place them in correct directory
|
|
121
|
+
const zippedApp = await downloadClonedProject(accountId, exportId);
|
|
122
|
+
await extractZipArchive(zippedApp, name, absoluteDestPath, {
|
|
123
|
+
includesRootDir: true,
|
|
124
|
+
hideLogs: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Create hsproject.json file
|
|
128
|
+
const configPath = path.join(baseDestPath, PROJECT_CONFIG_FILE);
|
|
129
|
+
const configContent = {
|
|
130
|
+
name,
|
|
131
|
+
srcDir: 'src',
|
|
132
|
+
platformVersion: '2023.2',
|
|
133
|
+
};
|
|
134
|
+
const success = writeProjectConfig(configPath, configContent);
|
|
135
|
+
|
|
136
|
+
const isListed = Boolean(listingInfo);
|
|
137
|
+
trackCommandMetadataUsage(
|
|
138
|
+
'clone-app',
|
|
139
|
+
{
|
|
140
|
+
projectName: name,
|
|
141
|
+
appId,
|
|
142
|
+
status,
|
|
143
|
+
preventProjectMigrations,
|
|
144
|
+
isListed,
|
|
145
|
+
},
|
|
146
|
+
accountId
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
SpinniesManager.succeed('cloneApp', {
|
|
150
|
+
text: i18n(`${i18nKey}.cloneStatus.done`),
|
|
151
|
+
succeedColor: 'white',
|
|
152
|
+
});
|
|
153
|
+
if (!success) {
|
|
154
|
+
logger.error(
|
|
155
|
+
i18n(`${i18nKey}.errors.couldNotWriteConfigPath`),
|
|
156
|
+
configPath
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
logger.log('');
|
|
160
|
+
uiLine();
|
|
161
|
+
logger.success(i18n(`${i18nKey}.cloneStatus.success`, { location }));
|
|
162
|
+
logger.log('');
|
|
163
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
trackCommandMetadataUsage(
|
|
167
|
+
'clone-app',
|
|
168
|
+
{ projectName: name, appId, status: 'FAILURE', error },
|
|
169
|
+
accountId
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
SpinniesManager.fail('cloneApp', {
|
|
173
|
+
text: i18n(`${i18nKey}.cloneStatus.failure`),
|
|
174
|
+
failColor: 'white',
|
|
175
|
+
});
|
|
176
|
+
// Migrations endpoints return a response object with an errors property. The errors property contains an array of errors.
|
|
177
|
+
if (error.errors && Array.isArray(error.errors)) {
|
|
178
|
+
error.errors.forEach(e =>
|
|
179
|
+
logApiErrorInstance(e, new ApiErrorContext({ accountId }))
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
logApiErrorInstance(error, new ApiErrorContext({ accountId }));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
exports.builder = yargs => {
|
|
188
|
+
yargs.options({
|
|
189
|
+
location: {
|
|
190
|
+
describe: i18n(`${i18nKey}.options.location.describe`),
|
|
191
|
+
type: 'string',
|
|
192
|
+
},
|
|
193
|
+
appId: {
|
|
194
|
+
describe: i18n(`${i18nKey}.options.appId.describe`),
|
|
195
|
+
type: 'number',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
yargs.example([
|
|
200
|
+
['$0 project clone-app', i18n(`${i18nKey}.examples.default`)],
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
addConfigOptions(yargs);
|
|
204
|
+
addAccountOptions(yargs);
|
|
205
|
+
addUseEnvironmentOptions(yargs);
|
|
206
|
+
|
|
207
|
+
return yargs;
|
|
208
|
+
};
|
|
@@ -16,20 +16,55 @@ const {
|
|
|
16
16
|
fetchProject,
|
|
17
17
|
} = require('@hubspot/local-dev-lib/api/projects');
|
|
18
18
|
const { loadAndValidateOptions } = require('../../lib/validation');
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
getProjectConfig,
|
|
21
|
+
pollDeployStatus,
|
|
22
|
+
getProjectDetailUrl,
|
|
23
|
+
} = require('../../lib/projects');
|
|
20
24
|
const { projectNamePrompt } = require('../../lib/prompts/projectNamePrompt');
|
|
21
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
deployBuildIdPrompt,
|
|
27
|
+
} = require('../../lib/prompts/deployBuildIdPrompt');
|
|
22
28
|
const { i18n } = require('../../lib/lang');
|
|
23
|
-
const { uiBetaTag } = require('../../lib/ui');
|
|
29
|
+
const { uiBetaTag, uiLink } = require('../../lib/ui');
|
|
24
30
|
const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
|
|
25
31
|
|
|
26
32
|
const i18nKey = 'commands.project.subcommands.deploy';
|
|
27
33
|
const { EXIT_CODES } = require('../../lib/enums/exitCodes');
|
|
28
34
|
const { uiCommandReference, uiAccountDescription } = require('../../lib/ui');
|
|
29
35
|
|
|
30
|
-
exports.command = 'deploy
|
|
36
|
+
exports.command = 'deploy';
|
|
31
37
|
exports.describe = uiBetaTag(i18n(`${i18nKey}.describe`), false);
|
|
32
38
|
|
|
39
|
+
const validateBuildId = (
|
|
40
|
+
buildId,
|
|
41
|
+
deployedBuildId,
|
|
42
|
+
latestBuildId,
|
|
43
|
+
projectName,
|
|
44
|
+
accountId
|
|
45
|
+
) => {
|
|
46
|
+
if (Number(buildId) > latestBuildId) {
|
|
47
|
+
return i18n(`${i18nKey}.errors.buildIdDoesNotExist`, {
|
|
48
|
+
buildId: buildId,
|
|
49
|
+
projectName,
|
|
50
|
+
linkToProject: uiLink(
|
|
51
|
+
i18n(`${i18nKey}.errors.viewProjectsBuilds`),
|
|
52
|
+
getProjectDetailUrl(projectName, accountId)
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (Number(buildId) === deployedBuildId) {
|
|
57
|
+
return i18n(`${i18nKey}.errors.buildAlreadyDeployed`, {
|
|
58
|
+
buildId: buildId,
|
|
59
|
+
linkToProject: uiLink(
|
|
60
|
+
i18n(`${i18nKey}.errors.viewProjectsBuilds`),
|
|
61
|
+
getProjectDetailUrl(projectName, accountId)
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
|
|
33
68
|
exports.handler = async options => {
|
|
34
69
|
await loadAndValidateOptions(options);
|
|
35
70
|
|
|
@@ -59,27 +94,47 @@ exports.handler = async options => {
|
|
|
59
94
|
let buildIdToDeploy = buildIdOption;
|
|
60
95
|
|
|
61
96
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
const { latestBuild, deployedBuildId } = await fetchProject(
|
|
98
|
+
accountId,
|
|
99
|
+
projectName
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!latestBuild || !latestBuild.buildId) {
|
|
103
|
+
logger.error(i18n(`${i18nKey}.errors.noBuilds`));
|
|
104
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (buildIdToDeploy) {
|
|
108
|
+
const validationResult = validateBuildId(
|
|
109
|
+
buildIdToDeploy,
|
|
110
|
+
deployedBuildId,
|
|
111
|
+
latestBuild.buildId,
|
|
112
|
+
projectName,
|
|
113
|
+
accountId
|
|
66
114
|
);
|
|
67
|
-
if (
|
|
68
|
-
logger.error(
|
|
69
|
-
process.exit(EXIT_CODES.ERROR);
|
|
115
|
+
if (validationResult !== true) {
|
|
116
|
+
logger.error(validationResult);
|
|
117
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
70
118
|
}
|
|
71
|
-
|
|
119
|
+
} else {
|
|
120
|
+
const deployBuildIdPromptResponse = await deployBuildIdPrompt(
|
|
72
121
|
latestBuild.buildId,
|
|
73
122
|
deployedBuildId,
|
|
74
|
-
|
|
123
|
+
buildId =>
|
|
124
|
+
validateBuildId(
|
|
125
|
+
buildId,
|
|
126
|
+
deployedBuildId,
|
|
127
|
+
latestBuild.buildId,
|
|
128
|
+
projectName,
|
|
129
|
+
accountId
|
|
130
|
+
)
|
|
75
131
|
);
|
|
76
|
-
|
|
77
|
-
buildIdToDeploy = buildIdPromptResponse.buildId;
|
|
132
|
+
buildIdToDeploy = deployBuildIdPromptResponse.buildId;
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
if (!buildIdToDeploy) {
|
|
81
136
|
logger.error(i18n(`${i18nKey}.errors.noBuildId`));
|
|
82
|
-
process.exit(EXIT_CODES.ERROR);
|
|
137
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
83
138
|
}
|
|
84
139
|
|
|
85
140
|
const deployResp = await deployProject(
|
|
@@ -88,13 +143,13 @@ exports.handler = async options => {
|
|
|
88
143
|
buildIdToDeploy
|
|
89
144
|
);
|
|
90
145
|
|
|
91
|
-
if (deployResp.error) {
|
|
146
|
+
if (!deployResp || deployResp.error) {
|
|
92
147
|
logger.error(
|
|
93
148
|
i18n(`${i18nKey}.errors.deploy`, {
|
|
94
149
|
details: deployResp.error.message,
|
|
95
150
|
})
|
|
96
151
|
);
|
|
97
|
-
return;
|
|
152
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
98
153
|
}
|
|
99
154
|
|
|
100
155
|
await pollDeployStatus(
|
|
@@ -104,7 +159,7 @@ exports.handler = async options => {
|
|
|
104
159
|
buildIdToDeploy
|
|
105
160
|
);
|
|
106
161
|
} catch (e) {
|
|
107
|
-
if (e.
|
|
162
|
+
if (e.response && e.response.status === 404) {
|
|
108
163
|
logger.error(
|
|
109
164
|
i18n(`${i18nKey}.errors.projectNotFound`, {
|
|
110
165
|
projectName: chalk.bold(projectName),
|
|
@@ -112,13 +167,12 @@ exports.handler = async options => {
|
|
|
112
167
|
command: uiCommandReference('hs project upload'),
|
|
113
168
|
})
|
|
114
169
|
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
logger.error(e.error.message);
|
|
170
|
+
} else if (e.response && e.response.status === 400) {
|
|
171
|
+
logger.error(e.message);
|
|
118
172
|
} else {
|
|
119
173
|
logApiErrorInstance(e, new ApiErrorContext({ accountId, projectName }));
|
|
120
174
|
}
|
|
121
|
-
process.exit(EXIT_CODES.ERROR);
|
|
175
|
+
return process.exit(EXIT_CODES.ERROR);
|
|
122
176
|
}
|
|
123
177
|
};
|
|
124
178
|
|
|
@@ -128,16 +182,17 @@ exports.builder = yargs => {
|
|
|
128
182
|
describe: i18n(`${i18nKey}.options.project.describe`),
|
|
129
183
|
type: 'string',
|
|
130
184
|
},
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
build: {
|
|
186
|
+
alias: ['buildId'],
|
|
187
|
+
describe: i18n(`${i18nKey}.options.build.describe`),
|
|
133
188
|
type: 'number',
|
|
134
189
|
},
|
|
135
190
|
});
|
|
136
191
|
|
|
137
|
-
yargs.example([['$0 project deploy', i18n(`${i18nKey}.examples.default`)]]);
|
|
138
192
|
yargs.example([
|
|
193
|
+
['$0 project deploy', i18n(`${i18nKey}.examples.default`)],
|
|
139
194
|
[
|
|
140
|
-
'$0 project deploy --project="my-project" --
|
|
195
|
+
'$0 project deploy --project="my-project" --build=5',
|
|
141
196
|
i18n(`${i18nKey}.examples.withOptions`),
|
|
142
197
|
],
|
|
143
198
|
]);
|
|
@@ -124,7 +124,7 @@ exports.handler = async options => {
|
|
|
124
124
|
|
|
125
125
|
await fetchAndDisplayBuilds(project, { limit });
|
|
126
126
|
} catch (e) {
|
|
127
|
-
if (e.
|
|
127
|
+
if (e.response && e.response.status === 404) {
|
|
128
128
|
logger.error(`Project ${projectConfig.name} not found. `);
|
|
129
129
|
} else {
|
|
130
130
|
logApiErrorInstance(
|
package/commands/project/logs.js
CHANGED
|
@@ -49,7 +49,7 @@ const getPrivateAppsUrl = accountId => {
|
|
|
49
49
|
// We currently cannot fetch logs directly to the CLI. See internal CLI issue #413 for more information.
|
|
50
50
|
|
|
51
51
|
// const handleLogsError = (e, name, projectName) => {
|
|
52
|
-
// if (e.
|
|
52
|
+
// if (e.response && e.response.status === 404) {
|
|
53
53
|
// logger.debug(`Log fetch error: ${e.message}`);
|
|
54
54
|
// logger.log(i18n(`${i18nKey}.logs.noLogsFound`, { name }));
|
|
55
55
|
// } else {
|
|
@@ -5,14 +5,16 @@ const {
|
|
|
5
5
|
getAccountId,
|
|
6
6
|
addUseEnvironmentOptions,
|
|
7
7
|
} = require('../../lib/commonOpts');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
trackCommandUsage,
|
|
10
|
+
trackCommandMetadataUsage,
|
|
11
|
+
} = require('../../lib/usageTracking');
|
|
9
12
|
const { loadAndValidateOptions } = require('../../lib/validation');
|
|
10
13
|
const {
|
|
11
14
|
createProjectPrompt,
|
|
12
15
|
} = require('../../lib/prompts/createProjectPrompt');
|
|
13
16
|
const { i18n } = require('../../lib/lang');
|
|
14
17
|
const {
|
|
15
|
-
fetchPublicAppOptions,
|
|
16
18
|
selectPublicAppPrompt,
|
|
17
19
|
} = require('../../lib/prompts/selectPublicAppPrompt');
|
|
18
20
|
const { poll } = require('../../lib/polling');
|
|
@@ -32,6 +34,7 @@ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
|
|
|
32
34
|
const { promptUser } = require('../../lib/prompts/promptUtils');
|
|
33
35
|
const { isAppDeveloperAccount } = require('../../lib/accountTypes');
|
|
34
36
|
const { ensureProjectExists } = require('../../lib/projects');
|
|
37
|
+
const { handleKeypress } = require('../../lib/process');
|
|
35
38
|
const {
|
|
36
39
|
migrateApp,
|
|
37
40
|
checkMigrationStatus,
|
|
@@ -42,6 +45,9 @@ const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
|
|
|
42
45
|
const { downloadProject } = require('@hubspot/local-dev-lib/api/projects');
|
|
43
46
|
const { extractZipArchive } = require('@hubspot/local-dev-lib/archive');
|
|
44
47
|
const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls');
|
|
48
|
+
const {
|
|
49
|
+
fetchPublicAppMetadata,
|
|
50
|
+
} = require('@hubspot/local-dev-lib/api/appsDev');
|
|
45
51
|
|
|
46
52
|
const i18nKey = 'commands.project.subcommands.migrateApp';
|
|
47
53
|
|
|
@@ -58,15 +64,16 @@ exports.handler = async options => {
|
|
|
58
64
|
trackCommandUsage('migrate-app', {}, accountId);
|
|
59
65
|
|
|
60
66
|
if (!isAppDeveloperAccount(accountConfig)) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
uiLine();
|
|
68
|
+
logger.error(i18n(`${i18nKey}.errors.invalidAccountTypeTitle`));
|
|
69
|
+
logger.log(
|
|
70
|
+
i18n(`${i18nKey}.errors.invalidAccountTypeDescription`, {
|
|
65
71
|
useCommand: uiCommandReference('hs accounts use'),
|
|
66
72
|
authCommand: uiCommandReference('hs auth'),
|
|
67
73
|
})
|
|
68
74
|
);
|
|
69
|
-
|
|
75
|
+
uiLine();
|
|
76
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
const { appId } =
|
|
@@ -75,38 +82,61 @@ exports.handler = async options => {
|
|
|
75
82
|
: await selectPublicAppPrompt({
|
|
76
83
|
accountId,
|
|
77
84
|
accountName,
|
|
78
|
-
|
|
79
|
-
migrateApp: true,
|
|
85
|
+
isMigratingApp: true,
|
|
80
86
|
});
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let appName;
|
|
89
|
+
let preventProjectMigrations;
|
|
90
|
+
let listingInfo;
|
|
91
|
+
try {
|
|
92
|
+
const selectedApp = await fetchPublicAppMetadata(appId, accountId);
|
|
93
|
+
// preventProjectMigrations returns true if we have not added app to allowlist config.
|
|
94
|
+
// listingInfo will only exist for marketplace apps
|
|
95
|
+
preventProjectMigrations = selectedApp.preventProjectMigrations;
|
|
96
|
+
listingInfo = selectedApp.listingInfo;
|
|
97
|
+
if (preventProjectMigrations && listingInfo) {
|
|
98
|
+
logger.error(i18n(`${i18nKey}.errors.invalidApp`, { appId }));
|
|
99
|
+
process.exit(EXIT_CODES.ERROR);
|
|
100
|
+
}
|
|
101
|
+
appName = selectedApp.name;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logApiErrorInstance(error, new ApiErrorContext({ accountId }));
|
|
85
104
|
process.exit(EXIT_CODES.ERROR);
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
let projectName;
|
|
108
|
+
let projectLocation;
|
|
109
|
+
try {
|
|
110
|
+
const { name, location } = await createProjectPrompt('', options, true);
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
noLogs: true,
|
|
96
|
-
});
|
|
112
|
+
projectName = options.name || name;
|
|
113
|
+
projectLocation = options.location || location;
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
const { projectExists } = await ensureProjectExists(
|
|
116
|
+
accountId,
|
|
117
|
+
projectName,
|
|
118
|
+
{
|
|
119
|
+
allowCreate: false,
|
|
120
|
+
noLogs: true,
|
|
121
|
+
}
|
|
103
122
|
);
|
|
123
|
+
|
|
124
|
+
if (projectExists) {
|
|
125
|
+
logger.error(
|
|
126
|
+
i18n(`${i18nKey}.errors.projectAlreadyExists`, {
|
|
127
|
+
projectName,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
process.exit(EXIT_CODES.ERROR);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logApiErrorInstance(error, new ApiErrorContext({ accountId }));
|
|
104
134
|
process.exit(EXIT_CODES.ERROR);
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
logger.log('');
|
|
108
138
|
uiLine();
|
|
109
|
-
logger.log(uiBetaTag(i18n(`${i18nKey}.warning.title
|
|
139
|
+
logger.log(uiBetaTag(i18n(`${i18nKey}.warning.title`, { appName }), false));
|
|
110
140
|
logger.log(i18n(`${i18nKey}.warning.projectConversion`));
|
|
111
141
|
logger.log(i18n(`${i18nKey}.warning.appConfig`));
|
|
112
142
|
logger.log('');
|
|
@@ -122,6 +152,7 @@ exports.handler = async options => {
|
|
|
122
152
|
type: 'confirm',
|
|
123
153
|
message: i18n(`${i18nKey}.createAppPrompt`),
|
|
124
154
|
});
|
|
155
|
+
process.stdin.resume();
|
|
125
156
|
|
|
126
157
|
if (!shouldCreateApp) {
|
|
127
158
|
process.exit(EXIT_CODES.SUCCESS);
|
|
@@ -134,6 +165,14 @@ exports.handler = async options => {
|
|
|
134
165
|
text: i18n(`${i18nKey}.migrationStatus.inProgress`),
|
|
135
166
|
});
|
|
136
167
|
|
|
168
|
+
handleKeypress(async key => {
|
|
169
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'q') {
|
|
170
|
+
SpinniesManager.remove('migrateApp');
|
|
171
|
+
logger.log(i18n(`${i18nKey}.migrationInterrupted`));
|
|
172
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
137
176
|
const migrateResponse = await migrateApp(accountId, appId, projectName);
|
|
138
177
|
const { id } = migrateResponse;
|
|
139
178
|
const pollResponse = await poll(checkMigrationStatus, accountId, id);
|
|
@@ -152,6 +191,13 @@ exports.handler = async options => {
|
|
|
152
191
|
{ includesRootDir: true, hideLogs: true }
|
|
153
192
|
);
|
|
154
193
|
|
|
194
|
+
const isListed = Boolean(listingInfo);
|
|
195
|
+
trackCommandMetadataUsage(
|
|
196
|
+
'migrate-app',
|
|
197
|
+
{ projectName, appId, status, preventProjectMigrations, isListed },
|
|
198
|
+
accountId
|
|
199
|
+
);
|
|
200
|
+
|
|
155
201
|
SpinniesManager.succeed('migrateApp', {
|
|
156
202
|
text: i18n(`${i18nKey}.migrationStatus.done`),
|
|
157
203
|
succeedColor: 'white',
|
|
@@ -169,14 +215,23 @@ exports.handler = async options => {
|
|
|
169
215
|
process.exit(EXIT_CODES.SUCCESS);
|
|
170
216
|
}
|
|
171
217
|
} catch (error) {
|
|
218
|
+
trackCommandMetadataUsage(
|
|
219
|
+
'migrate-app',
|
|
220
|
+
{ projectName, appId, status: 'FAILURE', error },
|
|
221
|
+
accountId
|
|
222
|
+
);
|
|
172
223
|
SpinniesManager.fail('migrateApp', {
|
|
173
224
|
text: i18n(`${i18nKey}.migrationStatus.failure`),
|
|
174
225
|
failColor: 'white',
|
|
175
226
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
// Migrations endpoints return a response object with an errors property. The errors property contains an array of errors.
|
|
228
|
+
if (error.errors && Array.isArray(error.errors)) {
|
|
229
|
+
error.errors.forEach(e =>
|
|
230
|
+
logApiErrorInstance(e, new ApiErrorContext({ accountId }))
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
logApiErrorInstance(error, new ApiErrorContext({ accountId }));
|
|
234
|
+
}
|
|
180
235
|
|
|
181
236
|
process.exit(EXIT_CODES.ERROR);
|
|
182
237
|
}
|
package/commands/project.js
CHANGED
|
@@ -12,6 +12,7 @@ const open = require('./project/open');
|
|
|
12
12
|
const dev = require('./project/dev');
|
|
13
13
|
const add = require('./project/add');
|
|
14
14
|
const migrateApp = require('./project/migrateApp');
|
|
15
|
+
const cloneApp = require('./project/cloneApp');
|
|
15
16
|
|
|
16
17
|
const i18nKey = 'commands.project';
|
|
17
18
|
|
|
@@ -22,18 +23,20 @@ exports.builder = yargs => {
|
|
|
22
23
|
addConfigOptions(yargs);
|
|
23
24
|
addAccountOptions(yargs);
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
yargs
|
|
27
|
+
.command(create)
|
|
28
|
+
.command(add)
|
|
29
|
+
.command(watch)
|
|
30
|
+
.command(dev)
|
|
31
|
+
.command(upload)
|
|
32
|
+
.command(deploy)
|
|
33
|
+
.command(logs)
|
|
34
|
+
.command(listBuilds)
|
|
35
|
+
.command(download)
|
|
36
|
+
.command(open)
|
|
37
|
+
.command(migrateApp)
|
|
38
|
+
.command(cloneApp)
|
|
39
|
+
.demandCommand(1, '');
|
|
37
40
|
|
|
38
41
|
return yargs;
|
|
39
42
|
};
|