@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.
Files changed (43) hide show
  1. package/commands/auth.js +2 -4
  2. package/commands/functions/deploy.js +2 -2
  3. package/commands/init.js +2 -4
  4. package/commands/module/marketplace-validate.js +1 -1
  5. package/commands/project/__tests__/deploy.test.js +432 -0
  6. package/commands/project/cloneApp.js +208 -0
  7. package/commands/project/deploy.js +82 -27
  8. package/commands/project/listBuilds.js +1 -1
  9. package/commands/project/logs.js +1 -1
  10. package/commands/project/migrateApp.js +85 -30
  11. package/commands/project/upload.js +1 -1
  12. package/commands/project.js +15 -12
  13. package/commands/sandbox/create.js +17 -48
  14. package/commands/sandbox/delete.js +5 -2
  15. package/commands/sandbox/sync.js +12 -2
  16. package/commands/sandbox.js +2 -1
  17. package/commands/theme/marketplace-validate.js +1 -1
  18. package/lang/en.lyaml +77 -76
  19. package/lib/LocalDevManager.js +59 -11
  20. package/lib/buildAccount.js +3 -3
  21. package/lib/constants.js +3 -1
  22. package/lib/interpolationHelpers.js +3 -0
  23. package/lib/localDev.js +21 -11
  24. package/lib/marketplace-validate.js +11 -3
  25. package/lib/polling.js +16 -10
  26. package/lib/projects.js +143 -100
  27. package/lib/prompts/accountNamePrompt.js +78 -0
  28. package/lib/prompts/activeInstallConfirmationPrompt.js +20 -0
  29. package/lib/prompts/createProjectPrompt.js +12 -2
  30. package/lib/prompts/deployBuildIdPrompt.js +22 -0
  31. package/lib/prompts/installPublicAppPrompt.js +13 -4
  32. package/lib/prompts/personalAccessKeyPrompt.js +2 -2
  33. package/lib/prompts/sandboxesPrompt.js +12 -41
  34. package/lib/prompts/selectPublicAppPrompt.js +41 -22
  35. package/lib/sandboxSync.js +49 -68
  36. package/lib/sandboxes.js +8 -149
  37. package/lib/serverlessLogs.js +2 -2
  38. package/lib/ui/index.js +74 -0
  39. package/package.json +5 -4
  40. package/lib/prompts/buildIdPrompt.js +0 -35
  41. package/lib/prompts/developerTestAccountNamePrompt.js +0 -29
  42. package/lib/prompts/enterAccountNamePrompt.js +0 -33
  43. 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 { getProjectConfig, pollDeployStatus } = require('../../lib/projects');
19
+ const {
20
+ getProjectConfig,
21
+ pollDeployStatus,
22
+ getProjectDetailUrl,
23
+ } = require('../../lib/projects');
20
24
  const { projectNamePrompt } = require('../../lib/prompts/projectNamePrompt');
21
- const { buildIdPrompt } = require('../../lib/prompts/buildIdPrompt');
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 [--project] [--buildId]';
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
- if (!buildIdOption) {
63
- const { latestBuild, deployedBuildId } = await fetchProject(
64
- accountId,
65
- projectName
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 (!latestBuild || !latestBuild.buildId) {
68
- logger.error(i18n(`${i18nKey}.errors.noBuilds`));
69
- process.exit(EXIT_CODES.ERROR);
115
+ if (validationResult !== true) {
116
+ logger.error(validationResult);
117
+ return process.exit(EXIT_CODES.ERROR);
70
118
  }
71
- const buildIdPromptResponse = await buildIdPrompt(
119
+ } else {
120
+ const deployBuildIdPromptResponse = await deployBuildIdPrompt(
72
121
  latestBuild.buildId,
73
122
  deployedBuildId,
74
- projectName
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.statusCode === 404) {
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
- if (e.statusCode === 400) {
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
- buildId: {
132
- describe: i18n(`${i18nKey}.options.buildId.describe`),
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" --buildId=5',
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.statusCode === 404) {
127
+ if (e.response && e.response.status === 404) {
128
128
  logger.error(`Project ${projectConfig.name} not found. `);
129
129
  } else {
130
130
  logApiErrorInstance(
@@ -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.statusCode === 404) {
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 { trackCommandUsage } = require('../../lib/usageTracking');
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
- logger.error(
62
- i18n(`${i18nKey}.errors.invalidAccountType`, {
63
- accountName,
64
- accountType: accountConfig.accountType,
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
- process.exit(EXIT_CODES.ERROR);
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
- options,
79
- migrateApp: true,
85
+ isMigratingApp: true,
80
86
  });
81
87
 
82
- const publicApps = await fetchPublicAppOptions(accountId, accountName);
83
- if (!publicApps.find(a => a.id === appId)) {
84
- logger.error(i18n(`${i18nKey}.errors.invalidAppId`, { appId }));
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
- const { name, location } = await createProjectPrompt('', options, true);
89
-
90
- const projectName = options.name || name;
91
- const projectLocation = options.location || location;
107
+ let projectName;
108
+ let projectLocation;
109
+ try {
110
+ const { name, location } = await createProjectPrompt('', options, true);
92
111
 
93
- const { projectExists } = await ensureProjectExists(accountId, projectName, {
94
- allowCreate: false,
95
- noLogs: true,
96
- });
112
+ projectName = options.name || name;
113
+ projectLocation = options.location || location;
97
114
 
98
- if (projectExists) {
99
- logger.error(
100
- i18n(`${i18nKey}.errors.projectAlreadyExists`, {
101
- projectName,
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`), false));
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
- logApiErrorInstance(
177
- error.error || error,
178
- new ApiErrorContext({ accountId })
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
  }
@@ -92,7 +92,7 @@ exports.handler = async options => {
92
92
  logger.log(
93
93
  i18n(`${i18nKey}.logs.autoDeployDisabled`, {
94
94
  deployCommand: uiCommandReference(
95
- `hs project deploy --buildId=${result.buildId}`
95
+ `hs project deploy --build=${result.buildId}`
96
96
  ),
97
97
  })
98
98
  );
@@ -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
- // TODO: deploy must be updated
26
- yargs.command(create).demandCommand(0, '');
27
- yargs.command(add).demandCommand(0, '');
28
- yargs.command(watch).demandCommand(0, '');
29
- yargs.command(dev).demandCommand(0, '');
30
- yargs.command(upload).demandCommand(0, '');
31
- yargs.command(deploy).demandCommand(1, '');
32
- yargs.command(logs).demandCommand(1, '');
33
- yargs.command(listBuilds).demandCommand(0, '');
34
- yargs.command(download).demandCommand(0, '');
35
- yargs.command(open).demandCommand(0, '');
36
- yargs.command(migrateApp).demandCommand(0, '');
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
  };