@hubspot/cli 7.7.29-experimental.0 → 7.7.30-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.
@@ -1,7 +1,22 @@
1
1
  import yargs from 'yargs';
2
2
  import projectAddCommand from '../add.js';
3
3
  import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, } from '../../../lib/constants.js';
4
+ import { v3AddComponent } from '../../../lib/projects/add/v3AddComponent.js';
5
+ import { legacyAddComponent } from '../../../lib/projects/add/legacyAddComponent.js';
6
+ import { getProjectConfig } from '../../../lib/projects/config.js';
7
+ import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
8
+ import { trackCommandUsage } from '../../../lib/usageTracking.js';
4
9
  vi.mock('../../../lib/commonOpts');
10
+ vi.mock('../../../lib/projects/add/v3AddComponent');
11
+ vi.mock('../../../lib/projects/add/legacyAddComponent');
12
+ vi.mock('../../../lib/projects/config');
13
+ vi.mock('../../../lib/projects/buildAndDeploy');
14
+ vi.mock('../../../lib/usageTracking');
15
+ const mockedV3AddComponent = vi.mocked(v3AddComponent);
16
+ const mockedLegacyAddComponent = vi.mocked(legacyAddComponent);
17
+ const mockedGetProjectConfig = vi.mocked(getProjectConfig);
18
+ const mockedUseV3Api = vi.mocked(useV3Api);
19
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
5
20
  describe('commands/project/add', () => {
6
21
  const yargsMock = yargs;
7
22
  describe('command', () => {
@@ -40,4 +55,55 @@ describe('commands/project/add', () => {
40
55
  });
41
56
  });
42
57
  });
58
+ describe('handler', () => {
59
+ const mockProjectConfig = {
60
+ name: 'test-project',
61
+ srcDir: 'src',
62
+ platformVersion: 'v3',
63
+ };
64
+ const mockProjectDir = '/path/to/project';
65
+ const mockArgs = {
66
+ derivedAccountId: 123,
67
+ name: 'test-component',
68
+ type: 'module',
69
+ };
70
+ beforeEach(() => {
71
+ mockedGetProjectConfig.mockResolvedValue({
72
+ projectConfig: mockProjectConfig,
73
+ projectDir: mockProjectDir,
74
+ });
75
+ mockedTrackCommandUsage.mockResolvedValue();
76
+ mockedV3AddComponent.mockResolvedValue();
77
+ mockedLegacyAddComponent.mockResolvedValue();
78
+ vi.spyOn(process, 'exit').mockImplementation(() => {
79
+ throw new Error('process.exit called');
80
+ });
81
+ });
82
+ it('should call v3AddComponent with accountId for v3 projects', async () => {
83
+ mockedUseV3Api.mockReturnValue(true);
84
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
85
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
86
+ expect(mockedV3AddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig, 123);
87
+ expect(mockedLegacyAddComponent).not.toHaveBeenCalled();
88
+ });
89
+ it('should call legacyAddComponent for non-v3 projects', async () => {
90
+ mockedUseV3Api.mockReturnValue(false);
91
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
92
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', undefined, 123);
93
+ expect(mockedLegacyAddComponent).toHaveBeenCalledWith(mockArgs, mockProjectDir, mockProjectConfig);
94
+ expect(mockedV3AddComponent).not.toHaveBeenCalled();
95
+ });
96
+ it('should exit with error when project config is not found', async () => {
97
+ mockedGetProjectConfig.mockResolvedValue({
98
+ projectConfig: null,
99
+ projectDir: null,
100
+ });
101
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
102
+ throw new Error('process.exit called');
103
+ });
104
+ await expect(projectAddCommand.handler(mockArgs)).rejects.toThrow('process.exit called');
105
+ expect(mockExit).toHaveBeenCalledWith(1);
106
+ mockExit.mockRestore();
107
+ });
108
+ });
43
109
  });
@@ -1,5 +1,5 @@
1
1
  import { YargsCommandModule, CommonArgs } from '../../types/Yargs.js';
