@hubspot/cli 7.6.0-beta.11 → 7.6.0-beta.13

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 (158) hide show
  1. package/commands/app/__tests__/migrate.test.js +1 -0
  2. package/commands/getStarted.js +7 -20
  3. package/commands/mcp/setup.d.ts +0 -1
  4. package/commands/mcp/setup.js +11 -11
  5. package/commands/project/__tests__/add.test.js +64 -0
  6. package/commands/project/__tests__/create.test.js +57 -0
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.d.ts +1 -1
  9. package/commands/project/add.js +3 -5
  10. package/commands/project/create.js +7 -2
  11. package/commands/project/deploy.js +9 -61
  12. package/commands/project/dev/index.js +1 -1
  13. package/commands/project/dev/unifiedFlow.js +4 -1
  14. package/commands/project/migrate.js +26 -7
  15. package/commands/project/upload.js +2 -2
  16. package/commands/project/validate.js +1 -1
  17. package/commands/project/watch.js +2 -2
  18. package/lang/en.d.ts +20 -5
  19. package/lang/en.js +38 -22
  20. package/lang/en.lyaml +12 -12
  21. package/lib/__tests__/hasFeature.test.js +145 -7
  22. package/lib/__tests__/importData.test.js +1 -1
  23. package/lib/app/__tests__/migrate.test.js +14 -51
  24. package/lib/app/migrate.d.ts +2 -8
  25. package/lib/app/migrate.js +5 -73
  26. package/lib/constants.d.ts +4 -0
  27. package/lib/constants.js +4 -0
  28. package/lib/errorHandlers/index.d.ts +4 -0
  29. package/lib/errorHandlers/index.js +1 -1
  30. package/lib/hasFeature.js +6 -0
  31. package/lib/importData.js +1 -1
  32. package/lib/links.d.ts +1 -0
  33. package/lib/links.js +10 -3
  34. package/lib/mcp/setup.d.ts +0 -2
  35. package/lib/mcp/setup.js +4 -29
  36. package/lib/projects/__tests__/AppDevModeInterface.test.js +71 -44
  37. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  38. package/lib/projects/__tests__/components.test.js +164 -7
  39. package/lib/projects/__tests__/deploy.test.js +164 -0
  40. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  41. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  42. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  43. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  44. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  45. package/lib/projects/add/legacyAddComponent.js +5 -1
  46. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  47. package/lib/projects/add/v3AddComponent.js +22 -5
  48. package/lib/projects/components.d.ts +1 -0
  49. package/lib/projects/components.js +27 -1
  50. package/lib/projects/create/__tests__/v3.test.js +97 -9
  51. package/lib/projects/create/index.js +2 -2
  52. package/lib/projects/create/legacy.js +1 -1
  53. package/lib/projects/create/v3.d.ts +2 -2
  54. package/lib/projects/create/v3.js +35 -12
  55. package/lib/projects/deploy.d.ts +13 -0
  56. package/lib/projects/deploy.js +63 -0
  57. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  58. package/lib/projects/localDev/AppDevModeInterface.js +110 -47
  59. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  60. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  61. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  62. package/lib/projects/localDev/LocalDevState.js +9 -1
  63. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  64. package/lib/projects/localDev/helpers/project.js +6 -8
  65. package/lib/projects/platformVersion.d.ts +1 -0
  66. package/lib/projects/platformVersion.js +10 -0
  67. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  68. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  69. package/lib/projects/upload.js +1 -1
  70. package/lib/projects/urls.d.ts +1 -0
  71. package/lib/projects/urls.js +3 -0
  72. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  73. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  74. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  75. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  76. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  77. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  78. package/lib/prompts/installAppPrompt.d.ts +6 -1
  79. package/lib/prompts/installAppPrompt.js +6 -1
  80. package/lib/prompts/projectAddPrompt.js +1 -1
  81. package/lib/prompts/promptUtils.d.ts +5 -0
  82. package/lib/prompts/promptUtils.js +9 -0
  83. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  84. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  85. package/lib/theme/__tests__/migrate.test.js +233 -0
  86. package/lib/theme/migrate.d.ts +13 -0
  87. package/lib/theme/migrate.js +90 -0
  88. package/lib/ui/index.js +3 -6
  89. package/lib/usageTracking.js +2 -2
  90. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  91. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  92. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  93. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  94. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  95. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  96. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  97. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  98. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  99. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  100. package/mcp-server/tools/cms/HsListTool.js +1 -1
  101. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  102. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  103. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  106. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  107. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  108. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  109. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  110. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  111. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  112. package/mcp-server/tools/index.js +13 -1
  113. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  114. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  115. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  116. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  117. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  118. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  119. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  120. package/mcp-server/tools/project/DocsSearchTool.js +7 -7
  121. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  122. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  123. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  124. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  125. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  126. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  127. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  128. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  129. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  130. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
  131. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  132. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  133. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  134. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  135. package/mcp-server/tools/project/constants.d.ts +1 -1
  136. package/mcp-server/tools/project/constants.js +14 -6
  137. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  138. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  139. package/mcp-server/utils/cliConfig.d.ts +1 -0
  140. package/mcp-server/utils/cliConfig.js +12 -0
  141. package/package.json +4 -3
  142. package/types/LocalDev.d.ts +2 -1
  143. package/types/Projects.d.ts +1 -0
  144. package/ui/components/BoxWithTitle.d.ts +8 -0
  145. package/ui/components/BoxWithTitle.js +9 -0
  146. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  147. package/ui/components/HorizontalSelectPrompt.js +30 -0
  148. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  149. package/ui/components/StatusMessageBoxes.js +31 -0
  150. package/ui/lib/ui-testing-utils.d.ts +9 -0
  151. package/ui/lib/ui-testing-utils.js +47 -0
  152. package/ui/lib/useTerminalSize.d.ts +13 -0
  153. package/ui/lib/useTerminalSize.js +31 -0
  154. package/ui/styles.d.ts +18 -0
  155. package/ui/styles.js +18 -0
  156. package/ui/views/UiSandbox.d.ts +5 -0
  157. package/ui/views/UiSandbox.js +25 -0
  158. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
