@hubspot/cli 7.6.0-beta.9 → 7.6.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 (217) hide show
  1. package/api/__tests__/migrate.test.js +5 -5
  2. package/api/migrate.d.ts +4 -5
  3. package/api/migrate.js +2 -10
  4. package/commands/__tests__/getStarted.test.js +2 -2
  5. package/commands/__tests__/mcp.test.js +1 -1
  6. package/commands/__tests__/project.test.js +0 -3
  7. package/commands/app/__tests__/migrate.test.js +1 -1
  8. package/commands/app/migrate.js +4 -5
  9. package/commands/app/secret/add.js +2 -1
  10. package/commands/app/secret/delete.js +2 -1
  11. package/commands/app/secret/list.js +2 -1
  12. package/commands/app/secret/update.js +2 -1
  13. package/commands/app/secret.js +2 -1
  14. package/commands/app.js +2 -2
  15. package/commands/config/set.js +0 -1
  16. package/commands/getStarted.d.ts +0 -2
  17. package/commands/getStarted.js +72 -24
  18. package/commands/mcp/__tests__/setup.test.js +2 -2
  19. package/commands/mcp/setup.d.ts +0 -1
  20. package/commands/mcp/setup.js +14 -13
  21. package/commands/mcp.js +3 -3
  22. package/commands/project/__tests__/add.test.js +64 -0
  23. package/commands/project/__tests__/create.test.js +57 -0
  24. package/commands/project/__tests__/deploy.test.js +3 -5
  25. package/commands/project/__tests__/devUnifiedFlow.test.js +20 -11
  26. package/commands/project/__tests__/logs.test.js +0 -3
  27. package/commands/project/__tests__/migrate.test.js +1 -2
  28. package/commands/project/__tests__/migrateApp.test.js +1 -2
  29. package/commands/project/__tests__/profile.test.js +1 -1
  30. package/commands/project/add.d.ts +1 -1
  31. package/commands/project/add.js +4 -10
  32. package/commands/project/create.js +10 -11
  33. package/commands/project/deploy.js +11 -63
  34. package/commands/project/dev/deprecatedFlow.js +2 -1
  35. package/commands/project/dev/index.js +36 -15
  36. package/commands/project/dev/unifiedFlow.js +14 -10
  37. package/commands/project/download.js +1 -2
  38. package/commands/project/installDeps.js +1 -2
  39. package/commands/project/listBuilds.js +2 -2
  40. package/commands/project/logs.js +2 -2
  41. package/commands/project/migrate.js +41 -13
  42. package/commands/project/migrateApp.js +1 -2
  43. package/commands/project/open.js +1 -2
  44. package/commands/project/profile/add.js +3 -3
  45. package/commands/project/profile/delete.js +1 -2
  46. package/commands/project/profile.js +2 -3
  47. package/commands/project/upload.js +4 -4
  48. package/commands/project/validate.js +2 -2
  49. package/commands/project/watch.js +4 -4
  50. package/commands/project.js +1 -2
  51. package/commands/sandbox/delete.js +1 -1
  52. package/commands/testAccount/importData.d.ts +1 -1
  53. package/commands/testAccount/importData.js +1 -1
  54. package/commands/testAccount.js +1 -1
  55. package/lang/en.d.ts +104 -56
  56. package/lang/en.js +118 -68
  57. package/lang/en.lyaml +12 -12
  58. package/lib/__tests__/hasFeature.test.js +145 -7
  59. package/lib/__tests__/importData.test.js +1 -1
  60. package/lib/app/__tests__/migrate.test.js +26 -31
  61. package/lib/app/migrate.d.ts +3 -10
  62. package/lib/app/migrate.js +14 -25
  63. package/lib/constants.d.ts +9 -0
  64. package/lib/constants.js +9 -0
  65. package/lib/errorHandlers/index.d.ts +4 -0
  66. package/lib/errorHandlers/index.js +1 -1
  67. package/lib/hasFeature.js +6 -0
  68. package/lib/importData.js +1 -1
  69. package/lib/links.d.ts +1 -0
  70. package/lib/links.js +10 -3
  71. package/lib/mcp/setup.d.ts +0 -2
  72. package/lib/mcp/setup.js +4 -29
  73. package/lib/middleware/fireAlarmMiddleware.js +15 -5
  74. package/lib/projects/__tests__/AppDevModeInterface.test.js +72 -44
  75. package/lib/projects/__tests__/LocalDevProcess.test.js +228 -16
  76. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +16 -21
  77. package/lib/projects/__tests__/components.test.js +164 -7
  78. package/lib/projects/__tests__/deploy.test.js +229 -0
  79. package/lib/projects/__tests__/{localDevHelpers.test.js → localDevProjectHelpers.test.js} +5 -3
  80. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  81. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  82. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  83. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  84. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  85. package/lib/projects/add/legacyAddComponent.js +5 -1
  86. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  87. package/lib/projects/add/v3AddComponent.js +22 -5
  88. package/lib/projects/components.d.ts +1 -0
  89. package/lib/projects/components.js +27 -1
  90. package/lib/projects/create/__tests__/v3.test.js +174 -11
  91. package/lib/projects/create/index.js +2 -2
  92. package/lib/projects/create/legacy.js +1 -1
  93. package/lib/projects/create/v3.d.ts +2 -2
  94. package/lib/projects/create/v3.js +38 -13
  95. package/lib/projects/deploy.d.ts +13 -0
  96. package/lib/projects/deploy.js +63 -0
  97. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  98. package/lib/projects/localDev/AppDevModeInterface.js +132 -48
  99. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  100. package/lib/projects/localDev/LocalDevLogger.d.ts +4 -0
  101. package/lib/projects/localDev/LocalDevLogger.js +22 -0
  102. package/lib/projects/localDev/LocalDevManager.js +1 -1
  103. package/lib/projects/localDev/LocalDevProcess.d.ts +7 -5
  104. package/lib/projects/localDev/LocalDevProcess.js +93 -20
  105. package/lib/projects/localDev/LocalDevState.d.ts +13 -9
  106. package/lib/projects/localDev/LocalDevState.js +26 -17
  107. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -0
  108. package/lib/projects/localDev/LocalDevWebsocketServer.js +55 -23
  109. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -14
  110. package/lib/projects/localDev/helpers/account.js +233 -0
  111. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  112. package/lib/projects/localDev/helpers/project.js +176 -0
  113. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +4 -0
  114. package/lib/projects/localDev/localDevWebsocketServerUtils.js +10 -0
  115. package/lib/projects/platformVersion.d.ts +1 -0
  116. package/lib/projects/platformVersion.js +10 -0
  117. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  118. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +4 -14
  119. package/lib/projects/upload.js +1 -1
  120. package/lib/projects/urls.d.ts +2 -0
  121. package/lib/projects/urls.js +7 -0
  122. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  123. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  124. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  125. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  126. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  127. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  128. package/lib/prompts/installAppPrompt.d.ts +6 -1
  129. package/lib/prompts/installAppPrompt.js +6 -1
  130. package/lib/prompts/projectAddPrompt.js +3 -2
  131. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  132. package/lib/prompts/promptUtils.d.ts +7 -1
  133. package/lib/prompts/promptUtils.js +17 -1
  134. package/lib/prompts/selectProjectTemplatePrompt.js +3 -1
  135. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  136. package/lib/theme/__tests__/migrate.test.js +233 -0
  137. package/lib/theme/migrate.d.ts +13 -0
  138. package/lib/theme/migrate.js +90 -0
  139. package/lib/ui/index.js +3 -6
  140. package/lib/usageTracking.js +2 -2
  141. package/mcp-server/server.js +2 -1
  142. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  143. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  144. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  145. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  146. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  147. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  148. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  149. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  150. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  151. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  152. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  153. package/mcp-server/tools/cms/HsListTool.js +58 -0
  154. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  155. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  156. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  157. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  158. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  159. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  160. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  161. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  162. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  163. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  164. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  165. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  166. package/mcp-server/tools/index.d.ts +1 -0
  167. package/mcp-server/tools/index.js +16 -0
  168. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  169. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  170. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  171. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  172. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  173. package/mcp-server/tools/project/DocFetchTool.js +3 -3
  174. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  175. package/mcp-server/tools/project/DocsSearchTool.js +8 -8
  176. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  177. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  178. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  179. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  180. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  181. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  182. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  183. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  184. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +3 -3
  185. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +15 -13
  186. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  187. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  188. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  189. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  190. package/mcp-server/tools/project/constants.d.ts +1 -1
  191. package/mcp-server/tools/project/constants.js +14 -6
  192. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  193. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  194. package/mcp-server/utils/cliConfig.d.ts +1 -0
  195. package/mcp-server/utils/cliConfig.js +12 -0
  196. package/package.json +5 -4
  197. package/types/LocalDev.d.ts +21 -4
  198. package/types/Projects.d.ts +1 -0
  199. package/types/Prompts.d.ts +1 -0
  200. package/ui/components/BoxWithTitle.d.ts +8 -0
  201. package/ui/components/BoxWithTitle.js +9 -0
  202. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  203. package/ui/components/HorizontalSelectPrompt.js +30 -0
  204. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  205. package/ui/components/StatusMessageBoxes.js +31 -0
  206. package/ui/index.js +1 -1
  207. package/ui/lib/ui-testing-utils.d.ts +9 -0
  208. package/ui/lib/ui-testing-utils.js +47 -0
  209. package/ui/lib/useTerminalSize.d.ts +13 -0
  210. package/ui/lib/useTerminalSize.js +31 -0
  211. package/ui/styles.d.ts +18 -0
  212. package/ui/styles.js +18 -0
  213. package/ui/views/UiSandbox.d.ts +5 -0
  214. package/ui/views/UiSandbox.js +25 -0
  215. package/lib/projects/localDev/helpers.js +0 -388
  216. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
  217. /package/lib/projects/__tests__/{localDevHelpers.test.d.ts → localDevProjectHelpers.test.d.ts} +0 -0