2
- type ProjectAddArgs = CommonArgs & {
2
+ export type ProjectAddArgs = CommonArgs & {
3
3
  type?: string;
4
4
  name?: string;
5
5
  features?: string[];
@@ -23,7 +23,7 @@ async function handler(args) {
23
23
  }
24
24
  const isV3ProjectCreate = useV3Api(projectConfig.platformVersion);
25
25
  if (isV3ProjectCreate) {
26
- await v3AddComponent(args, projectDir, projectConfig);
26
+ await v3AddComponent(args, projectDir, projectConfig, derivedAccountId);
27
27
  }
28
28
  else {
29
29
  await legacyAddComponent(args, projectDir, projectConfig);
@@ -41,7 +41,7 @@ async function handler(args) {
41
41
  type: selectProjectTemplatePromptResponse.projectTemplate?.name ||
42
42
  (selectProjectTemplatePromptResponse.componentTemplates || [])
43
43
  // @ts-expect-error
44
- .map((item) => item.label)
44
+ .map((item) => item.type)
45
45
  .join(','),
46
46
  }, derivedAccountId);
47
47
  const projectDest = path.resolve(getCwd(), projectNameAndDestPromptResponse.dest);
package/lang/en.d.ts CHANGED
@@ -884,12 +884,10 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
884
884
  readonly configuringCursor: "Configuring Cursor...";
885
885
  readonly failedToConfigureCursor: "Failed to configure Cursor";
886
886
  readonly configuredCursor: "Configured Cursor";
887
- readonly cursorNotFound: "Cursor not found - skipping configuration";
888
887
  readonly alreadyInstalled: "HubSpot CLI mcp server already installed, reinstalling";
889
888
  readonly configuringWindsurf: "Configuring Windsurf...";
890
889
  readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
891
890
  readonly configuredWindsurf: "Configured Windsurf";
892
- readonly windsurfNotFound: "Windsurf not found - skipping configuration";
893
891
  readonly configuringVsCode: "Configuring VSCode...";
894
892
  readonly failedToConfigureVsCode: "Failed to configure VSCode";
895
893
  readonly configuredVsCode: "Configured VSCode";
package/lang/en.js CHANGED
@@ -889,13 +889,11 @@ export const commands = {
889
889
  configuringCursor: 'Configuring Cursor...',
890
890
  failedToConfigureCursor: 'Failed to configure Cursor',
891
891
  configuredCursor: 'Configured Cursor',
892
- cursorNotFound: 'Cursor not found - skipping configuration',
893
892
  alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
894
893
  // Windsurf
895
894
  configuringWindsurf: 'Configuring Windsurf...',
896
895
  failedToConfigureWindsurf: 'Failed to configure Windsurf',
897
896
  configuredWindsurf: 'Configured Windsurf',
898
- windsurfNotFound: 'Windsurf not found - skipping configuration',
899
897
  // VS Code
900
898
  configuringVsCode: 'Configuring VSCode...',
901
899
  failedToConfigureVsCode: 'Failed to configure VSCode',
@@ -16,8 +16,8 @@ interface McpCommand {
16
16
  args: string[];
17
17
  }
18
18
  export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
19
- export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
20
19
  export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
21
- export declare function setupCursor(mcpCommand?: McpCommand): Promise<boolean>;
22
- export declare function setupWindsurf(mcpCommand?: McpCommand): Promise<boolean>;
20
+ export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
21
+ export declare function setupCursor(mcpCommand?: McpCommand): boolean;
22
+ export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
23
23
  export {};
package/lib/mcp/setup.js CHANGED
@@ -4,6 +4,10 @@ import { promptUser } from '../prompts/promptUtils.js';
4
4
  import SpinniesManager from '../ui/SpinniesManager.js';
5
5
  import { logError } from '../errorHandlers/index.js';
6
6
  import { execAsync } from '../../mcp-server/utils/command.js';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import fs from 'fs-extra';
10
+ import { existsSync } from 'fs';
7
11
  const mcpServerName = 'hubspot-cli-mcp';
8
12
  const claudeCode = 'claude';
9
13
  const windsurf = 'windsurf';
@@ -68,6 +72,96 @@ async function runSetupFunction(func) {
68
72
  throw new Error();
69
73
  }
70
74
  }
75
+ function setupMcpConfigFile(config) {
76
+ try {
77
+ SpinniesManager.add('spinner', {
78
+ text: config.configuringMessage,
79
+ });
80
+ if (!existsSync(config.configPath)) {
81
+ fs.writeFileSync(config.configPath, JSON.stringify({}, null, 2));
82
+ }
83
+ let mcpConfig = {};
84
+ let configContent;
85
+ try {
86
+ configContent = fs.readFileSync(config.configPath, 'utf8');
87
+ }
88
+ catch (error) {
89
+ SpinniesManager.fail('spinner', {
90
+ text: config.failedMessage,
91
+ });
92
+ logError(error);
93
+ return false;
94
+ }
95
+ try {
96
+ // In the event the file exists, but is empty, initialize it to and empty object
97
+ if (configContent.trim() === '') {
98
+ mcpConfig = {};
99
+ }
100
+ else {
101
+ mcpConfig = JSON.parse(configContent);
102
+ }
103
+ }
104
+ catch (error) {
105
+ SpinniesManager.fail('spinner', {
106
+ text: config.failedMessage,
107
+ });
108
+ uiLogger.error(commands.mcp.setup.errors.errorParsingJsonFIle(config.configPath, error instanceof Error ? error.message : `${error}`));
109
+ return false;
110
+ }
111
+ // Initialize mcpServers if it doesn't exist
112
+ if (!mcpConfig.mcpServers) {
113
+ mcpConfig.mcpServers = {};
114
+ }
115
+ // Add or update HubSpot CLI MCP server
116
+ mcpConfig.mcpServers[mcpServerName] = {
117
+ ...config.mcpCommand,
118
+ };
119
+ // Write the updated config
120
+ fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
121
+ SpinniesManager.succeed('spinner', {
122
+ text: config.configuredMessage,
123
+ });
124
+ return true;
125
+ }
126
+ catch (error) {
127
+ SpinniesManager.fail('spinner', {
128
+ text: config.failedMessage,
129
+ });
130
+ logError(error);
131
+ return false;
132
+ }
133
+ }
134
+ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
135
+ try {
136
+ SpinniesManager.add('vsCode', {
137
+ text: commands.mcp.setup.spinners.configuringVsCode,
138
+ });
139
+ const mcpConfig = JSON.stringify({
140
+ name: mcpServerName,
141
+ ...buildCommandWithAgentString(mcpCommand, vscode),
142
+ });
143
+ await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
144
+ SpinniesManager.succeed('vsCode', {
145
+ text: commands.mcp.setup.spinners.configuredVsCode,
146
+ });
147
+ return true;
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error &&
151
+ error.message.includes('code: command not found')) {
152
+ SpinniesManager.fail('vsCode', {
153
+ text: commands.mcp.setup.spinners.vsCodeNotFound,
154
+ });
155
+ }
156
+ else {
157
+ SpinniesManager.fail('vsCode', {
158
+ text: commands.mcp.setup.spinners.failedToConfigureVsCode,
159
+ });
160
+ logError(error);
161
+ }
162
+ return false;
163
+ }
164
+ }
71
165
  export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
