@hubspot/cli 7.9.0-beta.1 → 7.9.0-experimental.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 (105) hide show
  1. package/commands/__tests__/project.test.js +2 -0
  2. package/commands/account/createOverride.js +2 -12
  3. package/commands/account/removeOverride.js +2 -10
  4. package/commands/cms/theme/preview.js +1 -4
  5. package/commands/getStarted.js +7 -19
  6. package/commands/project/__tests__/deploy.test.js +4 -3
  7. package/commands/project/__tests__/updateDeps.test.d.ts +1 -0
  8. package/commands/project/__tests__/updateDeps.test.js +142 -0
  9. package/commands/project/create.js +0 -1
  10. package/commands/project/updateDeps.d.ts +6 -0
  11. package/commands/project/updateDeps.js +80 -0
  12. package/commands/project.js +2 -0
  13. package/commands/testAccount/create.js +1 -1
  14. package/lang/en.d.ts +30 -15
  15. package/lang/en.js +32 -18
  16. package/lib/__tests__/dependencyManagement.test.js +273 -1
  17. package/lib/__tests__/npm.test.js +1 -1
  18. package/lib/__tests__/sandboxSync.test.js +1 -1
  19. package/lib/__tests__/usageTracking.test.js +2 -2
  20. package/lib/commonOpts.js +2 -5
  21. package/lib/configMigrate.js +3 -3
  22. package/lib/dependencyManagement.d.ts +8 -2
  23. package/lib/dependencyManagement.js +75 -12
  24. package/lib/doctor/DiagnosticInfoBuilder.js +1 -1
  25. package/lib/doctor/Doctor.js +1 -1
  26. package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +4 -2
  27. package/lib/doctor/__tests__/Doctor.test.js +1 -1
  28. package/lib/jsonLoader.d.ts +14 -0
  29. package/lib/jsonLoader.js +60 -0
  30. package/lib/middleware/__test__/requestMiddleware.test.js +1 -1
  31. package/lib/middleware/autoUpdateMiddleware.js +1 -1
  32. package/lib/middleware/commandTargetingUtils.js +1 -0
  33. package/lib/middleware/fireAlarmMiddleware.js +1 -1
  34. package/lib/middleware/notificationsMiddleware.js +1 -1
  35. package/lib/middleware/requestMiddleware.js +1 -1
  36. package/lib/npm.d.ts +3 -0
  37. package/lib/npm.js +7 -1
  38. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -0
  39. package/lib/projects/__tests__/platformVersion.test.js +5 -1
  40. package/lib/projects/create/__tests__/v2.test.js +20 -14
  41. package/lib/projects/create/v2.js +8 -13
  42. package/lib/projects/localDev/LocalDevLogger.js +2 -2
  43. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
  44. package/lib/projects/localDev/LocalDevWebsocketServer.js +1 -1
  45. package/lib/projects/platformVersion.js +1 -1
  46. package/lib/prompts/promptUtils.d.ts +8 -0
  47. package/lib/prompts/promptUtils.js +7 -1
  48. package/lib/prompts/selectProjectTemplatePrompt.js +4 -0
  49. package/lib/sandboxSync.js +1 -1
  50. package/lib/usageTracking.js +2 -2
  51. package/mcp-server/tools/cms/HsCreateFunctionTool.js +2 -2
  52. package/mcp-server/tools/cms/HsCreateModuleTool.js +2 -2
  53. package/mcp-server/tools/cms/HsCreateTemplateTool.js +2 -2
  54. package/mcp-server/tools/cms/HsFunctionLogsTool.js +2 -9
  55. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  56. package/mcp-server/tools/cms/HsListTool.js +1 -1
  57. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +7 -4
  58. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +7 -3
  59. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +7 -4
  60. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +5 -1
  61. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +8 -3
  62. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +8 -3
  63. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +4 -1
  64. package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -5
  65. package/mcp-server/tools/project/CreateProjectTool.js +2 -2
  66. package/mcp-server/tools/project/DeployProjectTool.d.ts +4 -1
  67. package/mcp-server/tools/project/DeployProjectTool.js +4 -3
  68. package/mcp-server/tools/project/DocFetchTool.d.ts +4 -1
  69. package/mcp-server/tools/project/DocFetchTool.js +7 -6
  70. package/mcp-server/tools/project/DocsSearchTool.js +5 -5
  71. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +4 -1
  72. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +7 -5
  73. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +8 -2
  74. package/mcp-server/tools/project/GetApplicationInfoTool.js +7 -6
  75. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -4
  76. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +4 -1
  77. package/mcp-server/tools/project/GuidedWalkthroughTool.js +6 -14
  78. package/mcp-server/tools/project/UploadProjectTools.d.ts +4 -1
  79. package/mcp-server/tools/project/UploadProjectTools.js +4 -3
  80. package/mcp-server/tools/project/ValidateProjectTool.d.ts +4 -1
  81. package/mcp-server/tools/project/ValidateProjectTool.js +5 -4
  82. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +6 -1
  83. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +7 -3
  84. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +7 -2
  85. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +8 -3
  86. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +6 -2
  87. package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +8 -3
  88. package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +9 -5
  89. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +6 -2
  90. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +43 -13
  91. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +8 -2
  92. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +8 -2
  93. package/mcp-server/utils/__tests__/content.test.d.ts +1 -0
  94. package/mcp-server/utils/__tests__/content.test.js +166 -0
  95. package/mcp-server/utils/__tests__/feedbackTracking.test.d.ts +1 -0
  96. package/mcp-server/utils/__tests__/feedbackTracking.test.js +121 -0
  97. package/mcp-server/utils/content.d.ts +1 -1
  98. package/mcp-server/utils/content.js +8 -1
  99. package/mcp-server/utils/feedbackTracking.d.ts +1 -0
  100. package/mcp-server/utils/feedbackTracking.js +41 -0
  101. package/package.json +2 -2
  102. package/commands/project/__tests__/fixtures/exampleProject.json +0 -33
  103. package/lang/en.lyaml +0 -1508
  104. package/lib/lang.d.ts +0 -8
  105. package/lib/lang.js +0 -72
