@hubspot/cli 7.9.0 → 7.9.1-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 (214) hide show
  1. package/bin/cli.js +5 -4
  2. package/commands/__tests__/getStarted.test.js +10 -0
  3. package/commands/__tests__/project.test.js +2 -0
  4. package/commands/account/__tests__/rename.test.js +42 -0
  5. package/commands/account/auth.js +10 -14
  6. package/commands/account/clean.js +11 -19
  7. package/commands/account/createOverride.js +17 -23
  8. package/commands/account/info.js +8 -5
  9. package/commands/account/list.js +13 -18
  10. package/commands/account/remove.js +23 -22
  11. package/commands/account/removeOverride.js +8 -16
  12. package/commands/account/rename.d.ts +1 -1
  13. package/commands/account/rename.js +6 -3
  14. package/commands/account/use.js +19 -8
  15. package/commands/app/__tests__/migrate.test.js +8 -4
  16. package/commands/app/migrate.js +2 -2
  17. package/commands/auth.js +18 -14
  18. package/commands/cms/theme/preview.js +1 -4
  19. package/commands/config/migrate.js +5 -5
  20. package/commands/config/set.js +1 -2
  21. package/commands/customObject/createSchema.js +2 -3
  22. package/commands/customObject/updateSchema.js +2 -3
  23. package/commands/getStarted.js +15 -22
  24. package/commands/hubdb/__tests__/list.test.js +1 -0
  25. package/commands/hubdb/list.js +2 -2
  26. package/commands/hubdb.d.ts +1 -1
  27. package/commands/init.js +36 -32
  28. package/commands/project/__tests__/deploy.test.js +10 -5
  29. package/commands/project/__tests__/devUnifiedFlow.test.js +6 -4
  30. package/commands/project/__tests__/logs.test.js +4 -0
  31. package/commands/project/__tests__/updateDeps.test.js +142 -0
  32. package/commands/project/__tests__/validate.test.js +2 -2
  33. package/commands/project/cloneApp.js +2 -2
  34. package/commands/project/create.js +0 -1
  35. package/commands/project/deploy.js +2 -2
  36. package/commands/project/dev/deprecatedFlow.js +4 -5
  37. package/commands/project/dev/index.js +14 -4
  38. package/commands/project/dev/unifiedFlow.js +4 -5
  39. package/commands/project/listBuilds.js +7 -1
  40. package/commands/project/logs.js +2 -3
  41. package/commands/project/profile/add.js +6 -7
  42. package/commands/project/profile/delete.js +2 -2
  43. package/commands/project/updateDeps.d.ts +6 -0
  44. package/commands/project/updateDeps.js +80 -0
  45. package/commands/project/upload.js +9 -3
  46. package/commands/project/validate.js +9 -3
  47. package/commands/project/watch.js +7 -2
  48. package/commands/project.js +2 -0
  49. package/commands/sandbox/__tests__/create.test.js +14 -5
  50. package/commands/sandbox/create.js +4 -5
  51. package/commands/sandbox/delete.js +23 -20
  52. package/commands/testAccount/__tests__/create.test.js +68 -0
  53. package/commands/testAccount/create.d.ts +8 -0
  54. package/commands/testAccount/create.js +135 -45
  55. package/commands/testAccount/delete.js +9 -8
  56. package/commands/testAccount/importData.d.ts +1 -1
  57. package/lang/en.d.ts +3199 -3185
  58. package/lang/en.js +52 -14
  59. package/lib/__tests__/buildAccount.test.js +22 -30
  60. package/lib/__tests__/commonOpts.test.js +9 -13
  61. package/lib/__tests__/dependencyManagement.test.js +273 -1
  62. package/lib/__tests__/developerTestAccounts.test.js +29 -17
  63. package/lib/__tests__/importData.test.js +20 -10
  64. package/lib/__tests__/oauth.test.js +19 -8
  65. package/lib/__tests__/sandboxSync.test.js +33 -11
  66. package/lib/__tests__/sandboxes.test.js +30 -19
  67. package/lib/__tests__/usageTracking.test.js +10 -10
  68. package/lib/__tests__/validation.test.js +32 -32
  69. package/lib/accountTypes.d.ts +9 -9
  70. package/lib/accountTypes.js +2 -4
  71. package/lib/app/__tests__/migrate.test.js +15 -0
  72. package/lib/app/__tests__/migrate_legacy.test.js +9 -0
  73. package/lib/app/migrate_legacy.d.ts +2 -2
  74. package/lib/buildAccount.d.ts +4 -4
  75. package/lib/buildAccount.js +7 -14
  76. package/lib/commonOpts.js +5 -8
  77. package/lib/configMigrate.d.ts +2 -2
  78. package/lib/configMigrate.js +42 -18
  79. package/lib/configOptions.js +3 -2
  80. package/lib/constants.d.ts +1 -0
  81. package/lib/constants.js +6 -0
  82. package/lib/dependencyManagement.d.ts +8 -2
  83. package/lib/dependencyManagement.js +75 -12
  84. package/lib/developerTestAccounts.d.ts +3 -3
  85. package/lib/developerTestAccounts.js +4 -7
  86. package/lib/doctor/DiagnosticInfoBuilder.d.ts +1 -1
  87. package/lib/doctor/DiagnosticInfoBuilder.js +9 -6
  88. package/lib/doctor/Doctor.js +4 -3
  89. package/lib/doctor/__tests__/Diagnosis.test.js +4 -3
  90. package/lib/doctor/__tests__/DiagnosticInfoBuilder.test.js +17 -9
  91. package/lib/doctor/__tests__/Doctor.test.js +14 -0
  92. package/lib/importData.js +8 -7
  93. package/lib/links.js +5 -5
  94. package/lib/mcp/__tests__/setup.test.d.ts +1 -0
  95. package/lib/mcp/__tests__/setup.test.js +127 -0
  96. package/lib/mcp/setup.d.ts +4 -12
  97. package/lib/mcp/setup.js +34 -1
  98. package/lib/middleware/__test__/commandTargetingUtils.test.js +3 -3
  99. package/lib/middleware/__test__/configMiddleware.test.js +23 -22
  100. package/lib/middleware/__test__/gitMiddleware.test.js +9 -7
  101. package/lib/middleware/autoUpdateMiddleware.d.ts +3 -1
  102. package/lib/middleware/autoUpdateMiddleware.js +10 -2
  103. package/lib/middleware/commandTargetingUtils.js +2 -2
  104. package/lib/middleware/configMiddleware.d.ts +6 -1
  105. package/lib/middleware/configMiddleware.js +36 -15
  106. package/lib/middleware/gitMiddleware.js +8 -4
  107. package/lib/npm.d.ts +3 -0
  108. package/lib/npm.js +6 -0
  109. package/lib/oauth.d.ts +2 -2
  110. package/lib/oauth.js +8 -10
  111. package/lib/projects/__tests__/AppDevModeInterface.test.js +17 -6
  112. package/lib/projects/__tests__/DevServerManager.test.js +1 -0
  113. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  114. package/lib/projects/__tests__/components.test.js +148 -24
  115. package/lib/projects/__tests__/deploy.test.js +1 -0
  116. package/lib/projects/__tests__/platformVersion.test.js +5 -1
  117. package/lib/projects/__tests__/projects.test.js +13 -42
  118. package/lib/projects/components.js +76 -20
  119. package/lib/projects/config.js +5 -9
  120. package/lib/projects/create/__tests__/v2.test.js +11 -0
  121. package/lib/projects/localDev/AppDevModeInterface.js +2 -2
  122. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +2 -2
  123. package/lib/projects/localDev/LocalDevLogger.js +4 -4
  124. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +3 -3
  125. package/lib/projects/localDev/helpers/account.d.ts +10 -10
  126. package/lib/projects/localDev/helpers/account.js +6 -11
  127. package/lib/projects/platformVersion.js +1 -1
  128. package/lib/projects/urls.js +5 -6
  129. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.d.ts +1 -0
  130. package/lib/prompts/__tests__/createDeveloperTestAccountConfigPrompt.test.js +153 -0
  131. package/lib/prompts/__tests__/downloadProjectPrompt.test.js +7 -5
  132. package/lib/prompts/accountNamePrompt.js +3 -3
  133. package/lib/prompts/accountsPrompt.d.ts +1 -1
  134. package/lib/prompts/accountsPrompt.js +6 -7
  135. package/lib/prompts/confirmImportDataPrompt.js +2 -2
  136. package/lib/prompts/createDeveloperTestAccountConfigPrompt.d.ts +5 -0
  137. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +76 -66
  138. package/lib/prompts/downloadProjectPrompt.d.ts +1 -0
  139. package/lib/prompts/downloadProjectPrompt.js +5 -2
  140. package/lib/prompts/importDataTestAccountSelectPrompt.js +4 -5
  141. package/lib/prompts/personalAccessKeyPrompt.js +2 -2
  142. package/lib/prompts/projectDevTargetAccountPrompt.d.ts +3 -3
  143. package/lib/prompts/projectDevTargetAccountPrompt.js +5 -7
  144. package/lib/prompts/sandboxesPrompt.js +7 -8
  145. package/lib/prompts/setAsDefaultAccountPrompt.js +7 -6
  146. package/lib/sandboxSync.d.ts +2 -2
  147. package/lib/sandboxSync.js +3 -9
  148. package/lib/sandboxes.d.ts +4 -4
  149. package/lib/sandboxes.js +6 -11
  150. package/lib/serverlessLogs.js +2 -2
  151. package/lib/theme/__tests__/migrate.test.js +15 -0
  152. package/lib/ui/index.js +6 -3
  153. package/lib/usageTracking.js +15 -8
  154. package/lib/validation.js +13 -11
  155. package/mcp-server/tools/cms/HsCreateFunctionTool.js +8 -2
  156. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +4 -4
  157. package/mcp-server/tools/cms/HsCreateModuleTool.js +8 -2
  158. package/mcp-server/tools/cms/HsCreateTemplateTool.js +8 -2
  159. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +4 -4
  160. package/mcp-server/tools/cms/HsFunctionLogsTool.js +6 -2
  161. package/mcp-server/tools/cms/HsListFunctionsTool.js +5 -1
  162. package/mcp-server/tools/cms/HsListTool.js +5 -1
  163. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -0
  164. package/mcp-server/tools/index.js +4 -0
  165. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
  166. package/mcp-server/tools/project/AddFeatureToProjectTool.js +9 -3
  167. package/mcp-server/tools/project/CreateProjectTool.js +8 -2
  168. package/mcp-server/tools/project/CreateTestAccountTool.d.ts +41 -0
  169. package/mcp-server/tools/project/CreateTestAccountTool.js +150 -0
  170. package/mcp-server/tools/project/DeployProjectTool.d.ts +1 -1
  171. package/mcp-server/tools/project/DeployProjectTool.js +8 -2
  172. package/mcp-server/tools/project/DocFetchTool.d.ts +1 -1
  173. package/mcp-server/tools/project/DocFetchTool.js +9 -5
  174. package/mcp-server/tools/project/DocsSearchTool.d.ts +1 -1
  175. package/mcp-server/tools/project/DocsSearchTool.js +12 -8
  176. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.d.ts +1 -1
  177. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +11 -7
  178. package/mcp-server/tools/project/GetApplicationInfoTool.d.ts +1 -1
  179. package/mcp-server/tools/project/GetApplicationInfoTool.js +11 -7
  180. package/mcp-server/tools/project/GetBuildStatusTool.d.ts +26 -0
  181. package/mcp-server/tools/project/GetBuildStatusTool.js +164 -0
  182. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +1 -1
  183. package/mcp-server/tools/project/GetConfigValuesTool.js +11 -7
  184. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +1 -1
  185. package/mcp-server/tools/project/GuidedWalkthroughTool.js +7 -3
  186. package/mcp-server/tools/project/UploadProjectTools.d.ts +9 -3
  187. package/mcp-server/tools/project/UploadProjectTools.js +51 -5
  188. package/mcp-server/tools/project/ValidateProjectTool.d.ts +1 -1
  189. package/mcp-server/tools/project/ValidateProjectTool.js +7 -3
  190. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.d.ts +1 -0
  191. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +454 -0
  192. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +5 -1
  193. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +25 -13
  194. package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +7 -5
  195. package/mcp-server/tools/project/__tests__/GetApplicationInfoTool.test.js +7 -5
  196. package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.d.ts +1 -0
  197. package/mcp-server/tools/project/__tests__/GetBuildStatusTool.test.js +240 -0
  198. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +8 -6
  199. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +56 -4
  200. package/mcp-server/utils/__tests__/content.test.js +21 -20
  201. package/mcp-server/utils/__tests__/feedbackTracking.test.js +33 -28
  202. package/mcp-server/utils/content.d.ts +1 -1
  203. package/mcp-server/utils/content.js +2 -2
  204. package/mcp-server/utils/feedbackTracking.d.ts +1 -1
  205. package/mcp-server/utils/feedbackTracking.js +3 -3
  206. package/mcp-server/utils/toolUsageTracking.js +4 -3
  207. package/package.json +8 -7
  208. package/lang/en.lyaml +0 -1508
  209. package/lib/lang.d.ts +0 -8
  210. package/lib/lang.js +0 -72
  211. package/mcp-server/utils/__tests__/cliConfig.test.js +0 -110
  212. package/mcp-server/utils/cliConfig.d.ts +0 -1
  213. package/mcp-server/utils/cliConfig.js +0 -12
  214. /package/{mcp-server/utils/__tests__/cliConfig.test.d.ts → commands/project/__tests__/updateDeps.test.d.ts} +0 -0
