@hubspot/cli 5.3.1 → 5.4.1-beta.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 (39) hide show
  1. package/bin/cli.js +24 -5
  2. package/commands/__tests__/projects.test.js +105 -0
  3. package/commands/accounts/clean.js +1 -1
  4. package/commands/cms/convertFields.js +13 -7
  5. package/commands/project/__tests__/deploy.test.js +1 -1
  6. package/commands/project/__tests__/installDeps.test.js +168 -0
  7. package/commands/project/__tests__/logs.test.js +305 -0
  8. package/commands/project/add.js +24 -12
  9. package/commands/project/cloneApp.js +13 -21
  10. package/commands/project/deploy.js +4 -1
  11. package/commands/project/dev.js +22 -11
  12. package/commands/project/download.js +6 -3
  13. package/commands/project/installDeps.js +78 -0
  14. package/commands/project/logs.js +80 -242
  15. package/commands/project/migrateApp.js +8 -9
  16. package/commands/project/upload.js +5 -3
  17. package/commands/project/watch.js +3 -9
  18. package/commands/project.js +2 -0
  19. package/commands/sandbox/create.js +1 -0
  20. package/commands/sandbox.js +0 -2
  21. package/lang/en.lyaml +40 -75
  22. package/lib/LocalDevManager.js +1 -22
  23. package/lib/__tests__/dependencyManagement.test.js +245 -0
  24. package/lib/__tests__/projectLogsManager.test.js +210 -0
  25. package/lib/dependencyManagement.js +157 -0
  26. package/lib/errorHandlers/apiErrors.js +1 -3
  27. package/lib/errorHandlers/overrideErrors.js +57 -36
  28. package/lib/localDev.js +25 -16
  29. package/lib/projectLogsManager.js +144 -0
  30. package/lib/projects.js +17 -7
  31. package/lib/projectsWatch.js +2 -5
  32. package/lib/prompts/__tests__/projectsLogsPrompt.test.js +46 -0
  33. package/lib/prompts/createProjectPrompt.js +4 -0
  34. package/lib/prompts/projectAddPrompt.js +4 -21
  35. package/lib/prompts/projectDevTargetAccountPrompt.js +16 -25
  36. package/lib/prompts/projectsLogsPrompt.js +17 -108
  37. package/lib/sandboxSync.js +13 -15
  38. package/package.json +6 -6
  39. package/commands/sandbox/sync.js +0 -225