@@ -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
  });
@@ -7,6 +7,7 @@ import { projectAddPromptV3 } from '../../../prompts/projectAddPrompt.js';
7
7
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
8
8
  import { logger } from '@hubspot/local-dev-lib/logger';
9
9
  import { getProjectMetadata } from '@hubspot/project-parsing-lib/src/lib/project.js';
10
+ import { trackCommandUsage } from '../../../usageTracking.js';
10
11
  import { commands } from '../../../../lang/en.js';
11
12
  vi.mock('fs');
12
13
  vi.mock('../../../prompts/promptUtils');
@@ -16,6 +17,7 @@ vi.mock('../../../prompts/projectAddPrompt');
16
17
  vi.mock('@hubspot/local-dev-lib/github');
17
18
  vi.mock('@hubspot/local-dev-lib/logger');
18
19
  vi.mock('@hubspot/project-parsing-lib/src/lib/project');
20
+ vi.mock('../../../usageTracking');
19
21
  const mockedFs = vi.mocked(fs);
20
22
  const mockedGetConfigForPlatformVersion = vi.mocked(getConfigForPlatformVersion);
21
23
  const mockedConfirmPrompt = vi.mocked(confirmPrompt);
@@ -24,14 +26,20 @@ const mockedProjectAddPromptV3 = vi.mocked(projectAddPromptV3);
24
26
  const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
25
27
  const mockedLogger = vi.mocked(logger);
26
28
  const mockedGetProjectMetadata = vi.mocked(getProjectMetadata);