@@ -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,16 +1,18 @@
1
- import { getAccountId, getConfigAccounts } from '@hubspot/local-dev-lib/config';
1
+ import { getAllConfigAccounts, getConfigAccountIfExists, getConfigAccountById, } from '@hubspot/local-dev-lib/config';
2
2
  import { uiLogger } from '../ui/logger.js';
3
3
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
4
4
  import { fetchDeveloperTestAccounts } from '@hubspot/local-dev-lib/api/developerTestAccounts';
5
5
  import { mockHubSpotHttpError } from '../testUtils.js';
6
6
  import * as errorHandlers from '../errorHandlers/index.js';
7
7
  import { getHasDevTestAccounts, handleDeveloperTestAccountCreateError, validateDevTestAccountUsageLimits, } from '../developerTestAccounts.js';
8
+ import { logError } from '../errorHandlers/index.js';
8
9
  vi.mock('@hubspot/local-dev-lib/config');
9
10
  vi.mock('../ui/logger.js');
10
11
  vi.mock('@hubspot/local-dev-lib/api/developerTestAccounts');
11
12
  vi.mock('../errorHandlers');
12
- const mockedGetAccountId = getAccountId;
13
- const mockedGetConfigAccounts = getConfigAccounts;
13
+ const mockedGetConfigAccountIfExists = getConfigAccountIfExists;
14
+ const mockedGetAllConfigAccounts = getAllConfigAccounts;
15
+ const mockedGetConfigAccountById = getConfigAccountById;
14
16
  const mockedFetchDeveloperTestAccounts = fetchDeveloperTestAccounts;