@@ -1,7 +1,29 @@
1
1
  import fs from 'fs';
2
- import { handleComponentCollision } from '../components.js';
2
+ import { handleComponentCollision, updateHsMetaFilesWithAutoGeneratedFields, } from '../components.js';
3
+ import { uiLogger } from '../../ui/logger.js';
4
+ import { coerceToValidUid } from '@hubspot/project-parsing-lib';
3
5
  vi.mock('fs');
6
+ vi.mock('../../ui/logger.js');
7
+ vi.mock('@hubspot/project-parsing-lib', () => ({
8
+ coerceToValidUid: vi.fn(),
9
+ metafileExtension: '.module.meta.json',
10
+ }));
11
+ vi.mock('@hubspot/project-parsing-lib/src/lib/constants.js', () => ({
12
+ AppKey: 'app',
13
+ }));
14
+ vi.mock('../../../lang/en.js', () => ({
15
+ lib: {
16
+ projects: {
17
+ updateHsMetaFilesWithAutoGeneratedFields: {
18
+ header: 'Updating component metadata files...',
19
+ applicationLog: (type, uid, name) => `Updated ${type} component with uid: ${uid} and name: ${name}`,
20
+ componentLog: (type, uid) => `Updated ${type} component with uid: ${uid}`,
21
+ },
22
+ },
23
+ },
24
+ }));
4
25
  const mockedFs = vi.mocked(fs);