29
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
27
30
  describe('lib/projects/add/v3AddComponent', () => {
28
31
  const mockProjectConfig = {
29
32
  name: 'test-project',
30
33
  srcDir: 'src',
31
34
  platformVersion: 'v3',
32
35
  };
33
- const mockArgs = { name: 'test-component', type: 'module' };
36
+ const mockArgs = {
37
+ name: 'test-component',
38
+ type: 'module',
39
+ derivedAccountId: 1234,
40
+ };
34
41
  const projectDir = '/path/to/project';
42
+ const mockAccountId = 123;
35
43
  const mockComponentTemplate = {
36
44
  label: 'Test Component',
37
45
  path: 'test-component',
@@ -62,6 +70,7 @@ describe('lib/projects/add/v3AddComponent', () => {
62
70
  authType: 'oauth',
63
71
  distribution: 'private',
64
72
  });
73
+ mockedTrackCommandUsage.mockResolvedValue();
65
74
  });
66
75
  describe('v3AddComponent()', () => {
67
76
  it('successfully adds a component when app already exists', async () => {
@@ -79,10 +88,13 @@ describe('lib/projects/add/v3AddComponent', () => {
79
88
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
80
89
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
81
90
  mockedCloneGithubRepo.mockResolvedValue(true);
82
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
91
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
83
92
  expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('v3');
84
93
  expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
85
94
  expect(mockedProjectAddPromptV3).toHaveBeenCalled();
95
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
96
+ type: 'module',
97
+ }, mockAccountId);
86
98
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
87
99
  sourceDir: ['v3/test-component'],
88
100
  hideLogs: true,
@@ -106,8 +118,11 @@ describe('lib/projects/add/v3AddComponent', () => {
106
118
  mockedConfirmPrompt.mockResolvedValue(true);
107
119
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
108
120
  mockedCloneGithubRepo.mockResolvedValue(true);
109
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
121
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
110
122
  expect(mockedCreateV3App).toHaveBeenCalled();
123
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
124
+ type: 'module',
125
+ }, mockAccountId);
111
126
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
112
127
  sourceDir: ['v3/test-component', 'v3/app-template'],
113
128
  }));
@@ -132,8 +147,11 @@ describe('lib/projects/add/v3AddComponent', () => {
132
147
  mockedConfirmPrompt.mockResolvedValue(true);
133
148
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
134
149
  mockedCloneGithubRepo.mockResolvedValue(true);
135
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
150
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
136
151
  expect(mockedCreateV3App).not.toHaveBeenCalled();
152
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
153
+ type: '',
154
+ }, mockAccountId);
137
155
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
138
156
  });
139
157
  it('throws an error when app count exceeds maximum', async () => {
@@ -146,7 +164,7 @@ describe('lib/projects/add/v3AddComponent', () => {
146
164
  };
147
165
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
148
166
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataMaxApps);
149
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('This project currently has the maximum number of apps: 1');
167
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
150
168
  });
