@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.
- package/commands/project/__tests__/add.test.js +66 -0
- package/commands/project/add.d.ts +1 -1
- package/commands/project/add.js +1 -1
- package/commands/project/create.js +1 -1
- package/lang/en.d.ts +0 -2
- package/lang/en.js +0 -2
- package/lib/mcp/setup.d.ts +3 -3
- package/lib/mcp/setup.js +112 -37
- package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -7
- package/lib/projects/add/v3AddComponent.d.ts +1 -1
- package/lib/projects/add/v3AddComponent.js +6 -1
- package/package.json +1 -1
|
@@ -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
|
});
|
package/commands/project/add.js
CHANGED
|
@@ -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.
|
|
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',
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -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
|
|
22
|
-
export declare function
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
157
|
-
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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