@hubspot/cli 7.6.0-beta.10 → 7.6.0-beta.11
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/getStarted.js +63 -2
- package/commands/project/__tests__/devUnifiedFlow.test.js +14 -5
- package/commands/project/dev/index.js +32 -12
- package/commands/project/dev/unifiedFlow.js +4 -6
- package/lang/en.d.ts +19 -8
- package/lang/en.js +19 -7
- package/lib/projects/__tests__/AppDevModeInterface.test.js +1 -0
- package/lib/projects/localDev/AppDevModeInterface.js +22 -1
- package/lib/projects/localDev/helpers/account.js +2 -2
- package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
- package/lib/prompts/promptUtils.d.ts +2 -1
- package/lib/prompts/promptUtils.js +5 -1
- package/mcp-server/server.js +2 -1
- package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
- package/mcp-server/tools/cms/HsListTool.js +58 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
- package/mcp-server/tools/index.d.ts +1 -0
- package/mcp-server/tools/index.js +4 -0
- package/mcp-server/tools/project/DocFetchTool.js +2 -2
- package/mcp-server/tools/project/DocsSearchTool.js +2 -2
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +2 -2
- package/package.json +2 -2
- package/types/Prompts.d.ts +1 -0
package/commands/getStarted.js
CHANGED
|
@@ -4,7 +4,7 @@ import open from 'open';
|
|
|
4
4
|
import { getCwd } from '@hubspot/local-dev-lib/path';
|
|
5
5
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
6
6
|
import { commands } from '../lang/en.js';
|
|
7
|
-
import { trackCommandUsage } from '../lib/usageTracking.js';
|
|
7
|
+
import { trackCommandMetadataUsage, trackCommandUsage, } from '../lib/usageTracking.js';
|
|
8
8
|
import { EXIT_CODES } from '../lib/enums/exitCodes.js';
|
|
9
9
|
import { makeYargsBuilder } from '../lib/yargsUtils.js';
|
|
10
10
|
import { promptUser } from '../lib/prompts/promptUtils.js';
|
|
@@ -27,9 +27,9 @@ export const describe = undefined;
|
|
|
27
27
|
async function handler(args) {
|
|
28
28
|
const { derivedAccountId } = args;
|
|
29
29
|
const env = getEnv(derivedAccountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD;
|
|
30
|
+
await trackCommandUsage('get-started', {}, derivedAccountId);
|
|
30
31
|
// TODO: Put this in constants.ts once we have a defined place for the template before INBOUND
|
|
31
32
|
const templateSource = 'robrown-hubspot/hubspot-project-components-ua-app-objects-beta';
|
|
32
|
-
trackCommandUsage('get-started', {}, derivedAccountId);
|
|
33
33
|
uiInfoSection(commands.getStarted.startTitle, () => {
|
|
34
34
|
uiLogger.log(commands.getStarted.startDescription);
|
|
35
35
|
});
|
|
@@ -51,6 +51,8 @@ async function handler(args) {
|
|
|
51
51
|
default: GET_STARTED_OPTIONS.APP,
|
|
52
52
|
},
|
|
53
53
|
]);
|
|
54
|
+
// Track user's initial choice
|
|
55
|
+
await trackCommandMetadataUsage('get-started', { step: 'select-option', type: selectedOption }, derivedAccountId);
|
|
54
56
|
if (selectedOption === GET_STARTED_OPTIONS.CMS) {
|
|
55
57
|
uiLogger.log(' ');
|
|
56
58
|
uiLogger.log(commands.getStarted.designManager);
|
|
@@ -63,6 +65,11 @@ async function handler(args) {
|
|
|
63
65
|
message: commands.getStarted.openDesignManagerPrompt,
|
|
64
66
|
},
|
|
65
67
|
]);
|
|
68
|
+
// Track Design Manager browser action
|
|
69
|
+
await trackCommandMetadataUsage('get-started', {
|
|
70
|
+
step: 'open-design-manager',
|
|
71
|
+
type: shouldOpen ? 'opened' : 'declined',
|
|
72
|
+
}, derivedAccountId);
|
|
66
73
|
if (shouldOpen) {
|
|
67
74
|
uiLogger.log('');
|
|
68
75
|
openLink(derivedAccountId, 'design-manager');
|
|
@@ -88,6 +95,11 @@ async function handler(args) {
|
|
|
88
95
|
if (existingProjectConfig &&
|
|
89
96
|
existingProjectDir &&
|
|
90
97
|
projectDest.startsWith(existingProjectDir)) {
|
|
98
|
+
// Track nested project error
|
|
99
|
+
await trackCommandMetadataUsage('get-started', {
|
|
100
|
+
successful: false,
|
|
101
|
+
step: 'project-creation',
|
|
102
|
+
}, derivedAccountId);
|
|
91
103
|
uiLogger.log(' ');
|
|
92
104
|
uiLogger.error(commands.project.create.errors.cannotNestProjects(existingProjectDir));
|
|
93
105
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -101,8 +113,16 @@ async function handler(args) {
|
|
|
101
113
|
tag: latestRepoReleaseTag,
|
|
102
114
|
hideLogs: true,
|
|
103
115
|
});
|
|
116
|
+
await trackCommandMetadataUsage('get-started', {
|
|
117
|
+
successful: true,
|
|
118
|
+
step: 'github-clone',
|
|
119
|
+
}, derivedAccountId);
|
|
104
120
|
}
|
|
105
121
|
catch (err) {
|
|
122
|
+
await trackCommandMetadataUsage('get-started', {
|
|
123
|
+
successful: false,
|
|
124
|
+
step: 'github-clone',
|
|
125
|
+
}, derivedAccountId);
|
|
106
126
|
debugError(err);
|
|
107
127
|
uiLogger.log(' ');
|
|
108
128
|
uiLogger.error(commands.project.create.errors.failedToDownloadProject);
|
|
@@ -121,6 +141,11 @@ async function handler(args) {
|
|
|
121
141
|
uiLogger.log(' ');
|
|
122
142
|
uiLogger.log(commands.getStarted.prompts.projectCreated.description);
|
|
123
143
|
uiLogger.log(' ');
|
|
144
|
+
// Track successful project creation
|
|
145
|
+
await trackCommandMetadataUsage('get-started', {
|
|
146
|
+
successful: true,
|
|
147
|
+
step: 'project-creation',
|
|
148
|
+
}, derivedAccountId);
|
|
124
149
|
// 5. Install dependencies
|
|
125
150
|
const installLocations = await getProjectPackageJsonLocations(projectDest);
|
|
126
151
|
try {
|
|
@@ -147,11 +172,21 @@ async function handler(args) {
|
|
|
147
172
|
default: true,
|
|
148
173
|
},
|
|
149
174
|
]);
|
|
175
|
+
// Track upload decision
|
|
176
|
+
await trackCommandMetadataUsage('get-started', {
|
|
177
|
+
step: 'upload-decision',
|
|
178
|
+
type: shouldUpload ? 'upload' : 'skip',
|
|
179
|
+
}, derivedAccountId);
|
|
150
180
|
if (shouldUpload) {
|
|
151
181
|
try {
|
|
152
182
|
// Get the project config for the newly created project
|
|
153
183
|
const { projectConfig: newProjectConfig, projectDir: newProjectDir } = await getProjectConfig(projectDest);
|
|
154
184
|
if (!newProjectConfig || !newProjectDir) {
|
|
185
|
+
// Track config file not found error
|
|
186
|
+
await trackCommandMetadataUsage('get-started', {
|
|
187
|
+
successful: false,
|
|
188
|
+
step: 'config-file-not-found',
|
|
189
|
+
}, derivedAccountId);
|
|
155
190
|
uiLogger.log(' ');
|
|
156
191
|
uiLogger.error(commands.getStarted.errors.configFileNotFound);
|
|
157
192
|
process.exit(EXIT_CODES.ERROR);
|
|
@@ -173,11 +208,22 @@ async function handler(args) {
|
|
|
173
208
|
skipValidation: false,
|
|
174
209
|
});
|
|
175
210
|
if (uploadError) {
|
|
211
|
+
// Track upload failure
|
|
212
|
+
await trackCommandMetadataUsage('get-started', {
|
|
213
|
+
successful: false,
|
|
214
|
+
step: 'upload',
|
|
215
|
+
}, derivedAccountId);
|
|
176
216
|
uiLogger.log(' ');
|
|
177
217
|
uiLogger.error(commands.getStarted.errors.uploadFailed);
|
|
178
218
|
debugError(uploadError);
|
|
179
219
|
}
|
|
180
220
|
else if (result) {
|
|
221
|
+
// Track successful upload completion
|
|
222
|
+
await trackCommandMetadataUsage('get-started', {
|
|
223
|
+
successful: true,
|
|
224
|
+
step: 'upload',
|
|
225
|
+
}, derivedAccountId);
|
|
226
|
+
uiLogger.log(' ');
|
|
181
227
|
uiLogger.success(commands.getStarted.logs.uploadSuccess);
|
|
182
228
|
const { data: { results }, } = await fetchPublicAppsForPortal(derivedAccountId);
|
|
183
229
|
const lastCreatedApp = results.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
@@ -192,6 +238,11 @@ async function handler(args) {
|
|
|
192
238
|
message: commands.getStarted.openInstallUrl,
|
|
193
239
|
},
|
|
194
240
|
]);
|
|
241
|
+
// Track Developer Overview browser action
|
|
242
|
+
await trackCommandMetadataUsage('get-started', {
|
|
243
|
+
step: 'open-distribution-page',
|
|
244
|
+
type: shouldOpenOverview ? 'opened' : 'declined',
|
|
245
|
+
}, derivedAccountId);
|
|
195
246
|
if (shouldOpenOverview) {
|
|
196
247
|
open(getStaticAuthAppInstallUrl({
|
|
197
248
|
targetAccountId: derivedAccountId,
|
|
@@ -207,6 +258,11 @@ async function handler(args) {
|
|
|
207
258
|
}
|
|
208
259
|
}
|
|
209
260
|
catch (err) {
|
|
261
|
+
// Track upload exception
|
|
262
|
+
await trackCommandMetadataUsage('get-started', {
|
|
263
|
+
successful: false,
|
|
264
|
+
step: 'upload',
|
|
265
|
+
}, derivedAccountId);
|
|
210
266
|
uiLogger.log(' ');
|
|
211
267
|
uiLogger.error(commands.getStarted.errors.uploadFailed);
|
|
212
268
|
debugError(err);
|
|
@@ -214,6 +270,11 @@ async function handler(args) {
|
|
|
214
270
|
}
|
|
215
271
|
}
|
|
216
272
|
}
|
|
273
|
+
// Track successful completion of get-started command
|
|
274
|
+
await trackCommandMetadataUsage('get-started', {
|
|
275
|
+
successful: true,
|
|
276
|
+
step: 'command-completed',
|
|
277
|
+
}, derivedAccountId);
|
|
217
278
|
process.exit(EXIT_CODES.SUCCESS);
|
|
218
279
|
}
|
|
219
280
|
function getStartedBuilder(yargs) {
|
|
@@ -380,17 +380,26 @@ describe('unifiedProjectDevFlow', () => {
|
|
|
380
380
|
beforeEach(() => {
|
|
381
381
|
isTestAccountOrSandbox.mockReturnValue(false);
|
|
382
382
|
});
|
|
383
|
-
it('should
|
|
384
|
-
|
|
383
|
+
it('should log info message when default account is a sandbox or test account', async () => {
|
|
384
|
+
isTestAccountOrSandbox.mockReturnValue(true);
|
|
385
|
+
await unifiedProjectDevFlow({
|
|
386
|
+
args: mockArgs,
|
|
387
|
+
targetProjectAccountId: mockTargetProjectAccountId,
|
|
388
|
+
projectConfig: mockProjectConfig,
|
|
389
|
+
projectDir: mockProjectDir,
|
|
390
|
+
});
|
|
391
|
+
expect(uiLogger.log).toHaveBeenCalledWith(commands.project.dev.logs.defaultSandboxOrDevTestTestingAccountExplanation(mockTargetProjectAccountId));
|
|
392
|
+
});
|
|
393
|
+
it('should log info message when testingAccount flag is provided', async () => {
|
|
394
|
+
const providedTestingAccountId = 999;
|
|
385
395
|
await unifiedProjectDevFlow({
|
|
386
396
|
args: mockArgs,
|
|
387
397
|
targetProjectAccountId: mockTargetProjectAccountId,
|
|
398
|
+
providedTargetTestingAccountId: providedTestingAccountId,
|
|
388
399
|
projectConfig: mockProjectConfig,
|
|
389
400
|
projectDir: mockProjectDir,
|
|
390
401
|
});
|
|
391
|
-
expect(
|
|
392
|
-
expect(uiLogger.log).toHaveBeenCalledWith(commands.project.dev.logs.accountTypeInformation);
|
|
393
|
-
expect(uiLogger.log).toHaveBeenCalledWith(commands.project.dev.logs.learnMoreMessage);
|
|
402
|
+
expect(uiLogger.log).toHaveBeenCalledWith(commands.project.dev.logs.testingAccountFlagExplanation(providedTestingAccountId));
|
|
394
403
|
});
|
|
395
404
|
});
|
|
396
405
|
});
|
|
@@ -7,7 +7,7 @@ import { deprecatedProjectDevFlow } from './deprecatedFlow.js';
|
|
|
7
7
|
import { unifiedProjectDevFlow } from './unifiedFlow.js';
|
|
8
8
|
import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
|
|
9
9
|
import { makeYargsBuilder } from '../../../lib/yargsUtils.js';
|
|
10
|
-
import { loadProfile,
|
|
10
|
+
import { loadProfile, exitIfUsingProfiles, } from '../../../lib/projectProfiles.js';
|
|
11
11
|
import { commands } from '../../../lang/en.js';
|
|
12
12
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
13
13
|
const command = 'dev';
|
|
@@ -33,19 +33,37 @@ async function handler(args) {
|
|
|
33
33
|
process.exit(EXIT_CODES.ERROR);
|
|
34
34
|
}
|
|
35
35
|
validateAccountFlags(testingAccount, projectAccount, userProvidedAccount, useV3);
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
uiBetaTag(commands.project.dev.logs.betaMessage);
|
|
37
|
+
if (useV3) {
|
|
38
|
+
uiLogger.log(commands.project.dev.logs.learnMoreMessageV3);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
uiLogger.log(commands.project.dev.logs.learnMoreMessageLegacy);
|
|
42
|
+
}
|
|
43
|
+
let targetProjectAccountId;
|
|
38
44
|
let profile;
|
|
45
|
+
// Using the new --projectAccount flag
|
|
46
|
+
if (projectAccount) {
|
|
47
|
+
targetProjectAccountId = getAccountId(projectAccount);
|
|
48
|
+
if (targetProjectAccountId) {
|
|
49
|
+
uiLogger.log('');
|
|
50
|
+
uiLogger.log(commands.project.dev.logs.projectAccountFlagExplanation(targetProjectAccountId));
|
|
51
|
+
}
|
|
52
|
+
// Using the legacy --account flag
|
|
53
|
+
}
|
|
54
|
+
else if (userProvidedAccount && derivedAccountId) {
|
|
55
|
+
targetProjectAccountId = derivedAccountId;
|
|
56
|
+
}
|
|
39
57
|
if (!targetProjectAccountId && useV3Api(projectConfig.platformVersion)) {
|
|
40
58
|
if (args.profile) {
|
|
41
|
-
logProfileHeader(args.profile);
|
|
42
59
|
profile = loadProfile(projectConfig, projectDir, args.profile);
|
|
43
60
|
if (!profile) {
|
|
44
61
|
uiLine();
|
|
45
62
|
process.exit(EXIT_CODES.ERROR);
|
|
46
63
|
}
|
|
47
64
|
targetProjectAccountId = profile.accountId;
|
|
48
|
-
|
|
65
|
+
uiLogger.log('');
|
|
66
|
+
uiLogger.log(commands.project.dev.logs.profileProjectAccountExplanation(targetProjectAccountId, args.profile));
|
|
49
67
|
}
|
|
50
68
|
else {
|
|
51
69
|
// A profile must be specified if this project has profiles configured
|
|
@@ -55,10 +73,12 @@ async function handler(args) {
|
|
|
55
73
|
if (!targetProjectAccountId) {
|
|
56
74
|
// The user is not using profile or account flags, so we can use the derived accountId
|
|
57
75
|
targetProjectAccountId = derivedAccountId;
|
|
76
|
+
if (useV3) {
|
|
77
|
+
uiLogger.log('');
|
|
78
|
+
uiLogger.log(commands.project.dev.logs.defaultProjectAccountExplanation(targetProjectAccountId));
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
trackCommandUsage('project-dev', {}, targetProjectAccountId);
|
|
60
|
-
uiBetaTag(commands.project.dev.logs.betaMessage);
|
|
61
|
-
uiLogger.log(commands.project.dev.logs.learnMoreLocalDevServer);
|
|
62
82
|
if (useV3Api(projectConfig.platformVersion)) {
|
|
63
83
|
const targetTestingAccountId = (testingAccount && getAccountId(testingAccount)) || undefined;
|
|
64
84
|
await unifiedProjectDevFlow({
|
|
@@ -86,13 +106,13 @@ function projectDevBuilder(yargs) {
|
|
|
86
106
|
description: commands.project.dev.options.profile,
|
|
87
107
|
hidden: true,
|
|
88
108
|
});
|
|
89
|
-
yargs.options('
|
|
109
|
+
yargs.options('testing-account', {
|
|
90
110
|
type: 'string',
|
|
91
111
|
description: commands.project.dev.options.testingAccount,
|
|
92
112
|
hidden: true,
|
|
93
|
-
implies: ['
|
|
113
|
+
implies: ['project-account'],
|
|
94
114
|
});
|
|
95
|
-
yargs.options('
|
|
115
|
+
yargs.options('project-account', {
|
|
96
116
|
type: 'string',
|
|
97
117
|
description: commands.project.dev.options.projectAccount,
|
|
98
118
|
hidden: true,
|
|
@@ -100,8 +120,8 @@ function projectDevBuilder(yargs) {
|
|
|
100
120
|
});
|
|
101
121
|
yargs.example([['$0 project dev', commands.project.dev.examples.default]]);
|
|
102
122
|
yargs.conflicts('profile', 'account');
|
|
103
|
-
yargs.conflicts('profile', '
|
|
104
|
-
yargs.conflicts('profile', '
|
|
123
|
+
yargs.conflicts('profile', 'testing-account');
|
|
124
|
+
yargs.conflicts('profile', 'project-account');
|
|
105
125
|
return yargs;
|
|
106
126
|
}
|
|
107
127
|
export const builder = makeYargsBuilder(projectDevBuilder, command, describe, {
|
|
@@ -16,7 +16,6 @@ import LocalDevProcess from '../../../lib/projects/localDev/LocalDevProcess.js';
|
|
|
16
16
|
import LocalDevWatcher from '../../../lib/projects/localDev/LocalDevWatcher.js';
|
|
17
17
|
import { handleExit, handleKeypress } from '../../../lib/process.js';
|
|
18
18
|
import { isTestAccountOrSandbox, isUnifiedAccount, } from '../../../lib/accountTypes.js';
|
|
19
|
-
import { uiLine } from '../../../lib/ui/index.js';
|
|
20
19
|
import { uiLogger } from '../../../lib/ui/logger.js';
|
|
21
20
|
import { commands } from '../../../lang/en.js';
|
|
22
21
|
import LocalDevWebsocketServer from '../../../lib/projects/localDev/LocalDevWebsocketServer.js';
|
|
@@ -69,13 +68,9 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
|
|
|
69
68
|
!targetTestingAccountId &&
|
|
70
69
|
targetProjectAccountIsTestAccountOrSandbox) {
|
|
71
70
|
targetTestingAccountId = targetProjectAccountId;
|
|
71
|
+
uiLogger.log(commands.project.dev.logs.defaultSandboxOrDevTestTestingAccountExplanation(targetProjectAccountId));
|
|
72
72
|
}
|
|
73
73
|
else if (!targetTestingAccountId) {
|
|
74
|
-
uiLogger.log('');
|
|
75
|
-
uiLine();
|
|
76
|
-
uiLogger.log(commands.project.dev.logs.accountTypeInformation);
|
|
77
|
-
uiLogger.log(commands.project.dev.logs.learnMoreMessage);
|
|
78
|
-
uiLine();
|
|
79
74
|
uiLogger.log('');
|
|
80
75
|
const accountType = await selectAccountTypePrompt(targetProjectAccountConfig);
|
|
81
76
|
if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST) {
|
|
@@ -101,6 +96,9 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
|
|
|
101
96
|
targetTestingAccountId = targetProjectAccountId;
|
|
102
97
|
}
|
|
103
98
|
}
|
|
99
|
+
else {
|
|
100
|
+
uiLogger.log(commands.project.dev.logs.testingAccountFlagExplanation(targetTestingAccountId));
|
|
101
|
+
}
|
|
104
102
|
// Check if project exists in HubSpot
|
|
105
103
|
const { projectExists, project: uploadedProject } = await ensureProjectExists(targetProjectAccountId, projectConfig.name, {
|
|
106
104
|
allowCreate: false,
|
package/lang/en.d.ts
CHANGED
|
@@ -1009,10 +1009,15 @@ Profiles enable you to reference variables in your component configuration files
|
|
|
1009
1009
|
readonly logs: {
|
|
1010
1010
|
readonly betaMessage: "HubSpot projects local development";
|
|
1011
1011
|
readonly placeholderAccountSelection: "Using default account as target account (for now)";
|
|
1012
|
-
readonly learnMoreLocalDevServer: string;
|
|
1013
1012
|
readonly accountTypeInformation: "Testing in a developer test account is strongly recommended, but you can use a sandbox account if your plan allows you to create one.";
|
|
1014
|
-
readonly
|
|
1015
|
-
|
|
1013
|
+
readonly learnMoreMessageV3: `Learn more about ${string} | ${string}`;
|
|
1014
|
+
readonly learnMoreMessageLegacy: string;
|
|
1015
|
+
readonly profileProjectAccountExplanation: (accountId: number, profileName: string) => string;
|
|
1016
|
+
readonly defaultProjectAccountExplanation: (accountId: number) => string;
|
|
1017
|
+
readonly projectAccountFlagExplanation: (accountId: number) => string;
|
|
1018
|
+
readonly accountFlagExplanation: (accountId: number) => string;
|
|
1019
|
+
readonly defaultSandboxOrDevTestTestingAccountExplanation: (accountId: number) => string;
|
|
1020
|
+
readonly testingAccountFlagExplanation: (accountId: number) => string;
|
|
1016
1021
|
};
|
|
1017
1022
|
readonly errors: {
|
|
1018
1023
|
readonly noProjectConfig: "No project detected. Please run this command again from a project directory.";
|
|
@@ -1022,8 +1027,8 @@ Visit our ${string} to learn more.`;
|
|
|
1022
1027
|
readonly noRunnableComponents: `No supported components were found in this project. Run ${string} to see a list of available components and add one to your project.`;
|
|
1023
1028
|
readonly accountNotCombined: `
|
|
1024
1029
|
Local development of unified apps is currently only compatible with accounts that are opted into the unified apps beta. Make sure that this account is opted in or switch accounts using ${string}.`;
|
|
1025
|
-
readonly unsupportedAccountFlagLegacy: "The --
|
|
1026
|
-
readonly unsupportedAccountFlagV3: "The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --
|
|
1030
|
+
readonly unsupportedAccountFlagLegacy: "The --project-account and --testing-account flags are not supported for projects with platform versions earlier than 2025.2.";
|
|
1031
|
+
readonly unsupportedAccountFlagV3: "The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --testing-account and --project-account flags to specify accounts to use for local dev";
|
|
1027
1032
|
};
|
|
1028
1033
|
readonly examples: {
|
|
1029
1034
|
readonly default: "Start local dev for the current project";
|
|
@@ -2557,6 +2562,12 @@ export declare const lib: {
|
|
|
2557
2562
|
readonly autoInstallDeclined: "You must install your app on your target test account to proceed with local development.";
|
|
2558
2563
|
readonly autoInstallSuccess: (appName: string, targetTestAccountId: number) => string;
|
|
2559
2564
|
readonly autoInstallError: (appName: string, targetTestAccountId: number) => string;
|
|
2565
|
+
readonly fetchAppData: {
|
|
2566
|
+
readonly checking: (appName: string) => string;
|
|
2567
|
+
readonly success: (appName: string, accountId: number) => string;
|
|
2568
|
+
readonly notInstalled: (appName: string, accountId: number) => string;
|
|
2569
|
+
readonly activeInstallations: (appName: string, installCount: number) => string;
|
|
2570
|
+
};
|
|
2560
2571
|
};
|
|
2561
2572
|
readonly LocalDevWebsocketServer: {
|
|
2562
2573
|
readonly errors: {
|
|
@@ -2616,11 +2627,11 @@ export declare const lib: {
|
|
|
2616
2627
|
readonly notAuthedError: (parentAccountId: number | string, accountIdentifier: string) => string;
|
|
2617
2628
|
};
|
|
2618
2629
|
readonly selectAccountTypePrompt: {
|
|
2619
|
-
readonly message: "[--account] Choose the type of account to test on";
|
|
2620
|
-
readonly developerTestAccountOption: "Test on a developer test account";
|
|
2630
|
+
readonly message: "[--testing-account] Choose the type of account to test on";
|
|
2631
|
+
readonly developerTestAccountOption: "Test on a developer test account (recommended)";
|
|
2621
2632
|
readonly sandboxAccountOption: "Test on a sandbox account";
|
|
2622
2633
|
readonly sandboxAccountOptionDisabled: "Disabled - requires access to sandbox accounts";
|
|
2623
|
-
readonly productionAccountOption:
|
|
2634
|
+
readonly productionAccountOption: (accountId?: number) => string;
|
|
2624
2635
|
};
|
|
2625
2636
|
readonly confirmDefaultAccountIsTarget: {
|
|
2626
2637
|
readonly configError: `An error occurred while reading the default account from your config. Run ${string} to re-auth this account`;
|
package/lang/en.js
CHANGED
|
@@ -1010,9 +1010,15 @@ export const commands = {
|
|
|
1010
1010
|
logs: {
|
|
1011
1011
|
betaMessage: 'HubSpot projects local development',
|
|
1012
1012
|
placeholderAccountSelection: 'Using default account as target account (for now)',
|
|
1013
|
-
learnMoreLocalDevServer: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
|
|
1014
1013
|
accountTypeInformation: 'Testing in a developer test account is strongly recommended, but you can use a sandbox account if your plan allows you to create one.',
|
|
1015
|
-
|
|
1014
|
+
learnMoreMessageV3: `Learn more about ${uiLink('HubSpot projects local dev', 'https://hubspot.mintlify.io/en-us/developer-tooling/local-development/hubspot-cli/project-commands#start-a-local-development-server')} | ${uiLink('HubSpot account types', 'https://developers.hubspot.com/docs/getting-started/account-types')}`,
|
|
1015
|
+
learnMoreMessageLegacy: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
|
|
1016
|
+
profileProjectAccountExplanation: (accountId, profileName) => `Using account ${uiAccountDescription(accountId)} from profile ${chalk.bold(profileName)} for project upload`,
|
|
1017
|
+
defaultProjectAccountExplanation: (accountId) => `Using default account ${uiAccountDescription(accountId)} for project upload`,
|
|
1018
|
+
projectAccountFlagExplanation: (accountId) => `Using account ${uiAccountDescription(accountId)} provided by the --project-account flag for project upload`,
|
|
1019
|
+
accountFlagExplanation: (accountId) => `Using account ${uiAccountDescription(accountId)} provided by the --account flag for project upload`,
|
|
1020
|
+
defaultSandboxOrDevTestTestingAccountExplanation: (accountId) => `Using default account ${uiAccountDescription(accountId)} for testing`,
|
|
1021
|
+
testingAccountFlagExplanation: (accountId) => `Using account ${uiAccountDescription(accountId)} provided by the --testing-account flag for testing`,
|
|
1016
1022
|
},
|
|
1017
1023
|
errors: {
|
|
1018
1024
|
noProjectConfig: 'No project detected. Please run this command again from a project directory.',
|
|
@@ -1021,8 +1027,8 @@ export const commands = {
|
|
|
1021
1027
|
invalidProjectComponents: 'Projects cannot contain both private and public apps. Move your apps to separate projects before attempting local development.',
|
|
1022
1028
|
noRunnableComponents: `No supported components were found in this project. Run ${uiCommandReference('hs project add')} to see a list of available components and add one to your project.`,
|
|
1023
1029
|
accountNotCombined: `\nLocal development of unified apps is currently only compatible with accounts that are opted into the unified apps beta. Make sure that this account is opted in or switch accounts using ${uiCommandReference('hs account use')}.`,
|
|
1024
|
-
unsupportedAccountFlagLegacy: 'The --
|
|
1025
|
-
unsupportedAccountFlagV3: 'The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --
|
|
1030
|
+
unsupportedAccountFlagLegacy: 'The --project-account and --testing-account flags are not supported for projects with platform versions earlier than 2025.2.',
|
|
1031
|
+
unsupportedAccountFlagV3: 'The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --testing-account and --project-account flags to specify accounts to use for local dev',
|
|
1026
1032
|
},
|
|
1027
1033
|
examples: {
|
|
1028
1034
|
default: 'Start local dev for the current project',
|
|
@@ -2553,6 +2559,12 @@ export const lib = {
|
|
|
2553
2559
|
autoInstallDeclined: 'You must install your app on your target test account to proceed with local development.',
|
|
2554
2560
|
autoInstallSuccess: (appName, targetTestAccountId) => `Successfully installed app ${appName} on account ${uiAccountDescription(targetTestAccountId)}\n`,
|
|
2555
2561
|
autoInstallError: (appName, targetTestAccountId) => `Error installing app ${appName} on account ${uiAccountDescription(targetTestAccountId)}. You may still be able to install your app in your browser.`,
|
|
2562
|
+
fetchAppData: {
|
|
2563
|
+
checking: (appName) => `Checking installations for your app ${appName}...`,
|
|
2564
|
+
success: (appName, accountId) => `Your app ${appName} is installed on account ${uiAccountDescription(accountId, false)}`,
|
|
2565
|
+
notInstalled: (appName, accountId) => `Your app ${appName} is not currently installed on account ${uiAccountDescription(accountId, false)}`,
|
|
2566
|
+
activeInstallations: (appName, installCount) => chalk.bold(`Your app ${appName} is installed in ${chalk.bold(`${installCount} ${installCount === 1 ? 'account' : 'accounts'}`)}`),
|
|
2567
|
+
},
|
|
2556
2568
|
},
|
|
2557
2569
|
LocalDevWebsocketServer: {
|
|
2558
2570
|
errors: {
|
|
@@ -2612,11 +2624,11 @@ export const lib = {
|
|
|
2612
2624
|
notAuthedError: (parentAccountId, accountIdentifier) => `To develop this project locally, run ${uiCommandReference(`hs auth --account=${parentAccountId}`)} to authenticate the App Developer Account ${parentAccountId} associated with ${accountIdentifier}.`,
|
|
2613
2625
|
},
|
|
2614
2626
|
selectAccountTypePrompt: {
|
|
2615
|
-
message: '[--account] Choose the type of account to test on',
|
|
2616
|
-
developerTestAccountOption: 'Test on a developer test account',
|
|
2627
|
+
message: '[--testing-account] Choose the type of account to test on',
|
|
2628
|
+
developerTestAccountOption: 'Test on a developer test account (recommended)',
|
|
2617
2629
|
sandboxAccountOption: 'Test on a sandbox account',
|
|
2618
2630
|
sandboxAccountOptionDisabled: 'Disabled - requires access to sandbox accounts',
|
|
2619
|
-
productionAccountOption: `<${chalk.red('!')} Test on
|
|
2631
|
+
productionAccountOption: (accountId) => `<${chalk.red('!')} Test on your project account: ${uiAccountDescription(accountId, false)} ${chalk.red('!')}>`,
|
|
2620
2632
|
},
|
|
2621
2633
|
confirmDefaultAccountIsTarget: {
|
|
2622
2634
|
configError: `An error occurred while reading the default account from your config. Run ${uiCommandReference('hs auth')} to re-auth this account`,
|
|
@@ -37,6 +37,7 @@ vi.mock('../../ui/logger');
|
|
|
37
37
|
vi.mock('../../errorHandlers/index');
|
|
38
38
|
vi.mock('../localDev/LocalDevState');
|
|
39
39
|
vi.mock('../localDev/LocalDevLogger');
|
|
40
|
+
vi.mock('../../ui/SpinniesManager');
|
|
40
41
|
describe('AppDevModeInterface', () => {
|
|
41
42
|
let appDevModeInterface;
|
|
42
43
|
let mockLocalDevState;
|
|
@@ -14,6 +14,7 @@ import { lib } from '../../../lang/en.js';
|
|
|
14
14
|
import { uiLogger } from '../../ui/logger.js';
|
|
15
15
|
import { getOauthAppInstallUrl, getStaticAuthAppInstallUrl, } from '../../app/urls.js';
|
|
16
16
|
import { isDeveloperTestAccount, isSandbox } from '../../accountTypes.js';
|
|
17
|
+
import SpinniesManager from '../../ui/SpinniesManager.js';
|
|
17
18
|
class AppDevModeInterface {
|
|
18
19
|
localDevState;
|
|
19
20
|
localDevLogger;
|
|
@@ -86,6 +87,9 @@ class AppDevModeInterface {
|
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
async fetchAppData() {
|
|
90
|
+
SpinniesManager.add('fetchAppData', {
|
|
91
|
+
text: lib.AppDevModeInterface.fetchAppData.checking(this.appNode?.config.name || ''),
|
|
92
|
+
});
|
|
89
93
|
const { data: { results: portalApps }, } = await fetchPublicAppsForPortal(this.localDevState.targetProjectAccountId);
|
|
90
94
|
const appData = portalApps.find(({ sourceId }) => sourceId === this.appNode?.uid);
|
|
91
95
|
if (!appData) {
|
|
@@ -105,8 +109,11 @@ class AppDevModeInterface {
|
|
|
105
109
|
if (!this.appData || !this.marketplaceAppInstalls) {
|
|
106
110
|
return;
|
|
107
111
|
}
|
|
112
|
+
SpinniesManager.fail('fetchAppData', {
|
|
113
|
+
text: lib.AppDevModeInterface.fetchAppData.activeInstallations(this.appNode?.config.name || '', this.marketplaceAppInstalls),
|
|
114
|
+
failColor: 'yellow',
|
|
115
|
+
});
|
|
108
116
|
uiLine();
|
|
109
|
-
uiLogger.warn(lib.LocalDevManager.activeInstallWarning.installCount(this.appData.name, this.marketplaceAppInstalls));
|
|
110
117
|
uiLogger.log(lib.LocalDevManager.activeInstallWarning.explanation);
|
|
111
118
|
uiLine();
|
|
112
119
|
const proceed = await confirmPrompt(lib.LocalDevManager.activeInstallWarning.confirmationPrompt, { defaultAnswer: false });
|
|
@@ -177,8 +184,22 @@ class AppDevModeInterface {
|
|
|
177
184
|
}
|
|
178
185
|
const { needsInstall, isReinstall } = await this.checkTestAccountAppInstallation();
|
|
179
186
|
if (needsInstall) {
|
|
187
|
+
if (SpinniesManager.pick('fetchAppData')) {
|
|
188
|
+
SpinniesManager.fail('fetchAppData', {
|
|
189
|
+
text: lib.AppDevModeInterface.fetchAppData.notInstalled(this.appNode.config.name, this.localDevState.targetTestingAccountId),
|
|
190
|
+
failColor: 'white',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
180
193
|
await this.installAppOrOpenInstallUrl(isReinstall || false);
|
|
181
194
|
}
|
|
195
|
+
else {
|
|
196
|
+
if (SpinniesManager.pick('fetchAppData')) {
|
|
197
|
+
SpinniesManager.succeed('fetchAppData', {
|
|
198
|
+
text: lib.AppDevModeInterface.fetchAppData.success(this.appNode.config.name, this.localDevState.targetTestingAccountId),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
uiLogger.log('');
|
|
202
|
+
}
|
|
182
203
|
}
|
|
183
204
|
catch (e) {
|
|
184
205
|
logError(e);
|
|
@@ -206,6 +206,7 @@ export async function hasSandboxes(account) {
|
|
|
206
206
|
// Top level prompt to choose the type of account to test on
|
|
207
207
|
export async function selectAccountTypePrompt(accountConfig) {
|
|
208
208
|
const hasAccessToSandboxes = await hasSandboxes(accountConfig);
|
|
209
|
+
const accountId = getAccountIdentifier(accountConfig);
|
|
209
210
|
const result = await listPrompt(lib.localDevHelpers.account.selectAccountTypePrompt.message, {
|
|
210
211
|
choices: [
|
|
211
212
|
{
|
|
@@ -223,8 +224,7 @@ export async function selectAccountTypePrompt(accountConfig) {
|
|
|
223
224
|
: false,
|
|
224
225
|
},
|
|
225
226
|
{
|
|
226
|
-
name: lib.localDevHelpers.account.selectAccountTypePrompt
|
|
227
|
-
.productionAccountOption,
|
|
227
|
+
name: lib.localDevHelpers.account.selectAccountTypePrompt.productionAccountOption(accountId),
|
|
228
228
|
value: null,
|
|
229
229
|
},
|
|
230
230
|
],
|
|
@@ -5,11 +5,12 @@ export declare function promptUser<T extends GenericPromptResponse>(config: Prom
|
|
|
5
5
|
export declare function confirmPrompt(message: string, options?: {
|
|
6
6
|
defaultAnswer?: boolean;
|
|
7
7
|
}): Promise<boolean>;
|
|
8
|
-
export declare function listPrompt<T = string>(message: string, { choices, when, defaultAnswer, validate, }: {
|
|
8
|
+
export declare function listPrompt<T = string>(message: string, { choices, when, defaultAnswer, validate, loop, }: {
|
|
9
9
|
choices: PromptChoices<T>;
|
|
10
10
|
when?: PromptWhen;
|
|
11
11
|
defaultAnswer?: string | number | boolean;
|
|
12
12
|
validate?: (input: T[]) => (boolean | string) | Promise<boolean | string>;
|
|
13
|
+
loop?: boolean;
|
|
13
14
|
}): Promise<T>;
|
|
14
15
|
export declare function inputPrompt(message: string, { when, validate, defaultAnswer, }?: {
|
|
15
16
|
when?: boolean | (() => boolean);
|
|
@@ -102,6 +102,7 @@ function handleRawListPrompt(config) {
|
|
|
102
102
|
choices: choices,
|
|
103
103
|
pageSize: config.pageSize,
|
|
104
104
|
default: config.default,
|
|
105
|
+
loop: config.loop,
|
|
105
106
|
}).then(resp => ({ [config.name]: resp }));
|
|
106
107
|
}
|
|
107
108
|
function handleNumberPrompt(config) {
|
|
@@ -125,6 +126,7 @@ function handleCheckboxPrompt(config) {
|
|
|
125
126
|
choices: choices,
|
|
126
127
|
pageSize: config.pageSize,
|
|
127
128
|
validate: config.validate,
|
|
129
|
+
loop: config.loop,
|
|
128
130
|
}).then(resp => ({ [config.name]: resp }));
|
|
129
131
|
}
|
|
130
132
|
function handleConfirmPrompt(config) {
|
|
@@ -147,6 +149,7 @@ function handleSelectPrompt(config) {
|
|
|
147
149
|
choices: choices,
|
|
148
150
|
default: config.default,
|
|
149
151
|
pageSize: config.pageSize,
|
|
152
|
+
loop: config.loop,
|
|
150
153
|
}).then(resp => ({ [config.name]: resp }));
|
|
151
154
|
}
|
|
152
155
|
export async function confirmPrompt(message, options = {}) {
|
|
@@ -157,7 +160,7 @@ export async function confirmPrompt(message, options = {}) {
|
|
|
157
160
|
});
|
|
158
161
|
return choice;
|
|
159
162
|
}
|
|
160
|
-
export async function listPrompt(message, { choices, when, defaultAnswer, validate, }) {
|
|
163
|
+
export async function listPrompt(message, { choices, when, defaultAnswer, validate, loop, }) {
|
|
161
164
|
const { choice } = await promptUser({
|
|
162
165
|
name: 'choice',
|
|
163
166
|
type: 'list',
|
|
@@ -166,6 +169,7 @@ export async function listPrompt(message, { choices, when, defaultAnswer, valida
|
|
|
166
169
|
when,
|
|
167
170
|
default: defaultAnswer,
|
|
168
171
|
validate,
|
|
172
|
+
loop,
|
|
169
173
|
});
|
|
170
174
|
return choice;
|
|
171
175
|
}
|
package/mcp-server/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { registerProjectTools } from './tools/index.js';
|
|
3
|
+
import { registerProjectTools, registerCmsTools } from './tools/index.js';
|
|
4
4
|
const server = new McpServer({
|
|
5
5
|
name: 'HubSpot CLI MCP Server',
|
|
6
6
|
version: '0.0.1',
|
|
@@ -11,6 +11,7 @@ const server = new McpServer({
|
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
13
|
registerProjectTools(server);
|
|
14
|
+
registerCmsTools(server);
|
|
14
15
|
// Start receiving messages on stdin and sending messages on stdout
|
|
15
16
|
const transport = new StdioServerTransport();
|
|
16
17
|
server.connect(transport);
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
path: z.ZodOptional<z.ZodString>;
|
|
7
|
+
account: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
absoluteCurrentWorkingDirectory: string;
|
|
10
|
+
account?: string | undefined;
|
|
11
|
+
path?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
absoluteCurrentWorkingDirectory: string;
|
|
14
|
+
account?: string | undefined;
|
|
15
|
+
path?: string | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export type HsListInputSchema = z.infer<typeof inputSchemaZodObject>;
|
|
18
|
+
export declare class HsListTool extends Tool<HsListInputSchema> {
|
|
19
|
+
constructor(mcpServer: McpServer);
|
|
20
|
+
handler({ path, account, absoluteCurrentWorkingDirectory, }: HsListInputSchema): Promise<TextContentResponse>;
|
|
21
|
+
register(): RegisteredTool;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Tool } from '../../types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { addFlag } from '../../utils/command.js';
|
|
4
|
+
import { absoluteCurrentWorkingDirectory } from '../project/constants.js';
|
|
5
|
+
import { runCommandInDir } from '../../utils/project.js';
|
|
6
|
+
import { formatTextContents } from '../../utils/content.js';
|
|
7
|
+
import { trackToolUsage } from '../../utils/toolUsageTracking.js';
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
absoluteCurrentWorkingDirectory,
|
|
10
|
+
path: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The remote directory path in the HubSpot CMS to list contents. If not specified, lists the root directory.')
|
|
13
|
+
.optional(),
|
|
14
|
+
account: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('The HubSpot account id or name from the HubSpot config file to use for the operation.')
|
|
17
|
+
.optional(),
|
|
18
|
+
};
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
+
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
21
|
+
const toolName = 'list-hubspot-cms-remote-contents';
|
|
22
|
+
export class HsListTool extends Tool {
|
|
23
|
+
constructor(mcpServer) {
|
|
24
|
+
super(mcpServer);
|
|
25
|
+
}
|
|
26
|
+
async handler({ path, account, absoluteCurrentWorkingDirectory, }) {
|
|
27
|
+
await trackToolUsage(toolName);
|
|
28
|
+
let command = 'hs list';
|
|
29
|
+
if (path) {
|
|
30
|
+
command += ` ${path}`;
|
|
31
|
+
}
|
|
32
|
+
if (account) {
|
|
33
|
+
command = addFlag(command, 'account', account);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { stdout, stderr } = await runCommandInDir(absoluteCurrentWorkingDirectory, command);
|
|
37
|
+
return formatTextContents(stdout, stderr);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Error executing hs list command: ${errorMessage}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
register() {
|
|
52
|
+
return this.mcpServer.registerTool(toolName, {
|
|
53
|
+
title: 'List HubSpot CMS Directory Contents',
|
|
54
|
+
description: 'List remote contents of a HubSpot CMS directory.',
|
|
55
|
+
inputSchema,
|
|
56
|
+
}, this.handler);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { HsListTool } from '../HsListTool.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('HsListTool', () => {
|
|
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 HsListTool(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('list-hubspot-cms-remote-contents', {
|
|
31
|
+
title: 'List HubSpot CMS Directory Contents',
|
|
32
|
+
description: 'List remote contents of a HubSpot CMS directory.',
|
|
33
|
+
inputSchema: expect.any(Object),
|
|
34
|
+
}, expect.any(Function));
|
|
35
|
+
expect(result).toBe(mockRegisteredTool);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('handler', () => {
|
|
39
|
+
it('should execute hs list command with no parameters', async () => {
|
|
40
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
41
|
+
stdout: 'file1.html\nfile2.js\nfolder/',
|
|
42
|
+
stderr: '',
|
|
43
|
+
});
|
|
44
|
+
const result = await tool.handler({
|
|
45
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
46
|
+
});
|
|
47
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list');
|
|
48
|
+
expect(result.content).toHaveLength(2);
|
|
49
|
+
expect(result.content[0].text).toContain('file1.html\nfile2.js\nfolder/');
|
|
50
|
+
expect(result.content[1].text).toBe('');
|
|
51
|
+
});
|
|
52
|
+
it('should execute hs list command with path parameter', async () => {
|
|
53
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
54
|
+
stdout: 'nested-file.html',
|
|
55
|
+
stderr: '',
|
|
56
|
+
});
|
|
57
|
+
const result = await tool.handler({
|
|
58
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
59
|
+
path: '/my-modules',
|
|
60
|
+
});
|
|
61
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list /my-modules');
|
|
62
|
+
expect(result.content).toHaveLength(2);
|
|
63
|
+
expect(result.content[0].text).toContain('nested-file.html');
|
|
64
|
+
expect(result.content[1].text).toBe('');
|
|
65
|
+
});
|
|
66
|
+
it('should execute hs list command with account parameter', async () => {
|
|
67
|
+
mockAddFlag.mockReturnValue('hs list --account test-account');
|
|
68
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
69
|
+
stdout: 'account-specific-files.html',
|
|
70
|
+
stderr: '',
|
|
71
|
+
});
|
|
72
|
+
const result = await tool.handler({
|
|
73
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
74
|
+
account: 'test-account',
|
|
75
|
+
});
|
|
76
|
+
expect(mockAddFlag).toHaveBeenCalledWith('hs list', 'account', 'test-account');
|
|
77
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list --account test-account');
|
|
78
|
+
expect(result.content).toHaveLength(2);
|
|
79
|
+
expect(result.content[0].text).toContain('account-specific-files.html');
|
|
80
|
+
expect(result.content[1].text).toBe('');
|
|
81
|
+
});
|
|
82
|
+
it('should execute hs list command with both path and account parameters', async () => {
|
|
83
|
+
mockAddFlag.mockReturnValue('hs list /my-path --account test-account');
|
|
84
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
85
|
+
stdout: 'path-and-account-files.html',
|
|
86
|
+
stderr: '',
|
|
87
|
+
});
|
|
88
|
+
const result = await tool.handler({
|
|
89
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
90
|
+
path: '/my-path',
|
|
91
|
+
account: 'test-account',
|
|
92
|
+
});
|
|
93
|
+
expect(mockAddFlag).toHaveBeenCalledWith('hs list /my-path', 'account', 'test-account');
|
|
94
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list /my-path --account test-account');
|
|
95
|
+
expect(result.content).toHaveLength(2);
|
|
96
|
+
expect(result.content[0].text).toContain('path-and-account-files.html');
|
|
97
|
+
expect(result.content[1].text).toBe('');
|
|
98
|
+
});
|
|
99
|
+
it('should handle command execution errors', async () => {
|
|
100
|
+
mockRunCommandInDir.mockRejectedValue(new Error('Command failed'));
|
|
101
|
+
const result = await tool.handler({
|
|
102
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
103
|
+
});
|
|
104
|
+
expect(result.content).toHaveLength(1);
|
|
105
|
+
expect(result.content[0].text).toContain('Error executing hs list command: Command failed');
|
|
106
|
+
});
|
|
107
|
+
it('should handle stderr output', async () => {
|
|
108
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
109
|
+
stdout: 'file1.html',
|
|
110
|
+
stderr: 'Warning: Some warning message',
|
|
111
|
+
});
|
|
112
|
+
const result = await tool.handler({
|
|
113
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
114
|
+
});
|
|
115
|
+
expect(result.content).toHaveLength(2);
|
|
116
|
+
expect(result.content[0].text).toContain('file1.html');
|
|
117
|
+
expect(result.content[1].text).toContain('Warning: Some warning message');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -7,6 +7,7 @@ import { ValidateProjectTool } from './project/ValidateProjectTool.js';
|
|
|
7
7
|
import { GetConfigValuesTool } from './project/GetConfigValuesTool.js';
|
|
8
8
|
import { DocsSearchTool } from './project/DocsSearchTool.js';
|
|
9
9
|
import { DocFetchTool } from './project/DocFetchTool.js';
|
|
10
|
+
import { HsListTool } from './cms/HsListTool.js';
|
|
10
11
|
export function registerProjectTools(mcpServer) {
|
|
11
12
|
return [
|
|
12
13
|
new UploadProjectTools(mcpServer).register(),
|
|
@@ -20,3 +21,6 @@ export function registerProjectTools(mcpServer) {
|
|
|
20
21
|
new DocFetchTool(mcpServer).register(),
|
|
21
22
|
];
|
|
22
23
|
}
|
|
24
|
+
export function registerCmsTools(mcpServer) {
|
|
25
|
+
return [new HsListTool(mcpServer).register()];
|
|
26
|
+
}
|
|
@@ -41,8 +41,8 @@ export class DocFetchTool extends Tool {
|
|
|
41
41
|
}
|
|
42
42
|
register() {
|
|
43
43
|
return this.mcpServer.registerTool(toolName, {
|
|
44
|
-
title: 'Fetch
|
|
45
|
-
description: '
|
|
44
|
+
title: 'Fetch HubSpot Developer Documentation (single file)',
|
|
45
|
+
description: 'Always use this immediately after `search-hubspot-docs` and before creating a plan, writing code, or answering technical questions. This tool retrieves the full, authoritative content of a HubSpot Developer Documentation page from its URL, ensuring responses are accurate, up-to-date, and grounded in the official docs.',
|
|
46
46
|
inputSchema,
|
|
47
47
|
}, this.handler);
|
|
48
48
|
}
|
|
@@ -54,8 +54,8 @@ export class DocsSearchTool extends Tool {
|
|
|
54
54
|
}
|
|
55
55
|
register() {
|
|
56
56
|
return this.mcpServer.registerTool(toolName, {
|
|
57
|
-
title: 'Search
|
|
58
|
-
description: '
|
|
57
|
+
title: 'Search HubSpot Developer Documentation',
|
|
58
|
+
description: 'Use this first whenever you need details about HubSpot APIs, SDKs, integrations, or developer platform features. This searches the official HubSpot Developer Documentation and returns the most relevant pages, each with a URL for use in `fetch-hubspot-doc`. Always follow this with a fetch to get the full, authoritative content before making plans or writing answers.',
|
|
59
59
|
inputSchema,
|
|
60
60
|
}, this.handler);
|
|
61
61
|
}
|
|
@@ -25,8 +25,8 @@ describe('mcp-server/tools/project/DocFetchTool', () => {
|
|
|
25
25
|
it('should register tool with correct parameters', () => {
|
|
26
26
|
const result = tool.register();
|
|
27
27
|
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('fetch-hubspot-doc', {
|
|
28
|
-
title: 'Fetch
|
|
29
|
-
description: '
|
|
28
|
+
title: 'Fetch HubSpot Developer Documentation (single file)',
|
|
29
|
+
description: 'Always use this immediately after `search-hubspot-docs` and before creating a plan, writing code, or answering technical questions. This tool retrieves the full, authoritative content of a HubSpot Developer Documentation page from its URL, ensuring responses are accurate, up-to-date, and grounded in the official docs.',
|
|
30
30
|
inputSchema: expect.any(Object),
|
|
31
31
|
}, tool.handler);
|
|
32
32
|
expect(result).toBe(mockRegisteredTool);
|
|
@@ -28,8 +28,8 @@ describe('mcp-server/tools/project/DocsSearchTool', () => {
|
|
|
28
28
|
it('should register tool with correct parameters', () => {
|
|
29
29
|
const result = tool.register();
|
|
30
30
|
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('search-hubspot-docs', {
|
|
31
|
-
title: 'Search
|
|
32
|
-
description: '
|
|
31
|
+
title: 'Search HubSpot Developer Documentation',
|
|
32
|
+
description: 'Use this first whenever you need details about HubSpot APIs, SDKs, integrations, or developer platform features. This searches the official HubSpot Developer Documentation and returns the most relevant pages, each with a URL for use in `fetch-hubspot-doc`. Always follow this with a fetch to get the full, authoritative content before making plans or writing answers.',
|
|
33
33
|
inputSchema: expect.any(Object),
|
|
34
34
|
}, tool.handler);
|
|
35
35
|
expect(result).toBe(mockRegisteredTool);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "7.6.0-beta.
|
|
3
|
+
"version": "7.6.0-beta.11",
|
|
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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"@hubspot/project-parsing-lib": "0.8.0",
|
|
11
11
|
"@hubspot/serverless-dev-runtime": "7.0.6",
|
|
12
12
|
"@hubspot/theme-preview-dev-server": "0.0.10",
|
|
13
|
-
"@hubspot/ui-extensions-dev-server": "0.9.
|
|
13
|
+
"@hubspot/ui-extensions-dev-server": "0.9.8",
|
|
14
14
|
"archiver": "7.0.1",
|
|
15
15
|
"boxen": "8.0.1",
|
|
16
16
|
"chalk": "5.4.1",
|