26
+ const mockCoerceToValidUid = vi.mocked(coerceToValidUid);
5
27
  describe('lib/projects/components', () => {
6
28
  describe('handleComponentCollision()', () => {
7
29
  const mockCollision = {
@@ -152,10 +174,9 @@ describe('lib/projects/components', () => {
152
174
  collisions: [
153
175
  'regular.js',
154
176
  'nested/path/file.ts',
155
- 'component.module.meta.json',
177
+ 'component.meta.json',
156
178
  'another.meta.json',
157
179
  'package.json',
158
- 'nested/package.json',
159
180
  ],
160
181
  };
161
182
  // Mock metafileExtension
@@ -166,16 +187,152 @@ describe('lib/projects/components', () => {
166
187
  const mockMetaContent = '{}';
167
188
  mockedFs.readFileSync.mockReturnValue(mockMetaContent);
168
189
  mockedFs.writeFileSync.mockImplementation(() => { });
190
+ // Track what files are being copied to debug the issue
169
191
  mockedFs.copyFileSync.mockImplementation(() => { });
170
192
  // Mock console.log for package.json handling
171
193
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
172
194
  handleComponentCollision(collision);
173
- // Should copy 2 source files
174
- expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2);
195
+ expect(mockedFs.readFileSync).toHaveBeenCalledTimes(2);
175
196
  // Should handle 2 metafiles
176
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/component.module.meta.json', 'utf-8');
177
- expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/another.meta.json', 'utf-8');
197
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith('/dest/path/package.json', 'utf-8');
198
+ expect(mockedFs.readFileSync).toHaveBeenCalledWith('/src/path/package.json', 'utf-8');
178
199
  consoleSpy.mockRestore();
179
200
  });
180
201
  });
202
+ describe('updateHsMetaFilesWithAutoGeneratedFields()', () => {
203
+ const mockUiLogger = vi.mocked(uiLogger);
204
+ beforeEach(() => {
205
+ vi.resetAllMocks();
206
+ mockCoerceToValidUid.mockImplementation((input) => input);
207
+ });
208
+ afterEach(() => {
209
+ vi.restoreAllMocks();
210
+ });
211
+ it('updates component metadata files with project-specific UIDs', () => {
212
+ const projectName = 'my-project';
213
+ const hsMetaFilePaths = [
214
+ '/path/to/component1.meta.json',
215
+ '/path/to/component2.meta.json',
216
+ ];
217
+ const component1 = {
218
+ type: 'card',
219
+ uid: 'old-uid-1',
220
+ config: {
221
+ name: 'Old Name',
222
+ },
223
+ };
224
+ const component2 = {
225
+ type: 'function',
226
+ uid: 'old-uid-2',
227
+ };
228
+ mockedFs.readFileSync
229
+ .mockReturnValueOnce(JSON.stringify(component1))
230
+ .mockReturnValueOnce(JSON.stringify(component2));
231
+ mockedFs.writeFileSync.mockImplementation(() => { });
232
+ mockCoerceToValidUid
233
+ .mockReturnValueOnce('card-my-project')
234
+ .mockReturnValueOnce('function-my-project');
235
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
236
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
237
+ type: 'card',
238
+ uid: 'card-my-project',
239
+ config: {
240
+ name: 'Old Name',
241
+ },
242
+ }, null, 2));
243
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component2.meta.json', JSON.stringify({
244
+ type: 'function',
245
+ uid: 'function-my-project',
246
+ }, null, 2));
247
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
248
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated card component with uid: card-my-project');
249
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated function component with uid: function-my-project');
250
+ });
251
+ it('handles app components by updating both uid and config.name', () => {
252
+ const projectName = 'test-app';
253
+ const hsMetaFilePaths = ['/path/to/app.meta.json'];
254
+ const appComponent = {
255
+ type: 'app',
256
+ uid: 'old-app-uid',
257
+ config: {
258
+ name: 'Old App Name',
259
+ other: 'property',
260
+ },
261
+ };
262
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
263
+ mockedFs.writeFileSync.mockImplementation(() => { });
264
+ mockCoerceToValidUid.mockReturnValue('app-test-app');
265
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
266
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
267
+ type: 'app',
268
+ uid: 'app-test-app',
269
+ config: {
270
+ name: 'test-app-Application',
271
+ other: 'property',
272
+ },
273
+ }, null, 2));
274
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-test-app and name: test-app-Application');
275
+ });
276
+ it('handles UID collisions by using timestamps', () => {
277
+ const projectName = 'collision-project';
278
+ const hsMetaFilePaths = ['/path/to/component1.meta.json'];
279
+ const existingUids = ['card-collision-project'];
280
+ const component1 = { type: 'card', uid: 'old-uid-1' };
281
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(component1));
282
+ mockedFs.writeFileSync.mockImplementation(() => { });
283
+ // Mock Date.now to return consistent value for testing
284
+ vi.spyOn(Date, 'now').mockReturnValue(1234567890);
285
+ mockCoerceToValidUid
286
+ .mockReturnValueOnce('card-collision-project')
287
+ .mockReturnValueOnce('card-1234567890-collision-project');
288
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids);
289
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component1.meta.json', JSON.stringify({
290
+ type: 'card',
291
+ uid: 'card-1234567890-collision-project',
292
+ }, null, 2));
293
+ });
294
+ it('falls back to original uid when coerceToValidUid returns null', () => {
295
+ const projectName = 'fallback-project';
296
+ const hsMetaFilePaths = ['/path/to/component.meta.json'];
297
+ const component = {
298
+ type: 'card',
299
+ uid: 'original-uid',
300
+ };
301
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(component));
302
+ mockedFs.writeFileSync.mockImplementation(() => { });
303
+ mockCoerceToValidUid.mockReturnValue(undefined);
304
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
305
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/component.meta.json', JSON.stringify({
306
+ type: 'card',
307
+ uid: 'original-uid',
308
+ }, null, 2));
309
+ });
310
+ it('handles empty hsMetaFilePaths array', () => {
311
+ const projectName = 'empty-project';
312
+ const hsMetaFilePaths = [];
313
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
314
+ expect(mockedFs.readFileSync).not.toHaveBeenCalled();
315
+ expect(mockedFs.writeFileSync).not.toHaveBeenCalled();
316
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updating component metadata files...');
317
+ expect(mockUiLogger.log).toHaveBeenCalledWith('');
318
+ });
319
+ it('handles components without config property for app type', () => {
320
+ const projectName = 'no-config-project';
321
+ const hsMetaFilePaths = ['/path/to/app.meta.json'];
322
+ const appComponent = {
323
+ type: 'app',
324
+ uid: 'app-uid',
325
+ // No config property
326
+ };
327
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(appComponent));
328
+ mockedFs.writeFileSync.mockImplementation(() => { });
329
+ mockCoerceToValidUid.mockReturnValue('app-no-config-project');
330
+ updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths);
331
+ expect(mockedFs.writeFileSync).toHaveBeenCalledWith('/path/to/app.meta.json', JSON.stringify({
332
+ type: 'app',
333
+ uid: 'app-no-config-project',
334
+ }, null, 2));
335
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Updated app component with uid: app-no-config-project');
336
+ });
337
+ });
181
338
  });
