@hubspot/cli 8.0.10-experimental.1 → 8.0.10-experimental.2
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 +64 -11
- 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 +2 -6
- package/lang/en.js +2 -6
- package/lib/__tests__/serverlessLogs.test.js +71 -65
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/generateSelectors.js +1 -2
- 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/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 +1 -1
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
- package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -1
- package/mcp-server/tools/project/CreateProjectTool.js +1 -1
- package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
- package/mcp-server/tools/project/DeployProjectTool.js +1 -1
- 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__/UploadProjectTools.test.js +10 -2
- package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
- package/mcp-server/tools/project/constants.d.ts +1 -1
- 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 -4
- package/lib/cms/devServerProcess.d.ts +0 -13
- package/lib/cms/devServerProcess.js +0 -200
- 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
|
@@ -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>;
|
package/lib/mcp/setup.js
CHANGED
|
@@ -47,23 +47,56 @@ export async function addMcpServerToConfig(targets) {
|
|
|
47
47
|
else {
|
|
48
48
|
derivedTargets = targets;
|
|
49
49
|
}
|
|
50
|
+
// Prompt for standalone mode
|
|
51
|
+
const { useStandaloneMode } = await promptUser({
|
|
52
|
+
name: 'useStandaloneMode',
|
|
53
|
+
type: 'confirm',
|
|
54
|
+
message: commands.mcp.setup.prompts.standaloneMode,
|
|
55
|
+
default: false,
|
|
56
|
+
});
|
|
57
|
+
const { cliVersion } = useStandaloneMode
|
|
58
|
+
? await promptUser({
|
|
59
|
+
name: 'cliVersion',
|
|
60
|
+
type: 'input',
|
|
61
|
+
message: commands.mcp.setup.prompts.cliVersion,
|
|
62
|
+
validate: (v) => !v || /^[\d]+\.[\d]+\.[\d]+([-+][\w.]+)?$/.test(v.trim())
|
|
63
|
+
? true
|
|
64
|
+
: 'Please enter a valid semver version (e.g. 8.0.1) or leave blank for latest.',
|
|
65
|
+
})
|
|
66
|
+
: { cliVersion: '' };
|
|
67
|
+
const cliPackage = cliVersion
|
|
68
|
+
? `@hubspot/cli@${cliVersion}`
|
|
69
|
+
: '@hubspot/cli';
|
|
70
|
+
const standaloneEnv = {
|
|
71
|
+
HUBSPOT_MCP_STANDALONE: 'true',
|
|
72
|
+
};
|
|
73
|
+
if (cliVersion) {
|
|
74
|
+
standaloneEnv.HUBSPOT_CLI_VERSION = cliVersion;
|
|
75
|
+
}
|
|
76
|
+
const mcpCommand = useStandaloneMode
|
|
77
|
+
? {
|
|
78
|
+
command: 'npx',
|
|
79
|
+
args: ['-y', '-p', cliPackage, 'hs', 'mcp', 'start'],
|
|
80
|
+
env: standaloneEnv,
|
|
81
|
+
}
|
|
82
|
+
: defaultMcpCommand;
|
|
50
83
|
if (derivedTargets.includes(claudeCode)) {
|
|
51
|
-
await runSetupFunction(setupClaudeCode);
|
|
84
|
+
await runSetupFunction(() => setupClaudeCode(mcpCommand));
|
|
52
85
|
}
|
|
53
86
|
if (derivedTargets.includes(cursor)) {
|
|
54
|
-
await runSetupFunction(setupCursor);
|
|
87
|
+
await runSetupFunction(() => setupCursor(mcpCommand));
|
|
55
88
|
}
|
|
56
89
|
if (derivedTargets.includes(windsurf)) {
|
|
57
|
-
await runSetupFunction(setupWindsurf);
|
|
90
|
+
await runSetupFunction(() => setupWindsurf(mcpCommand));
|
|
58
91
|
}
|
|
59
92
|
if (derivedTargets.includes(vscode)) {
|
|
60
|
-
await runSetupFunction(setupVsCode);
|
|
93
|
+
await runSetupFunction(() => setupVsCode(mcpCommand));
|
|
61
94
|
}
|
|
62
95
|
if (derivedTargets.includes(codex)) {
|
|
63
|
-
await runSetupFunction(setupCodex);
|
|
96
|
+
await runSetupFunction(() => setupCodex(mcpCommand));
|
|
64
97
|
}
|
|
65
98
|
if (derivedTargets.includes(gemini)) {
|
|
66
|
-
await runSetupFunction(setupGemini);
|
|
99
|
+
await runSetupFunction(() => setupGemini(mcpCommand));
|
|
67
100
|
}
|
|
68
101
|
uiLogger.info(commands.mcp.setup.success(derivedTargets));
|
|
69
102
|
return derivedTargets;
|
|
@@ -121,10 +154,7 @@ function setupMcpConfigFile(config) {
|
|
|
121
154
|
if (!mcpConfig.mcpServers) {
|
|
122
155
|
mcpConfig.mcpServers = {};
|
|
123
156
|
}
|
|
124
|
-
|
|
125
|
-
mcpConfig.mcpServers[mcpServerName] = {
|
|
126
|
-
...config.mcpCommand,
|
|
127
|
-
};
|
|
157
|
+
mcpConfig.mcpServers[mcpServerName] = config.mcpCommand;
|
|
128
158
|
// Write the updated config
|
|
129
159
|
fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
|
|
130
160
|
SpinniesManager.succeed('spinner', {
|
|
@@ -145,10 +175,12 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
|
145
175
|
SpinniesManager.add('vsCode', {
|
|
146
176
|
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
147
177
|
});
|
|
148
|
-
const
|
|
178
|
+
const commandWithAgent = buildCommandWithAgentString(mcpCommand, vscode);
|
|
179
|
+
const configObject = {
|
|
149
180
|
name: mcpServerName,
|
|
150
|
-
...
|
|
151
|
-
}
|
|
181
|
+
...commandWithAgent,
|
|
182
|
+
};
|
|
183
|
+
const mcpConfig = JSON.stringify(configObject);
|
|
152
184
|
await execAsync(`code --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
153
185
|
SpinniesManager.succeed('vsCode', {
|
|
154
186
|
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
@@ -172,25 +204,27 @@ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
207
|
+
SpinniesManager.add('claudeCode', {
|
|
208
|
+
text: commands.mcp.setup.spinners.configuringClaudeCode,
|
|
209
|
+
});
|
|
175
210
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
211
|
+
// Check if claude command is available
|
|
212
|
+
await execAsync('claude --version');
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
SpinniesManager.fail('claudeCode', {
|
|
216
|
+
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
178
217
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
SpinniesManager.fail('claudeCode', {
|
|
185
|
-
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
186
|
-
});
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
189
221
|
// Run claude mcp add command
|
|
190
|
-
const
|
|
222
|
+
const commandWithAgent = buildCommandWithAgentString(mcpCommand, claudeCode);
|
|
223
|
+
const configObject = {
|
|
191
224
|
type: 'stdio',
|
|
192
|
-
...
|
|
193
|
-
}
|
|
225
|
+
...commandWithAgent,
|
|
226
|
+
};
|
|
227
|
+
const mcpConfig = JSON.stringify(configObject);
|
|
194
228
|
const { stdout } = await execAsync('claude mcp list');
|
|
195
229
|
if (stdout.includes(mcpServerName)) {
|
|
196
230
|
SpinniesManager.update('claudeCode', {
|
|
@@ -248,7 +282,7 @@ export async function setupCodex(mcpCommand = defaultMcpCommand) {
|
|
|
248
282
|
return false;
|
|
249
283
|
}
|
|
250
284
|
const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, codex);
|
|
251
|
-
await execAsync(`codex mcp add "${mcpServerName}" -- ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
285
|
+
await execAsync(`codex mcp add "${mcpServerName}"${buildEnvFlagString(mcpCommand)} -- ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
252
286
|
SpinniesManager.succeed('codexSpinner', {
|
|
253
287
|
text: commands.mcp.setup.spinners.configuredCodex,
|
|
254
288
|
});
|
|
@@ -277,7 +311,7 @@ export async function setupGemini(mcpCommand = defaultMcpCommand) {
|
|
|
277
311
|
return false;
|
|
278
312
|
}
|
|
279
313
|
const mcpCommandWithAgent = buildCommandWithAgentString(mcpCommand, gemini);
|
|
280
|
-
await execAsync(`gemini mcp add -s user "${mcpServerName}" ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
314
|
+
await execAsync(`gemini mcp add -s user${buildEnvFlagString(mcpCommand)} "${mcpServerName}" ${mcpCommandWithAgent.command} ${mcpCommandWithAgent.args.join(' ')}`);
|
|
281
315
|
SpinniesManager.succeed('geminiSpinner', {
|
|
282
316
|
text: commands.mcp.setup.spinners.configuredGemini,
|
|
283
317
|
});
|
|
@@ -296,3 +330,16 @@ function buildCommandWithAgentString(mcpCommand, agent) {
|
|
|
296
330
|
mcpCommandCopy.args.push('--ai-agent', agent);
|
|
297
331
|
return mcpCommandCopy;
|
|
298
332
|
}
|
|
333
|
+
function buildEnvFlagString(mcpCommand) {
|
|
334
|
+
const envFlags = [];
|
|
335
|
+
if (mcpCommand.env) {
|
|
336
|
+
const env = Object.entries(mcpCommand.env);
|
|
337
|
+
env.forEach(([key, value]) => {
|
|
338
|
+
envFlags.push(`--env ${key}="${value}"`);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (envFlags.length === 0) {
|
|
342
|
+
return '';
|
|
343
|
+
}
|
|
344
|
+
return ` ${envFlags.join(' ')}`;
|
|
345
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { uiLogger } from '../../../ui/logger.js';
|
|
2
1
|
import * as github from '@hubspot/local-dev-lib/api/github';
|
|
3
|
-
import { EXIT_CODES } from '../../../enums/exitCodes.js';
|
|
4
2
|
import { getProjectComponentListFromRepo, getProjectTemplateListFromRepo, } from '../legacy.js';
|
|
5
3
|
import { PROJECT_COMPONENT_TYPES, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, } from '../../../constants.js';
|
|
6
4
|
vi.mock('@hubspot/local-dev-lib/api/github');
|
|
@@ -38,16 +36,6 @@ describe('lib/projects/create/legacy', () => {
|
|
|
38
36
|
});
|
|
39
37
|
});
|
|
40
38
|
describe('getProjectTemplateListFromRepo()', () => {
|
|
41
|
-
let exitMock;
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
// @ts-expect-error - Mocking process.exit
|
|
44
|
-
exitMock = vi
|
|
45
|
-
.spyOn(process, 'exit')
|
|
46
|
-
.mockImplementation(() => undefined);
|
|
47
|
-
});
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
exitMock.mockRestore();
|
|
50
|
-
});
|
|
51
39
|
it('returns a list of project templates', async () => {
|
|
52
40
|
// @ts-expect-error - Mocking AxiosResponse
|
|
53
41
|
mockedFetchRepoFile.mockResolvedValue({
|
|
@@ -56,20 +44,16 @@ describe('lib/projects/create/legacy', () => {
|
|
|
56
44
|
const templates = await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
57
45
|
expect(templates).toEqual(repoConfig[PROJECT_COMPONENT_TYPES.PROJECTS]);
|
|
58
46
|
});
|
|
59
|
-
it('
|
|
47
|
+
it('throws an error if the request for the template list fails', async () => {
|
|
60
48
|
mockedFetchRepoFile.mockRejectedValue(new Error('Not found'));
|
|
61
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
62
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to fetch the config.json file from the target repository/));
|
|
63
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
49
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Failed to fetch the config.json file from the target repository');
|
|
64
50
|
});
|
|
65
|
-
it('
|
|
51
|
+
it('throws an error if there are no projects listed in the repo config', async () => {
|
|
66
52
|
// @ts-expect-error - Mocking AxiosResponse
|
|
67
53
|
mockedFetchRepoFile.mockResolvedValue({});
|
|
68
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
69
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Unable to find any projects in the target repository's config.json file/));
|
|
70
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
54
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Unable to find any projects in the target repository');
|
|
71
55
|
});
|
|
72
|
-
it('
|
|
56
|
+
it('throws an error if any of the projects in the repo config are missing required properties', async () => {
|
|
73
57
|
// @ts-expect-error - Mocking AxiosResponse
|
|
74
58
|
mockedFetchRepoFile.mockResolvedValue({
|
|
75
59
|
data: {
|
|
@@ -82,9 +66,7 @@ describe('lib/projects/create/legacy', () => {
|
|
|
82
66
|
],
|
|
83
67
|
},
|
|
84
68
|
});
|
|
85
|
-
await getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref');
|
|
86
|
-
expect(uiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Found misconfigured projects in the target repository's config.json file/));
|
|
87
|
-
expect(exitMock).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
69
|
+
await expect(getProjectTemplateListFromRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, 'gh-ref')).rejects.toThrow('Found misconfigured projects in the target repository');
|
|
88
70
|
});
|
|
89
71
|
});
|
|
90
72
|
});
|
|
@@ -4,9 +4,7 @@ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH
|
|
|
4
4
|
import { isV2Project } from '../platformVersion.js';
|
|
5
5
|
import { v2ComponentFlow } from './v2.js';
|
|
6
6
|
import { getProjectTemplateListFromRepo } from './legacy.js';
|
|
7
|
-
import { uiLogger } from '../../ui/logger.js';
|
|
8
7
|
import { commands } from '../../../lang/en.js';
|
|
9
|
-
import { EXIT_CODES } from '../../enums/exitCodes.js';
|
|
10
8
|
export async function handleProjectCreationFlow(args) {
|
|
11
9
|
const { platformVersion, templateSource, projectBase, auth: providedAuth, distribution: providedDistribution, } = args;
|
|
12
10
|
const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
|
|
@@ -25,8 +23,7 @@ export async function handleProjectCreationFlow(args) {
|
|
|
25
23
|
}
|
|
26
24
|
const projectTemplates = await getProjectTemplateListFromRepo(repo, DEFAULT_PROJECT_TEMPLATE_BRANCH);
|
|
27
25
|
if (!projectTemplates.length) {
|
|
28
|
-
|
|
29
|
-
process.exit(EXIT_CODES.ERROR);
|
|
26
|
+
throw new Error(commands.project.create.errors.failedToFetchProjectList);
|
|
30
27
|
}
|
|
31
28
|
const selectProjectTemplatePromptResponse = await selectProjectTemplatePrompt(args, projectTemplates);
|
|
32
29
|
return {
|