@hubspot/cli 7.7.32-experimental.0 → 7.7.34-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 (118) hide show
  1. package/commands/app/__tests__/install.test.js +47 -0
  2. package/commands/app/install.d.ts +8 -0
  3. package/commands/app/install.js +122 -0
  4. package/commands/app.js +6 -1
  5. package/commands/getStarted.js +7 -20
  6. package/commands/project/__tests__/add.test.js +3 -5
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.js +2 -4
  9. package/commands/project/deploy.js +9 -61
  10. package/commands/project/dev/index.js +1 -1
  11. package/commands/project/dev/unifiedFlow.js +4 -1
  12. package/commands/project/upload.d.ts +2 -2
  13. package/commands/project/upload.js +3 -3
  14. package/commands/project/validate.js +1 -1
  15. package/commands/project/watch.js +2 -2
  16. package/commands/testAccount/create.js +3 -0
  17. package/lang/en.d.ts +30 -4
  18. package/lang/en.js +31 -5
  19. package/lib/__tests__/hasFeature.test.js +145 -7
  20. package/lib/__tests__/importData.test.js +1 -1
  21. package/lib/app/migrate.js +9 -2
  22. package/lib/constants.d.ts +2 -0
  23. package/lib/constants.js +2 -0
  24. package/lib/errorHandlers/index.d.ts +4 -0
  25. package/lib/errorHandlers/index.js +1 -1
  26. package/lib/hasFeature.js +6 -0
  27. package/lib/importData.js +1 -1
  28. package/lib/mcp/setup.js +1 -1
  29. package/lib/projectProfiles.d.ts +1 -1
  30. package/lib/projectProfiles.js +10 -2
  31. package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
  32. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  33. package/lib/projects/__tests__/deploy.test.d.ts +1 -0
  34. package/lib/projects/__tests__/deploy.test.js +164 -0
  35. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  36. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  37. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  38. package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
  39. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  40. package/lib/projects/add/legacyAddComponent.js +5 -1
  41. package/lib/projects/add/v3AddComponent.d.ts +1 -0
  42. package/lib/projects/add/v3AddComponent.js +2 -2
  43. package/lib/projects/create/__tests__/v3.test.js +97 -9
  44. package/lib/projects/create/index.js +2 -2
  45. package/lib/projects/create/legacy.js +1 -1
  46. package/lib/projects/create/v3.d.ts +2 -2
  47. package/lib/projects/create/v3.js +35 -12
  48. package/lib/projects/deploy.d.ts +13 -0
  49. package/lib/projects/deploy.js +63 -0
  50. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
  51. package/lib/projects/localDev/AppDevModeInterface.js +65 -36
  52. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  53. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  54. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  55. package/lib/projects/localDev/LocalDevState.js +9 -1
  56. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  57. package/lib/projects/localDev/helpers/project.js +6 -7
  58. package/lib/projects/platformVersion.d.ts +1 -0
  59. package/lib/projects/platformVersion.js +10 -0
  60. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  61. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  62. package/lib/projects/structure.d.ts +2 -2
  63. package/lib/projects/upload.d.ts +2 -1
  64. package/lib/projects/upload.js +2 -1
  65. package/lib/projects/urls.d.ts +1 -0
  66. package/lib/projects/urls.js +3 -0
  67. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  68. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  69. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  70. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  71. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  72. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  73. package/lib/prompts/installAppPrompt.d.ts +6 -1
  74. package/lib/prompts/installAppPrompt.js +6 -1
  75. package/lib/prompts/projectAddPrompt.js +1 -1
  76. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  77. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  78. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  79. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  80. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  81. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  82. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  83. package/mcp-server/tools/cms/HsListTool.js +1 -1
  84. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -1
  85. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -1
  86. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -1
  87. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  88. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  89. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -1
  90. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  91. package/mcp-server/tools/index.js +2 -0
  92. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  93. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  94. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  95. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  96. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  97. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  98. package/mcp-server/tools/project/DocsSearchTool.js +2 -2
  99. package/mcp-server/tools/project/GetConfigValuesTool.js +4 -4
  100. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  101. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  102. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  103. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  104. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  105. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  106. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  107. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
  108. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  109. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  110. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  111. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  112. package/mcp-server/tools/project/constants.d.ts +1 -1
  113. package/mcp-server/tools/project/constants.js +14 -6
  114. package/package.json +3 -3
  115. package/types/LocalDev.d.ts +2 -1
  116. package/types/Projects.d.ts +1 -0
  117. package/types/Yargs.d.ts +1 -1
  118. /package/{lib/projects/__tests__/buildAndDeploy.test.d.ts → commands/app/__tests__/install.test.d.ts} +0 -0
