@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.
@@ -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 display account type information when prompting', async () => {
384
- selectAccountTypePrompt.mockResolvedValue(HUBSPOT_ACCOUNT_TYPES.STANDARD);
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(uiLine).toHaveBeenCalled();
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, logProfileFooter, logProfileHeader, exitIfUsingProfiles, } from '../../../lib/projectProfiles.js';
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
- let targetProjectAccountId = (projectAccount && getAccountId(projectAccount)) ||
37
- (userProvidedAccount && derivedAccountId);
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
- logProfileFooter(profile);
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('testingAccount', {
109
+ yargs.options('testing-account', {
90
110
  type: 'string',
91
111
  description: commands.project.dev.options.testingAccount,
92
112
  hidden: true,
93
- implies: ['projectAccount'],
113
+ implies: ['project-account'],
94
114
  });
95
- yargs.options('projectAccount', {
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', 'testingAccount');
104
- yargs.conflicts('profile', 'projectAccount');
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 learnMoreMessage: `
1015
- Visit our ${string} to learn more.`;
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 --projectAccount and --testingAccount flags are not supported for projects with platform versions earlier than 2025.2.";
1026
- readonly unsupportedAccountFlagV3: "The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --testingAccount and --projectAccount flags to specify accounts to use for local dev";
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: `<${string} Test on this account ${string}>`;
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
- learnMoreMessage: `\nVisit our ${uiLink('docs on Developer Test and Sandbox accounts', 'https://developers.hubspot.com/docs/getting-started/account-types')} to learn more.`,
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 --projectAccount and --testingAccount flags are not supported for projects with platform versions earlier than 2025.2.',
1025
- unsupportedAccountFlagV3: 'The --account flag is is not supported supported for projects with platform versions 2025.2 and newer. Use --testingAccount and --projectAccount flags to specify accounts to use for local dev',
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 this account ${chalk.red('!')}>`,
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
  ],
@@ -151,6 +151,7 @@ async function selectTargetAccountPrompt(defaultAccountId, accountType, choices)
151
151
  accountType,
152
152
  }),
153
153
  choices,
154
+ loop: false,
154
155
  },
155
156
  ]);
156
157
  return targetAccountInfo;
@@ -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
  }
@@ -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,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
+ });
@@ -1,2 +1,3 @@
1
1
  import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  export declare function registerProjectTools(mcpServer: McpServer): RegisteredTool[];
3
+ export declare function registerCmsTools(mcpServer: McpServer): RegisteredTool[];
@@ -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 a single HubSpot Developer Documentation file',
45
- description: 'Fetch a single HubSpot Developer Documentation file by URL. Call after search-hubspot-docs.',
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 the HubSpot docs',
58
- description: 'Search the HubSpot docs for information. This will return results that include a url to be used in fetch-hubspot-doc.',
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 a single HubSpot Developer Documentation file',
29
- description: 'Fetch a single HubSpot Developer Documentation file by URL. Call after search-hubspot-docs.',
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 the HubSpot docs',
32
- description: 'Search the HubSpot docs for information. This will return results that include a url to be used in fetch-hubspot-doc.',
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.10",
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.4",
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",
@@ -23,5 +23,6 @@ export type PromptConfig<T extends GenericPromptResponse> = {
23
23
  validate?: (answer?: any) => PromptOperand | Promise<PromptOperand>;
24
24
  mask?: string;
25
25
  filter?: (input: string) => string;
26
+ loop?: boolean;
26
27
  };
27
28
  export {};