151
169
  it('throws an error when components list is empty', async () => {
152
170
  const mockEmptyConfig = {
@@ -154,7 +172,7 @@ describe('lib/projects/add/v3AddComponent', () => {
154
172
  parentComponents: [],
155
173
  };
156
174
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockEmptyConfig);
157
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
175
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
158
176
  });
159
177
  it('throws an error when app meta file cannot be parsed', async () => {
160
178
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
@@ -162,7 +180,7 @@ describe('lib/projects/add/v3AddComponent', () => {
162
180
  mockedFs.readFileSync.mockImplementation(() => {
163
181
  throw new Error('File read error');
164
182
  });
165
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('Unable to parse app file');
183
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
166
184
  });
167
185
  it('throws an error when cloning fails', async () => {
168
186
  const mockAppMeta = {
@@ -179,7 +197,123 @@ describe('lib/projects/add/v3AddComponent', () => {
179
197
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
180
198
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
181
199
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
182
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
200
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
201
+ });
202
+ it('should track usage with multiple component types', async () => {
203
+ const mockAppMeta = {
204
+ config: {
205
+ distribution: 'private',
206
+ auth: { type: 'oauth' },
207
+ },
208
+ };
209
+ const mockSecondComponentTemplate = {
210
+ label: 'Test Card',
211
+ path: 'test-card',
212
+ type: 'card',
213
+ supportedAuthTypes: ['oauth'],
214
+ supportedDistributions: ['private'],
215
+ };
216
+ const mockPromptResponse = {
217
+ componentTemplate: [mockComponentTemplate, mockSecondComponentTemplate],
218
+ };
219
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
220
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
221
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
222
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
223
+ mockedCloneGithubRepo.mockResolvedValue(true);
224
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
225
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
226
+ type: 'module,card',
227
+ }, mockAccountId);
228
+ });
229
+ it('should track usage with empty type when no components are selected', async () => {
230
+ const mockProjectMetadataNoApps = {
231
+ hsMetaFiles: [],
232
+ components: {
233
+ app: {
234
+ count: 1,
235
+ maxCount: 1,
236
+ hsMetaFiles: ['/path/to/app.meta.json'],
237
+ },
238
+ module: { count: 0, maxCount: 5, hsMetaFiles: [] },
239
+ },
240
+ };
241
+ const mockPromptResponse = {
242
+ componentTemplate: [],
243
+ };
244
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
245
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
246
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
247
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
248
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
249
+ type: '',
250
+ }, mockAccountId);
251
+ });
252
+ it('should track usage with cliSelector when available', async () => {
253
+ const mockAppMeta = {
254
+ config: {
255
+ distribution: 'private',
256
+ auth: { type: 'oauth' },
257
+ },
258
+ };
259
+ const mockComponentTemplateWithCliSelector = {
260
+ label: 'Workflow Action Tool',
261
+ path: 'workflow-action-tool',
262
+ type: 'workflow-action',
263
+ cliSelector: 'workflow-action-tool',
264
+ supportedAuthTypes: ['oauth'],
265
+ supportedDistributions: ['private'],
266
+ };
267
+ const mockPromptResponse = {
268
+ componentTemplate: [mockComponentTemplateWithCliSelector],
269
+ };
270
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
271
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
272
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
273
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
274
+ mockedCloneGithubRepo.mockResolvedValue(true);
275
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
276
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
277
+ type: 'workflow-action-tool',
278
+ }, mockAccountId);
279
+ });
280
+ it('should track usage with cliSelector for multiple components', async () => {
281
+ const mockAppMeta = {
282
+ config: {
283
+ distribution: 'private',
284
+ auth: { type: 'oauth' },
285
+ },
286
+ };
287
+ const mockComponentWithCliSelector = {
288
+ label: 'Workflow Action Tool',
289
+ path: 'workflow-action-tool',
290
+ type: 'workflow-action',
291
+ cliSelector: 'workflow-action-tool',
292
+ supportedAuthTypes: ['oauth'],
293
+ supportedDistributions: ['private'],
294
+ };
295
+ const mockComponentWithoutCliSelector = {
296
+ label: 'Regular Module',
297
+ path: 'module',
298
+ type: 'module',
299
+ supportedAuthTypes: ['oauth'],
300
+ supportedDistributions: ['private'],
301
+ };
302
+ const mockPromptResponse = {
303
+ componentTemplate: [
304
+ mockComponentWithCliSelector,
305
+ mockComponentWithoutCliSelector,
306
+ ],
307
+ };
308
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
309
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
310
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
311
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
312
+ mockedCloneGithubRepo.mockResolvedValue(true);
313
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
314
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
315
+ type: 'workflow-action-tool,module',
316
+ }, mockAccountId);
183
317
  });
184
318
  });
185
319
  });
@@ -2,4 +2,4 @@ import { ProjectConfig } from '../../../types/Projects.js';
2
2
  export declare function legacyAddComponent(args: {
3
3
  name?: string;
4
4
  type?: string;
5
- }, projectDir: string, projectConfig: ProjectConfig): Promise<void>;
5
+ }, projectDir: string, projectConfig: ProjectConfig, derivedAccountId: number): Promise<void>;
@@ -8,7 +8,8 @@ import path from 'path';
8
8
  import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