72
166
  try {
73
167
  SpinniesManager.add('claudeCode', {
@@ -117,44 +211,25 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
117
211
  return false;
118
212
  }
119
213
  }
120
- async function setupVsCodeBasedIntegration(commandName, configuringText, configuredText, notFoundText, failedText, mcpCommand = defaultMcpCommand) {
121
- try {
122
- SpinniesManager.add(commandName, {
123
- text: configuringText,
124
- });
125
- const mcpConfig = JSON.stringify({
126
- name: mcpServerName,
127
- ...buildCommandWithAgentString(mcpCommand, vscode),
128
- });
129
- await execAsync(`${commandName} --add-mcp ${JSON.stringify(mcpConfig)}`);
130
- SpinniesManager.succeed(commandName, {
131
- text: configuredText,
132
- });
133
- return true;
134
- }
135
- catch (error) {
136
- if (error instanceof Error && error.message.includes(commandName)) {
137
- SpinniesManager.fail(commandName, {
138
- text: notFoundText,
139
- });
140
- }
141
- else {
142
- SpinniesManager.fail(commandName, {
143
- text: failedText,
144
- });
145
- logError(error);
146
- }
147
- return false;
148
- }
149
- }
150
- export async function setupVsCode(mcpCommand = defaultMcpCommand) {
151
- return setupVsCodeBasedIntegration('code', commands.mcp.setup.spinners.configuringVsCode, commands.mcp.setup.spinners.configuredVsCode, commands.mcp.setup.spinners.vsCodeNotFound, commands.mcp.setup.spinners.failedToConfigureVsCode, mcpCommand);
152
- }
153
- export async function setupCursor(mcpCommand = defaultMcpCommand) {
154
- return setupVsCodeBasedIntegration('cursor', commands.mcp.setup.spinners.configuringCursor, commands.mcp.setup.spinners.configuredCursor, commands.mcp.setup.spinners.cursorNotFound, commands.mcp.setup.spinners.failedToConfigureCursor, mcpCommand);
214
+ export function setupCursor(mcpCommand = defaultMcpCommand) {
215
+ const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
216
+ return setupMcpConfigFile({
217
+ configPath: cursorConfigPath,
218
+ configuringMessage: commands.mcp.setup.spinners.configuringCursor,
219
+ configuredMessage: commands.mcp.setup.spinners.configuredCursor,
220
+ failedMessage: commands.mcp.setup.spinners.failedToConfigureCursor,
221
+ mcpCommand: buildCommandWithAgentString(mcpCommand, cursor),
222
+ });
155
223
  }
156
- export async function setupWindsurf(mcpCommand = defaultMcpCommand) {
157
- return setupVsCodeBasedIntegration('windsurf', commands.mcp.setup.spinners.configuringWindsurf, commands.mcp.setup.spinners.configuredWindsurf, commands.mcp.setup.spinners.windsurfNotFound, commands.mcp.setup.spinners.failedToConfigureWindsurf, mcpCommand);
224
+ export function setupWindsurf(mcpCommand = defaultMcpCommand) {
225
+ const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
226
+ return setupMcpConfigFile({
227
+ configPath: windsurfConfigPath,
228
+ configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
229
+ configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
230
+ failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
231
+ mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
232
+ });
158
233
  }
159
234
  function buildCommandWithAgentString(mcpCommand, agent) {
160
235
  const mcpCommandCopy = structuredClone(mcpCommand);
@@ -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,6 +26,7 @@ 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',
@@ -32,6 +35,7 @@ describe('lib/projects/add/v3AddComponent', () => {
32
35
  };
33
36
  const mockArgs = { name: 'test-component', type: 'module' };
34
37
  const projectDir = '/path/to/project';
38
+ const mockAccountId = 123;
35
39
  const mockComponentTemplate = {
36
40
  label: 'Test Component',
37
41
  path: 'test-component',
@@ -62,6 +66,7 @@ describe('lib/projects/add/v3AddComponent', () => {
62
66
  authType: 'oauth',
63
67
  distribution: 'private',
64
68
  });
69
+ mockedTrackCommandUsage.mockResolvedValue();
65
70
  });
66
71
  describe('v3AddComponent()', () => {
67
72
  it('successfully adds a component when app already exists', async () => {
@@ -79,10 +84,13 @@ describe('lib/projects/add/v3AddComponent', () => {
79
84
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
80
85
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
81
86
  mockedCloneGithubRepo.mockResolvedValue(true);
82
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
87
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
83
88
  expect(mockedGetConfigForPlatformVersion).toHaveBeenCalledWith('v3');
84
89
  expect(mockedGetProjectMetadata).toHaveBeenCalledWith('/path/to/project/src');
85
90
  expect(mockedProjectAddPromptV3).toHaveBeenCalled();
91
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
92
+ type: 'module',
93
+ }, mockAccountId);
86
94
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
87
95
  sourceDir: ['v3/test-component'],
88
96
  hideLogs: true,
@@ -106,8 +114,11 @@ describe('lib/projects/add/v3AddComponent', () => {
106
114
  mockedConfirmPrompt.mockResolvedValue(true);
107
115
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
108
116
  mockedCloneGithubRepo.mockResolvedValue(true);
109
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
117
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
110
118
  expect(mockedCreateV3App).toHaveBeenCalled();
119
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
120
+ type: 'module',
121
+ }, mockAccountId);
111
122
  expect(mockedCloneGithubRepo).toHaveBeenCalledWith(expect.any(String), projectDir, expect.objectContaining({
112
123
  sourceDir: ['v3/test-component', 'v3/app-template'],
113
124
  }));
@@ -132,8 +143,11 @@ describe('lib/projects/add/v3AddComponent', () => {
132
143
  mockedConfirmPrompt.mockResolvedValue(true);
133
144
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
134
145
  mockedCloneGithubRepo.mockResolvedValue(true);
135
- await v3AddComponent(mockArgs, projectDir, mockProjectConfig);
146
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
136
147
  expect(mockedCreateV3App).not.toHaveBeenCalled();
148
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
149
+ type: '',
150
+ }, mockAccountId);
137
151
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
138
152
  });
139
153
  it('throws an error when app count exceeds maximum', async () => {
@@ -146,7 +160,7 @@ describe('lib/projects/add/v3AddComponent', () => {
146
160
  };
147
161
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
148
162
  mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataMaxApps);
149
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('This project currently has the maximum number of apps: 1');
163
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('This project currently has the maximum number of apps: 1');
150
164
  });
151
165
  it('throws an error when components list is empty', async () => {
152
166
  const mockEmptyConfig = {
@@ -154,7 +168,7 @@ describe('lib/projects/add/v3AddComponent', () => {
154
168
  parentComponents: [],
155
169
  };
156
170
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockEmptyConfig);
157
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
171
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
158
172
  });
159
173
  it('throws an error when app meta file cannot be parsed', async () => {
160
174
  mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
@@ -162,7 +176,7 @@ describe('lib/projects/add/v3AddComponent', () => {
162
176
  mockedFs.readFileSync.mockImplementation(() => {
163
177
  throw new Error('File read error');
164
178
  });
165
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow('Unable to parse app file');
179
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow('Unable to parse app file');
166
180
  });
167
181
  it('throws an error when cloning fails', async () => {
168
182
  const mockAppMeta = {
@@ -179,7 +193,57 @@ describe('lib/projects/add/v3AddComponent', () => {
179
193
  mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
180
194
  mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
181
195
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
182
- await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
196
+ await expect(v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
197
+ });
198
+ it('should track usage with multiple component types', async () => {
199
+ const mockAppMeta = {
200
+ config: {
201
+ distribution: 'private',
202
+ auth: { type: 'oauth' },
203
+ },
204
+ };
205
+ const mockSecondComponentTemplate = {
206
+ label: 'Test Card',
207
+ path: 'test-card',
208
+ type: 'card',
209
+ supportedAuthTypes: ['oauth'],
210
+ supportedDistributions: ['private'],
211
+ };
212
+ const mockPromptResponse = {
213
+ componentTemplate: [mockComponentTemplate, mockSecondComponentTemplate],
214
+ };
215
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
216
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
217
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
218
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
219
+ mockedCloneGithubRepo.mockResolvedValue(true);
220
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
221
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
222
+ type: 'module,card',
223
+ }, mockAccountId);
224
+ });
225
+ it('should track usage with empty type when no components are selected', async () => {
226
+ const mockProjectMetadataNoApps = {
227
+ hsMetaFiles: [],
228
+ components: {
229
+ app: {
230
+ count: 1,
231
+ maxCount: 1,
232
+ hsMetaFiles: ['/path/to/app.meta.json'],
233
+ },
234
+ module: { count: 0, maxCount: 5, hsMetaFiles: [] },
235
+ },
236
+ };
237
+ const mockPromptResponse = {
238
+ componentTemplate: [],
239
+ };
240
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
241
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadataNoApps);
242
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
243
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
244
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
245
+ type: '',
246
+ }, mockAccountId);
183
247
  });
184
248
  });
185
249
  });
@@ -5,4 +5,4 @@ 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
+ }, projectDir: string, projectConfig: ProjectConfig, accountId: number): Promise<void>;
@@ -12,7 +12,8 @@ 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;
@@ -47,6 +48,10 @@ export async function v3AddComponent(args, projectDir, projectConfig) {
47
48
  }
48
49
  const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
49
50
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
51
+ const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.7.29-experimental.0",
3
+ "version": "7.7.30-experimental.0",
4
4
  "description": "The official CLI for developing on HubSpot",
5
5
  "license": "Apache-2.0",
6
6
  "repository": "https://github.com/HubSpot/hubspot-cli",