@@ -39,6 +39,7 @@ describe('LocalDevProcess', () => {
39
39
  targetTestingAccountId: 456,
40
40
  projectId: 789,
41
41
  initialProjectNodes: {},
42
+ initialProjectProfileData: {},
42
43
  env: ENVIRONMENTS.PROD,
43
44
  projectName: 'test-project',
44
45
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,164 @@
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 result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
130
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
131
+ expect(result).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
+ const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
154
+ expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
155
+ expect(result).toBeUndefined();
156
+ });
157
+ it('handles general deploy failure', async () => {
158
+ mockDeployProject.mockResolvedValue({ data: null });
159
+ const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
160
+ expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
161
+ expect(result).toBeUndefined();
162
+ });
163
+ });
164
+ });
@@ -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
  });
@@ -33,7 +33,11 @@ describe('lib/projects/add/v3AddComponent', () => {
33
33
  srcDir: 'src',
34
34
  platformVersion: 'v3',
35
35
  };
36
- const mockArgs = { name: 'test-component', type: 'module' };
36
+ const mockArgs = {
37
+ name: 'test-component',
38
+ type: 'module',
39
+ derivedAccountId: 1234,
40
+ };
37
41
  const projectDir = '/path/to/project';
38
42
  const mockAccountId = 123;
39
43
  const mockComponentTemplate = {
@@ -245,5 +249,71 @@ describe('lib/projects/add/v3AddComponent', () => {
245
249
  type: '',
246
250
  }, mockAccountId);
247
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);
317
+ });
248
318
  });
249
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
+ derivedAccountId: number;
8
9
  }, projectDir: string, projectConfig: ProjectConfig, accountId: number): Promise<void>;
@@ -46,9 +46,9 @@ export async function v3AddComponent(args, projectDir, projectConfig, accountId)
46
46
  derivedDistribution = apps[0].config?.distribution;
47
47
  derivedAuthType = apps[0].config?.auth?.type;
48
48
  }
49
- const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
49
+ const componentTemplateChoices = await calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, args.derivedAccountId, currentProjectMetadata);
50
50
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
51
- const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.type);
51
+ const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.cliSelector || componentTemplate.type);
52
52
  await trackCommandUsage('project-add', {
53
53
  type: componentTypes?.join(','),
54
54
  }, accountId);
@@ -26,9 +26,9 @@ describe('lib/projects/create/v3', () => {
26
26
  card: { count: 3, maxCount: 3, hsMetaFiles: [] },
27
27
  },
28
28
  };