@@ -0,0 +1,229 @@
1
+ import { vi } from 'vitest';
2
+ import { validateBuildIdForDeploy, logDeployErrors, handleProjectDeploy, } from '../deploy.js';
3
+ import { uiLogger } from '../../ui/logger.js';
4
+ import { commands } from '../../../lang/en.js';
5
+ import { PROJECT_ERROR_TYPES } from '../../constants.js';
6
+ import { deployProject } from '@hubspot/local-dev-lib/api/projects';
7
+ import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
8
+ // Mock external dependencies
9
+ vi.mock('../../ui/logger.js');
10
+ vi.mock('@hubspot/local-dev-lib/api/projects');
11
+ vi.mock('../pollProjectBuildAndDeploy.js');
12
+ const mockUiLogger = vi.mocked(uiLogger);
13
+ const mockDeployProject = vi.mocked(deployProject);
14
+ const mockPollDeployStatus = vi.mocked(pollDeployStatus);
15
+ describe('lib/projects/deploy', () => {
16
+ beforeEach(() => {
17
+ vi.resetAllMocks();
18
+ });
19
+ describe('validateBuildIdForDeploy()', () => {
20
+ const accountId = 12345;
21
+ const projectName = 'test-project';
22
+ it('returns true when build ID is valid for deployment', () => {
23
+ const buildId = 5;
24
+ const deployedBuildId = 3;
25
+ const latestBuildId = 10;
26
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
27
+ expect(result).toBe(true);
28
+ });
29
+ it('returns error message when build ID does not exist', () => {
30
+ const buildId = 15;
31
+ const deployedBuildId = 3;
32
+ const latestBuildId = 10;
33
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
34
+ expect(result).toBe(commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName));
35
+ });
36
+ it('returns error message when build is already deployed', () => {
37
+ const buildId = 3;
38
+ const deployedBuildId = 3;
39
+ const latestBuildId = 10;
40
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
41
+ expect(result).toBe(commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName));
42
+ });
43
+ it('handles edge case when deployedBuildId is undefined', () => {
44
+ const buildId = 5;
45
+ const deployedBuildId = undefined;
46
+ const latestBuildId = 10;
47
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
48
+ expect(result).toBe(true);
49
+ });
50
+ });
51
+ describe('logDeployErrors()', () => {
52
+ it('logs main error message and individual error messages', () => {
53
+ const errorData = {
54
+ message: 'Deploy failed with errors',
55
+ errors: [
56
+ {
57
+ message: 'Component error 1',
58
+ subCategory: 'SOME_ERROR',
59
+ context: { COMPONENT_NAME: 'test-component' },
60
+ },
61
+ {
62
+ message: 'Component error 2',
63
+ subCategory: 'ANOTHER_ERROR',
64
+ context: { COMPONENT_NAME: 'another-component' },
65
+ },
66
+ ],
67
+ };
68
+ logDeployErrors(errorData);
69
+ expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy failed with errors');
70
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 1');
71
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 2');
72
+ });
73
+ it('handles DEPLOY_CONTAINS_REMOVALS error type specially', () => {
74
+ const errorData = {
75
+ message: 'Deploy contains removals',
76
+ errors: [
77
+ {
78
+ message: 'Component will be removed',
79
+ subCategory: PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS,
80
+ context: { COMPONENT_NAME: 'removed-component' },
81
+ },
82
+ ],
83
+ };
84
+ logDeployErrors(errorData);
85
+ expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy contains removals');
86
+ expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployContainsRemovals('removed-component'));
87
+ });
88
+ it('handles empty errors array', () => {
89
+ const errorData = {
90
+ message: 'No specific errors',
91
+ errors: [],
92
+ };
93
+ logDeployErrors(errorData);
94
+ expect(mockUiLogger.error).toHaveBeenCalledWith('No specific errors');
95
+ expect(mockUiLogger.log).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+ describe('handleProjectDeploy()', () => {
99
+ const targetAccountId = 12345;
100
+ const projectName = 'test-project';
101
+ const buildId = 5;
102
+ const useV3Api = true;
103
+ const force = false;
104
+ it('successfully deploys and returns deploy result', async () => {
105
+ const mockDeployResponseData = {
106
+ id: 'deploy-123',
107
+ buildResultType: 'DEPLOY_QUEUED',
108
+ links: {
109
+ status: 'http://status-url',
110
+ },
111
+ };
112
+ const mockDeployResult = {
113
+ deployId: 123,
114
+ buildId: 5,
115
+ status: 'SUCCESS',
116
+ enqueuedAt: '2023-01-01T00:00:00Z',
117
+ startedAt: '2023-01-01T00:01:00Z',
118
+ finishedAt: '2023-01-01T00:05:00Z',
119
+ portalId: targetAccountId,
120
+ projectName: 'test-project',
121
+ userId: 456,
122
+ source: 'HUBSPOT_USER',
123
+ subdeployStatuses: [],
124
+ };
125
+ mockDeployProject.mockResolvedValue({
126
+ data: mockDeployResponseData,
127
+ });
128
+ mockPollDeployStatus.mockResolvedValue(mockDeployResult);
129
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
130
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
131
+ expect(deploy).toEqual(mockDeployResult);
132
+ });
133
+ it('handles blocked deploy with warnings', async () => {
134
+ const mockBlockedResponse = {
135
+ buildResultType: 'DEPLOY_BLOCKED',
136
+ issues: [
137
+ {
138
+ uid: 'component-1',
139
+ componentTypeName: 'module',
140
+ errorMessages: [],
141
+ blockingMessages: [
142
+ {
143
+ message: 'This is a warning',
144
+ isWarning: true,
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ };
150
+ mockDeployProject.mockResolvedValue({
151
+ data: mockBlockedResponse,
152
+ });
153
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
154
+ expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
155
+ });
156
+ it('handles blocked deploy with errors (cannot be forced)', async () => {
157
+ const mockBlockedResponse = {
158
+ buildResultType: 'DEPLOY_BLOCKED',
159
+ issues: [
160
+ {
161
+ uid: 'component-1',
162
+ componentTypeName: 'module',
163
+ errorMessages: [],
164
+ blockingMessages: [
165
+ {
166
+ message: 'This is an error',
167
+ isWarning: false,
168
+ },
169
+ ],
170
+ },
171
+ ],
172
+ };
173
+ mockDeployProject.mockResolvedValue({
174
+ data: mockBlockedResponse,
175
+ });
176
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
177
+ expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deployBlockedHeader);
178
+ expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentWarning('component-1', 'module', 'This is an error'));
179
+ });
180
+ it('handles blocked deploy with no blocking messages', async () => {
181
+ const mockBlockedResponse = {
182
+ buildResultType: 'DEPLOY_BLOCKED',
183
+ issues: [
184
+ {
185
+ uid: 'component-1',
186
+ componentTypeName: 'module',
187
+ errorMessages: [],
188
+ blockingMessages: [],
189
+ },
190
+ ],
191
+ };
192
+ mockDeployProject.mockResolvedValue({
193
+ data: mockBlockedResponse,
194
+ });
195
+ await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
196
+ expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
197
+ expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployIssueComponentGeneric('component-1', 'module'));
198
+ });
199
+ it('handles general deploy failure', async () => {
200
+ mockDeployProject.mockResolvedValue({ data: null });
201
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
202
+ expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
203
+ expect(deploy).toBeUndefined();
204
+ });
205
+ it('handles undefined deploy response', async () => {
206
+ mockDeployProject.mockResolvedValue({ data: undefined });
207
+ const deploy = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
208
+ expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
209
+ expect(deploy).toBeUndefined();
210
+ });
211
+ it('passes correct parameters to deployProject', async () => {
212
+ const mockDeployResponseData = {
213
+ id: 'deploy-123',
214
+ buildResultType: 'DEPLOY_QUEUED',
215
+ links: {
216
+ status: 'http://status-url',
217
+ },
218
+ };
219
+ mockDeployProject.mockResolvedValue({
220
+ data: mockDeployResponseData,
221
+ });
222
+ mockPollDeployStatus.mockResolvedValue({});
223
+ await handleProjectDeploy(targetAccountId, projectName, buildId, false, true);
224
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, false, // useV3Api
225
+ true // force
226
+ );
227
+ });
228
+ });
229
+ });
@@ -5,7 +5,7 @@ import { downloadProject } from '@hubspot/local-dev-lib/api/projects';
5
5
  import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
6
6
  import { isDeepEqual } from '@hubspot/local-dev-lib/isDeepEqual';
7
7
  import { translate } from '@hubspot/project-parsing-lib';
8
- import { isDeployedProjectUpToDateWithLocal } from '../localDev/helpers.js';
8
+ import { isDeployedProjectUpToDateWithLocal } from '../localDev/helpers/project.js';
9
9
  // Mock all external dependencies
10
10
  vi.mock('@hubspot/local-dev-lib/api/projects');
11
11
  vi.mock('@hubspot/local-dev-lib/archive');
@@ -99,7 +99,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
99
99
  it('should clean up temp directory even when errors occur', async () => {
100
100
  // Mock downloadProject to throw an error after temp dir is created
101
101
  downloadProject.mockRejectedValue(new Error('Download Error'));
102
- await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Download Error');
102
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
103
+ expect(result).toBe(false);
103
104
  expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
104
105
  });
105
106
  it('should handle translateForLocalDev errors', async () => {
@@ -111,7 +112,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
111
112
  extractZipArchive.mockResolvedValue(undefined);
112
113
  // Mock translate to throw an error
113
114
  translate.mockRejectedValue(new Error('Translation Error'));
114
- await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Translation Error');
115
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
116
+ expect(result).toBe(false);
115
117
  expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
116
118
  });
117
119
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
- import { useV3Api } from '../buildAndDeploy.js';
2
- describe('buildAndDeploy', () => {
1
+ import { useV3Api } from '../platformVersion.js';
2
+ describe('platformVersion', () => {
3
3
  describe('useV3Api', () => {
4
4
  it('returns true if platform version is UNSTABLE', () => {
5
5
  expect(useV3Api('UNSTABLE')).toBe(true);
@@ -4,6 +4,7 @@ import { getProjectComponentListFromRepo } from '../../create/legacy.js';
4
4
  import { projectAddPrompt } from '../../../prompts/projectAddPrompt.js';
5
5
  import { logger } from '@hubspot/local-dev-lib/logger';
6
6
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
7
+ import { trackCommandUsage } from '../../../usageTracking.js';
7
8
  import { ComponentTypes, } from '../../../../types/Projects.js';
8
9
  import { commands } from '../../../../lang/en.js';
9
10
  vi.mock('../../structure');
@@ -11,21 +12,25 @@ vi.mock('../../create/legacy');
11
12
  vi.mock('../../../prompts/projectAddPrompt');
12
13
  vi.mock('@hubspot/local-dev-lib/logger');
13
14
  vi.mock('@hubspot/local-dev-lib/github');
15
+ vi.mock('../../../usageTracking.js');
14
16
  const mockedFindProjectComponents = vi.mocked(findProjectComponents);
15
17
  const mockedGetProjectComponentListFromRepo = vi.mocked(getProjectComponentListFromRepo);
16
18
  const mockedProjectAddPrompt = vi.mocked(projectAddPrompt);
17
19
  const mockedLogger = vi.mocked(logger);
18
20
  const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
21
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
19
22
  describe('lib/projects/add/legacyAddComponent', () => {
20
23
  const mockProjectConfig = {
21
24
  name: 'test-project',
22
25
  srcDir: 'src',
23
26
  platformVersion: 'v1',
24
27
  };
28
+ const accountId = 1234567890;
25
29
  const mockArgs = { name: 'test-component', type: 'module' };
26
30
  const projectDir = '/path/to/project';
27
31
  beforeEach(() => {
28
32
  vi.resetAllMocks();
33
+ mockedTrackCommandUsage.mockResolvedValue();
29
34
  });
30
35
  describe('legacyAddComponent()', () => {
31
36
  it('successfully adds a component to a project without public apps', async () => {
@@ -58,7 +63,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
58
63
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
59
64
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
60
65
  mockedCloneGithubRepo.mockResolvedValue(true);
61
- await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
66
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
62
67
  expect(mockedFindProjectComponents).toHaveBeenCalledWith(projectDir);
63
68
  expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
64
69
  expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
@@ -67,6 +72,9 @@ describe('lib/projects/add/legacyAddComponent', () => {
67
72
  branch: 'main',
68
73
  hideLogs: true,
69
74
  }));
75
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
76
+ type: 'module',
77
+ }, accountId);
70
78
  expect(mockedLogger.log).toHaveBeenCalledWith(commands.project.add.creatingComponent('test-project'));
71
79
  expect(mockedLogger.success).toHaveBeenCalledWith(commands.project.add.success('new-component'));
72
80
  });
@@ -97,7 +105,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
97
105
  },
98
106
  ];
99
107
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
100
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
108
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
101
109
  expect(mockedGetProjectComponentListFromRepo).not.toHaveBeenCalled();
102
110
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
103
111
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
@@ -118,7 +126,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
118
126
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
119
127
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
120
128
  mockedCloneGithubRepo.mockResolvedValue(true);
121
- await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
129
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
122
130
  expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
123
131
  expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
124
132
  expect(mockedCloneGithubRepo).toHaveBeenCalled();
@@ -140,7 +148,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
140
148
  ];
141
149
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
142
150
  mockedGetProjectComponentListFromRepo.mockResolvedValue([]);
143
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
151
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
144
152
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
145
153
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
146
154
  });
@@ -162,7 +170,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
162
170
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
163
171
  // @ts-expect-error Breaking stuff on purpose
164
172
  mockedGetProjectComponentListFromRepo.mockResolvedValue(null);
165
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
173
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
166
174
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
167
175
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
168
176
  });
@@ -196,9 +204,44 @@ describe('lib/projects/add/legacyAddComponent', () => {
196
204
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
197
205
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
198
206
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
199
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
207
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
200
208
  expect(mockedCloneGithubRepo).toHaveBeenCalled();
201
209
  expect(mockedLogger.success).not.toHaveBeenCalled();
202
210
  });
211
+ it('calls trackCommandUsage with correct component type', async () => {
212
+ const mockComponents = [
213
+ {
214
+ type: ComponentTypes.PrivateApp,
215
+ config: {
216
+ name: 'private-app',
217
+ description: '',
218
+ uid: '',
219
+ scopes: [],
220
+ public: false,
221
+ },
222
+ runnable: true,
223
+ path: '/path/to/private-app',
224
+ },
225
+ ];
226
+ const mockComponentList = [
227
+ { label: 'Card Component', path: 'card-component', type: 'card' },
228
+ ];
229
+ const mockPromptResponse = {
230
+ name: 'new-card',
231
+ componentTemplate: {
232
+ label: 'Card Component',
233
+ path: 'card-template-path',
234
+ type: 'card',
235
+ },
236
+ };
237
+ mockedFindProjectComponents.mockResolvedValue(mockComponents);
238
+ mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
239
+ mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
240
+ mockedCloneGithubRepo.mockResolvedValue(true);
241
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
242
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
243
+ type: 'card',
244
+ }, accountId);
245
+ });
203
246
  });
204
247
  });