@@ -0,0 +1,305 @@
1
+ jest.mock('../../../lib/commonOpts');
2
+ jest.mock('../../../lib/usageTracking');
3
+ jest.mock('../../../lib/validation');
4
+ jest.mock('../../../lib/projectLogsManager');
5
+ jest.mock('../../../lib/prompts/projectsLogsPrompt');
6
+ jest.mock('@hubspot/local-dev-lib/logger');
7
+ jest.mock('../../../lib/errorHandlers/apiErrors');
8
+ jest.mock('../../../lib/ui/table');
9
+ jest.mock('../../../lib/ui');
10
+ jest.mock('../../../lib/errorHandlers/apiErrors');
11
+
12
+ // Deps where we don't want mocks
13
+ const libUi = jest.requireActual('../../../lib/ui');
14
+
15
+ const { uiLine, uiLink, uiBetaTag } = require('../../../lib/ui');
16
+
17
+ uiBetaTag.mockImplementation(libUi.uiBetaTag);
18
+
19
+ const {
20
+ addUseEnvironmentOptions,
21
+ getAccountId,
22
+ } = require('../../../lib/commonOpts');
23
+ const ProjectLogsManager = require('../../../lib/projectLogsManager');
24
+ const {
25
+ projectLogsPrompt,
26
+ } = require('../../../lib/prompts/projectsLogsPrompt');
27
+ const { getTableContents, getTableHeader } = require('../../../lib/ui/table');
28
+
29
+ const { trackCommandUsage } = require('../../../lib/usageTracking');
30
+ const { logApiErrorInstance } = require('../../../lib/errorHandlers/apiErrors');
31
+
32
+ const {
33
+ handler,
34
+ describe: logsDescribe,
35
+ command,
36
+ builder,
37
+ } = require('../logs');
38
+ const { EXIT_CODES } = require('../../../lib/enums/exitCodes');
39
+
40
+ describe('commands/project/logs', () => {
41
+ let processExitSpy;
42
+ beforeEach(() => {
43
+ processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
44
+ });
45
+
46
+ describe('command', () => {
47
+ it('should have the proper command string', async () => {
48
+ expect(command).toEqual('logs');
49
+ });
50
+ });
51
+
52
+ describe('describe', () => {
53
+ it('should contain the beta tag', () => {
54
+ expect(logsDescribe).toContain('[BETA]');
55
+ });
56
+ it('should provide an accurate description of what the command is doing', () => {
57
+ expect(logsDescribe).toMatch(
58
+ /Get execution logs for a serverless function within a project/
59
+ );
60
+ });
61
+ });
62
+
63
+ describe('builder', () => {
64
+ let yargsMock = {};
65
+ beforeEach(() => {
66
+ yargsMock = {
67
+ options: jest.fn().mockImplementation(() => yargsMock),
68
+ conflicts: jest.fn().mockImplementation(() => yargsMock),
69
+ example: jest.fn().mockImplementation(() => yargsMock),
70
+ };
71
+ });
72
+
73
+ it('should add all of the options', () => {
74
+ builder(yargsMock);
75
+ expect(yargsMock.options).toHaveBeenCalledTimes(1);
76
+ expect(yargsMock.options).toHaveBeenCalledWith({
77
+ function: {
78
+ alias: 'function',
79
+ requiresArg: true,
80
+ describe: 'App function name',
81
+ type: 'string',
82
+ },
83
+ latest: {
84
+ alias: 'l',
85
+ type: 'boolean',
86
+ describe: 'Retrieve most recent log only',
87
+ },
88
+ compact: {
89
+ type: 'boolean',
90
+ describe: 'Output compact logs',
91
+ },
92
+ tail: {
93
+ alias: ['t', 'follow'],
94
+ describe: 'Tail logs',
95
+ type: 'boolean',
96
+ },
97
+ limit: {
98
+ type: 'number',
99
+ describe: 'Limit the number of logs to output',
100
+ },
101
+ });
102
+ });
103
+
104
+ it('should add the environment options', () => {
105
+ builder(yargsMock);
106
+ expect(addUseEnvironmentOptions).toHaveBeenCalledTimes(1);
107
+ expect(addUseEnvironmentOptions).toHaveBeenCalledWith(yargsMock);
108
+ });
109
+
110
+ it('should set tail and limit as conflicting arguments', () => {
111
+ builder(yargsMock);
112
+ expect(yargsMock.conflicts).toHaveBeenCalledTimes(1);
113
+ expect(yargsMock.conflicts).toHaveBeenCalledWith('tail', 'limit');
114
+ });
115
+
116
+ it('should set examples', () => {
117
+ builder(yargsMock);
118
+ expect(yargsMock.example).toHaveBeenCalledTimes(1);
119
+ expect(yargsMock.example).toHaveBeenCalledWith([
120
+ [
121
+ '$0 project logs',
122
+ 'Open the project logs prompt to get logs for a serverless function',
123
+ ],
124
+ [
125
+ '$0 project logs --function=my-function',
126
+ 'Get logs for function named "my-function" within the app named "app" within the project named "my-project"',
127
+ ],
128
+ ]);
129
+ });
130
+ });
131
+
132
+ describe('handler', () => {
133
+ const accountId = 12345678;
134
+
135
+ beforeEach(() => {
136
+ getAccountId.mockReturnValue(accountId);
137
+ projectLogsPrompt.mockResolvedValue({ functionName: 'foo' });
138
+ });
139
+
140
+ it('should get the account id', async () => {
141
+ const options = {
142
+ foo: 'bar',
143
+ };
144
+ await handler(options);
145
+ expect(getAccountId).toHaveBeenCalledTimes(1);
146
+ expect(getAccountId).toHaveBeenCalledWith(options);
147
+ });
148
+
149
+ it('should track the command usage', async () => {
150
+ const options = {
151
+ foo: 'bar',
152
+ };
153
+ await handler(options);
154
+ expect(trackCommandUsage).toHaveBeenCalledTimes(1);
155
+ expect(trackCommandUsage).toHaveBeenCalledWith(
156
+ 'project-logs',
157
+ null,
158
+ accountId
159
+ );
160
+ });
161
+
162
+ it('should initialize the ProjectLogsManager', async () => {
163
+ const options = {
164
+ foo: 'bar',
165
+ };
166
+ await handler(options);
167
+ expect(ProjectLogsManager.init).toHaveBeenCalledTimes(1);
168
+ expect(ProjectLogsManager.init).toHaveBeenCalledWith(accountId);
169
+ });
170
+
171
+ it('should prompt the user for input', async () => {
172
+ const functionNames = ['function1', 'function2'];
173
+ ProjectLogsManager.getFunctionNames.mockReturnValue(functionNames);
174
+ const options = {
175
+ foo: 'bar',
176
+ };
177
+ await handler(options);
178
+ expect(projectLogsPrompt).toHaveBeenCalledTimes(1);
179
+ expect(projectLogsPrompt).toHaveBeenCalledWith({
180
+ functionChoices: functionNames,
181
+ promptOptions: options,
182
+ });
183
+ });
184
+
185
+ it('should set the function', async () => {
186
+ const selectedFunction = 'function1';
187
+ ProjectLogsManager.getFunctionNames.mockReturnValue([
188
+ selectedFunction,
189
+ 'function2',
190
+ ]);
191
+ projectLogsPrompt.mockReturnValue({
192
+ functionName: selectedFunction,
193
+ });
194
+
195
+ await handler({});
196
+ expect(ProjectLogsManager.setFunction).toHaveBeenCalledTimes(1);
197
+ expect(ProjectLogsManager.setFunction).toHaveBeenCalledWith(
198
+ selectedFunction
199
+ );
200
+ });
201
+
202
+ it('should log public functions correctly', async () => {
203
+ const functionNames = ['function1', 'function2'];
204
+ const selectedFunction = 'function1';
205
+ ProjectLogsManager.getFunctionNames.mockReturnValue(functionNames);
206
+ projectLogsPrompt.mockReturnValue({
207
+ functionName: selectedFunction,
208
+ });
209
+
210
+ const tableHeaders = ['Header 1', 'Header 2'];
211
+ getTableHeader.mockReturnValue(tableHeaders);
212
+
213
+ ProjectLogsManager.isPublicFunction = true;
214
+ ProjectLogsManager.accountId = accountId;
215
+ ProjectLogsManager.functionName = selectedFunction;
216
+ ProjectLogsManager.endpointName = 'my-endpoint';
217
+ ProjectLogsManager.appId = 123456;
218
+
219
+ await handler({});
220
+ expect(getTableHeader).toHaveBeenCalledTimes(1);
221
+ expect(getTableHeader).toHaveBeenCalledWith([
222
+ 'Account',
223
+ 'Function',
224
+ 'Endpoint',
225
+ ]);
226
+
227
+ expect(getTableContents).toHaveBeenCalledTimes(1);
228
+ expect(getTableContents).toHaveBeenCalledWith(
229
+ [
230
+ tableHeaders,
231
+ [
232
+ ProjectLogsManager.accountId,
233
+ ProjectLogsManager.functionName,
234
+ ProjectLogsManager.endpointName,
235
+ ],
236
+ ],
237
+ { border: { bodyLeft: ' ' } }
238
+ );
239
+ expect(uiLink).toHaveBeenCalledTimes(1);
240
+ expect(uiLink).toHaveBeenCalledWith(
241
+ 'View function logs in HubSpot',
242
+ `https://app.hubspot.com/private-apps/${accountId}/${ProjectLogsManager.appId}/logs/serverlessGatewayExecution?path=${ProjectLogsManager.endpointName}`
243
+ );
244
+ expect(uiLine).toHaveBeenCalledTimes(1);
245
+ });
246
+
247
+ it('should log private functions correctly', async () => {
248
+ const functionNames = ['function1', 'function2'];
249
+ const selectedFunction = 'function1';
250
+
251
+ ProjectLogsManager.getFunctionNames.mockReturnValue(functionNames);
252
+ projectLogsPrompt.mockReturnValue({
253
+ functionName: selectedFunction,
254
+ });
255
+
256
+ const tableHeaders = ['Header 1', 'Header 2'];
257
+ getTableHeader.mockReturnValue(tableHeaders);
258
+
259
+ ProjectLogsManager.isPublicFunction = false;
260
+ ProjectLogsManager.accountId = accountId;
261
+ ProjectLogsManager.functionName = selectedFunction;
262
+ ProjectLogsManager.appId = 456789;
263
+
264
+ await handler({});
265
+ expect(getTableHeader).toHaveBeenCalledTimes(1);
266
+ expect(getTableHeader).toHaveBeenCalledWith(['Account', 'Function']);
267
+
268
+ expect(getTableContents).toHaveBeenCalledTimes(1);
269
+ expect(getTableContents).toHaveBeenCalledWith(
270
+ [
271
+ tableHeaders,
272
+ [ProjectLogsManager.accountId, ProjectLogsManager.functionName],
273
+ ],
274
+ { border: { bodyLeft: ' ' } }
275
+ );
276
+
277
+ expect(uiLink).toHaveBeenCalledWith(
278
+ 'View function logs in HubSpot',
279
+ `https://app.hubspot.com/private-apps/${accountId}/${ProjectLogsManager.appId}/logs/crm?serverlessFunction=${selectedFunction}`
280
+ );
281
+
282
+ expect(uiLine).toHaveBeenCalledTimes(1);
283
+ });
284
+
285
+ it('should handle errors correctly', async () => {
286
+ const error = new Error('Something went wrong');
287
+ ProjectLogsManager.init.mockImplementation(() => {
288
+ throw error;
289
+ });
290
+
291
+ ProjectLogsManager.projectName = 'Super cool project';
292
+
293
+ await handler({});
294
+
295
+ expect(logApiErrorInstance).toHaveBeenCalledTimes(1);
296
+ expect(logApiErrorInstance).toHaveBeenCalledWith(error, {
297
+ accountId: accountId,
298
+ projectName: ProjectLogsManager.projectName,
299
+ });
300
+
301
+ expect(processExitSpy).toHaveBeenCalledTimes(1);
302
+ expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
303
+ });
304
+ });
305
+ });
@@ -6,7 +6,10 @@ const { fetchReleaseData } = require('@hubspot/local-dev-lib/github');
6
6
  const { trackCommandUsage } = require('../../lib/usageTracking');