29
- it('returns enabled components when they meet all requirements', () => {
30
- const choices = calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', mockProjectMetadataForChoices);
31
- expect(choices).toHaveLength(3); // includes separator
29
+ it('returns enabled components when they meet all requirements', async () => {
30
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', 123, mockProjectMetadataForChoices);
31
+ expect(choices).toHaveLength(4); // includes separator
32
32
  expect(choices[0]).toEqual({
33
33
  name: 'Module Component',
34
34
  value: mockComponents[0],
@@ -39,8 +39,8 @@ describe('lib/projects/create/v3', () => {
39
39
  disabled: expect.stringContaining('maximum'),
40
40
  });
41
41
  });
42
- it('disables components when auth type is not supported', () => {
43
- const choices = calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', mockProjectMetadataForChoices);
42
+ it('disables components when auth type is not supported', async () => {
43
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', 123, mockProjectMetadataForChoices);
44
44
  // All components should be disabled, so they come after the separator
45
45
  expect(choices[1]).toEqual({
46
46
  name: expect.stringContaining('Module Component'),
@@ -48,8 +48,8 @@ describe('lib/projects/create/v3', () => {
48
48
  disabled: expect.stringContaining('privatekey'),
49
49
  });
50
50
  });
51
- it('disables components when distribution is not supported', () => {
52
- const choices = calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', mockProjectMetadataForChoices);
51
+ it('disables components when distribution is not supported', async () => {
52
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', 123, mockProjectMetadataForChoices);
53
53
  // All components should be disabled, so they come after the separator
54
54
  expect(choices[1]).toEqual({
55
55
  name: expect.stringContaining('Module Component'),
@@ -57,7 +57,7 @@ describe('lib/projects/create/v3', () => {
57
57
  disabled: expect.stringContaining('enterprise'),
58
58
  });
59
59
  });
60
- it('handles components without auth type or distribution restrictions', () => {
60
+ it('handles components without auth type or distribution restrictions', async () => {
61
61
  const componentsWithoutRestrictions = [
62
62
  {
63
63
  label: 'Unrestricted Component',
@@ -65,7 +65,7 @@ describe('lib/projects/create/v3', () => {
65
65
  type: 'module',
66
66
  },
67
67
  ];
68
- const choices = calculateComponentTemplateChoices(componentsWithoutRestrictions, 'oauth', 'private', {
68
+ const choices = await calculateComponentTemplateChoices(componentsWithoutRestrictions, 'oauth', 'private', 123, {
69
69
  hsMetaFiles: [],
70
70
  components: { module: { count: 0, maxCount: 5, hsMetaFiles: [] } },
71
71
  });
@@ -74,5 +74,93 @@ describe('lib/projects/create/v3', () => {
74
74
  value: componentsWithoutRestrictions[0],
75
75
  });
76
76
  });
77
+ it('handles components with cliSelector field (metadata compatibility)', async () => {
78
+ const componentWithCliSelector = [
79
+ {
80
+ label: 'Workflow Action Tool',
81
+ path: 'workflow-action-tool',
82
+ type: 'workflow-action',
83
+ cliSelector: 'workflow-action-tool',
84
+ supportedAuthTypes: ['oauth'],
85
+ supportedDistributions: ['private'],
86
+ },
87
+ ];
88
+ const projectMetadataWithWorkflowAction = {
89
+ hsMetaFiles: [],
90
+ components: {
91
+ 'workflow-action': { count: 2, maxCount: 3, hsMetaFiles: [] },
92
+ },
93
+ };
94
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 213, projectMetadataWithWorkflowAction);
95
+ expect(choices).toHaveLength(1); // no disabled components
96
+ expect(choices[0]).toEqual({
97
+ name: 'Workflow Action Tool',
98
+ value: componentWithCliSelector[0],
99
+ });
100
+ });
101
+ it('disables component when project metadata count exceeds maximum', async () => {
102
+ const componentWithCliSelector = [
103
+ {
104
+ label: 'Workflow Action Tool',
105
+ path: 'workflow-action-tool',
106
+ type: 'workflow-action',
107
+ cliSelector: 'workflow-action-tool',
108
+ supportedAuthTypes: ['oauth'],
109
+ supportedDistributions: ['private'],
110
+ },
111
+ ];
112
+ const projectMetadataAtMaxWorkflowAction = {
113
+ hsMetaFiles: [],
114
+ components: {
115
+ 'workflow-action': { count: 3, maxCount: 3, hsMetaFiles: [] },
116
+ },
117
+ };
118
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, projectMetadataAtMaxWorkflowAction);
119
+ expect(choices).toHaveLength(3); // includes separators
120
+ expect(choices[1]).toEqual({
121
+ name: expect.stringContaining('Workflow Action Tool'),
122
+ value: componentWithCliSelector[0],
123
+ disabled: expect.stringContaining('maximum'),
124
+ });
125
+ });
126
+ it('handles undefined projectMetadata without throwing errors', async () => {
127
+ const componentWithCliSelector = [
128
+ {
129
+ label: 'Workflow Action Tool',
130
+ path: 'workflow-action-tool',
131
+ type: 'workflow-action',
132
+ cliSelector: 'workflow-action-tool',
133
+ supportedAuthTypes: ['oauth'],
134
+ supportedDistributions: ['private'],
135
+ },
136
+ ];
137
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, undefined);
138
+ expect(choices).toHaveLength(1); // no disabled components
139
+ expect(choices[0]).toEqual({
140
+ name: 'Workflow Action Tool',
141
+ value: componentWithCliSelector[0],
142
+ });
143
+ });
144
+ it('handles projectMetadata with undefined components property (after fix)', async () => {
145
+ const componentWithCliSelector = [
146
+ {
147
+ label: 'Workflow Action Tool',
148
+ path: 'workflow-action-tool',
149
+ type: 'workflow-action',
150
+ cliSelector: 'workflow-action-tool',
151
+ supportedAuthTypes: ['oauth'],
152
+ supportedDistributions: ['private'],
153
+ },
154
+ ];
155
+ const projectMetadataWithoutComponents = {
156
+ hsMetaFiles: [],
157
+ components: undefined,
158
+ };
159
+ // This test verifies the null check fix works
160
+ // Currently this will fail because the fix checks for projectMetadata.components
161
+ await expect(async () => calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123,
162
+ // @ts-expect-error breaking stuff on purpose
163
+ projectMetadataWithoutComponents)).rejects.toThrow();
164
+ });
77
165
  });
78
166
  });
@@ -1,7 +1,7 @@
1
1
  import { selectProjectTemplatePrompt, } from '../../prompts/selectProjectTemplatePrompt.js';
2
2
  import { projectNameAndDestPrompt } from '../../prompts/projectNameAndDestPrompt.js';
3
3
  import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, EMPTY_PROJECT, } from '../../constants.js';
4
- import { useV3Api } from '../buildAndDeploy.js';
4
+ import { useV3Api } from '../platformVersion.js';
5
5
  import { v3ComponentFlow } from './v3.js';
6
6
  import { getProjectTemplateListFromRepo } from './legacy.js';
7
7
  import { uiLogger } from '../../ui/logger.js';
@@ -12,7 +12,7 @@ export async function handleProjectCreationFlow(args) {
12
12
  const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
13
13
  const projectNameAndDestPromptResponse = await projectNameAndDestPrompt(args);
14
14
  if (useV3Api(platformVersion)) {
15
- const { componentTemplateChoices, authType, distribution, repoConfig, projectContents, } = await v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution);
15
+ const { componentTemplateChoices, authType, distribution, repoConfig, projectContents, } = await v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, args.derivedAccountId);
16
16
  const selectProjectTemplatePromptResponse = await selectProjectTemplatePrompt(args, undefined, projectContents !== EMPTY_PROJECT ? componentTemplateChoices : undefined);
17
17
  return {
18
18
  authType,
@@ -3,7 +3,7 @@ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH
3
3
  import { EXIT_CODES } from '../../enums/exitCodes.js';
4
4
  import { debugError } from '../../errorHandlers/index.js';
5
5
  import { uiLogger } from '../../ui/logger.js';
6
- import { useV3Api } from '../buildAndDeploy.js';
6
+ import { useV3Api } from '../platformVersion.js';
7
7
  import { lib } from '../../../lang/en.js';
8
8
  const PROJECT_TEMPLATE_PROPERTIES = ['name', 'label', 'path'];
9
9
  export const EMPTY_PROJECT_TEMPLATE_NAME = 'no-template';
@@ -6,7 +6,7 @@ export declare function createV3App(providedAuth: string | undefined, providedDi
6
6
  authType: string;
7
7
  distribution: string;
8
8
  }>;
9
- export declare function calculateComponentTemplateChoices(components: ComponentTemplate[], authType: string | undefined, distribution: string | undefined, projectMetadata?: ProjectMetadata): (ComponentTemplateChoice | Separator)[];
9
+ export declare function calculateComponentTemplateChoices(components: ComponentTemplate[], authType: string | undefined, distribution: string | undefined, accountId: number, projectMetadata?: ProjectMetadata): Promise<(ComponentTemplateChoice | Separator)[]>;
10
10
  type V3ComponentInfo = {
11
11
  authType?: string;
12
12
  distribution?: string;
@@ -14,7 +14,7 @@ type V3ComponentInfo = {
14
14
  projectContents?: string;
15
15
  componentTemplateChoices?: (ComponentTemplateChoice | Separator)[];
16
16
  };
17
- export declare function v3ComponentFlow(platformVersion: string, projectBase: string | undefined, providedAuth: string | undefined, providedDistribution: string | undefined): Promise<V3ComponentInfo>;
17
+ export declare function v3ComponentFlow(platformVersion: string, projectBase: string | undefined, providedAuth: string | undefined, providedDistribution: string | undefined, accountId: number): Promise<V3ComponentInfo>;
18
18
  export declare function generateComponentPaths({ selectProjectTemplatePromptResponse, platformVersion, repoConfig, projectContents, authType, distribution, }: {
19
19
  selectProjectTemplatePromptResponse: SelectProjectTemplatePromptResponse;
20
20
  platformVersion: string;