15
17
  const APP_DEVELOPER_ACCOUNT_1 = {
16
18
  name: 'app-developer-1',
@@ -33,41 +35,43 @@ const accounts = [
33
35
  parentAccountId: APP_DEVELOPER_ACCOUNT_1.accountId,
34
36
  accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST,
35
37
  env: 'prod',
38
+ authType: 'personalaccesskey',
36
39
  },
37
40
  ];
38
41
  describe('lib/developerTestAccounts', () => {
39
42
  describe('getHasDevTestAccounts()', () => {
40
43
  it('should return true if there are developer test accounts associated with the account', () => {
41
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId);
42
- mockedGetConfigAccounts.mockReturnValueOnce(accounts);
44
+ mockedGetAllConfigAccounts.mockReturnValueOnce(accounts);
43
45
  const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_1);
44
46
  expect(result).toBe(true);
45
47
  });
46
48
  it('should return false if there are no developer test accounts associated with the account', () => {
47
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_2.accountId);
48
- mockedGetConfigAccounts.mockReturnValueOnce(accounts);
49
+ mockedGetConfigAccountIfExists.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_2);
50
+ mockedGetAllConfigAccounts.mockReturnValueOnce(accounts);
49
51
  const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_2);
50
52
  expect(result).toBe(false);
51
53
  });