7
7
  const { i18n } = require('../../lib/lang');
8
8
  const { projectAddPrompt } = require('../../lib/prompts/projectAddPrompt');
9
- const { createProjectComponent } = require('../../lib/projects');
9
+ const {
10
+ createProjectComponent,
11
+ getProjectComponentsByVersion,
12
+ } = require('../../lib/projects');
10
13
  const { loadAndValidateOptions } = require('../../lib/validation');
11
14
  const { uiBetaTag } = require('../../lib/ui');
12
15
  const {
@@ -21,34 +24,37 @@ exports.describe = uiBetaTag(i18n(`${i18nKey}.describe`), false);
21
24
  exports.handler = async options => {
22
25
  await loadAndValidateOptions(options);
23
26
 
24
- const accountId = getAccountId(options);
25
-
26
27
  logger.log('');
27
28
  logger.log(i18n(`${i18nKey}.creatingComponent.message`));
28
29
  logger.log('');
29
30
 
31
+ const accountId = getAccountId(options);
32
+
30
33
  const releaseData = await fetchReleaseData(
31
34
  HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH
32
35
  );
33
36
  const projectComponentsVersion = releaseData.tag_name;
34
37
 
35
- const { type, name } = await projectAddPrompt(
36
- projectComponentsVersion,
37
- options
38
+ const components = await getProjectComponentsByVersion(
39
+ projectComponentsVersion
38
40
  );
39
41
 
42
+ let { component, name } = await projectAddPrompt(components, options);
43
+
44
+ name = name || options.name;
45
+
46
+ if (!component) {
47
+ component = components.find(t => t.path === options.type);
48
+ }
49
+
40
50
  trackCommandUsage('project-add', null, accountId);
41
51
 
42
52
  try {
43
- await createProjectComponent(
44
- options.type || type,
45
- options.name || name,
46
- projectComponentsVersion
47
- );
53
+ await createProjectComponent(component, name, projectComponentsVersion);
48
54
  logger.log('');
49
55
  logger.log(
50
56
  i18n(`${i18nKey}.success.message`, {
51
- componentName: options.name || name,
57
+ componentName: name,
52
58
  })
53
59
  );
54
60
  } catch (error) {
@@ -69,6 +75,12 @@ exports.builder = yargs => {
69
75
  });
70
76
 
71
77
  yargs.example([['$0 project add', i18n(`${i18nKey}.examples.default`)]]);
78
+ yargs.example([
79
+ [
80
+ '$0 project add --name="my-component" --type="components/example-app"',
81
+ i18n(`${i18nKey}.examples.withFlags`),
82
+ ],
83
+ ]);
72
84
 
73
85
  return yargs;
74
86
  };
@@ -38,13 +38,10 @@ const {
38
38
  checkCloneStatus,
39
39
  downloadClonedProject,
40
40
  } = require('@hubspot/local-dev-lib/api/projects');
41
- const { getCwd } = require('@hubspot/local-dev-lib/path');
41
+ const { getCwd, sanitizeFileName } = require('@hubspot/local-dev-lib/path');
42
42
  const { logger } = require('@hubspot/local-dev-lib/logger');
43
43
  const { getAccountConfig } = require('@hubspot/local-dev-lib/config');
44
44
  const { extractZipArchive } = require('@hubspot/local-dev-lib/archive');
45
- const {
46
- fetchPublicAppMetadata,
47
- } = require('@hubspot/local-dev-lib/api/appsDev');
48
45
 
49
46
  const i18nKey = 'commands.project.subcommands.cloneApp';
50
47
 
@@ -76,8 +73,6 @@ exports.handler = async options => {
76
73
  let appId;
77
74
  let name;
78
75
  let location;
79
- let preventProjectMigrations;
80
- let listingInfo;
81
76
  try {
82
77
  appId = options.appId;
83
78
  if (!appId) {
@@ -89,11 +84,6 @@ exports.handler = async options => {
89
84
  });
90
85
  appId = appIdResponse.appId;
91
86
  }
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
87
 
98
88
  const projectResponse = await createProjectPrompt('', options, true);
99
89
  name = projectResponse.name;
@@ -119,10 +109,15 @@ exports.handler = async options => {
119
109
 
120
110
  // Extract zipped app files and place them in correct directory
121
111
  const zippedApp = await downloadClonedProject(accountId, exportId);
122
- await extractZipArchive(zippedApp, name, absoluteDestPath, {
123
- includesRootDir: true,
124
- hideLogs: true,
125
- });
112
+ await extractZipArchive(
113
+ zippedApp,
114
+ sanitizeFileName(name),
115
+ absoluteDestPath,
116
+ {
117
+ includesRootDir: true,
118
+ hideLogs: true,
119
+ }
120
+ );
126
121
 
127
122
  // Create hsproject.json file
128
123
  const configPath = path.join(baseDestPath, PROJECT_CONFIG_FILE);
@@ -133,15 +128,12 @@ exports.handler = async options => {
133
128
  };
134
129
  const success = writeProjectConfig(configPath, configContent);
135
130
 
136
- const isListed = Boolean(listingInfo);
137
131
  trackCommandMetadataUsage(
138
132
  'clone-app',
139
133
  {
140
- projectName: name,
141
- appId,
142
- status,
143
- preventProjectMigrations,
144
- isListed,
134
+ type: name,
135
+ assetType: appId,
136
+ successful: success,
145
137
  },
146
138
  accountId
147
139
  );
@@ -170,7 +170,10 @@ exports.handler = async options => {
170
170
  } else if (e.response && e.response.status === 400) {
171
171
  logger.error(e.message);
172
172
  } else {
173
- logApiErrorInstance(e, new ApiErrorContext({ accountId, projectName }));
173
+ logApiErrorInstance(
174
+ e,
175
+ new ApiErrorContext({ accountId, request: 'project deploy' })
176
+ );
174
177
  }
175
178
  return process.exit(EXIT_CODES.ERROR);
176
179
  }
@@ -46,12 +46,12 @@ const {
46
46
  confirmDefaultAccountIsTarget,
47
47
  suggestRecommendedNestedAccount,
48
48
  checkIfAppDeveloperAccount,
49
- checkIfDeveloperTestAccount,
50
49
  createSandboxForLocalDev,
51
50
  createDeveloperTestAccountForLocalDev,
52
51
  createNewProjectForLocalDev,
53
52
  createInitialBuildForNewProject,
54
53
  useExistingDevTestAccount,
54
+ validateAccountOption,
55
55
  } = require('../../lib/localDev');
56
56
 
57
57
  const i18nKey = 'commands.project.subcommands.dev';
@@ -86,11 +86,20 @@ exports.handler = async options => {
86
86
  validateProjectConfig(projectConfig, projectDir);
87
87
 
88
88
  const components = await findProjectComponents(projectDir);
89
- const componentTypes = getProjectComponentTypes(components);
89
+ const runnableComponents = components.filter(component => component.runnable);
90
+ const componentTypes = getProjectComponentTypes(runnableComponents);
90
91
  const hasPrivateApps = !!componentTypes[COMPONENT_TYPES.privateApp];
91
92
  const hasPublicApps = !!componentTypes[COMPONENT_TYPES.publicApp];
92
93
 
93
- if (hasPrivateApps && hasPublicApps) {
94
+ if (runnableComponents.length === 0) {
95
+ logger.error(
96
+ i18n(`${i18nKey}.errors.noRunnableComponents`, {
97
+ projectDir,
98
+ command: uiCommandReference('hs project add'),
99
+ })
100
+ );
101
+ process.exit(EXIT_CODES.SUCCESS);
102
+ } else if (hasPrivateApps && hasPublicApps) {
94
103
  logger.error(i18n(`${i18nKey}.errors.invalidProjectComponents`));
95
104
  process.exit(EXIT_CODES.SUCCESS);
96
105
  }
@@ -106,14 +115,13 @@ exports.handler = async options => {
106
115
  // The account that we are locally testing against
107
116
  let targetTestingAccountId = options.account ? accountId : null;
108
117
 
109
- if (options.account && hasPublicApps) {
110
- checkIfDeveloperTestAccount(accountConfig);
111
- targetProjectAccountId = accountConfig.parentAccountId;
112
- targetTestingAccountId = accountId;
113
- }
118
+ if (options.account) {
119
+ validateAccountOption(accountConfig, hasPublicApps);
114
120
 
115
- let createNewSandbox = false;
116
- let createNewDeveloperTestAccount = false;
121
+ if (hasPublicApps) {
122
+ targetProjectAccountId = accountConfig.parentAccountId;
123
+ }
124
+ }
117
125
 
118
126
  // The user is targeting an account type that we recommend developing on
119
127
  if (!targetProjectAccountId && defaultAccountIsRecommendedType) {
@@ -143,6 +151,9 @@ exports.handler = async options => {
143
151
  checkIfAppDeveloperAccount(accountConfig);
144
152
  }
145
153
 
154
+ let createNewSandbox = false;
155
+ let createNewDeveloperTestAccount = false;
156
+
146
157
  if (!targetProjectAccountId) {
147
158
  const {
148
159
  targetAccountId,
@@ -222,7 +233,7 @@ exports.handler = async options => {
222
233
  }
223
234
 
224
235
  const LocalDev = new LocalDevManager({
225
- components,
236
+ runnableComponents,
226
237
  debug: options.debug,
227
238
  deployedBuild,
228
239
  isGithubLinked,
@@ -6,7 +6,7 @@ const {
6
6
  addUseEnvironmentOptions,
7
7
  } = require('../../lib/commonOpts');
8
8
  const { trackCommandUsage } = require('../../lib/usageTracking');
9
- const { getCwd } = require('@hubspot/local-dev-lib/path');
9
+ const { getCwd, sanitizeFileName } = require('@hubspot/local-dev-lib/path');
10
10
  const {
11
11
  logApiErrorInstance,
12
12
  ApiErrorContext,
@@ -95,7 +95,7 @@ exports.handler = async options => {
95
95
 
96
96
  await extractZipArchive(
97
97
  zippedProject,
98
- projectName,
98
+ sanitizeFileName(projectName),
99
99
  path.resolve(absoluteDestPath),
100
100
  { includesRootDir: false }
101
101
  );
@@ -108,7 +108,10 @@ exports.handler = async options => {
108
108
  );
109
109
  process.exit(EXIT_CODES.SUCCESS);
110
110
  } catch (e) {
111
- logApiErrorInstance(e, new ApiErrorContext({ accountId, projectName }));
111
+ logApiErrorInstance(
112
+ e,
113
+ new ApiErrorContext({ accountId, request: 'project download' })
114
+ );
112
115
  process.exit(EXIT_CODES.ERROR);
113
116
  }
114
117
  };
@@ -0,0 +1,78 @@
1
+ const {
2
+ installPackages,
3
+ getProjectPackageJsonLocations,
4
+ } = require('../../lib/dependencyManagement');
5
+ const { logger } = require('@hubspot/local-dev-lib/logger');
6
+ const { EXIT_CODES } = require('../../lib/enums/exitCodes');
7
+ const { getProjectConfig } = require('../../lib/projects');
8
+ const { promptUser } = require('../../lib/prompts/promptUtils');
9
+ const path = require('path');
10
+ const { i18n } = require('../../lib/lang');
11
+ const { trackCommandUsage } = require('../../lib/usageTracking');
12
+ const { getAccountId } = require('../../lib/commonOpts');
13
+
14
+ const i18nKey = `commands.project.subcommands.installDeps`;
15
+
16
+ exports.command = 'install-deps [packages..]';
17
+ // Intentionally making this null to hide command
18
+ exports.describe = null;
19
+ // exports.describe = uiBetaTag(i18n(`${i18nKey}.help.describe`), false);
20
+
21
+ exports.handler = async ({ packages }) => {
22
+ try {
23
+ const accountId = getAccountId();
24
+ trackCommandUsage('project-install-deps', null, accountId);
25
+
26
+ const projectConfig = await getProjectConfig();
27
+ if (!projectConfig || !projectConfig.projectDir) {
28
+ logger.error(i18n(`${i18nKey}.noProjectConfig`));
29
+ return process.exit(EXIT_CODES.ERROR);
30
+ }
31
+
32
+ const { projectDir } = projectConfig;
33
+
34
+ let installLocations = await getProjectPackageJsonLocations();
35
+ if (packages) {
36
+ const { selectedInstallLocations } = await promptUser([
37
+ {
38
+ name: 'selectedInstallLocations',
39
+ type: 'checkbox',
40
+ when: () => packages && packages.length > 0,
41
+ message: i18n(`${i18nKey}.installLocationPrompt`),
42
+ choices: installLocations.map(dir => ({
43
+ name: path.relative(projectDir, dir),
44
+ value: dir,
45
+ })),
46
+ validate: choices => {
47
+ if (choices === undefined || choices.length === 0) {
48
+ return i18n(`${i18nKey}.installLocationPromptRequired`);
49
+ }
50
+ return true;
51
+ },
52
+ },
53
+ ]);
54
+ if (selectedInstallLocations) {
55
+ installLocations = selectedInstallLocations;
56
+ }
57
+ }
58
+
59
+ await installPackages({
60
+ packages,
61
+ installLocations,
62
+ });
63
+ } catch (e) {
64
+ logger.debug(e);
65
+ logger.error(e.message);
66
+ return process.exit(EXIT_CODES.ERROR);
67
+ }
68
+ };
69
+
70
+ exports.builder = yargs => {
71
+ yargs.example([
72
+ ['$0 project install-deps', i18n(`${i18nKey}.help.installAppDepsExample`)],
73
+ [
74
+ '$0 project install-deps dependency1 dependency2',
75
+ i18n(`${i18nKey}.help.addDepToSubComponentExample`),
76
+ ],
77
+ ]);
78
+ };