package/lang/en.js CHANGED
@@ -51,12 +51,11 @@ export const commands = {
51
51
  uploadProject: (accountName) => `Would you like to upload this project to account "${accountName}" now?`,
52
52
  projectCreated: {
53
53
  title: chalk.bold('Next steps:'),
54
- description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project install-deps')} to ${chalk.bold('install dependencies')} and ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
54
+ description: `Let's prepare and upload your project to HubSpot.\nYou can use ${uiCommandReference('hs project upload')} to ${chalk.bold('upload')} your project.`,
55
55
  },
56
56
  },
57
57
  logs: {
58
58
  appSelected: `We'll create a new project with a sample app for you.\nProjects are what you can use to create apps with HubSpot.\nUsually you'll use the ${uiCommandReference('hs project create')} command, but we'll go ahead and make one now.`,
59
- dependenciesInstalled: 'Dependencies installed successfully.',
60
59
  uploadingProject: 'Uploading your project to HubSpot...',
61
60
  uploadSuccess: 'Project uploaded successfully!',
62
61
  developerOverviewLink: 'Open this link to navigate to your HubSpot developer portal',
@@ -64,7 +63,6 @@ export const commands = {
64
63
  errors: {
65
64
  uploadFailed: 'Failed to upload project to HubSpot.',
66
65
  configFileNotFound: 'Could not find project configuration for upload.',
67
- installDepsFailed: 'Failed to install dependencies.',
68
66
  },
69
67
  },
70
68
  completion: {
@@ -1382,7 +1380,7 @@ export const commands = {
1382
1380
  welcomeMessage: `\n${chalk.bold('Welcome to HubSpot Developer Projects!')}`,
1383
1381
  },
1384
1382
  prompts: {
1385
- parentComponents: '[--project-base] What would you like in your project?',
1383
+ parentComponents: '[--project-base] Choose what to include in your project:',
1386
1384
  emptyProject: 'Empty Project',
1387
1385
  app: 'App',
1388
1386
  },
@@ -1526,9 +1524,9 @@ export const commands = {
1526
1524
  failedToDownloadComponent: 'Failed to download project. Please try again later.',
1527
1525
  invalidComponentType: (componentType) => `'${componentType}' is not a valid project component type.`,
1528
1526
  maxExceeded: (maxCount) => `This project has the maximum allowed(${maxCount})`,
1529
- authTypeNotAllowed: (authType) => `Auth type '${authType}' not allowed.`,
1530
- distributionNotAllowed: (dist) => `Distribution '${dist}' not allowed.`,
1531
- portalDoesNotHaveAccessToThisFeature: (accountId) => `The account ${uiAccountDescription(accountId)} does not have access to this feature.`,
1527
+ authTypeNotAllowed: (authType) => `Requires auth type '${authType}'.`,
1528
+ distributionNotAllowed: (dist) => `Requires distribution '${dist}'.`,
1529
+ portalDoesNotHaveAccessToThisFeature: () => "This account doesn't have access to this feature.",
1532
1530
  locationInProject: 'This command must be run from within a project directory.',
1533
1531
  failedToFetchComponentList: 'Failed to fetch the list of available features. Please try again later.',
1534
1532
  projectContainsPublicApp: 'This project contains a public app. This command is currently only compatible with projects that contain private apps.',
@@ -1765,6 +1763,22 @@ export const commands = {
1765
1763
  noPackageJsonInProject: (projectName) => `No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
1766
1764
  packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
1767
1765
  },
1766
+ updateDeps: {
1767
+ help: {
1768
+ describe: 'Update the npm dependencies for your project, or update specific dependencies in a subcomponent of a project.',
1769
+ updateAppDepsExample: 'Update the dependencies for the project',
1770
+ updateDepToSubComponentExample: 'Update the npm dependencies in one or more project subcomponents',
1771
+ },
1772
+ installLocationPrompt: 'Choose which project components you would like to update the dependencies for:',
1773
+ installLocationPromptRequired: 'You must choose at least one subcomponent',
1774
+ updatingDependencies: (directory) => `Updating dependencies in ${directory}`,
1775
+ updateSuccessful: (directory) => `Updated dependencies in ${directory}`,
1776
+ updatingDependenciesToLocation: (dependencies, directory) => `Updating ${dependencies} in ${directory}`,
1777
+ updatingDependenciesFailed: (directory) => `Updating dependencies for ${directory} failed`,
1778
+ noProjectConfig: 'No project detected. Run this command from a project directory.',
1779
+ noPackageJsonInProject: (projectName) => `No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files. ${uiLink('Learn how to create a project from scratch', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app#customize-a-new-project-using-the-cli')}`,
1780
+ packageManagerNotInstalled: (packageManager) => `This command depends on ${packageManager}, install ${uiLink(packageManager, 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm')}`,
1781
+ },
1768
1782
  validate: {
1769
1783
  describe: 'Validate the project before uploading',
1770
1784
  mustBeRanWithinAProject: 'This command must be run from within a project directory.',
@@ -2082,7 +2096,7 @@ export const commands = {
2082
2096
  },
2083
2097
  },
2084
2098
  create: {
2085
- describe: 'Create a test account from a config file',
2099
+ describe: `Create a test account from scratch or from a config file. Use ${uiCommandReference('hs test-account create-config')} to generate a config file. \n${uiLink('Learn more', 'https://developers.hubspot.com/docs/developer-tooling/local-development/configurable-test-accounts')}`,
2086
2100
  configPathPrompt: '[--config-path] Enter the path to the test account config: ',
2087
2101
  createTestAccountFromConfigPrompt: 'How would you like to create your test account?',
2088
2102
  createFromConfigOption: 'Create test account from config file',
@@ -2765,8 +2779,8 @@ export const lib = {
2765
2779
  failedToInitialize: 'Missing required arguments to initialize Local Dev',
2766
2780
  noDeployedBuild: (projectName, accountIdentifier, uploadCommand) => `Your project ${chalk.bold(projectName)} exists in ${accountIdentifier}, but has no deployed build. Projects must be successfully deployed to be developed locally. Address any build and deploy errors your project may have, then run ${uploadCommand} to upload and deploy your project.`,
2767
2781
  noComponents: 'There are no components in this project.',
2768
- betaMessage: 'HubSpot projects local development',
2769
- learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
2782
+ headerMessage: 'HubSpot projects local development',
2783
+ learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands'),
2770
2784
  running: (projectName, accountIdentifier) => chalk.hex(UI_COLORS.SORBET)(`Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`),
2771
2785
  quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`,
2772
2786
  viewProjectLink: (name, accountId) => uiLink('View project in HubSpot', getProjectDetailUrl(name, accountId) || ''),
@@ -2949,8 +2963,8 @@ export const lib = {
2949
2963
  prompt: {
2950
2964
  marketPlaceDistribution: 'On the HubSpot marketplace',
2951
2965
  privateDistribution: 'Privately',
2952
- distribution: '[--distribution] How would you like to distribute your application?',
2953
- auth: '[--auth] What type of authentication would you like your application to use',
2966
+ distribution: '[--distribution] Choose how to distribute your application:',
2967
+ auth: '[--auth] Choose your authentication type:',
2954
2968
  staticAuth: 'Static Auth',
2955
2969
  oauth: 'OAuth',
2956
2970
  },
@@ -2964,7 +2978,7 @@ export const lib = {
2964
2978
  },
2965
2979
  },
2966
2980
  add: {
2967
- nothingAdded: 'No features added.',
2981
+ nothingAdded: 'No features were added to the project. Use the space bar to select features from the list.',
2968
2982
  },
2969
2983
  updateHsMetaFilesWithAutoGeneratedFields: {
2970
2984
  header: 'Created the following components and features:',
@@ -3355,8 +3369,8 @@ export const lib = {
3355
3369
  },
3356
3370
  },
3357
3371
  projectNameAndDestPrompt: {
3358
- enterName: '[--name] Give your project a name: ',
3359
- enterDest: '[--dest] Enter the folder to create the project in:',
3372
+ enterName: '[--name] Enter your project name:',
3373
+ enterDest: '[--dest] Choose where to create the project:',
3360
3374
  errors: {
3361
3375
  nameRequired: 'A project name is required',
3362
3376
  destRequired: 'A project dest is required',
@@ -3366,7 +3380,7 @@ export const lib = {
3366
3380
  },
3367
3381
  selectProjectTemplatePrompt: {
3368
3382
  selectTemplate: '[--template] Choose a project template: ',
3369
- features: '[--features] Which features would you like your app to include?',
3383
+ features: '[--features] Choose which features to add:',
3370
3384
  errors: {
3371
3385
  invalidTemplate: (template) => `[--template] Could not find template "${template}". Please choose an available template:`,
3372
3386
  projectTemplateRequired: 'Project template is required when projectTemplates is provided',
@@ -3400,8 +3414,8 @@ export const lib = {
3400
3414
  },
3401
3415
  },
3402
3416
  projectAddPrompt: {
3403
- selectType: '[--type] Select an app feature to add: ',
3404
- selectFeatures: '[--features] Select an app feature to add: ',
3417
+ selectType: '[--type] Choose which features to add: ',
3418
+ selectFeatures: '[--features] Choose which features to add: ',
3405
3419
  enterName: '[--name] Give your component a name: ',
3406
3420
  errors: {
3407
3421
  nameRequired: 'A component name is required',
@@ -1,5 +1,5 @@
1
1
  import util from 'util';
2
- import { installPackages, getProjectPackageJsonLocations, } from '../dependencyManagement.js';
2
+ import { installPackages, updatePackages, getProjectPackageJsonLocations, isPackageInstalled, } from '../dependencyManagement.js';
3
3
  import { walk } from '@hubspot/local-dev-lib/fs';
4
4
  import path from 'path';
5
5
  import { getProjectConfig } from '../projects/config.js';
@@ -105,6 +105,54 @@ describe('lib/dependencyManagement', () => {
105
105
  cwd: extensionsDir,
106
106
  });
107
107
  });
108
+ it('should install packages as dev dependencies when dev flag is true', async () => {
109
+ const packages = ['eslint', 'prettier'];
110
+ await installPackages({ packages, installLocations, dev: true });
111
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
112
+ for (const location of installLocations) {
113
+ expect(execMock).toHaveBeenCalledWith(`npm install --save-dev eslint prettier`, {
114
+ cwd: location,
115
+ });
116
+ }
117
+ });
118
+ it('should install packages as regular dependencies when dev flag is false', async () => {
119
+ const packages = ['react', 'react-dom'];
120
+ await installPackages({ packages, installLocations, dev: false });
121
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
122
+ for (const location of installLocations) {
123
+ expect(execMock).toHaveBeenCalledWith(`npm install react react-dom`, {
124
+ cwd: location,
125
+ });
126
+ }
127
+ });
128
+ it('should install packages as regular dependencies when dev flag is not provided', async () => {
129
+ const packages = ['axios'];
130
+ await installPackages({ packages, installLocations });
131
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
132
+ for (const location of installLocations) {
133
+ expect(execMock).toHaveBeenCalledWith(`npm install axios`, {
134
+ cwd: location,
135
+ });
136
+ }
137
+ });
138
+ it('should not use --save-dev flag when dev is true but no packages are provided', async () => {
139
+ await installPackages({ installLocations, dev: true });
140
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
141
+ for (const location of installLocations) {
142
+ expect(execMock).toHaveBeenCalledWith(`npm install `, {
143
+ cwd: location,
144
+ });
145
+ }
146
+ });
147
+ it('should not use --save-dev flag when dev is true but packages array is empty', async () => {
148
+ await installPackages({ packages: [], installLocations, dev: true });
149
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
150
+ for (const location of installLocations) {
151
+ expect(execMock).toHaveBeenCalledWith(`npm install `, {
152
+ cwd: location,
153
+ });
154
+ }
155
+ });
108
156
  it('should throw an error when installing the dependencies fails', async () => {
109
157
  execMock = vi.fn().mockImplementation(command => {
110
158
  if (command === 'npm --version') {
@@ -133,6 +181,92 @@ describe('lib/dependencyManagement', () => {
133
181
  });
134
182
  });
135
183
  });
184
+ describe('updatePackages()', () => {
185
+ it('should setup a loading spinner', async () => {
186
+ const packages = ['package1', 'package2'];
187
+ await updatePackages({ packages, installLocations });
188
+ expect(SpinniesManager.init).toHaveBeenCalledTimes(installLocations.length);
189
+ expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
190
+ expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
191
+ });
192
+ it('should update the provided packages in all the provided install locations', async () => {
193
+ const packages = ['package1', 'package2'];
194
+ await updatePackages({ packages, installLocations });
195
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
196
+ expect(SpinniesManager.add).toHaveBeenCalledTimes(installLocations.length);
197
+ expect(SpinniesManager.succeed).toHaveBeenCalledTimes(installLocations.length);
198
+ for (const location of installLocations) {
199
+ expect(execMock).toHaveBeenCalledWith(`npm update package1 package2`, {
200
+ cwd: location,
201
+ });
202
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
203
+ text: `Updating [package1, package2] in ${location}`,
204
+ });
205
+ expect(SpinniesManager.succeed).toHaveBeenCalledWith(`updatingDependencies-${location}`, {
206
+ text: `Updated dependencies in ${location}`,
207
+ });
208
+ }
209
+ });
210
+ it('should use the provided install locations', async () => {
211
+ await updatePackages({ installLocations });
212
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length);
213
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
214
+ cwd: appFunctionsDir,
215
+ });
216
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
217
+ cwd: extensionsDir,
218
+ });
219
+ });
220
+ it('should locate the projects package.json files when install locations is not provided', async () => {
221
+ const installLocations = [
222
+ path.join(appFunctionsDir, 'package.json'),
223
+ path.join(extensionsDir, 'package.json'),
224
+ ];
225
+ mockedWalk.mockResolvedValue(installLocations);
226
+ mockedGetProjectConfig.mockResolvedValue({
227
+ projectDir,
228
+ projectConfig: {
229
+ srcDir,
230
+ },
231
+ });
232
+ await updatePackages({});
233
+ // It's called once per each install location, plus once to check if npm installed
234
+ expect(execMock).toHaveBeenCalledTimes(installLocations.length + 1);
235
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
236
+ cwd: appFunctionsDir,
237
+ });
238
+ expect(execMock).toHaveBeenCalledWith(`npm update `, {
239
+ cwd: extensionsDir,
240
+ });
241
+ });
242
+ it('should throw an error when updating the dependencies fails', async () => {
243
+ execMock = vi.fn().mockImplementation(command => {
244
+ if (command === 'npm --version') {
245
+ return;
246
+ }
247
+ throw new Error('OH NO');
248
+ });
249
+ util.promisify = mockedPromisify(execMock);
250
+ // Mock walk to return the directory paths instead of package.json paths
251
+ mockedWalk.mockResolvedValue([appFunctionsDir, extensionsDir]);
252
+ mockedFs.existsSync.mockImplementation(filePath => {
253
+ const pathStr = filePath.toString();
254
+ if (pathStr === projectDir ||
255
+ pathStr === path.join(projectDir, srcDir)) {
256
+ return true;
257
+ }
258
+ return false;
259
+ });
260
+ await expect(() => updatePackages({ installLocations: [appFunctionsDir, extensionsDir] })).rejects.toThrowError(`Updating dependencies for ${appFunctionsDir} failed`);
261
+ expect(SpinniesManager.fail).toHaveBeenCalledTimes(installLocations.length);
262
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${appFunctionsDir}`, {
263
+ text: `Updating dependencies for ${appFunctionsDir} failed`,
264
+ });
265
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`updatingDependencies-${extensionsDir}`, {
266
+ text: `Updating dependencies for ${extensionsDir} failed`,
267
+ });
268
+ });
269
+ });
136
270
  describe('getProjectPackageJsonFiles()', () => {
137
271
  it('should throw an error when ran outside the boundary of a project', async () => {
138
272
  mockedGetProjectConfig.mockResolvedValue({});
@@ -149,6 +283,30 @@ describe('lib/dependencyManagement', () => {
149
283
  mockedFs.existsSync.mockReturnValueOnce(false);
150
284
  await expect(() => getProjectPackageJsonLocations()).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
151
285
  });
286
+ it('should throw "install" error message when isUpdate=false and no package.json files found', async () => {
287
+ mockedWalk.mockResolvedValue([]);
288
+ mockedFs.existsSync.mockImplementation(filePath => {
289
+ const pathStr = filePath.toString();
290
+ if (pathStr === projectDir ||
291
+ pathStr === path.join(projectDir, srcDir)) {
292
+ return true;
293
+ }
294
+ return false;
295
+ });
296
+ await expect(() => getProjectPackageJsonLocations(undefined, false)).rejects.toThrowError(new RegExp(`No dependencies to install. The project ${projectName} folder might be missing component or subcomponent files.`));
297
+ });
298
+ it('should throw "update" error message when isUpdate=true and no package.json files found', async () => {
299
+ mockedWalk.mockResolvedValue([]);
300
+ mockedFs.existsSync.mockImplementation(filePath => {
301
+ const pathStr = filePath.toString();
302
+ if (pathStr === projectDir ||
303
+ pathStr === path.join(projectDir, srcDir)) {
304
+ return true;
305
+ }
306
+ return false;
307
+ });
308
+ await expect(() => getProjectPackageJsonLocations(undefined, true)).rejects.toThrowError(new RegExp(`No dependencies to update. The project ${projectName} folder might be missing component or subcomponent files.`));
309
+ });
152
310
  it('should ignore package.json files in certain directories', async () => {
153
311
  const nodeModulesDir = path.join(appDir, 'node_modules');
154
312
  const viteDir = path.join(appDir, '.vite');
@@ -172,4 +330,118 @@ describe('lib/dependencyManagement', () => {
172
330
  expect(actual).toEqual([appFunctionsDir, extensionsDir]);
173
331
  });
174
332
  });
333
+ describe('isPackageInstalled()', () => {
334
+ const testDir = '/test/directory';
335
+ const readFileSyncSpy = vi.spyOn(fs, 'readFileSync');
336
+ const existsSyncSpy = vi.spyOn(fs, 'existsSync');
337
+ function mockNodeModulesExists(packageName, exists = true) {
338
+ existsSyncSpy.mockImplementation(filePath => {
339
+ const pathStr = filePath.toString();
340
+ return (exists && pathStr === path.join(testDir, 'node_modules', packageName));
341
+ });
342
+ }
343
+ beforeEach(() => {
344
+ vi.clearAllMocks();
345
+ readFileSyncSpy.mockReset();
346
+ existsSyncSpy.mockReset();
347
+ });
348
+ it('should return true if package is in dependencies and in node_modules', () => {
349
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
350
+ dependencies: {
351
+ eslint: '^9.0.0',
352
+ },
353
+ }));
354
+ mockNodeModulesExists('eslint', true);
355
+ const result = isPackageInstalled(testDir, 'eslint');
356
+ expect(result).toBe(true);
357
+ expect(readFileSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'package.json'), 'utf-8');
358
+ expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', 'eslint'));
359
+ });
360
+ it('should return true if package is in devDependencies and in node_modules', () => {
361
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
362
+ devDependencies: {
363
+ prettier: '^3.0.0',
364
+ },
365
+ }));
366
+ mockNodeModulesExists('prettier', true);
367
+ const result = isPackageInstalled(testDir, 'prettier');
368
+ expect(result).toBe(true);
369
+ });
370
+ it('should return false if package is in package.json but not in node_modules', () => {
371
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
372
+ dependencies: {
373
+ react: '^18.0.0',
374
+ },
375
+ }));
376
+ mockNodeModulesExists('react', false);
377
+ const result = isPackageInstalled(testDir, 'react');
378
+ expect(result).toBe(false);
379
+ });
380
+ it('should return false if package is not in package.json but is in node_modules', () => {
381
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
382
+ dependencies: {
383
+ typescript: '^5.0.0',
384
+ },
385
+ }));
386
+ mockNodeModulesExists('lodash', true);
387
+ const result = isPackageInstalled(testDir, 'lodash');
388
+ expect(result).toBe(false);
389
+ });
390
+ it('should return false if package is not in package.json and not in node_modules', () => {
391
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
392
+ dependencies: {},
393
+ }));
394
+ mockNodeModulesExists('nonexistent-package', false);
395
+ const result = isPackageInstalled(testDir, 'nonexistent-package');
396
+ expect(result).toBe(false);
397
+ });
398
+ it('should return false if package.json cannot be read', () => {
399
+ readFileSyncSpy.mockImplementationOnce(() => {
400
+ throw new Error('File not found');
401
+ });
402
+ const result = isPackageInstalled(testDir, 'eslint');
403
+ expect(result).toBe(false);
404
+ });
405
+ it('should return false if package.json has invalid JSON', () => {
406
+ readFileSyncSpy.mockReturnValueOnce('invalid json{');
407
+ const result = isPackageInstalled(testDir, 'eslint');
408
+ expect(result).toBe(false);
409
+ });
410
+ it('should return false if checking node_modules throws an error', () => {
411
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
412
+ dependencies: {
413
+ eslint: '^9.0.0',
414
+ },
415
+ }));
416
+ existsSyncSpy.mockImplementation(() => {
417
+ throw new Error('Permission denied');
418
+ });
419
+ const result = isPackageInstalled(testDir, 'eslint');
420
+ expect(result).toBe(false);
421
+ });
422
+ it('should handle scoped packages correctly', () => {
423
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
424
+ dependencies: {
425
+ '@typescript-eslint/parser': '^8.0.0',
426
+ },
427
+ }));
428
+ mockNodeModulesExists('@typescript-eslint/parser', true);
429
+ const result = isPackageInstalled(testDir, '@typescript-eslint/parser');
430
+ expect(result).toBe(true);
431
+ expect(existsSyncSpy).toHaveBeenCalledWith(path.join(testDir, 'node_modules', '@typescript-eslint/parser'));
432
+ });
433
+ it('should check both dependencies and devDependencies', () => {
434
+ readFileSyncSpy.mockReturnValueOnce(JSON.stringify({
435
+ dependencies: {
436
+ react: '^18.0.0',
437
+ },
438
+ devDependencies: {
439
+ eslint: '^9.0.0',
440
+ },
441
+ }));
442
+ mockNodeModulesExists('eslint', true);
443
+ const result = isPackageInstalled(testDir, 'eslint');
444
+ expect(result).toBe(true);
445
+ });
446
+ });
175
447
  });
