@hubspot/cli 7.7.28-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/commands/project/upload.d.ts +2 -2
- package/commands/project/upload.js +1 -1
- package/lib/mcp/setup.js +3 -4
- package/lib/projectProfiles.d.ts +1 -1
- package/lib/projectProfiles.js +2 -10
- 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/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
- package/mcp-server/tools/index.js +5 -1
- package/package.json +1 -1
- package/types/Yargs.d.ts +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);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CommonArgs,
|
|
2
|
-
type ProjectUploadArgs = CommonArgs & JSONOutputArgs &
|
|
1
|
+
import { CommonArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
|
|
2
|
+
type ProjectUploadArgs = CommonArgs & JSONOutputArgs & {
|
|
3
3
|
forceCreate: boolean;
|
|
4
4
|
message: string;
|
|
5
5
|
m: string;
|
|
@@ -25,7 +25,7 @@ async function handler(args) {
|
|
|
25
25
|
validateProjectConfig(projectConfig, projectDir);
|
|
26
26
|
let targetAccountId;
|
|
27
27
|
if (useV3Api(projectConfig.platformVersion)) {
|
|
28
|
-
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile
|
|
28
|
+
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
|
|
29
29
|
}
|
|
30
30
|
targetAccountId = targetAccountId || derivedAccountId;
|
|
31
31
|
const accountConfig = getAccountConfig(targetAccountId);
|
package/lib/mcp/setup.js
CHANGED
|
@@ -140,7 +140,7 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
|
140
140
|
name: mcpServerName,
|
|
141
141
|
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
142
142
|
});
|
|
143
|
-
await execAsync(`code --add-mcp
|
|
143
|
+
await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
144
144
|
SpinniesManager.succeed('vsCode', {
|
|
145
145
|
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
146
146
|
});
|
|
@@ -182,15 +182,14 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
182
182
|
});
|
|
183
183
|
await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
|
|
184
184
|
}
|
|
185
|
-
await execAsync(`claude mcp add-json "${mcpServerName}"
|
|
185
|
+
await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
|
|
186
186
|
SpinniesManager.succeed('claudeCode', {
|
|
187
187
|
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
188
188
|
});
|
|
189
189
|
return true;
|
|
190
190
|
}
|
|
191
191
|
catch (error) {
|
|
192
|
-
if (error instanceof Error &&
|
|
193
|
-
error.message.includes('claude: command not found')) {
|
|
192
|
+
if (error instanceof Error && error.message.includes('claude')) {
|
|
194
193
|
SpinniesManager.fail('claudeCode', {
|
|
195
194
|
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
196
195
|
});
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
|
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
5
|
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
|
|
6
6
|
export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
|
package/lib/projectProfiles.js
CHANGED
|
@@ -38,12 +38,7 @@ export function loadProfile(projectConfig, projectDir, profileName) {
|
|
|
38
38
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
42
|
-
...profile,
|
|
43
|
-
accountId: process.env.HUBSPOT_ACCOUNT_ID
|
|
44
|
-
? Number(process.env.HUBSPOT_ACCOUNT_ID)
|
|
45
|
-
: profile.accountId,
|
|
46
|
-
};
|
|
41
|
+
return profile;
|
|
47
42
|
}
|
|
48
43
|
catch (e) {
|
|
49
44
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
|
|
@@ -59,7 +54,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
|
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
|
-
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile
|
|
57
|
+
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
|
|
63
58
|
if (argsProfile) {
|
|
64
59
|
logProfileHeader(argsProfile);
|
|
65
60
|
const profile = loadProfile(projectConfig, projectDir, argsProfile);
|
|
@@ -68,9 +63,6 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
|
|
|
68
63
|
process.exit(EXIT_CODES.ERROR);
|
|
69
64
|
}
|
|
70
65
|
logProfileFooter(profile, true);
|
|
71
|
-
if (useEnv) {
|
|
72
|
-
return Number(process.env.HUBSPOT_ACCOUNT_ID);
|
|
73
|
-
}
|
|
74
66
|
return profile.accountId;
|
|
75
67
|
}
|
|
76
68
|
else {
|
|
@@ -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);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TextContentResponse, Tool } from '../../types.js';
|
|
2
|
+
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
declare const inputSchemaZodObject: z.ZodObject<{
|
|
5
|
+
absoluteCurrentWorkingDirectory: z.ZodString;
|
|
6
|
+
userSuppliedName: z.ZodOptional<z.ZodString>;
|
|
7
|
+
dest: z.ZodOptional<z.ZodString>;
|
|
8
|
+
moduleLabel: z.ZodOptional<z.ZodString>;
|
|
9
|
+
reactType: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
contentTypes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
11
|
+
global: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
availableForNewContent: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
absoluteCurrentWorkingDirectory: string;
|
|
15
|
+
dest?: string | undefined;
|
|
16
|
+
global?: boolean | undefined;
|
|
17
|
+
moduleLabel?: string | undefined;
|
|
18
|
+
reactType?: boolean | undefined;
|
|
19
|
+
contentTypes?: string | undefined;
|
|
20
|
+
availableForNewContent?: boolean | undefined;
|
|
21
|
+
userSuppliedName?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
absoluteCurrentWorkingDirectory: string;
|
|
24
|
+
dest?: string | undefined;
|
|
25
|
+
global?: boolean | undefined;
|
|
26
|
+
moduleLabel?: string | undefined;
|
|
27
|
+
reactType?: boolean | undefined;
|
|
28
|
+
contentTypes?: string | undefined;
|
|
29
|
+
availableForNewContent?: boolean | undefined;
|
|
30
|
+
userSuppliedName?: string | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
export type HsCreateModuleInputSchema = z.infer<typeof inputSchemaZodObject>;
|
|
33
|
+
export declare class HsCreateModuleTool extends Tool<HsCreateModuleInputSchema> {
|
|
34
|
+
constructor(mcpServer: McpServer);
|
|
35
|
+
handler({ userSuppliedName, dest, moduleLabel, reactType, contentTypes, global, availableForNewContent, absoluteCurrentWorkingDirectory, }: HsCreateModuleInputSchema): Promise<TextContentResponse>;
|
|
36
|
+
register(): RegisteredTool;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Tool } from '../../types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { absoluteCurrentWorkingDirectory } from '../project/constants.js';
|
|
4
|
+
import { runCommandInDir } from '../../utils/project.js';
|
|
5
|
+
import { formatTextContents, formatTextContent } from '../../utils/content.js';
|
|
6
|
+
import { trackToolUsage } from '../../utils/toolUsageTracking.js';
|
|
7
|
+
import { addFlag } from '../../utils/command.js';
|
|
8
|
+
import { CONTENT_TYPES } from '../../../types/Cms.js';
|
|
9
|
+
const inputSchema = {
|
|
10
|
+
absoluteCurrentWorkingDirectory,
|
|
11
|
+
userSuppliedName: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('REQUIRED - If not specified by the user, DO NOT choose. Ask the user to specify the name of the module they want to create.')
|
|
14
|
+
.optional(),
|
|
15
|
+
dest: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('The destination path where the module should be created on the current computer.')
|
|
18
|
+
.optional(),
|
|
19
|
+
moduleLabel: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe('Label for module creation. Required for non-interactive module creation. If not provided, ask the user to provide it.')
|
|
22
|
+
.optional(),
|
|
23
|
+
reactType: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.describe('Whether to create a React module. If the user has not specified that they want a React module, DO NOT choose for them, ask them what type of module they want to create HubL or React.')
|
|
26
|
+
.optional(),
|
|
27
|
+
contentTypes: z
|
|
28
|
+
.string()
|
|
29
|
+
.refine(val => {
|
|
30
|
+
if (!val)
|
|
31
|
+
return true; // optional
|
|
32
|
+
const types = val.split(',').map(t => t.trim().toUpperCase());
|
|
33
|
+
return types.every(type => CONTENT_TYPES.includes(type));
|
|
34
|
+
}, {
|
|
35
|
+
message: `Content types must be a comma-separated list of valid values: ${CONTENT_TYPES.join(', ')}`,
|
|
36
|
+
})
|
|
37
|
+
.describe(`Content types where the module can be used. Comma-separated list. Valid values: ${CONTENT_TYPES.join(', ')}. Defaults to "ANY".`)
|
|
38
|
+
.optional(),
|
|
39
|
+
global: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.describe('Whether the module is global. Defaults to false.')
|
|
42
|
+
.optional(),
|
|
43
|
+
availableForNewContent: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.describe('Whether the module is available for new content. Defaults to true.')
|
|
46
|
+
.optional(),
|
|
47
|
+
};
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
|
+
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
50
|
+
const toolName = 'create-hubspot-cms-module';
|
|
51
|
+
export class HsCreateModuleTool extends Tool {
|
|
52
|
+
constructor(mcpServer) {
|
|
53
|
+
super(mcpServer);
|
|
54
|
+
}
|
|
55
|
+
async handler({ userSuppliedName, dest, moduleLabel, reactType, contentTypes, global, availableForNewContent, absoluteCurrentWorkingDirectory, }) {
|
|
56
|
+
await trackToolUsage(toolName);
|
|
57
|
+
const content = [];
|
|
58
|
+
// Always require a name
|
|
59
|
+
if (!userSuppliedName) {
|
|
60
|
+
content.push(formatTextContent(`Ask the user to specify the name of the module they want to create.`));
|
|
61
|
+
}
|
|
62
|
+
// Require module label
|
|
63
|
+
if (!moduleLabel) {
|
|
64
|
+
content.push(formatTextContent(`Ask the user to provide a label for the module.`));
|
|
65
|
+
}
|
|
66
|
+
// Ask about React vs HubL if not specified
|
|
67
|
+
if (reactType === undefined) {
|
|
68
|
+
content.push(formatTextContent(`Ask the user what type of module they want to create: HubL or React?`));
|
|
69
|
+
}
|
|
70
|
+
// If we have missing required information, return the prompts
|
|
71
|
+
if (content.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
content,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Build the command
|
|
77
|
+
let command = 'hs create module';
|
|
78
|
+
if (userSuppliedName) {
|
|
79
|
+
command += ` "${userSuppliedName}"`;
|
|
80
|
+
}
|
|
81
|
+
if (dest) {
|
|
82
|
+
command += ` "${dest}"`;
|
|
83
|
+
}
|
|
84
|
+
// Add module-specific flags
|
|
85
|
+
if (moduleLabel) {
|
|
86
|
+
command = addFlag(command, 'module-label', moduleLabel);
|
|
87
|
+
}
|
|
88
|
+
if (reactType !== undefined) {
|
|
89
|
+
command = addFlag(command, 'react-type', reactType);
|
|
90
|
+
}
|
|
91
|
+
if (contentTypes) {
|
|
92
|
+
command = addFlag(command, 'content-types', contentTypes);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
command = addFlag(command, 'content-types', 'ANY');
|
|
96
|
+
}
|
|
97
|
+
if (global !== undefined) {
|
|
98
|
+
command = addFlag(command, 'global', global);
|
|
99
|
+
}
|
|
100
|
+
if (availableForNewContent !== undefined) {
|
|
101
|
+
command = addFlag(command, 'available-for-new-content', availableForNewContent);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const { stdout, stderr } = await runCommandInDir(absoluteCurrentWorkingDirectory, command);
|
|
105
|
+
return formatTextContents(stdout, stderr);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return formatTextContents(error instanceof Error ? error.message : `${error}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
register() {
|
|
112
|
+
return this.mcpServer.registerTool(toolName, {
|
|
113
|
+
title: 'Create HubSpot CMS Module',
|
|
114
|
+
description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
|
|
115
|
+
inputSchema,
|
|
116
|
+
}, this.handler);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { HsCreateModuleTool } from '../HsCreateModuleTool.js';
|
|
3
|
+
import { runCommandInDir } from '../../../utils/project.js';
|
|
4
|
+
import { addFlag } from '../../../utils/command.js';
|
|
5
|
+
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
|
+
vi.mock('../../../utils/project');
|
|
7
|
+
vi.mock('../../../utils/command');
|
|
8
|
+
vi.mock('../../../utils/toolUsageTracking', () => ({
|
|
9
|
+
trackToolUsage: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
const mockRunCommandInDir = runCommandInDir;
|
|
12
|
+
const mockAddFlag = addFlag;
|
|
13
|
+
describe('HsCreateModuleTool', () => {
|
|
14
|
+
let mockMcpServer;
|
|
15
|
+
let tool;
|
|
16
|
+
let mockRegisteredTool;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
// @ts-expect-error Not mocking the whole server
|
|
20
|
+
mockMcpServer = {
|
|
21
|
+
registerTool: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
mockRegisteredTool = {};
|
|
24
|
+
mockMcpServer.registerTool.mockReturnValue(mockRegisteredTool);
|
|
25
|
+
tool = new HsCreateModuleTool(mockMcpServer);
|
|
26
|
+
});
|
|
27
|
+
describe('register', () => {
|
|
28
|
+
it('should register the tool with the MCP server', () => {
|
|
29
|
+
const result = tool.register();
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-hubspot-cms-module', {
|
|
31
|
+
title: 'Create HubSpot CMS Module',
|
|
32
|
+
description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
|
|
33
|
+
inputSchema: expect.any(Object),
|
|
34
|
+
}, expect.any(Function));
|
|
35
|
+
expect(result).toBe(mockRegisteredTool);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('handler', () => {
|
|
39
|
+
it('should prompt for missing required parameters', async () => {
|
|
40
|
+
const result = await tool.handler({
|
|
41
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
42
|
+
});
|
|
43
|
+
expect(result.content).toHaveLength(3);
|
|
44
|
+
expect(result.content[0].text).toContain('Ask the user to specify the name of the module');
|
|
45
|
+
expect(result.content[1].text).toContain('Ask the user to provide a label for the module');
|
|
46
|
+
expect(result.content[2].text).toContain('Ask the user what type of module they want to create: HubL or React?');
|
|
47
|
+
});
|
|
48
|
+
it('should prompt for missing name only when other params provided', async () => {
|
|
49
|
+
const result = await tool.handler({
|
|
50
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
51
|
+
moduleLabel: 'Test Label',
|
|
52
|
+
reactType: false,
|
|
53
|
+
});
|
|
54
|
+
expect(result.content).toHaveLength(1);
|
|
55
|
+
expect(result.content[0].text).toContain('Ask the user to specify the name of the module');
|
|
56
|
+
});
|
|
57
|
+
it('should prompt for missing moduleLabel when name provided', async () => {
|
|
58
|
+
const result = await tool.handler({
|
|
59
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
60
|
+
userSuppliedName: 'Test Module',
|
|
61
|
+
reactType: true,
|
|
62
|
+
});
|
|
63
|
+
expect(result.content).toHaveLength(1);
|
|
64
|
+
expect(result.content[0].text).toContain('Ask the user to provide a label for the module');
|
|
65
|
+
});
|
|
66
|
+
it('should prompt for missing reactType when other required params provided', async () => {
|
|
67
|
+
const result = await tool.handler({
|
|
68
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
69
|
+
userSuppliedName: 'Test Module',
|
|
70
|
+
moduleLabel: 'Test Label',
|
|
71
|
+
});
|
|
72
|
+
expect(result.content).toHaveLength(1);
|
|
73
|
+
expect(result.content[0].text).toContain('Ask the user what type of module they want to create: HubL or React?');
|
|
74
|
+
});
|
|
75
|
+
it('should execute command with all required parameters (HubL module)', async () => {
|
|
76
|
+
mockAddFlag
|
|
77
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
78
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
79
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY');
|
|
80
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
81
|
+
stdout: 'Module created successfully',
|
|
82
|
+
stderr: '',
|
|
83
|
+
});
|
|
84
|
+
const result = await tool.handler({
|
|
85
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
86
|
+
userSuppliedName: 'Test Module',
|
|
87
|
+
moduleLabel: 'Test Label',
|
|
88
|
+
reactType: false,
|
|
89
|
+
});
|
|
90
|
+
expect(mockAddFlag).toHaveBeenCalledWith('hs create module "Test Module"', 'module-label', 'Test Label');
|
|
91
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('module-label'), 'react-type', false);
|
|
92
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('react-type'), 'content-types', 'ANY');
|
|
93
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', expect.stringContaining('hs create module'));
|
|
94
|
+
expect(result.content).toHaveLength(2);
|
|
95
|
+
expect(result.content[0].text).toContain('Module created successfully');
|
|
96
|
+
});
|
|
97
|
+
it('should execute command with React module', async () => {
|
|
98
|
+
mockAddFlag
|
|
99
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label')
|
|
100
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label --react-type true')
|
|
101
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label --react-type true --content-types ANY');
|
|
102
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
103
|
+
stdout: 'React module created successfully',
|
|
104
|
+
stderr: '',
|
|
105
|
+
});
|
|
106
|
+
const result = await tool.handler({
|
|
107
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
108
|
+
userSuppliedName: 'React Module',
|
|
109
|
+
moduleLabel: 'React Label',
|
|
110
|
+
reactType: true,
|
|
111
|
+
});
|
|
112
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('module-label'), 'react-type', true);
|
|
113
|
+
expect(result.content[0].text).toContain('React module created successfully');
|
|
114
|
+
});
|
|
115
|
+
it('should execute command with destination path', async () => {
|
|
116
|
+
mockAddFlag
|
|
117
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label')
|
|
118
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label --react-type false')
|
|
119
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label --react-type false --content-types ANY');
|
|
120
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
121
|
+
stdout: 'Module created at custom path',
|
|
122
|
+
stderr: '',
|
|
123
|
+
});
|
|
124
|
+
const result = await tool.handler({
|
|
125
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
126
|
+
userSuppliedName: 'Test Module',
|
|
127
|
+
dest: 'custom/path',
|
|
128
|
+
moduleLabel: 'Test Label',
|
|
129
|
+
reactType: false,
|
|
130
|
+
});
|
|
131
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', expect.stringContaining('"custom/path"'));
|
|
132
|
+
expect(result.content[0].text).toContain('Module created at custom path');
|
|
133
|
+
});
|
|
134
|
+
it('should execute command with custom content types', async () => {
|
|
135
|
+
mockAddFlag
|
|
136
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
137
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
138
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types LANDING_PAGE,BLOG_POST');
|
|
139
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
140
|
+
stdout: 'Module with custom content types created',
|
|
141
|
+
stderr: '',
|
|
142
|
+
});
|
|
143
|
+
const result = await tool.handler({
|
|
144
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
145
|
+
userSuppliedName: 'Test Module',
|
|
146
|
+
moduleLabel: 'Test Label',
|
|
147
|
+
reactType: false,
|
|
148
|
+
contentTypes: 'LANDING_PAGE,BLOG_POST',
|
|
149
|
+
});
|
|
150
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('react-type'), 'content-types', 'LANDING_PAGE,BLOG_POST');
|
|
151
|
+
expect(result.content[0].text).toContain('Module with custom content types created');
|
|
152
|
+
});
|
|
153
|
+
it('should execute command with global flag', async () => {
|
|
154
|
+
mockAddFlag
|
|
155
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label')
|
|
156
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false')
|
|
157
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false --content-types ANY')
|
|
158
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false --content-types ANY --global true');
|
|
159
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
160
|
+
stdout: 'Global module created',
|
|
161
|
+
stderr: '',
|
|
162
|
+
});
|
|
163
|
+
const result = await tool.handler({
|
|
164
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
165
|
+
userSuppliedName: 'Global Module',
|
|
166
|
+
moduleLabel: 'Global Label',
|
|
167
|
+
reactType: false,
|
|
168
|
+
global: true,
|
|
169
|
+
});
|
|
170
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('content-types'), 'global', true);
|
|
171
|
+
expect(result.content[0].text).toContain('Global module created');
|
|
172
|
+
});
|
|
173
|
+
it('should execute command with availableForNewContent flag', async () => {
|
|
174
|
+
mockAddFlag
|
|
175
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
176
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
177
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY')
|
|
178
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY --available-for-new-content false');
|
|
179
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
180
|
+
stdout: 'Module created with availableForNewContent false',
|
|
181
|
+
stderr: '',
|
|
182
|
+
});
|
|
183
|
+
const result = await tool.handler({
|
|
184
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
185
|
+
userSuppliedName: 'Test Module',
|
|
186
|
+
moduleLabel: 'Test Label',
|
|
187
|
+
reactType: false,
|
|
188
|
+
availableForNewContent: false,
|
|
189
|
+
});
|
|
190
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('content-types'), 'available-for-new-content', false);
|
|
191
|
+
expect(result.content[0].text).toContain('Module created with availableForNewContent false');
|
|
192
|
+
});
|
|
193
|
+
it('should handle command execution errors', async () => {
|
|
194
|
+
mockRunCommandInDir.mockRejectedValue(new Error('Command failed'));
|
|
195
|
+
const result = await tool.handler({
|
|
196
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
197
|
+
userSuppliedName: 'Test Module',
|
|
198
|
+
moduleLabel: 'Test Label',
|
|
199
|
+
reactType: false,
|
|
200
|
+
});
|
|
201
|
+
expect(result.content).toHaveLength(1);
|
|
202
|
+
expect(result.content[0].text).toContain('Command failed');
|
|
203
|
+
});
|
|
204
|
+
it('should handle stderr output', async () => {
|
|
205
|
+
mockAddFlag
|
|
206
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
207
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
208
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY');
|
|
209
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
210
|
+
stdout: 'Module created successfully',
|
|
211
|
+
stderr: 'Warning: Deprecated feature used',
|
|
212
|
+
});
|
|
213
|
+
const result = await tool.handler({
|
|
214
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
215
|
+
userSuppliedName: 'Test Module',
|
|
216
|
+
moduleLabel: 'Test Label',
|
|
217
|
+
reactType: false,
|
|
218
|
+
});
|
|
219
|
+
expect(result.content).toHaveLength(2);
|
|
220
|
+
expect(result.content[0].text).toContain('Module created successfully');
|
|
221
|
+
expect(result.content[1].text).toContain('Warning: Deprecated feature used');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -8,6 +8,7 @@ import { GetConfigValuesTool } from './project/GetConfigValuesTool.js';
|
|
|
8
8
|
import { DocsSearchTool } from './project/DocsSearchTool.js';
|
|
9
9
|
import { DocFetchTool } from './project/DocFetchTool.js';
|
|
10
10
|
import { HsListTool } from './cms/HsListTool.js';
|
|
11
|
+
import { HsCreateModuleTool } from './cms/HsCreateModuleTool.js';
|
|
11
12
|
export function registerProjectTools(mcpServer) {
|
|
12
13
|
return [
|
|
13
14
|
new UploadProjectTools(mcpServer).register(),
|
|
@@ -22,5 +23,8 @@ export function registerProjectTools(mcpServer) {
|
|
|
22
23
|
];
|
|
23
24
|
}
|
|
24
25
|
export function registerCmsTools(mcpServer) {
|
|
25
|
-
return [
|
|
26
|
+
return [
|
|
27
|
+
new HsListTool(mcpServer).register(),
|
|
28
|
+
new HsCreateModuleTool(mcpServer).register(),
|
|
29
|
+
];
|
|
26
30
|
}
|
package/package.json
CHANGED