@hubspot/cli 7.6.0-beta.14 → 7.6.0-beta.16

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.
@@ -6,8 +6,9 @@ import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
6
6
  import { trackCommandUsage } from '../../lib/usageTracking.js';
7
7
  import { hasFeature } from '../../lib/hasFeature.js';
8
8
  import { FEATURES } from '../../lib/constants.js';
9
+ import { uiBetaTag } from '../../lib/ui/index.js';
9
10
  const command = ['setup'];
10
- const describe = commands.mcp.setup.describe;
11
+ const describe = uiBetaTag(commands.mcp.setup.describe, false);
11
12
  async function handler(args) {
12
13
  const { derivedAccountId } = args;
13
14
  const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
package/commands/mcp.js CHANGED
@@ -9,7 +9,7 @@ function mcpBuilder(yargs) {
9
9
  yargs.command(startCommand).command(setupCommand).demandCommand(1, '');
10
10
  return yargs;
11
11
  }
12
- const builder = makeYargsBuilder(mcpBuilder, command, commands.mcp.describe, {
12
+ const builder = makeYargsBuilder(mcpBuilder, command, describe, {
13
13
  useGlobalOptions: true,
14
14
  });
15
15
  const mcpCommand = {
@@ -9,7 +9,7 @@ import { logError } from '../../../lib/errorHandlers/index.js';
9
9
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
10
10
  import { ensureProjectExists } from '../../../lib/projects/ensureProjectExists.js';
11
11
  import { createInitialBuildForNewProject, createNewProjectForLocalDev, compareLocalProjectToDeployed, } from '../../../lib/projects/localDev/helpers/project.js';
12
- import { useExistingDevTestAccount, createDeveloperTestAccountForLocalDev, selectAccountTypePrompt, } from '../../../lib/projects/localDev/helpers/account.js';
12
+ import { useExistingDevTestAccount, createDeveloperTestAccountForLocalDev, selectAccountTypePrompt, createSandboxForLocalDev, } from '../../../lib/projects/localDev/helpers/account.js';
13
13
  import { selectDeveloperTestTargetAccountPrompt, selectSandboxTargetAccountPrompt, } from '../../../lib/prompts/projectDevTargetAccountPrompt.js';
14
14
  import SpinniesManager from '../../../lib/ui/SpinniesManager.js';
15
15
  import LocalDevProcess from '../../../lib/projects/localDev/LocalDevProcess.js';
@@ -93,6 +93,9 @@ export async function unifiedProjectDevFlow({ args, targetProjectAccountId, prov
93
93
  const sandboxAccountPromptResponse = await selectSandboxTargetAccountPrompt(accounts, targetProjectAccountConfig);
94
94
  targetTestingAccountId =
95
95
  sandboxAccountPromptResponse.targetAccountId || undefined;
96
+ if (sandboxAccountPromptResponse.createNestedAccount) {
97
+ targetTestingAccountId = await createSandboxForLocalDev(targetProjectAccountId, targetProjectAccountConfig, env);
98
+ }
96
99
  }
97
100
  else {
98
101
  targetTestingAccountId = targetProjectAccountId;
@@ -121,7 +121,7 @@ async function handler(args) {
121
121
  const newDefaultAccount = await selectAccountFromConfig();
122
122
  updateDefaultAccount(newDefaultAccount);
123
123
  }
124
- else {
124
+ else if (isDefaultAccount && force) {
125
125
  // If force is specified, skip prompt and set the parent account id as the default account
126
126
  updateDefaultAccount(parentAccountId);
127
127
  }
@@ -5,7 +5,7 @@ import deleteTestAccountCommand from './testAccount/delete.js';
5
5
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
6
6
  import { commands } from '../lang/en.js';
7
7
  const command = ['test-account', 'test-accounts'];
8
- const describe = undefined; //commands.testAccount.describe;
8
+ const describe = commands.testAccount.describe;
9
9
  function testAccountBuilder(yargs) {
10
10
  yargs
11
11
  .command(createTestAccountCommand)
package/lang/en.js CHANGED
@@ -1016,7 +1016,7 @@ export const commands = {
1016
1016
  header: 'HubSpot projects local development',
1017
1017
  placeholderAccountSelection: 'Using default account as target account (for now)',
1018
1018
  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.',
1019
- 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')}`,
1019
+ learnMoreMessageV3: `Learn more about ${uiLink('HubSpot projects local dev', 'https://developers.hubspot.com/docs/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')}`,
1020
1020
  learnMoreMessageLegacy: uiLink('Learn more about the projects local dev server', 'https://developers.hubspot.com/docs/platform/project-cli-commands#start-a-local-development-server'),
1021
1021
  profileProjectAccountExplanation: (accountId, profileName) => `Using account ${uiAccountDescription(accountId)} from profile ${chalk.bold(profileName)} for project upload`,
1022
1022
  defaultProjectAccountExplanation: (accountId) => `Using default account ${uiAccountDescription(accountId)} for project upload`,
@@ -84,6 +84,7 @@ export declare const FEATURES: {
84
84
  readonly APPS_HOME: "UIE:AppHome";
85
85
  readonly MCP_ACCESS: "Developers:CLIMCPAccess";
86
86
  readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
87
+ readonly AGENT_TOOLS: "ThirdPartyAgentTools";
87
88
  };
88
89
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
89
90
  UPLOAD_SUCCESS: string;
package/lib/constants.js CHANGED
@@ -76,6 +76,7 @@ export const FEATURES = {
76
76
  APPS_HOME: 'UIE:AppHome',
77
77
  MCP_ACCESS: 'Developers:CLIMCPAccess',
78
78
  THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
79
+ AGENT_TOOLS: 'ThirdPartyAgentTools',
79
80
  };
80
81
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
81
82
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -99,7 +99,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
99
99
  it('should clean up temp directory even when errors occur', async () => {
100
100
  // Mock downloadProject to throw an error after temp dir is created
101
101
  downloadProject.mockRejectedValue(new Error('Download Error'));
102
- await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Download Error');
102
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
103
+ expect(result).toBe(false);
103
104
  expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
104
105
  });
105
106
  it('should handle translateForLocalDev errors', async () => {
@@ -111,7 +112,8 @@ describe('isDeployedProjectUpToDateWithLocal', () => {
111
112
  extractZipArchive.mockResolvedValue(undefined);
112
113
  // Mock translate to throw an error
113
114
  translate.mockRejectedValue(new Error('Translation Error'));
114
- await expect(isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes)).rejects.toThrow('Translation Error');
115
+ const result = await isDeployedProjectUpToDateWithLocal(mockProjectConfig, mockAccountId, mockBuildId, mockLocalProjectNodes);
116
+ expect(result).toBe(false);
115
117
  expect(fs.remove).toHaveBeenCalledWith(mockTempDir);
116
118
  });
117
119
  });
@@ -1,8 +1,17 @@
1
1
  import { calculateComponentTemplateChoices } from '../v3.js';
2
+ import { hasFeature } from '../../../hasFeature.js';
2
3
  vi.mock('@hubspot/local-dev-lib/logger');
3
4
  vi.mock('@hubspot/local-dev-lib/api/github');
5
+ vi.mock('../../../hasFeature.js');
6
+ const mockHasFeature = vi.mocked(hasFeature);
4
7
  describe('lib/projects/create/v3', () => {
8
+ beforeEach(() => {
9
+ mockHasFeature.mockResolvedValue(true);
10
+ });
5
11
  describe('calculateComponentTemplateChoices()', () => {
12
+ beforeEach(() => {
13
+ mockHasFeature.mockClear();
14
+ });
6
15
  const mockComponents = [
7
16
  {
8
17
  label: 'Module Component',
@@ -162,5 +171,71 @@ describe('lib/projects/create/v3', () => {
162
171
  // @ts-expect-error breaking stuff on purpose
163
172
  projectMetadataWithoutComponents)).rejects.toThrow();
164
173
  });
174
+ it('disables gated components when hasFeature returns false', async () => {
175
+ mockHasFeature.mockResolvedValue(false);
176
+ const gatedComponent = [
177
+ {
178
+ label: 'Workflow Action Tool',
179
+ path: 'workflow-action-tool',
180
+ type: 'workflow-action',
181
+ cliSelector: 'workflow-action-tool',
182
+ supportedAuthTypes: ['oauth'],
183
+ supportedDistributions: ['private'],
184
+ },
185
+ ];
186
+ const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, mockProjectMetadataForChoices);
187
+ expect(choices).toHaveLength(3); // includes separators
188
+ expect(choices[1]).toEqual({
189
+ name: expect.stringContaining('Workflow Action Tool'),
190
+ value: gatedComponent[0],
191
+ disabled: expect.stringContaining('does not have access to this feature'),
192
+ });
193
+ expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
194
+ });
195
+ it('enables gated components when hasFeature returns true', async () => {
196
+ mockHasFeature.mockResolvedValue(true);
197
+ const gatedComponent = [
198
+ {
199
+ label: 'Workflow Action Tool',
200
+ path: 'workflow-action-tool',
201
+ type: 'workflow-action',
202
+ cliSelector: 'workflow-action-tool',
203
+ supportedAuthTypes: ['oauth'],
204
+ supportedDistributions: ['private'],
205
+ },
206
+ ];
207
+ const projectMetadataWithWorkflowAction = {
208
+ hsMetaFiles: [],
209
+ components: {
210
+ 'workflow-action': { count: 0, maxCount: 3, hsMetaFiles: [] },
211
+ },
212
+ };
213
+ const choices = await calculateComponentTemplateChoices(gatedComponent, 'oauth', 'private', 123, projectMetadataWithWorkflowAction);
214
+ expect(choices).toHaveLength(1); // no disabled components
215
+ expect(choices[0]).toEqual({
216
+ name: 'Workflow Action Tool [workflow-action-tool]',
217
+ value: gatedComponent[0],
218
+ });
219
+ expect(mockHasFeature).toHaveBeenCalledWith(123, expect.any(String));
220
+ });
221
+ it('handles non-gated components without calling hasFeature', async () => {
222
+ const nonGatedComponent = [
223
+ {
224
+ label: 'Regular Component',
225
+ path: 'regular',
226
+ type: 'module',
227
+ supportedAuthTypes: ['oauth'],
228
+ supportedDistributions: ['private'],
229
+ },
230
+ ];
231
+ const choices = await calculateComponentTemplateChoices(nonGatedComponent, 'oauth', 'private', 123, mockProjectMetadataForChoices);
232
+ expect(choices).toHaveLength(1);
233
+ expect(choices[0]).toEqual({
234
+ name: 'Regular Component [module]',
235
+ value: nonGatedComponent[0],
236
+ });
237
+ // hasFeature should not be called for non-gated components
238
+ expect(mockHasFeature).not.toHaveBeenCalled();
239
+ });
165
240
  });
166
241
  });
@@ -52,6 +52,7 @@ export async function createV3App(providedAuth, providedDistribution) {
52
52
  const componentTypeToGateMap = {
53
53
  [AppEventsKey]: FEATURES.APP_EVENTS,
54
54
  [PagesKey]: FEATURES.APPS_HOME,
55
+ 'workflow-action-tool': FEATURES.AGENT_TOOLS,
55
56
  };
56
57
  export async function calculateComponentTemplateChoices(components, authType, distribution, accountId, projectMetadata) {
57
58
  const enabledComponents = [];
@@ -81,8 +82,9 @@ export async function calculateComponentTemplateChoices(components, authType, di
81
82
  !supportedDistributions.includes(distribution)) {
82
83
  disabledReasons.push(commands.project.add.error.distributionNotAllowed(distribution));
83
84
  }
84
- if (componentTypeToGateMap[template.type]) {
85
- const isUngated = await hasFeature(accountId, componentTypeToGateMap[template.type]);
85
+ const templateGate = componentTypeToGateMap[template.cliSelector || template.type];
86
+ if (templateGate) {
87
+ const isUngated = await hasFeature(accountId, templateGate);
86
88
  if (!isUngated) {
87
89
  disabledReasons.unshift(commands.project.add.error.portalDoesNotHaveAccessToThisFeature(accountId));
88
90
  }
@@ -18,7 +18,7 @@ import SpinniesManager from '../../../ui/SpinniesManager.js';
18
18
  import { EXIT_CODES } from '../../../enums/exitCodes.js';
19
19
  import { handleProjectUpload } from '../../upload.js';
20
20
  import { pollProjectBuildAndDeploy } from '../../pollProjectBuildAndDeploy.js';
21
- import { logError } from '../../../errorHandlers/index.js';
21
+ import { debugError, logError } from '../../../errorHandlers/index.js';
22
22
  import { ApiErrorContext } from '../../../errorHandlers/index.js';
23
23
  // Prompt the user to create a new project if one doesn't exist on their target account
24
24
  export async function createNewProjectForLocalDev(projectConfig, targetAccountId, shouldCreateWithoutConfirmation, hasPublicApps) {
@@ -163,6 +163,10 @@ export async function isDeployedProjectUpToDateWithLocal(projectConfig, accountI
163
163
  }, { profile });
164
164
  return isDeepEqual(localProjectNodes, deployedProjectNodes, ['localDev']);
165
165
  }
166
+ catch (err) {
167
+ debugError(err);
168
+ return false;
169
+ }
166
170
  finally {
167
171
  // Clean up temporary directory
168
172
  if (tempDir && (await fs.pathExists(tempDir))) {
@@ -66,7 +66,6 @@ export async function projectAddPromptV3(components, selectedFeatures) {
66
66
  when: !selectedFeatures && selectedComponents.length === 0,
67
67
  type: 'checkbox',
68
68
  choices: components,
69
- loop: false,
70
69
  pageSize: components.length,
71
70
  },
72
71
  ]);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "7.6.0-beta.14",
3
+ "version": "7.6.0-beta.16",
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",
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "@hubspot/local-dev-lib": "3.19.0",
9
+ "@hubspot/local-dev-lib": "3.19.1",
10
10
  "@hubspot/project-parsing-lib": "0.8.5",
11
11
  "@hubspot/serverless-dev-runtime": "7.0.6",
12
12
  "@hubspot/theme-preview-dev-server": "0.0.10",