52
54
  it('should return false if there are no accounts configured', () => {
53
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId);
54
- mockedGetConfigAccounts.mockReturnValueOnce(undefined);
55
+ mockedGetConfigAccountIfExists.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1);
56
+ mockedGetAllConfigAccounts.mockReturnValueOnce(undefined);
55
57
  const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_1);
56
58
  expect(result).toBe(false);
57
59
  });
58
60
  });
59
61
  describe('validateDevTestAccountUsageLimits()', () => {
60
62
  afterEach(() => {
61
- mockedGetAccountId.mockRestore();
63
+ mockedGetConfigAccountIfExists.mockRestore();
62
64
  mockedFetchDeveloperTestAccounts.mockRestore();
63
65
  });
64
66
  it('should return null if the account id is not found', async () => {
65
- mockedGetAccountId.mockReturnValueOnce(undefined);
67
+ mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({
68
+ data: null,
69
+ });
66
70
  const result = await validateDevTestAccountUsageLimits(APP_DEVELOPER_ACCOUNT_1);
67
71
  expect(result).toBe(null);
68
72
  });
69
73
  it('should return null if there is no developer test account data', async () => {
70
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId);
74
+ mockedGetConfigAccountIfExists.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1);
71
75
  mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({
72
76
  data: null,
73
77
  });
@@ -75,7 +79,7 @@ describe('lib/developerTestAccounts', () => {
75
79
  expect(result).toBe(null);
76
80
  });
77
81
  it('should return the test account data if the account has not reached the limit', async () => {
78
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId);
82
+ mockedGetConfigAccountIfExists.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1);
79
83
  const testAccountData = {
80
84
  maxTestPortals: 10,
81
85
  results: [],
@@ -87,7 +91,7 @@ describe('lib/developerTestAccounts', () => {
87
91
  expect(result).toEqual(expect.objectContaining(testAccountData));
88
92
  });
89
93
  it('should throw an error if the account has reached the limit', async () => {
90
- mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId);
94
+ mockedGetConfigAccountIfExists.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1);
91
95
  mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({
92
96
  data: {
93
97
  maxTestPortals: 0,
@@ -103,6 +107,14 @@ describe('lib/developerTestAccounts', () => {
103
107
  beforeEach(() => {
104
108
  loggerErrorSpy = vi.spyOn(uiLogger, 'error');
105
109
  logErrorSpy = vi.spyOn(errorHandlers, 'logError');
110
+ // Mock account config for uiAccountDescription calls
111
+ mockedGetConfigAccountById.mockReturnValue({
112
+ accountId: APP_DEVELOPER_ACCOUNT_1.accountId,
113
+ name: 'Test Account',
114
+ authType: 'personalaccesskey',
115
+ personalAccessKey: 'test-key',
116
+ env: 'prod',
117
+ });
106
118
  });
107
119
  afterEach(() => {
108
120
  loggerErrorSpy.mockRestore();
@@ -117,7 +129,7 @@ describe('lib/developerTestAccounts', () => {
117
129
  },
118
130
  });
119
131
  expect(() => handleDeveloperTestAccountCreateError(missingScopesError, APP_DEVELOPER_ACCOUNT_1.accountId, 'prod', 10)).toThrow('Missing scopes error');
120
- expect(loggerErrorSpy).toHaveBeenCalled();
132
+ expect(uiLogger.error).toHaveBeenCalled();
121
133
  });
122
134
  it('should log and throw an error if the account is missing the required scopes', () => {
123
135
  const portalLimitReachedError = mockHubSpotHttpError('Portal limit reached error', {
@@ -128,7 +140,7 @@ describe('lib/developerTestAccounts', () => {
128
140
  },
129
141
  });
130
142
  expect(() => handleDeveloperTestAccountCreateError(portalLimitReachedError, APP_DEVELOPER_ACCOUNT_1.accountId, 'prod', 10)).toThrow('Portal limit reached error');
131
- expect(loggerErrorSpy).toHaveBeenCalled();
143
+ expect(uiLogger.error).toHaveBeenCalled();
132
144
  });
133
145
  it('should log a generic error message for an unknown error type', () => {
134
146
  const someUnknownError = mockHubSpotHttpError('Some unknown error', {
@@ -139,7 +151,7 @@ describe('lib/developerTestAccounts', () => {
139
151
  },
140
152
  });
141
153
  expect(() => handleDeveloperTestAccountCreateError(someUnknownError, APP_DEVELOPER_ACCOUNT_1.accountId, 'prod', 10)).toThrow('Some unknown error');
142
- expect(logErrorSpy).toHaveBeenCalled();
154
+ expect(logError).toHaveBeenCalled();
143
155
  });
144
156
  });
145
157
  });
@@ -1,6 +1,6 @@
1
1
  import { uiLogger } from '../ui/logger.js';
2
2
  import { createImport } from '@hubspot/local-dev-lib/api/crm';
3
- import { getAccountConfig, getAccountId } from '@hubspot/local-dev-lib/config';
3
+ import { getConfigAccountById, getConfigAccountIfExists, } from '@hubspot/local-dev-lib/config';
4
4
  import { handleImportData, handleTargetTestAccountSelectionFlow, } from '../importData.js';
5
5
  import { lib } from '../../lang/en.js';
6
6
  import { isDeveloperTestAccount, isStandardAccount, isAppDeveloperAccount, } from '../accountTypes.js';
@@ -13,8 +13,8 @@ vi.mock('../prompts/importDataTestAccountSelectPrompt');
13
13
  describe('lib/importData', () => {
14
14
  const mockUiLogger = vi.mocked(uiLogger);
15
15
  const mockCreateImport = vi.mocked(createImport);
16
- const mockGetAccountConfig = vi.mocked(getAccountConfig);
17
- const mockGetAccountId = vi.mocked(getAccountId);
16
+ const mockGetConfigAccountById = vi.mocked(getConfigAccountById);
17
+ const mockGetConfigAccountIfExists = vi.mocked(getConfigAccountIfExists);
18
18
  const mockIsDeveloperTestAccount = vi.mocked(isDeveloperTestAccount);
19
19
  const mockIsStandardAccount = vi.mocked(isStandardAccount);
20
20
  const mockIsAppDeveloperAccount = vi.mocked(isAppDeveloperAccount);
@@ -24,8 +24,8 @@ describe('lib/importData', () => {
24
24
  mockUiLogger.success.mockReset();
25
25
  mockUiLogger.error.mockReset();
26
26
  mockCreateImport.mockReset();
27
- mockGetAccountConfig.mockReset();
28
- mockGetAccountId.mockReset();
27
+ mockGetConfigAccountById.mockReset();
28
+ mockGetConfigAccountIfExists.mockReset();
29
29
  mockIsDeveloperTestAccount.mockReset();
30
30
  mockIsStandardAccount.mockReset();
31
31
  mockIsAppDeveloperAccount.mockReset();
@@ -56,32 +56,42 @@ describe('lib/importData', () => {
56
56
  const userProvidedAccountId = '1234';
57
57
  const derivedAccountId = 123456789;
58
58
  it('should error if the userProvidedAccountId is not the right account type', async () => {
59
- mockGetAccountConfig.mockReturnValue({});
60
- mockGetAccountId.mockReturnValue(1234);
59
+ mockGetConfigAccountIfExists.mockReturnValue({
60
+ accountId: 1234,
61
+ });
61
62
  mockIsDeveloperTestAccount.mockReturnValue(false);
62
63
  await expect(handleTargetTestAccountSelectionFlow(derivedAccountId, userProvidedAccountId)).rejects.toThrow(lib.importData.errors.notDeveloperTestAccount);
63
64
  });
64
65
  it('should error if the derivedAccountId belongs to the wrong account type', async () => {
65
- mockGetAccountConfig.mockReturnValue({});
66
+ mockGetConfigAccountIfExists.mockReturnValue({
67
+ accountId: derivedAccountId,
68
+ });
69
+ mockGetConfigAccountById.mockReturnValue({
70
+ accountId: derivedAccountId,
71
+ });
66
72
  mockIsDeveloperTestAccount.mockReturnValue(false);
67
73
  mockIsStandardAccount.mockReturnValue(false);
68
74
  mockIsAppDeveloperAccount.mockReturnValue(false);
69
75
  await expect(handleTargetTestAccountSelectionFlow(derivedAccountId, undefined)).rejects.toThrow(lib.importData.errors.incorrectAccountType(derivedAccountId));
70
76
  });
71
77
  it('should return the derivedAccountId if it is a developer test account', async () => {
72
- mockGetAccountConfig.mockReturnValue({});
73
78
  mockIsDeveloperTestAccount.mockReturnValue(true);
79
+ mockGetConfigAccountById.mockReturnValue({
80
+ accountId: derivedAccountId,
81
+ });
74
82
  const result = await handleTargetTestAccountSelectionFlow(derivedAccountId, undefined);
75
83
  expect(result).toBe(derivedAccountId);
76
84
  });
77
85
  it('should return the result of the importDataTestAccountSelectPrompt if the derivedAccountId is a standard or app developer account', async () => {
78
- mockGetAccountConfig.mockReturnValue({});
79
86
  mockIsDeveloperTestAccount.mockReturnValue(false);
80
87
  mockIsStandardAccount.mockReturnValue(true);
81
88
  mockIsAppDeveloperAccount.mockReturnValue(true);
82
89
  mockImportDataTestAccountSelectPrompt.mockResolvedValue({
83
90
  selectedAccountId: 890223,
84
91
  });
92
+ mockGetConfigAccountById.mockReturnValue({
93
+ accountId: derivedAccountId,
94
+ });
85
95
  const result = await handleTargetTestAccountSelectionFlow(derivedAccountId, undefined);
86
96
  expect(result).toBe(890223);
87
97
  });
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import open from 'open';
3
3
  import { OAuth2Manager } from '@hubspot/local-dev-lib/models/OAuth2Manager';
4
- import { getAccountConfig } from '@hubspot/local-dev-lib/config';
4
+ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
5
5
  import { addOauthToAccountConfig } from '@hubspot/local-dev-lib/oauth';
6
6
  import { uiLogger } from '../ui/logger.js';
7
7
  import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
@@ -15,18 +15,22 @@ vi.mock('@hubspot/local-dev-lib/oauth');
15
15
  vi.mock('../ui/logger.js');
16
16
  const mockedExpress = express;
17
17
  const mockedOAuth2Manager = OAuth2Manager;
18
- const mockedGetAccountConfig = getAccountConfig;
18
+ const mockedGetConfigAccountById = getConfigAccountById;
19
19
  describe('lib/oauth', () => {
20
20
  const mockExpressReq = {
21
21
  query: { code: 'test-auth-code' },
22
22
  };
23
23
  const mockExpressResp = { send: vi.fn() };
24
24
  const mockAccountConfig = {
25
+ name: 'test-account',
25
26
  accountId: 123,
26
- clientId: 'test-client-id',
27
- clientSecret: 'test-client-secret',
28
- scopes: ['test-scope'],
27
+ authType: 'oauth2',
29
28
  env: ENVIRONMENTS.PROD,
29
+ auth: {
30
+ clientId: 'test-client-id',
31
+ clientSecret: 'test-client-secret',
32
+ scopes: ['test-scope'],
33
+ },
30
34
  };
31
35
  beforeEach(() => {
32
36
  mockedExpress.mockReturnValue({
@@ -49,7 +53,7 @@ describe('lib/oauth', () => {
49
53
  exchangeForTokens: vi.fn().mockResolvedValue({}),
50
54
  };
51
55
  mockedOAuth2Manager.mockImplementation(() => mockOAuth2Manager);
52
- mockedGetAccountConfig.mockReturnValue({
56
+ mockedGetConfigAccountById.mockReturnValue({
53
57
  env: ENVIRONMENTS.PROD,
54
58
  });
55
59
  await authenticateWithOauth(mockAccountConfig);
@@ -66,7 +70,10 @@ describe('lib/oauth', () => {
66
70
  it('should handle missing clientId', async () => {
67
71
  const invalidConfig = {
68
72
  ...mockAccountConfig,
69
- clientId: undefined,
73
+ auth: {
74
+ ...mockAccountConfig.auth,
75
+ clientId: undefined,
76
+ },
70
77
  };
71
78
  mockedOAuth2Manager.mockImplementation(() => ({
72
79
  account: invalidConfig,
@@ -74,6 +81,7 @@ describe('lib/oauth', () => {
74
81
  const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
75
82
  throw new Error('exit');
76
83
  });
84
+ // @ts-expect-error Testing invalid config
77
85
  await expect(authenticateWithOauth(invalidConfig)).rejects.toThrow('exit');
78
86
  expect(uiLogger.error).toHaveBeenCalled();
79
87
  expect(exitSpy).toHaveBeenCalled();
@@ -82,7 +90,10 @@ describe('lib/oauth', () => {
82
90
  it('should use default scopes when none provided', async () => {
83
91
  const configWithoutScopes = {
84
92
  ...mockAccountConfig,
85
- scopes: undefined,
93
+ auth: {
94
+ ...mockAccountConfig.auth,
95
+ scopes: [],
96
+ },
86
97
  };
87
98
  const mockOAuth2Manager = {
88
99
  account: configWithoutScopes,