@hubspot/cli 8.1.0 → 8.2.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/cms/__tests__/watch.test.js +0 -8
- package/commands/cms/function/logs.js +1 -0
- package/commands/cms/theme/preview.js +9 -64
- package/commands/cms/watch.d.ts +0 -1
- package/commands/cms/watch.js +2 -8
- package/commands/feedback.js +1 -1
- package/commands/mcp/__tests__/start.test.js +8 -1
- package/commands/mcp/setup.js +1 -9
- package/commands/mcp/start.js +0 -1
- package/commands/project/__tests__/create.test.js +1 -1
- package/commands/project/create.js +2 -2
- package/commands/project/watch.js +15 -2
- package/lang/en.d.ts +17 -6
- package/lang/en.js +18 -7
- package/lib/__tests__/commandSuggestion.test.js +2 -0
- package/lib/__tests__/serverlessLogs.test.js +79 -64
- package/lib/commandSuggestion.js +1 -7
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/generateSelectors.js +1 -2
- package/lib/getStartedV2Actions.d.ts +13 -0
- package/lib/getStartedV2Actions.js +53 -0
- package/lib/mcp/__tests__/setup.test.js +357 -28
- package/lib/mcp/setup.d.ts +1 -0
- package/lib/mcp/setup.js +77 -30
- package/lib/projects/create/__tests__/legacy.test.js +6 -24
- package/lib/projects/create/index.js +1 -4
- package/lib/projects/create/legacy.js +3 -8
- package/lib/projects/create/v2.js +1 -9
- package/lib/projects/ensureProjectExists.js +1 -2
- package/lib/projects/pollProjectBuildAndDeploy.js +90 -85
- package/lib/projects/upload.d.ts +1 -0
- package/lib/projects/upload.js +37 -46
- package/lib/projects/watch.d.ts +2 -1
- package/lib/projects/watch.js +32 -24
- package/lib/serverlessLogs.js +50 -44
- package/lib/theme/cmsDevServerProcess.d.ts +12 -0
- package/lib/theme/cmsDevServerProcess.js +148 -0
- package/lib/theme/cmsDevServerRunner.d.ts +14 -0
- package/lib/theme/cmsDevServerRunner.js +90 -0
- package/lib/usageTracking.js +8 -5
- package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
- package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
- package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
- package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
- package/mcp-server/tools/cms/HsListTool.js +1 -1
- package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -2
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -2
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +7 -11
- package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
- package/mcp-server/tools/project/CreateProjectTool.js +6 -11
- package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
- package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
- package/mcp-server/tools/project/GetBuildLogsTool.js +6 -7
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
- package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
- package/mcp-server/tools/project/UploadProjectTools.js +1 -1
- package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
- package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -2
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +0 -32
- package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +12 -1
- package/mcp-server/tools/project/constants.js +12 -16
- package/mcp-server/utils/__tests__/command.test.js +233 -3
- package/mcp-server/utils/__tests__/feedbackTracking.test.js +9 -64
- package/mcp-server/utils/command.d.ts +5 -0
- package/mcp-server/utils/command.js +24 -0
- package/mcp-server/utils/feedbackTracking.js +2 -17
- package/package.json +4 -5
- package/ui/components/getStarted/GetStartedFlow.js +79 -2
- package/ui/components/getStarted/reducer.d.ts +20 -0
- package/ui/components/getStarted/reducer.js +36 -0
- package/ui/components/getStarted/screens/InstallationScreen.d.ts +7 -0
- package/ui/components/getStarted/screens/InstallationScreen.js +16 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +2 -1
- package/ui/lib/constants.d.ts +1 -0
- package/ui/lib/constants.js +1 -0
- package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
- package/mcp-server/utils/__tests__/project.test.js +0 -140
- package/mcp-server/utils/project.d.ts +0 -5
- package/mcp-server/utils/project.js +0 -18
package/lib/commandSuggestion.js
CHANGED
|
@@ -18,12 +18,6 @@ export const commandSuggestionMappings = {
|
|
|
18
18
|
'theme generate-selectors': 'hs cms theme generate-selectors',
|
|
19
19
|
'theme marketplace-validate': 'hs cms theme marketplace-validate',
|
|
20
20
|
'theme preview': 'hs cms theme preview',
|
|
21
|
-
'custom-object schema create': 'hs custom-object create-schema',
|
|
22
|
-
'custom-object schema delete': 'hs custom-object delete-schema',
|
|
23
|
-
'custom-object schema fetch-all': 'hs custom-object fetch-all-schemas',
|
|
24
|
-
'custom-object schema fetch': 'hs custom-object fetch-schema',
|
|
25
|
-
'custom-object schema list': 'hs custom-object list-schemas',
|
|
26
|
-
'custom-object schema update': 'hs custom-object update-schema',
|
|
27
21
|
};
|
|
28
22
|
function createCommandSuggestionHandler(newCommand) {
|
|
29
23
|
return () => {
|
|
@@ -35,7 +29,7 @@ function createCommandSuggestion(oldCommand, newCommand) {
|
|
|
35
29
|
return {
|
|
36
30
|
command: oldCommand,
|
|
37
31
|
builder: async (yargs) => {
|
|
38
|
-
return yargs.strict(false);
|
|
32
|
+
return yargs.strict(false).help(false).version(false);
|
|
39
33
|
},
|
|
40
34
|
handler: createCommandSuggestionHandler(newCommand),
|
|
41
35
|
};
|
package/lib/constants.d.ts
CHANGED
|
@@ -81,7 +81,6 @@ export declare const FEATURES: {
|
|
|
81
81
|
readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
|
|
82
82
|
readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
|
|
83
83
|
readonly APPS_HOME: "UIE:AppHome";
|
|
84
|
-
readonly MCP_ACCESS: "Developers:CLIMCPAccess";
|
|
85
84
|
readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
|
|
86
85
|
readonly AGENT_TOOLS: "ThirdPartyAgentTools";
|
|
87
86
|
};
|
|
@@ -145,3 +144,4 @@ export declare const ACCOUNT_LEVELS: {
|
|
|
145
144
|
readonly ENTERPRISE: "ENTERPRISE";
|
|
146
145
|
};
|
|
147
146
|
export declare const ACCOUNT_LEVEL_CHOICES: readonly ["FREE", "STARTER", "PROFESSIONAL", "ENTERPRISE"];
|
|
147
|
+
export declare const FEEDBACK_URL = "https://developers.hubspot.com/feedback";
|
package/lib/constants.js
CHANGED
|
@@ -73,7 +73,6 @@ export const FEATURES = {
|
|
|
73
73
|
SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
|
|
74
74
|
APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
|
|
75
75
|
APPS_HOME: 'UIE:AppHome',
|
|
76
|
-
MCP_ACCESS: 'Developers:CLIMCPAccess',
|
|
77
76
|
THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
|
|
78
77
|
AGENT_TOOLS: 'ThirdPartyAgentTools',
|
|
79
78
|
};
|
|
@@ -146,3 +145,4 @@ export const ACCOUNT_LEVEL_CHOICES = [
|
|
|
146
145
|
ACCOUNT_LEVELS.PROFESSIONAL,
|
|
147
146
|
ACCOUNT_LEVELS.ENTERPRISE,
|
|
148
147
|
];
|
|
148
|
+
export const FEEDBACK_URL = 'https://developers.hubspot.com/feedback';
|
package/lib/generateSelectors.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { EXIT_CODES } from './enums/exitCodes.js';
|
|
3
2
|
import { commands } from '../lang/en.js';
|
|
4
3
|
import { uiLogger } from './ui/logger.js';
|
|
5
4
|
const CSS_COMMENTS_REGEX = new RegExp(/\/\*.*\*\//, 'g');
|
|
@@ -12,7 +11,7 @@ export function findFieldsJsonPath(basePath) {
|
|
|
12
11
|
const _path = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
13
12
|
if (!fs.existsSync(_path)) {
|
|
14
13
|
uiLogger.error(commands.cms.subcommands.theme.subcommands.generateSelectors.errors.invalidPath(basePath));
|
|
15
|
-
|
|
14
|
+
return null;
|
|
16
15
|
}
|
|
17
16
|
const files = fs.readdirSync(_path);
|
|
18
17
|
if (files.includes('fields.json')) {
|
|
@@ -35,3 +35,16 @@ export declare function uploadAndDeployAction({ accountId, projectDest, }: {
|
|
|
35
35
|
projectDest: string;
|
|
36
36
|
}): Promise<UploadAndDeployResult>;
|
|
37
37
|
export declare function trackGetStartedUsage(params: Record<string, unknown>, accountId: number): Promise<void>;
|
|
38
|
+
export type PollAppInstallationOptions = {
|
|
39
|
+
accountId: number;
|
|
40
|
+
projectId: number;
|
|
41
|
+
appUid: string;
|
|
42
|
+
requiredScopes?: string[];
|
|
43
|
+
optionalScopes?: string[];
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
intervalMs?: number;
|
|
46
|
+
onTimeout?: () => void;
|
|
47
|
+
};
|
|
48
|
+
export declare function pollAppInstallation({ accountId, projectId, appUid, requiredScopes, optionalScopes, timeoutMs, // 2 minutes
|
|
49
|
+
intervalMs, // 2 seconds
|
|
50
|
+
onTimeout, }: PollAppInstallationOptions): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fetchPublicAppsForPortal } from '@hubspot/local-dev-lib/api/appsDev';
|
|
4
|
+
import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
4
5
|
import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
|
|
5
6
|
import { getConfigAccountEnvironment } from '@hubspot/local-dev-lib/config';
|
|
6
7
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
@@ -144,3 +145,55 @@ export async function uploadAndDeployAction({ accountId, projectDest, }) {
|
|
|
144
145
|
export function trackGetStartedUsage(params, accountId) {
|
|
145
146
|
return trackCommandMetadataUsage('get-started', params, accountId);
|
|
146
147
|
}
|
|
148
|
+
export async function pollAppInstallation({ accountId, projectId, appUid, requiredScopes = [], optionalScopes = [], timeoutMs = 2 * 60 * 1000, // 2 minutes
|
|
149
|
+
intervalMs = 2000, // 2 seconds
|
|
150
|
+
onTimeout, }) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
let consecutiveErrors = 0;
|
|
153
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
154
|
+
let pollInterval = null;
|
|
155
|
+
let pollTimeout = null;
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
if (pollInterval) {
|
|
158
|
+
clearTimeout(pollInterval);
|
|
159
|
+
pollInterval = null;
|
|
160
|
+
}
|
|
161
|
+
if (pollTimeout) {
|
|
162
|
+
clearTimeout(pollTimeout);
|
|
163
|
+
pollTimeout = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
pollTimeout = setTimeout(() => {
|
|
167
|
+
cleanup();
|
|
168
|
+
if (onTimeout) {
|
|
169
|
+
onTimeout();
|
|
170
|
+
}
|
|
171
|
+
resolve(); // Resolve instead of reject to allow continuing with timeout state
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
const poll = async () => {
|
|
174
|
+
try {
|
|
175
|
+
const { data } = await fetchAppInstallationData(accountId, projectId, appUid, requiredScopes, optionalScopes);
|
|
176
|
+
// Reset error counter on successful fetch
|
|
177
|
+
consecutiveErrors = 0;
|
|
178
|
+
if (data.isInstalledWithScopeGroups) {
|
|
179
|
+
cleanup();
|
|
180
|
+
resolve();
|
|
181
|
+
}
|
|
182
|
+
else if (pollInterval) {
|
|
183
|
+
pollInterval = setTimeout(poll, intervalMs);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
consecutiveErrors++;
|
|
188
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(new Error(`Failed to check app installation status after ${MAX_CONSECUTIVE_ERRORS} consecutive errors`, { cause: error }));
|
|
191
|
+
}
|
|
192
|
+
else if (pollInterval !== null) {
|
|
193
|
+
pollInterval = setTimeout(poll, intervalMs);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
pollInterval = setTimeout(poll, 0);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import { execAsync } from '../../../mcp-server/utils/command.js';
|
|
2
|
-
import { setupCodex, setupGemini, supportedTools } from '../setup.js';
|
|
2
|
+
import { setupCodex, setupGemini, setupClaudeCode, setupCursor, setupWindsurf, setupVsCode, addMcpServerToConfig, supportedTools, } from '../setup.js';
|
|
3
3
|
import SpinniesManager from '../../ui/SpinniesManager.js';
|
|
4
4
|
import { logError } from '../../errorHandlers/index.js';
|
|
5
|
+
import { uiLogger } from '../../ui/logger.js';
|
|
6
|
+
import { promptUser } from '../../prompts/promptUtils.js';
|
|
5
7
|
import { commands } from '../../../lang/en.js';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
6
12
|
// Mock dependencies
|
|
7
13
|
vi.mock('../../../mcp-server/utils/command.js');
|
|
8
14
|
vi.mock('../../ui/SpinniesManager.js');
|
|
9
15
|
vi.mock('../../errorHandlers/index.js');
|
|
10
|
-
vi.mock('
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
cursor: 'Cursor',
|
|
17
|
-
gemini: 'Gemini CLI',
|
|
18
|
-
vsCode: 'VS Code',
|
|
19
|
-
windsurf: 'Windsurf',
|
|
20
|
-
success: vi.fn(targets => `Success message for ${targets.join(', ')}`),
|
|
21
|
-
spinners: {
|
|
22
|
-
configuringCodex: 'Configuring Codex...',
|
|
23
|
-
configuredCodex: 'Configured Codex',
|
|
24
|
-
codexNotFound: 'Codex command not found - skipping configuration',
|
|
25
|
-
codexInstallFailed: 'Failed to configure Codex',
|
|
26
|
-
configuringGemini: 'Configuring Gemini CLI...',
|
|
27
|
-
configuredGemini: 'Configured Gemini CLI',
|
|
28
|
-
geminiNotFound: 'Gemini CLI not found - skipping configuration',
|
|
29
|
-
geminiInstallFailed: 'Failed to configure Gemini CLI',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
16
|
+
vi.mock('../../ui/logger.js');
|
|
17
|
+
vi.mock('../../prompts/promptUtils.js');
|
|
18
|
+
vi.mock('fs-extra');
|
|
19
|
+
vi.mock('fs');
|
|
20
|
+
vi.mock('os');
|
|
21
|
+
vi.mock('path');
|
|
35
22
|
const mockedExecAsync = vi.mocked(execAsync);
|
|
36
23
|
const mockedSpinniesManager = vi.mocked(SpinniesManager);
|
|
37
24
|
const mockedLogError = vi.mocked(logError);
|
|
@@ -132,6 +119,21 @@ describe('lib/mcp/setup', () => {
|
|
|
132
119
|
});
|
|
133
120
|
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
134
121
|
});
|
|
122
|
+
it('should pass through environment variables in command', async () => {
|
|
123
|
+
const mockMcpCommandWithEnv = {
|
|
124
|
+
command: 'test-command',
|
|
125
|
+
args: ['--arg1'],
|
|
126
|
+
env: { HUBSPOT_MCP_STANDALONE: 'true' },
|
|
127
|
+
};
|
|
128
|
+
mockedExecAsync.mockResolvedValueOnce({
|
|
129
|
+
stdout: 'codex version 1.0.0',
|
|
130
|
+
stderr: '',
|
|
131
|
+
});
|
|
132
|
+
mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
133
|
+
const result = await setupCodex(mockMcpCommandWithEnv);
|
|
134
|
+
expect(result).toBe(true);
|
|
135
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('codex mcp add "HubSpotDev" --env HUBSPOT_MCP_STANDALONE="true" -- test-command --arg1 --ai-agent codex');
|
|
136
|
+
});
|
|
135
137
|
});
|
|
136
138
|
describe('setupGemini', () => {
|
|
137
139
|
const mockMcpCommand = {
|
|
@@ -189,6 +191,333 @@ describe('lib/mcp/setup', () => {
|
|
|
189
191
|
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
190
192
|
});
|
|
191
193
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
describe('setupClaudeCode', () => {
|
|
195
|
+
const mockMcpCommand = {
|
|
196
|
+
command: 'test-command',
|
|
197
|
+
args: ['--arg1', '--arg2'],
|
|
198
|
+
};
|
|
199
|
+
it('should successfully configure Claude Code when command is available', async () => {
|
|
200
|
+
mockedExecAsync
|
|
201
|
+
.mockResolvedValueOnce({ stdout: 'claude version 1.0.0', stderr: '' })
|
|
202
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
203
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
204
|
+
const result = await setupClaudeCode(mockMcpCommand);
|
|
205
|
+
expect(result).toBe(true);
|
|
206
|
+
expect(mockedSpinniesManager.add).toHaveBeenCalledWith('claudeCode', {
|
|
207
|
+
text: commands.mcp.setup.spinners.configuringClaudeCode,
|
|
208
|
+
});
|
|
209
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('claude --version');
|
|
210
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('claude mcp list');
|
|
211
|
+
expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('claudeCode', {
|
|
212
|
+
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it('should remove and re-add when server is already installed', async () => {
|
|
216
|
+
mockedExecAsync
|
|
217
|
+
.mockResolvedValueOnce({ stdout: 'claude version 1.0.0', stderr: '' })
|
|
218
|
+
.mockResolvedValueOnce({ stdout: 'HubSpotDev some-config', stderr: '' })
|
|
219
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
220
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
221
|
+
const result = await setupClaudeCode(mockMcpCommand);
|
|
222
|
+
expect(result).toBe(true);
|
|
223
|
+
expect(mockedSpinniesManager.update).toHaveBeenCalledWith('claudeCode', {
|
|
224
|
+
text: commands.mcp.setup.spinners.alreadyInstalled,
|
|
225
|
+
});
|
|
226
|
+
expect(mockedExecAsync).toHaveBeenCalledWith('claude mcp remove "HubSpotDev" --scope user');
|
|
227
|
+
});
|
|
228
|
+
it('should use default mcp command when none provided', async () => {
|
|
229
|
+
mockedExecAsync
|
|
230
|
+
.mockResolvedValueOnce({ stdout: 'claude version 1.0.0', stderr: '' })
|
|
231
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
232
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
233
|
+
const result = await setupClaudeCode();
|
|
234
|
+
expect(result).toBe(true);
|
|
235
|
+
expect(mockedExecAsync).toHaveBeenCalledWith(expect.stringContaining('claude mcp add-json "HubSpotDev"'));
|
|
236
|
+
});
|
|
237
|
+
it('should return false when claude command is not found', async () => {
|
|
238
|
+
mockedExecAsync.mockRejectedValueOnce(new Error('claude: command not found'));
|
|
239
|
+
const result = await setupClaudeCode(mockMcpCommand);
|
|
240
|
+
expect(result).toBe(false);
|
|
241
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('claudeCode', {
|
|
242
|
+
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
it('should return false and log error when mcp add fails', async () => {
|
|
246
|
+
const error = new Error('mcp add failed');
|
|
247
|
+
mockedExecAsync
|
|
248
|
+
.mockResolvedValueOnce({ stdout: 'claude version 1.0.0', stderr: '' })
|
|
249
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
250
|
+
.mockRejectedValueOnce(error);
|
|
251
|
+
const result = await setupClaudeCode(mockMcpCommand);
|
|
252
|
+
expect(result).toBe(false);
|
|
253
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('claudeCode', {
|
|
254
|
+
text: commands.mcp.setup.spinners.claudeCodeInstallFailed,
|
|
255
|
+
});
|
|
256
|
+
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe('setupCursor', () => {
|
|
260
|
+
const mockedFs = vi.mocked(fs);
|
|
261
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
262
|
+
const mockMcpCommand = {
|
|
263
|
+
command: 'test-command',
|
|
264
|
+
args: ['--arg1'],
|
|
265
|
+
};
|
|
266
|
+
beforeEach(() => {
|
|
267
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
268
|
+
vi.mocked(path.join).mockImplementation((...parts) => parts.join('/'));
|
|
269
|
+
});
|
|
270
|
+
it('should successfully configure Cursor when config file exists', () => {
|
|
271
|
+
mockedExistsSync.mockReturnValue(true);
|
|
272
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify({ mcpServers: { existingServer: {} } }));
|
|
273
|
+
const result = setupCursor(mockMcpCommand);
|
|
274
|
+
expect(result).toBe(true);
|
|
275
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('.cursor/mcp.json'), expect.stringContaining('HubSpotDev'));
|
|
276
|
+
expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('spinner', {
|
|
277
|
+
text: commands.mcp.setup.spinners.configuredCursor,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
it('should create config file when it does not exist', () => {
|
|
281
|
+
mockedExistsSync.mockReturnValue(false);
|
|
282
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
283
|
+
const result = setupCursor(mockMcpCommand);
|
|
284
|
+
expect(result).toBe(true);
|
|
285
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('.cursor/mcp.json'), JSON.stringify({}, null, 2));
|
|
286
|
+
});
|
|
287
|
+
it('should handle empty config file', () => {
|
|
288
|
+
mockedExistsSync.mockReturnValue(true);
|
|
289
|
+
mockedFs.readFileSync.mockReturnValue(' ');
|
|
290
|
+
const result = setupCursor(mockMcpCommand);
|
|
291
|
+
expect(result).toBe(true);
|
|
292
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
293
|
+
expect(writeCall).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
it('should return false when config file has invalid JSON', () => {
|
|
296
|
+
mockedExistsSync.mockReturnValue(true);
|
|
297
|
+
mockedFs.readFileSync.mockReturnValue('not valid json {{{');
|
|
298
|
+
const result = setupCursor(mockMcpCommand);
|
|
299
|
+
expect(result).toBe(false);
|
|
300
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('spinner', {
|
|
301
|
+
text: commands.mcp.setup.spinners.failedToConfigureCursor,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
it('should return false when reading config file fails', () => {
|
|
305
|
+
const error = new Error('Permission denied');
|
|
306
|
+
mockedExistsSync.mockReturnValue(true);
|
|
307
|
+
mockedFs.readFileSync.mockImplementation(() => {
|
|
308
|
+
throw error;
|
|
309
|
+
});
|
|
310
|
+
const result = setupCursor(mockMcpCommand);
|
|
311
|
+
expect(result).toBe(false);
|
|
312
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('spinner', {
|
|
313
|
+
text: commands.mcp.setup.spinners.failedToConfigureCursor,
|
|
314
|
+
});
|
|
315
|
+
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
316
|
+
});
|
|
317
|
+
it('should use default mcp command when none provided', () => {
|
|
318
|
+
mockedExistsSync.mockReturnValue(true);
|
|
319
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
320
|
+
const result = setupCursor();
|
|
321
|
+
expect(result).toBe(true);
|
|
322
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
323
|
+
const written = JSON.parse(writeCall[1]);
|
|
324
|
+
expect(written.mcpServers.HubSpotDev.command).toBe('hs');
|
|
325
|
+
});
|
|
326
|
+
it('should initialize mcpServers when missing from existing config', () => {
|
|
327
|
+
mockedExistsSync.mockReturnValue(true);
|
|
328
|
+
mockedFs.readFileSync.mockReturnValue(JSON.stringify({ someOtherKey: true }));
|
|
329
|
+
const result = setupCursor(mockMcpCommand);
|
|
330
|
+
expect(result).toBe(true);
|
|
331
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
332
|
+
const written = JSON.parse(writeCall[1]);
|
|
333
|
+
expect(written.mcpServers).toBeDefined();
|
|
334
|
+
expect(written.mcpServers.HubSpotDev).toBeDefined();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe('setupWindsurf', () => {
|
|
338
|
+
const mockedFs = vi.mocked(fs);
|
|
339
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
340
|
+
const mockMcpCommand = {
|
|
341
|
+
command: 'test-command',
|
|
342
|
+
args: ['--arg1'],
|
|
343
|
+
};
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
346
|
+
vi.mocked(path.join).mockImplementation((...parts) => parts.join('/'));
|
|
347
|
+
});
|
|
348
|
+
it('should successfully configure Windsurf', () => {
|
|
349
|
+
mockedExistsSync.mockReturnValue(true);
|
|
350
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
351
|
+
const result = setupWindsurf(mockMcpCommand);
|
|
352
|
+
expect(result).toBe(true);
|
|
353
|
+
expect(mockedSpinniesManager.add).toHaveBeenCalledWith('spinner', {
|
|
354
|
+
text: commands.mcp.setup.spinners.configuringWindsurf,
|
|
355
|
+
});
|
|
356
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('.codeium/windsurf/mcp_config.json'), expect.stringContaining('HubSpotDev'));
|
|
357
|
+
expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('spinner', {
|
|
358
|
+
text: commands.mcp.setup.spinners.configuredWindsurf,
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
it('should create config file when it does not exist', () => {
|
|
362
|
+
mockedExistsSync.mockReturnValue(false);
|
|
363
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
364
|
+
const result = setupWindsurf(mockMcpCommand);
|
|
365
|
+
expect(result).toBe(true);
|
|
366
|
+
expect(mockedFs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('.codeium/windsurf/mcp_config.json'), JSON.stringify({}, null, 2));
|
|
367
|
+
});
|
|
368
|
+
it('should return false on invalid JSON', () => {
|
|
369
|
+
mockedExistsSync.mockReturnValue(true);
|
|
370
|
+
mockedFs.readFileSync.mockReturnValue('{ invalid json');
|
|
371
|
+
const result = setupWindsurf(mockMcpCommand);
|
|
372
|
+
expect(result).toBe(false);
|
|
373
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('spinner', {
|
|
374
|
+
text: commands.mcp.setup.spinners.failedToConfigureWindsurf,
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
it('should use default mcp command when none provided', () => {
|
|
378
|
+
mockedExistsSync.mockReturnValue(true);
|
|
379
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
380
|
+
const result = setupWindsurf();
|
|
381
|
+
expect(result).toBe(true);
|
|
382
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
383
|
+
const written = JSON.parse(writeCall[1]);
|
|
384
|
+
expect(written.mcpServers.HubSpotDev.command).toBe('hs');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe('setupVsCode', () => {
|
|
388
|
+
const mockMcpCommand = {
|
|
389
|
+
command: 'test-command',
|
|
390
|
+
args: ['--arg1'],
|
|
391
|
+
};
|
|
392
|
+
it('should successfully configure VS Code', async () => {
|
|
393
|
+
mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
394
|
+
const result = await setupVsCode(mockMcpCommand);
|
|
395
|
+
expect(result).toBe(true);
|
|
396
|
+
expect(mockedSpinniesManager.add).toHaveBeenCalledWith('vsCode', {
|
|
397
|
+
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
398
|
+
});
|
|
399
|
+
expect(mockedExecAsync).toHaveBeenCalledWith(expect.stringContaining('code --add-mcp'));
|
|
400
|
+
expect(mockedSpinniesManager.succeed).toHaveBeenCalledWith('vsCode', {
|
|
401
|
+
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
it('should use default mcp command when none provided', async () => {
|
|
405
|
+
mockedExecAsync.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
406
|
+
const result = await setupVsCode();
|
|
407
|
+
expect(result).toBe(true);
|
|
408
|
+
expect(mockedExecAsync).toHaveBeenCalledWith(expect.stringContaining('code --add-mcp'));
|
|
409
|
+
});
|
|
410
|
+
it('should return false when code command is not found', async () => {
|
|
411
|
+
mockedExecAsync.mockRejectedValueOnce(new Error('code: command not found'));
|
|
412
|
+
const result = await setupVsCode(mockMcpCommand);
|
|
413
|
+
expect(result).toBe(false);
|
|
414
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('vsCode', {
|
|
415
|
+
text: commands.mcp.setup.spinners.vsCodeNotFound,
|
|
416
|
+
});
|
|
417
|
+
expect(mockedLogError).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
it('should return false and log error on other failures', async () => {
|
|
420
|
+
const error = new Error('Unexpected failure');
|
|
421
|
+
mockedExecAsync.mockRejectedValueOnce(error);
|
|
422
|
+
const result = await setupVsCode(mockMcpCommand);
|
|
423
|
+
expect(result).toBe(false);
|
|
424
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('vsCode', {
|
|
425
|
+
text: commands.mcp.setup.spinners.failedToConfigureVsCode,
|
|
426
|
+
});
|
|
427
|
+
expect(mockedLogError).toHaveBeenCalledWith(error);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
describe('addMcpServerToConfig', () => {
|
|
431
|
+
const mockedPromptUser = vi.mocked(promptUser);
|
|
432
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
433
|
+
const mockedFs = vi.mocked(fs);
|
|
434
|
+
const mockedUiLogger = vi.mocked(uiLogger);
|
|
435
|
+
beforeEach(() => {
|
|
436
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
437
|
+
vi.mocked(path.join).mockImplementation((...parts) => parts.join('/'));
|
|
438
|
+
mockedExistsSync.mockReturnValue(true);
|
|
439
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
440
|
+
});
|
|
441
|
+
it('should use provided targets without prompting', async () => {
|
|
442
|
+
mockedPromptUser.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
443
|
+
mockedExecAsync
|
|
444
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
445
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' });
|
|
446
|
+
const result = await addMcpServerToConfig(['cursor']);
|
|
447
|
+
expect(result).toEqual(['cursor']);
|
|
448
|
+
expect(mockedPromptUser).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'selectedTargets' }));
|
|
449
|
+
});
|
|
450
|
+
it('should prompt for targets when none provided', async () => {
|
|
451
|
+
mockedPromptUser
|
|
452
|
+
.mockResolvedValueOnce({ selectedTargets: ['cursor'] })
|
|
453
|
+
.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
454
|
+
mockedExistsSync.mockReturnValue(true);
|
|
455
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
456
|
+
const result = await addMcpServerToConfig(undefined);
|
|
457
|
+
expect(result).toEqual(['cursor']);
|
|
458
|
+
expect(mockedPromptUser).toHaveBeenCalledWith(expect.objectContaining({ name: 'selectedTargets' }));
|
|
459
|
+
});
|
|
460
|
+
it('should prompt for targets when empty array provided', async () => {
|
|
461
|
+
mockedPromptUser
|
|
462
|
+
.mockResolvedValueOnce({ selectedTargets: ['windsurf'] })
|
|
463
|
+
.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
464
|
+
mockedExistsSync.mockReturnValue(true);
|
|
465
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
466
|
+
const result = await addMcpServerToConfig([]);
|
|
467
|
+
expect(result).toEqual(['windsurf']);
|
|
468
|
+
expect(mockedPromptUser).toHaveBeenCalledWith(expect.objectContaining({ name: 'selectedTargets' }));
|
|
469
|
+
});
|
|
470
|
+
it('should use npx command in standalone mode', async () => {
|
|
471
|
+
mockedPromptUser
|
|
472
|
+
.mockResolvedValueOnce({ useStandaloneMode: true })
|
|
473
|
+
.mockResolvedValueOnce({ cliVersion: '' });
|
|
474
|
+
mockedExistsSync.mockReturnValue(true);
|
|
475
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
476
|
+
const result = await addMcpServerToConfig(['cursor']);
|
|
477
|
+
expect(result).toEqual(['cursor']);
|
|
478
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
479
|
+
const written = JSON.parse(writeCall[1]);
|
|
480
|
+
expect(written.mcpServers.HubSpotDev.command).toBe('npx');
|
|
481
|
+
expect(written.mcpServers.HubSpotDev.env?.HUBSPOT_MCP_STANDALONE).toBe('true');
|
|
482
|
+
});
|
|
483
|
+
it('should pin version in standalone mode when version is provided', async () => {
|
|
484
|
+
mockedPromptUser
|
|
485
|
+
.mockResolvedValueOnce({ useStandaloneMode: true })
|
|
486
|
+
.mockResolvedValueOnce({ cliVersion: '8.0.1' });
|
|
487
|
+
mockedExistsSync.mockReturnValue(true);
|
|
488
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
489
|
+
const result = await addMcpServerToConfig(['cursor']);
|
|
490
|
+
expect(result).toEqual(['cursor']);
|
|
491
|
+
const writeCall = mockedFs.writeFileSync.mock.calls.find(c => c[1].includes('HubSpotDev'));
|
|
492
|
+
const written = JSON.parse(writeCall[1]);
|
|
493
|
+
expect(written.mcpServers.HubSpotDev.args).toContain('@hubspot/cli@8.0.1');
|
|
494
|
+
expect(written.mcpServers.HubSpotDev.env?.HUBSPOT_CLI_VERSION).toBe('8.0.1');
|
|
495
|
+
});
|
|
496
|
+
it('should call success logger after all targets are configured', async () => {
|
|
497
|
+
mockedPromptUser.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
498
|
+
mockedExistsSync.mockReturnValue(true);
|
|
499
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
500
|
+
await addMcpServerToConfig(['cursor', 'windsurf']);
|
|
501
|
+
expect(mockedUiLogger.info).toHaveBeenCalledWith(commands.mcp.setup.success(['cursor', 'windsurf']));
|
|
502
|
+
});
|
|
503
|
+
it('should throw and fail spinner when setup function returns false', async () => {
|
|
504
|
+
mockedPromptUser.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
505
|
+
const error = new Error('Permission denied');
|
|
506
|
+
mockedExistsSync.mockReturnValue(true);
|
|
507
|
+
mockedFs.readFileSync.mockImplementation(() => {
|
|
508
|
+
throw error;
|
|
509
|
+
});
|
|
510
|
+
await expect(addMcpServerToConfig(['cursor'])).rejects.toThrow();
|
|
511
|
+
expect(mockedSpinniesManager.fail).toHaveBeenCalledWith('mcpSetup', {
|
|
512
|
+
text: commands.mcp.setup.spinners.failedToConfigure,
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
it('should configure multiple targets', async () => {
|
|
516
|
+
mockedPromptUser.mockResolvedValueOnce({ useStandaloneMode: false });
|
|
517
|
+
mockedExistsSync.mockReturnValue(true);
|
|
518
|
+
mockedFs.readFileSync.mockReturnValue('{}');
|
|
519
|
+
const result = await addMcpServerToConfig(['cursor', 'windsurf']);
|
|
520
|
+
expect(result).toEqual(['cursor', 'windsurf']);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
194
523
|
});
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const supportedTools: {
|
|
|
5
5
|
interface McpCommand {
|
|
6
6
|
command: string;
|
|
7
7
|
args: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
|
|
10
11
|
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|