@@ -1,6 +1,6 @@
1
1
  import util from 'util';
2
2
  import { isGloballyInstalled, getLatestCliVersion, DEFAULT_PACKAGE_MANAGER, } from '../npm.js';
3
- import pkg from '../../package.json' with { type: 'json' };
3
+ import { pkg } from '../jsonLoader.js';
4
4
  vi.mock('../../ui/logger.js');
5
5
  vi.mock('../ui/SpinniesManager');
6
6
  describe('lib/npm', () => {
@@ -59,7 +59,7 @@ describe('lib/sandboxSync', () => {
59
59
  it('throws error when account IDs are missing', async () => {
60
60
  mockedGetAccountId.mockReset();
61
61
  mockedGetAccountId.mockReturnValue(null);
62
- const errorRegex = new RegExp(`Couldn't sync ${mockChildAccount.portalId} because your account has been removed from`);
62
+ const errorRegex = new RegExp(`because your account has been removed from`);
63
63
  await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toThrow(errorRegex);
64
64
  });
65
65
  it('handles sync in progress error', async () => {
@@ -3,8 +3,8 @@ import { isTrackingAllowed, getAccountConfig, } from '@hubspot/local-dev-lib/con
3
3
  import { API_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
4
4
  import { uiLogger } from '../ui/logger.js';
5
5
  import { trackCommandUsage, trackHelpUsage, trackConvertFieldsUsage, trackAuthAction, trackCommandMetadataUsage, } from '../usageTracking.js';
6
- import packageJson from '../../package.json' with { type: 'json' };
7
- const version = packageJson.version;
6
+ import { pkg } from '../jsonLoader.js';
7
+ const version = pkg.version;
8
8
  vi.mock('@hubspot/local-dev-lib/trackUsage');
9
9
  vi.mock('@hubspot/local-dev-lib/config');
10
10
  vi.mock('../ui/logger.js');
package/lib/commonOpts.js CHANGED
@@ -149,16 +149,13 @@ export function setCLILogLevel(options) {
149
149
  setLogLevel(LOG_LEVEL.ERROR);
150
150
  SpinniesManager.setDisableOutput(true);
151
151
  }
152
- else if (debug) {
152
+ else if (debug || networkDebug) {
153
153
  setLogLevel(LOG_LEVEL.DEBUG);
154
+ process.env.HUBSPOT_NETWORK_LOGGING = 'true';
154
155
  }
155
156
  else {
156
157
  setLogLevel(LOG_LEVEL.LOG);
157
158
  }
158
- if (networkDebug) {
159
- process.env.HUBSPOT_NETWORK_LOGGING = 'true';
160
- setLogLevel(LOG_LEVEL.DEBUG);
161
- }
162
159
  }
163
160
  export function getCommandName(argv) {
164
161
  return String(argv && argv._ && argv._[0]) || '';
@@ -24,7 +24,7 @@ async function promptNewAccountName(account, globalConfig, renamedAccounts) {
24
24
  if (value === account.name) {
25
25
  return lib.configMigrate.handleAccountNameConflicts.errors.sameName;
26
26
  }
27
- const existingAccount = globalConfig.accounts.some(acc => acc.name === value);
27
+ const existingAccount = globalConfig.accounts?.some(acc => acc.name === value);
28
28
  const renamedAccount = renamedAccounts.some(acc => acc.name === value);
29
29
  if (existingAccount || renamedAccount) {
30
30
  return lib.configMigrate.handleAccountNameConflicts.errors.nameAlreadyInConfig(value);
@@ -85,8 +85,8 @@ async function handleAccountNameConflicts(globalConfig, deprecatedConfig, force)
85
85
  const accountsWithConflictsToRemove = new Set();
86
86
  const renamedAccounts = [];
87
87
  const accountsNotYetInGlobal = deprecatedConfig.portals.filter(portal => portal.portalId &&
88
- !globalConfig.accounts.some(acc => acc.accountId === portal.portalId));
89
- const accountsWithConflicts = accountsNotYetInGlobal.filter(localAccount => globalConfig.accounts.some(globalAccount => globalAccount.name === localAccount.name));
88
+ !globalConfig.accounts?.some(acc => acc.accountId === portal.portalId));
89
+ const accountsWithConflicts = accountsNotYetInGlobal.filter(localAccount => globalConfig.accounts?.some(globalAccount => globalAccount.name === localAccount.name));
90
90
  if (accountsWithConflicts.length > 0) {
91
91
  uiLogger.log('');
92
92
  uiLogger.warn(lib.configMigrate.handleAccountNameConflicts.warnings.accountNameConflictMessage(accountsWithConflicts.length));
@@ -1,6 +1,12 @@
1
- export declare function installPackages({ packages, installLocations, }: {
1
+ export declare function installPackages({ packages, installLocations, dev, }: {
2
2
  packages?: string[];
3
3
  installLocations?: string[];
4
+ dev?: boolean;
4
5
  }): Promise<void>;
5
- export declare function getProjectPackageJsonLocations(dir?: string): Promise<string[]>;
6
+ export declare function updatePackages({ packages, installLocations, }: {
7
+ packages?: string[];
8
+ installLocations?: string[];
9
+ }): Promise<void>;
10
+ export declare function getProjectPackageJsonLocations(dir?: string, isUpdate?: boolean): Promise<string[]>;
11
+ export declare function isPackageInstalled(directory: string, packageName: string): boolean;
6
12
  export declare function hasMissingPackages(directory: string): Promise<boolean>;