@hubspot/cli 7.7.28-experimental.0 → 7.7.29-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,5 +1,5 @@
1
- import { CommonArgs, EnvironmentArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type ProjectUploadArgs = CommonArgs & JSONOutputArgs & EnvironmentArgs & {
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, args.useEnv);
28
+ targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
29
29
  }
30
30
  targetAccountId = targetAccountId || derivedAccountId;
31
31
  const accountConfig = getAccountConfig(targetAccountId);
package/lang/en.d.ts CHANGED
@@ -884,10 +884,12 @@ 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";
887
888
  readonly alreadyInstalled: "HubSpot CLI mcp server already installed, reinstalling";
888
889
  readonly configuringWindsurf: "Configuring Windsurf...";
889
890
  readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
890
891
  readonly configuredWindsurf: "Configured Windsurf";
892
+ readonly windsurfNotFound: "Windsurf not found - skipping configuration";
891
893
  readonly configuringVsCode: "Configuring VSCode...";
892
894
  readonly failedToConfigureVsCode: "Failed to configure VSCode";
893
895
  readonly configuredVsCode: "Configured VSCode";
package/lang/en.js CHANGED
@@ -889,11 +889,13 @@ 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',
892
893
  alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
893
894
  // Windsurf
894
895
  configuringWindsurf: 'Configuring Windsurf...',
895
896
  failedToConfigureWindsurf: 'Failed to configure Windsurf',
896
897
  configuredWindsurf: 'Configured Windsurf',
898
+ windsurfNotFound: 'Windsurf not found - skipping configuration',
897
899
  // VS Code
898
900
  configuringVsCode: 'Configuring VSCode...',
899
901
  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 setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
20
19
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
21
- export declare function setupCursor(mcpCommand?: McpCommand): boolean;
22
- export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
20
+ 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>;
23
23
  export {};
package/lib/mcp/setup.js CHANGED
@@ -4,10 +4,6 @@ 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';
11
7
  const mcpServerName = 'hubspot-cli-mcp';
12
8
  const claudeCode = 'claude';
13
9
  const windsurf = 'windsurf';
@@ -72,96 +68,6 @@ async function runSetupFunction(func) {
72
68
  throw new Error();
73
69
  }
74
70
  }
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 '${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
- }
165
71
  export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
166
72
  try {
167
73
  SpinniesManager.add('claudeCode', {
@@ -182,15 +88,14 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
182
88
  });
183
89
  await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
184
90
  }
185
- await execAsync(`claude mcp add-json "${mcpServerName}" '${mcpConfig}' --scope user`);
91
+ await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
186
92
  SpinniesManager.succeed('claudeCode', {
187
93
  text: commands.mcp.setup.spinners.configuredClaudeCode,
188
94
  });
189
95
  return true;
190
96
  }
191
97
  catch (error) {
192
- if (error instanceof Error &&
193
- error.message.includes('claude: command not found')) {
98
+ if (error instanceof Error && error.message.includes('claude')) {
194
99
  SpinniesManager.fail('claudeCode', {
195
100
  text: commands.mcp.setup.spinners.claudeCodeNotFound,
196
101
  });
@@ -212,25 +117,44 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
212
117
  return false;
213
118
  }
214
119
  }
215
- export function setupCursor(mcpCommand = defaultMcpCommand) {
216
- const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
217
- return setupMcpConfigFile({
218
- configPath: cursorConfigPath,
219
- configuringMessage: commands.mcp.setup.spinners.configuringCursor,
220
- configuredMessage: commands.mcp.setup.spinners.configuredCursor,
221
- failedMessage: commands.mcp.setup.spinners.failedToConfigureCursor,
222
- mcpCommand: buildCommandWithAgentString(mcpCommand, cursor),
223
- });
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);
224
155
  }
225
- export function setupWindsurf(mcpCommand = defaultMcpCommand) {
226
- const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
227
- return setupMcpConfigFile({
228
- configPath: windsurfConfigPath,
229
- configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
230
- configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
231
- failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
232
- mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
233
- });
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);
234
158
  }
235
159
  function buildCommandWithAgentString(mcpCommand, agent) {
236
160
  const mcpCommandCopy = structuredClone(mcpCommand);
@@ -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, useEnv?: boolean): Promise<number | undefined>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
@@ -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, useEnv = false) {
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 {
@@ -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,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 [new HsListTool(mcpServer).register()];
26
+ return [
27
+ new HsListTool(mcpServer).register(),
28
+ new HsCreateModuleTool(mcpServer).register(),
29
+ ];
26
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.7.28-experimental.0",
3
+ "version": "7.7.29-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",
package/types/Yargs.d.ts CHANGED
@@ -15,7 +15,7 @@ export type AccountArgs = {
15
15
  account?: string;
16
16
  };
17
17
  export type EnvironmentArgs = {
18
- 'use-env'?: boolean;
18
+ 'use-env'?: string;
19
19
  };
20
20
  export type OverwriteArgs = Options & {
21
21
  o?: boolean;