9
9
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
10
10
  import { uiLogger } from '../../ui/logger.js';
11
- export async function legacyAddComponent(args, projectDir, projectConfig) {
11
+ import { trackCommandUsage } from '../../usageTracking.js';
12
+ export async function legacyAddComponent(args, projectDir, projectConfig, derivedAccountId) {
12
13
  // We currently only support adding private apps to projects
13
14
  let projectContainsPublicApp = false;
14
15
  try {
@@ -27,6 +28,9 @@ export async function legacyAddComponent(args, projectDir, projectConfig) {
27
28
  throw new Error(commands.project.add.error.failedToFetchComponentList);
28
29
  }
29
30
  const projectAddPromptResponse = await projectAddPrompt(components, args);
31
+ trackCommandUsage('project-add', {
32
+ type: projectAddPromptResponse.componentTemplate.type,
33
+ }, derivedAccountId);
30
34
  try {
31
35
  const componentPath = path.join(projectDir, projectConfig.srcDir, projectAddPromptResponse.name);
32
36
  await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, componentPath, {
@@ -5,4 +5,5 @@ export declare function v3AddComponent(args: {
5
5
  features?: string[];
6
6
  auth?: string;
7
7
  distribution?: string;
8
- }, projectDir: string, projectConfig: ProjectConfig): Promise<void>;
8
+ derivedAccountId: number;
9
+ }, projectDir: string, projectConfig: ProjectConfig, accountId: number): Promise<void>;
@@ -6,13 +6,14 @@ import path from 'path';
6
6
  import fs from 'fs';
7
7
  import { projectAddPromptV3 } from '../../prompts/projectAddPrompt.js';
8
8
  import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
9
- import { handleComponentCollision } from '../components.js';
9
+ import { updateHsMetaFilesWithAutoGeneratedFields, handleComponentCollision, } from '../components.js';
10
10
  import { getProjectMetadata, } from '@hubspot/project-parsing-lib/src/lib/project.js';
11
11
  import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
12
12
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
13
13
  import { debugError } from '../../errorHandlers/index.js';
14
14
  import { uiLogger } from '../../ui/logger.js';
15
- export async function v3AddComponent(args, projectDir, projectConfig) {
15
+ import { trackCommandUsage } from '../../usageTracking.js';
16
+ export async function v3AddComponent(args, projectDir, projectConfig, accountId) {
16
17
  uiLogger.log(commands.project.add.creatingComponent(projectConfig.name));
17
18
  const config = await getConfigForPlatformVersion(projectConfig.platformVersion);
18
19
  const { components, parentComponents } = config;
@@ -20,10 +21,10 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
20
21
  throw new Error(commands.project.add.error.failedToFetchComponentList);
21
22
  }
22
23
  const projectSrcDirectory = path.join(projectDir, projectConfig.srcDir);
23
- const projectMetadata = await getProjectMetadata(projectSrcDirectory);
24
+ const currentProjectMetadata = await getProjectMetadata(projectSrcDirectory);
24
25
  let derivedAuthType;
25
26
  let derivedDistribution;
26
- const appsMetadata = projectMetadata.components[AppKey];
27
+ const appsMetadata = currentProjectMetadata.components[AppKey];
27
28
  const shouldCreateApp = appsMetadata.count === 0;
28
29
  if (shouldCreateApp) {
29
30
  const { authType, distribution } = await createV3App(args.auth, args.distribution);
@@ -45,8 +46,12 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
45
46
  derivedDistribution = apps[0].config?.distribution;
46
47
  derivedAuthType = apps[0].config?.auth?.type;
47
48
  }
48
- const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, projectMetadata);
49
+ const componentTemplateChoices = await calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, args.derivedAccountId, currentProjectMetadata);
49
50
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
51
+ const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.cliSelector || componentTemplate.type);
52
+ await trackCommandUsage('project-add', {
53
+ type: componentTypes?.join(','),
54
+ }, accountId);
50
55
  try {
51
56
  const components = projectAddPromptResponse.componentTemplate?.map((componentTemplate) => {
52
57
  return path.join(projectConfig.platformVersion, componentTemplate.path);
@@ -71,6 +76,18 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
71
76
  branch: DEFAULT_PROJECT_TEMPLATE_BRANCH,
72
77
  handleCollision: handleComponentCollision,
73
78
  });
79
+ const updatedProjectMetadata = await getProjectMetadata(projectSrcDirectory);
80
+ const newHsMetaFiles = updatedProjectMetadata.hsMetaFiles.filter(hsMetaFile => !currentProjectMetadata.hsMetaFiles.includes(hsMetaFile));
81
+ const existingUids = currentProjectMetadata.hsMetaFiles.map(hsMetaFile => {
82
+ try {
83
+ const { uid } = JSON.parse(fs.readFileSync(hsMetaFile, 'utf8'));
84
+ return uid;
85
+ }
86
+ catch (err) {
87
+ return '';
88
+ }
89
+ });
90
+ updateHsMetaFilesWithAutoGeneratedFields(projectConfig.name, newHsMetaFiles, existingUids);
74
91
  uiLogger.success(commands.project.add.success(projectAddPromptResponse.componentTemplate
75
92
  .map(template => `'${template.label}'`)
76
93
  .join(', '), projectAddPromptResponse.componentTemplate.length > 1));
@@ -1,2 +1,3 @@
1
1
  import { Collision } from '@hubspot/local-dev-lib/types/Archive';
2
2
  export declare function handleComponentCollision({ dest, src, collisions }: Collision): void;
3
+ export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[]): void;
@@ -1,6 +1,9 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { metafileExtension } from '@hubspot/project-parsing-lib';
3
+ import { coerceToValidUid, metafileExtension, } from '@hubspot/project-parsing-lib';
4
+ import { uiLogger } from '../ui/logger.js';
5
+ import { AppKey } from '@hubspot/project-parsing-lib/src/lib/constants.js';
6
+ import { lib } from '../../lang/en.js';
4
7
  // Handles a collision between component source files
5
8
  export function handleComponentCollision({ dest, src, collisions }) {
6
9
  const hsMetaFiles = [];
@@ -74,3 +77,26 @@ function handlePackageJsonCollisions(dest, src, packageJsonFiles) {
74
77
  fs.writeFileSync(path.join(dest, file), JSON.stringify(existingPackageJsonContents, null, 2));
75
78
  });
76
79
  }
80
+ export function updateHsMetaFilesWithAutoGeneratedFields(projectName, hsMetaFilePaths, existingUids = []) {
81
+ uiLogger.log('');
82
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.header);
83
+ for (const hsMetaFile of hsMetaFilePaths) {
84
+ const component = JSON.parse(fs.readFileSync(hsMetaFile).toString());
85
+ let uid = coerceToValidUid(`${component.type}-${projectName}`) || component.uid;
86
+ if (existingUids.includes(uid)) {
87
+ uid =
88
+ coerceToValidUid(`${component.type}-${Date.now()}-${projectName}`) ||
89
+ component.uid;
90
+ }
91
+ component.uid = uid;
92
+ if (component.type === AppKey && component.config) {
93
+ component.config.name = `${projectName}-Application`;
94
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.applicationLog(component.type, component.uid, component.config.name));
95
+ }
96
+ else {
97
+ uiLogger.log(lib.projects.updateHsMetaFilesWithAutoGeneratedFields.componentLog(component.type, component.uid));
98
+ }
99
+ fs.writeFileSync(hsMetaFile, JSON.stringify(component, null, 2));
100
+ }
101
+ uiLogger.log('');
102
+ }