@hubspot/cli 7.7.22-experimental.0 → 7.7.24-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/commands/account/auth.js +15 -4
  2. package/commands/auth.js +1 -1
  3. package/commands/config/set.d.ts +1 -0
  4. package/commands/config/set.js +19 -9
  5. package/commands/mcp/start.d.ts +1 -0
  6. package/commands/mcp/start.js +12 -4
  7. package/commands/project/create.js +2 -2
  8. package/commands/project/validate.js +1 -0
  9. package/commands/sandbox/__tests__/create.test.js +207 -0
  10. package/commands/sandbox/create.d.ts +1 -1
  11. package/commands/sandbox/create.js +31 -16
  12. package/commands/testAccount/createConfig.js +1 -1
  13. package/lang/en.d.ts +17 -4
  14. package/lang/en.js +22 -6
  15. package/lang/en.lyaml +4 -2
  16. package/lib/__tests__/buildAccount.test.js +62 -4
  17. package/lib/__tests__/yargsUtils.test.js +12 -1
  18. package/lib/buildAccount.d.ts +4 -1
  19. package/lib/buildAccount.js +57 -2
  20. package/lib/commonOpts.js +25 -0
  21. package/lib/configOptions.d.ts +5 -0
  22. package/lib/configOptions.js +11 -1
  23. package/lib/constants.d.ts +8 -0
  24. package/lib/constants.js +8 -0
  25. package/lib/errorHandlers/index.js +1 -3
  26. package/lib/errors/ProjectValidationError.d.ts +4 -0
  27. package/lib/errors/ProjectValidationError.js +9 -0
  28. package/lib/mcp/setup.d.ts +4 -0
  29. package/lib/mcp/setup.js +36 -0
  30. package/lib/projects/__tests__/LocalDevProcess.test.js +35 -0
  31. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +170 -1
  32. package/lib/projects/add/legacyAddComponent.js +1 -1
  33. package/lib/projects/add/v3AddComponent.js +3 -2
  34. package/lib/projects/create/index.js +3 -3
  35. package/lib/projects/create/legacy.js +2 -2
  36. package/lib/projects/create/v3.d.ts +0 -2
  37. package/lib/projects/create/v3.js +1 -3
  38. package/lib/projects/localDev/LocalDevLogger.js +11 -2
  39. package/lib/projects/localDev/LocalDevProcess.d.ts +2 -0
  40. package/lib/projects/localDev/LocalDevProcess.js +15 -0
  41. package/lib/projects/localDev/LocalDevState.d.ts +1 -0
  42. package/lib/projects/localDev/LocalDevState.js +5 -0
  43. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -2
  44. package/lib/projects/localDev/LocalDevWebsocketServer.js +40 -30
  45. package/lib/projects/upload.js +5 -12
  46. package/lib/projects/urls.d.ts +1 -1
  47. package/lib/projects/urls.js +2 -2
  48. package/lib/sandboxes.d.ts +4 -0
  49. package/lib/sandboxes.js +4 -0
  50. package/lib/ui/index.d.ts +6 -0
  51. package/lib/ui/index.js +3 -5
  52. package/lib/yargsUtils.d.ts +1 -0
  53. package/lib/yargsUtils.js +6 -0
  54. package/mcp-server/tools/index.js +6 -4
  55. package/mcp-server/tools/project/{AddFeatureToProject.d.ts → AddFeatureToProjectTool.d.ts} +4 -4
  56. package/mcp-server/tools/project/{AddFeatureToProject.js → AddFeatureToProjectTool.js} +6 -14
  57. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  58. package/mcp-server/tools/project/CreateProjectTool.js +4 -14
  59. package/mcp-server/tools/project/{DeployProject.d.ts → DeployProjectTool.d.ts} +1 -1
  60. package/mcp-server/tools/project/{DeployProject.js → DeployProjectTool.js} +2 -2
  61. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +20 -0
  62. package/mcp-server/tools/project/GetConfigValuesTool.js +51 -0
  63. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  64. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  65. package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.js → AddFeatureToProjectTool.test.js} +7 -7
  66. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +3 -4
  67. package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js} +4 -4
  68. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +198 -0
  69. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -2
  70. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  71. package/mcp-server/tools/project/constants.d.ts +1 -0
  72. package/mcp-server/tools/project/constants.js +11 -0
  73. package/mcp-server/utils/__tests__/command.test.js +76 -3
  74. package/mcp-server/utils/command.d.ts +6 -0
  75. package/mcp-server/utils/command.js +19 -0
  76. package/package.json +3 -3
  77. package/mcp-server/utils/__tests__/project.test.js +0 -79
  78. package/mcp-server/utils/project.d.ts +0 -5
  79. package/mcp-server/utils/project.js +0 -14
  80. /package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.d.ts → AddFeatureToProjectTool.test.d.ts} +0 -0
  81. /package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts} +0 -0
  82. /package/mcp-server/{utils/__tests__/project.test.d.ts → tools/project/__tests__/GetConfigValuesTool.test.d.ts} +0 -0
package/lang/en.js CHANGED
@@ -4,8 +4,7 @@ import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constant
4
4
  import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '@hubspot/local-dev-lib/constants/config';
5
5
  import { uiAccountDescription, uiBetaTag, uiCommandReference, uiLink, UI_COLORS, } from '../lib/ui/index.js';
6
6
  import { getProjectDetailUrl, getProjectSettingsUrl, getLocalDevUiUrl, } from '../lib/projects/urls.js';
7
- import { PROJECT_CONFIG_FILE } from '../lib/constants.js';
8
- import { PROJECT_WITH_APP } from '../lib/projects/create/v3.js';
7
+ import { PROJECT_CONFIG_FILE, PROJECT_WITH_APP } from '../lib/constants.js';
9
8
  export const commands = {
10
9
  generalErrors: {
11
10
  srcIsProject: (src, command) => `"${src}" is in a project folder. Did you mean "hs project ${command}"?`,
@@ -80,6 +79,7 @@ export const commands = {
80
79
  personalAccessKey: 'Enter existing personal access key',
81
80
  },
82
81
  errors: {
82
+ invalidAccountIdProvided: `--account must be a number.`,
83
83
  failedToUpdateConfig: 'Failed to update the configuration file. Please try again.',
84
84
  migrationNotConfirmed: `Did not migrate your configuration file. Run ${uiCommandReference('hs auth')} to update your existing config, or use ${uiCommandReference('hs config migrate')} to switch to the new global configuration.`,
85
85
  mergeNotConfirmed: `Did not merge configuration files. When you are ready to merge the deprecated config file with the global config file, run ${uiCommandReference('hs config migrate')}.`,
@@ -253,6 +253,9 @@ export const commands = {
253
253
  allowAutoUpdates: {
254
254
  describe: 'Enable or disable auto updates',
255
255
  },
256
+ autoOpenBrowser: {
257
+ describe: 'Enable or disable automatic opening of the browser',
258
+ },
256
259
  },
257
260
  },
258
261
  },
@@ -828,9 +831,9 @@ export const commands = {
828
831
  setup: {
829
832
  installingDocSearch: 'Adding the docs-search mcp server',
830
833
  claudeCode: 'Claude Code',
831
- claudeDesktop: 'Claude Desktop',
832
834
  cursor: 'Cursor',
833
835
  windsurf: 'Windsurf',
836
+ vsCode: 'VSCode',
834
837
  args: {
835
838
  client: 'Target applications to configure',
836
839
  docsSearch: 'Should the docs search mcp server be installed',
@@ -842,20 +845,26 @@ export const commands = {
842
845
  },
843
846
  spinners: {
844
847
  failedToConfigure: 'Failed to configure the HubSpot mcp server.',
845
- configuringClaudeDesktop: 'Configuring Claude Desktop...',
846
- configuredClaudeDesktop: 'Configured Claude Desktop',
848
+ // Claude
847
849
  configuringClaudeCode: 'Configuring Claude Code...',
848
850
  configuredClaudeCode: 'Configured Claude Code',
849
851
  claudeCodeNotFound: 'Claude Code not found - skipping configuration',
850
852
  claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
851
853
  failedToConfigureClaudeDesktop: 'Failed to configure Claude Desktop',
854
+ // Cursor
852
855
  configuringCursor: 'Configuring Cursor...',
853
856
  failedToConfigureCursor: 'Failed to configure Cursor',
854
857
  configuredCursor: 'Configured Cursor',
855
858
  alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
859
+ // Windsurf
856
860
  configuringWindsurf: 'Configuring Windsurf...',
857
861
  failedToConfigureWindsurf: 'Failed to configure Windsurf',
858
862
  configuredWindsurf: 'Configured Windsurf',
863
+ // VS Code
864
+ configuringVsCode: 'Configuring VSCode...',
865
+ failedToConfigureVsCode: 'Failed to configure VSCode',
866
+ configuredVsCode: 'Configured VSCode',
867
+ vsCodeNotFound: 'VSCode not found - skipping configuration',
859
868
  },
860
869
  prompts: {
861
870
  targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
@@ -1375,6 +1384,7 @@ export const commands = {
1375
1384
  default: 'Validate the project before uploading',
1376
1385
  },
1377
1386
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
1387
+ failure: (projectName) => `Project ${projectName} is invalid`,
1378
1388
  options: {
1379
1389
  profile: {
1380
1390
  describe: 'The profile to target for this validation',
@@ -2451,7 +2461,8 @@ export const lib = {
2451
2461
  running: (projectName, accountIdentifier) => chalk.hex(UI_COLORS.SORBET)(`Running ${chalk.bold(projectName)} locally on ${accountIdentifier}, waiting for changes ...`),
2452
2462
  quitHelper: `Press ${chalk.bold('q')} to stop the local dev server`,
2453
2463
  viewProjectLink: (name, accountId) => uiLink('View project in HubSpot', getProjectDetailUrl(name, accountId) || ''),
2454
- viewLocalDevUILink: (accountId) => uiLink('View local dev session in HubSpot', getLocalDevUiUrl(accountId)),
2464
+ viewLocalDevUILink: (accountId, showWelcomeScreen) => uiLink('View local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen)),
2465
+ localDevUIAutoMessage: (accountId, showWelcomeScreen) => `Opening your ${uiLink('local dev session in HubSpot', getLocalDevUiUrl(accountId, showWelcomeScreen))}...`,
2455
2466
  viewTestAccountLink: 'View developer test account in HubSpot',
2456
2467
  exitingStart: 'Stopping local dev server ...',
2457
2468
  exitingSucceed: 'Successfully exited',
@@ -2780,6 +2791,11 @@ export const lib = {
2780
2791
  promptMessage: 'Enter http timeout duration',
2781
2792
  success: (timeout) => `HTTP timeout set to: ${timeout}`,
2782
2793
  },
2794
+ setAutoOpenBrowser: {
2795
+ fieldName: 'auto open browser',
2796
+ enabled: 'Auto opening your browser has been enabled',
2797
+ disabled: 'Auto opening your browser has been disabled',
2798
+ },
2783
2799
  },
2784
2800
  commonOpts: {
2785
2801
  options: {
package/lang/en.lyaml CHANGED
@@ -1038,12 +1038,14 @@ en:
1038
1038
  compressing: "Compressing build files to \"{{ path }}\""
1039
1039
  fileFiltered: "Ignore rule triggered for \"{{ filename }}\""
1040
1040
  ui:
1041
- betaTag: "{{#bold}}[BETA]{{/bold}}"
1041
+ betaTag: "[BETA]"
1042
+ betaTagWithStyle: "{{#bold}}[BETA]{{/bold}}"
1042
1043
  betaWarning:
1043
1044
  header: "{{#yellow}}***************************** WARNING ****************************{{/yellow}}"
1044
1045
  footer: "{{#yellow}}******************************************************************{{/yellow}}"
1045
1046
  infoTag: "{{#bold}}[INFO]{{/bold}}"
1046
- deprecatedTag: "{{#bold}}[DEPRECATED]{{/bold}}"
1047
+ deprecatedTag: "[DEPRECATED]"
1048
+ deprecatedTagWithStyle: "{{#bold}}[DEPRECATED]{{/bold}}"
1047
1049
  errorTag: "{{#bold}}[ERROR]{{/bold}}"
1048
1050
  deprecatedMessage: "The {{ command }} command is deprecated and will be disabled soon. {{ url }}"
1049
1051
  deprecatedDescription: "{{ message }}. The {{ command }} command is deprecated and will be disabled soon. {{ url }}"
@@ -1,7 +1,7 @@
1
1
  import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
2
2
  import { accountNameExistsInConfig, updateAccountConfig, writeConfig, getAccountId, } from '@hubspot/local-dev-lib/config';
3
3
  import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
4
- import { createSandbox } from '@hubspot/local-dev-lib/api/sandboxHubs';
4
+ import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
5
5
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
6
6
  import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
7
7
  import { cliAccountNamePrompt } from '../prompts/accountNamePrompt.js';
@@ -33,6 +33,8 @@ const mockedCreateDeveloperTestAccount = createDeveloperTestAccount;
33
33
  const mockedFetchDeveloperTestAccountGateSyncStatus = fetchDeveloperTestAccountGateSyncStatus;
34
34
  const mockedGenerateDeveloperTestAccountPersonalAccessKey = generateDeveloperTestAccountPersonalAccessKey;
35
35
  const mockedCreateSandbox = createSandbox;
36
+ const mockedCreateV2Sandbox = createV2Sandbox;
37
+ const mockedGetPersonalAccessKey = getSandboxPersonalAccessKey;
36
38
  describe('lib/buildAccount', () => {
37
39
  describe('saveAccountToConfig()', () => {
38
40
  const mockAccountConfig = {
@@ -166,16 +168,17 @@ describe('lib/buildAccount', () => {
166
168
  });
167
169
  describe('buildSandbox()', () => {
168
170
  const mockParentAccountConfig = {
169
- name: 'Developer Test Account',
171
+ name: 'Prod account',
170
172
  accountId: 123456,
171
- accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST,
173
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
172
174
  env: 'prod',
173
175
  };
174
176
  const mockSandbox = {
175
177
  sandboxHubId: 56789,
176
178
  parentHubId: 123456,
177
179
  createdAt: '2025-01-01',
178
- type: 'sandbox',
180
+ type: 'STANDARD',
181
+ version: 'V1',
179
182
  archived: false,
180
183
  name: 'Test Sandbox',
181
184
  domain: 'test-sandbox.hubspot.com',
@@ -221,4 +224,59 @@ describe('lib/buildAccount', () => {
221
224
  await expect(buildAccount.buildSandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, mockParentAccountConfig.env, false)).rejects.toThrow();
222
225
  });
223
226
  });
227
+ describe('buildV2Sandbox()', () => {
228
+ const mockParentAccountConfig = {
229
+ name: 'Prod account',
230
+ accountId: 123456,
231
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
232
+ env: 'prod',
233
+ };
234
+ const mockSandbox = {
235
+ sandboxHubId: 56789,
236
+ parentHubId: 123456,
237
+ createdAt: '2025-01-01',
238
+ type: 'STANDARD',
239
+ archived: false,
240
+ version: 'V2',
241
+ name: 'Test v2 Sandbox',
242
+ domain: 'test-v2-sandbox.hubspot.com',
243
+ createdByUser: {
244
+ id: 123456,
245
+ email: 'test@test.com',
246
+ firstName: 'Test',
247
+ lastName: 'User',
248
+ },
249
+ };
250
+ beforeEach(() => {
251
+ vi.spyOn(buildAccount, 'saveAccountToConfig').mockResolvedValue(mockParentAccountConfig.name);
252
+ mockedGetAccountId.mockReturnValue(mockParentAccountConfig.accountId);
253
+ mockedCreateV2Sandbox.mockResolvedValue({
254
+ data: mockSandbox,
255
+ });
256
+ mockedGetPersonalAccessKey.mockResolvedValue({
257
+ data: { personalAccessKey: { encodedOAuthRefreshToken: 'test-key' } },
258
+ });
259
+ });
260
+ afterEach(() => {
261
+ vi.clearAllMocks();
262
+ });
263
+ it('should create a v2 standard sandbox successfully and fetch a personal access key', async () => {
264
+ const result = await buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false);
265
+ expect(result).toEqual({ sandbox: mockSandbox });
266
+ expect(mockedGetPersonalAccessKey).toHaveBeenCalledWith(mockParentAccountConfig.accountId, mockSandbox.sandboxHubId);
267
+ });
268
+ it('should create a development sandbox successfully and fetch a personal access key', async () => {
269
+ const result = await buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, false, mockParentAccountConfig.env, false);
270
+ expect(result).toEqual({ sandbox: mockSandbox });
271
+ expect(mockedGetPersonalAccessKey).toHaveBeenCalledWith(mockParentAccountConfig.accountId, mockSandbox.sandboxHubId);
272
+ });
273
+ it('should throw error if account ID is not found', async () => {
274
+ mockedGetAccountId.mockReturnValue(null);
275
+ await expect(buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false)).rejects.toThrow();
276
+ });
277
+ it('should handle API errors when creating sandbox', async () => {
278
+ mockedCreateV2Sandbox.mockRejectedValue(new Error('test-error'));
279
+ await expect(buildAccount.buildV2Sandbox(mockSandbox.name, mockParentAccountConfig, HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, false, mockParentAccountConfig.env, false)).rejects.toThrow();
280
+ });
281
+ });
224
282
  });
@@ -1,4 +1,4 @@
1
- import { hasFlag, makeYargsBuilder } from '../yargsUtils.js';
1
+ import { hasFlag, makeYargsBuilder, getExclusiveConflicts, } from '../yargsUtils.js';
2
2
  import * as commonOpts from '../commonOpts.js';
3
3
  vi.mock('../commonOpts');
4
4
  const argvWithFlag = ['hs', 'command', '--test'];
@@ -36,4 +36,15 @@ describe('lib/yargsUtils', () => {
36
36
  expect(commonOpts.addCustomHelpOutput).toHaveBeenCalled();
37
37
  });
38
38
  });
39
+ describe('getExclusiveConflicts()', () => {
40
+ it('should return an object where each option conflicts with all others', () => {
41
+ const options = ['option1', 'option2', 'option3'];
42
+ const result = getExclusiveConflicts(options);
43
+ expect(result).toEqual({
44
+ option1: ['option2', 'option3'],
45
+ option2: ['option1', 'option3'],
46
+ option3: ['option1', 'option2'],
47
+ });
48
+ });
49
+ });
39
50
  });
@@ -1,7 +1,7 @@
1
1
  import { DeveloperTestAccountConfig } from '@hubspot/local-dev-lib/types/developerTestAccounts';
2
2
  import { Environment } from '@hubspot/local-dev-lib/types/Config';
3
3
  import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts';
4
- import { SandboxResponse } from '@hubspot/local-dev-lib/types/Sandbox';
4
+ import { SandboxResponse, V2Sandbox } from '@hubspot/local-dev-lib/types/Sandbox';
5
5
  import { SandboxAccountType } from '../types/Sandboxes.js';
6
6
  export declare function saveAccountToConfig(accountId: number | undefined, accountName: string, env: Environment, personalAccessKey?: string, force?: boolean): Promise<string>;
7
7
  export declare function createDeveloperTestAccountV3(parentAccountId: number, testAccountConfig: DeveloperTestAccountConfig): Promise<{
@@ -14,4 +14,7 @@ type SandboxAccount = SandboxResponse & {
14
14
  name: string;
15
15
  };
16
16
  export declare function buildSandbox(sandboxName: string, parentAccountConfig: CLIAccount, sandboxType: SandboxAccountType, env: Environment, force?: boolean): Promise<SandboxAccount>;
17
+ export declare function buildV2Sandbox(sandboxName: string, parentAccountConfig: CLIAccount, sandboxType: SandboxAccountType, syncObjectRecords: boolean, env: Environment, force?: boolean): Promise<{
18
+ sandbox: V2Sandbox;
19
+ }>;
17
20
  export {};
@@ -4,14 +4,14 @@ import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountId
4
4
  import { logger } from '@hubspot/local-dev-lib/logger';
5
5
  import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, generateDeveloperTestAccountPersonalAccessKey, } from '@hubspot/local-dev-lib/api/developerTestAccounts';
6
6
  import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
7
- import { createSandbox } from '@hubspot/local-dev-lib/api/sandboxHubs';
7
+ import { createSandbox, createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
8
8
  import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
9
9
  import { createDeveloperTestAccountConfigPrompt } from './prompts/createDeveloperTestAccountConfigPrompt.js';
10
10
  import { i18n } from './lang.js';
11
11
  import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
12
12
  import SpinniesManager from './ui/SpinniesManager.js';
13
13
  import { debugError, logError } from './errorHandlers/index.js';
14
- import { SANDBOX_API_TYPE_MAP, handleSandboxCreateError } from './sandboxes.js';
14
+ import { SANDBOX_API_TYPE_MAP, SANDBOX_TYPE_MAP_V2, handleSandboxCreateError, } from './sandboxes.js';
15
15
  import { handleDeveloperTestAccountCreateError } from './developerTestAccounts.js';
16
16
  import { lib } from '../lang/en.js';
17
17
  import { poll } from './polling.js';
@@ -199,3 +199,58 @@ export async function buildSandbox(sandboxName, parentAccountConfig, sandboxType
199
199
  }
200
200
  return sandbox;
201
201
  }
202
+ export async function buildV2Sandbox(sandboxName, parentAccountConfig, sandboxType, syncObjectRecords, env, force = false) {
203
+ let i18nKey;
204
+ if (sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX) {
205
+ i18nKey = 'lib.sandbox.create.loading.standard';
206
+ }
207
+ else {
208
+ i18nKey = 'lib.sandbox.create.loading.developer';
209
+ }
210
+ const id = getAccountIdentifier(parentAccountConfig);
211
+ const parentAccountId = getAccountId(id);
212
+ if (!parentAccountId) {
213
+ throw new Error(i18n(`${i18nKey}.fail`));
214
+ }
215
+ SpinniesManager.init({
216
+ succeedColor: 'white',
217
+ });
218
+ logger.log('');
219
+ SpinniesManager.add('buildV2Sandbox', {
220
+ text: i18n(`${i18nKey}.add`, {
221
+ accountName: sandboxName,
222
+ }),
223
+ });
224
+ let sandbox;
225
+ let pak;
226
+ try {
227
+ const sandboxTypeV2 = SANDBOX_TYPE_MAP_V2[sandboxType];
228
+ const { data } = await createV2Sandbox(parentAccountId, sandboxName, sandboxTypeV2, syncObjectRecords);
229
+ sandbox = { ...data };
230
+ const { data: { personalAccessKey }, } = await getSandboxPersonalAccessKey(parentAccountId, sandbox.sandboxHubId);
231
+ pak = personalAccessKey.encodedOAuthRefreshToken;
232
+ SpinniesManager.succeed('buildV2Sandbox', {
233
+ text: i18n(`${i18nKey}.succeed`, {
234
+ accountName: sandboxName,
235
+ accountId: sandbox.sandboxHubId,
236
+ }),
237
+ });
238
+ }
239
+ catch (e) {
240
+ debugError(e);
241
+ SpinniesManager.fail('buildV2Sandbox', {
242
+ text: i18n(`${i18nKey}.fail`, {
243
+ accountName: sandboxName,
244
+ }),
245
+ });
246
+ handleSandboxCreateError(e, env, sandboxName, parentAccountId);
247
+ }
248
+ try {
249
+ await saveAccountToConfig(sandbox.sandboxHubId, sandboxName, env, pak, force);
250
+ }
251
+ catch (err) {
252
+ logError(err);
253
+ throw err;
254
+ }
255
+ return { sandbox };
256
+ }
package/lib/commonOpts.js CHANGED
@@ -7,6 +7,7 @@ import { debugError } from './errorHandlers/index.js';
7
7
  import { EXIT_CODES } from './enums/exitCodes.js';
8
8
  import { uiCommandReference } from './ui/index.js';
9
9
  import { i18n } from './lang.js';
10
+ import { getTerminalUISupport, UI_COLORS } from './ui/index.js';
10
11
  export function addGlobalOptions(yargs) {
11
12
  yargs.version(false);
12
13
  yargs.option('debug', {
@@ -76,8 +77,32 @@ export function addJSONOutputOptions(yargs) {
76
77
  hidden: true,
77
78
  });
78
79
  }
80
+ // Remove this once we've upgraded to yargs 18.0.0
81
+ function uiBetaTagWithColor(message) {
82
+ const terminalUISupport = getTerminalUISupport();
83
+ const tag = i18n(`lib.ui.betaTagWithStyle`);
84
+ const result = `${terminalUISupport.color ? chalk.hex(UI_COLORS.SORBET)(tag) : tag} ${message}`;
85
+ return result;
86
+ }
87
+ // Remove this once we've upgraded to yargs 18.0.0
88
+ function uiDeprecatedTagWithColor(message) {
89
+ const terminalUISupport = getTerminalUISupport();
90
+ const tag = i18n(`lib.ui.deprecatedTagWithStyle`);
91
+ const result = `${terminalUISupport.color ? chalk.yellow(tag) : tag} ${message}`;
92
+ return result;
93
+ }
79
94
  export async function addCustomHelpOutput(yargs, command, describe) {
80
95
  try {
96
+ // Remove this once we've upgraded to yargs 18.0.0
97
+ if (describe && describe.includes(i18n(`lib.ui.betaTag`))) {
98
+ describe = describe.replace(i18n(`lib.ui.betaTag`) + ' ', '');
99
+ describe = uiBetaTagWithColor(describe);
100
+ }
101
+ // Remove this once we've upgraded to yargs 18.0.0
102
+ if (describe && describe.includes(i18n(`lib.ui.deprecatedTag`))) {
103
+ describe = describe.replace(i18n(`lib.ui.deprecatedTag`) + ' ', '');
104
+ describe = uiDeprecatedTagWithColor(describe);
105
+ }
81
106
  const parsedArgv = yargsParser(process.argv.slice(2));
82
107
  if (parsedArgv && parsedArgv.help) {
83
108
  const commandBase = `hs ${parsedArgv._.slice(0, -1).join(' ')}`;
@@ -15,3 +15,8 @@ export declare function setHttpTimeout({ accountId, httpTimeout, }: {
15
15
  accountId: number;
16
16
  httpTimeout?: string;
17
17
  }): Promise<void>;
18
+ export declare function setAutoOpenBrowser({ accountId, autoOpenBrowser, }: {
19
+ accountId: number;
20
+ autoOpenBrowser: boolean;
21
+ }): Promise<void>;
22
+ export declare function isAutoOpenBrowserEnabled(): boolean;
@@ -1,4 +1,4 @@
1
- import { updateAllowUsageTracking, updateAllowAutoUpdates, updateDefaultCmsPublishMode, updateHttpTimeout, } from '@hubspot/local-dev-lib/config';
1
+ import { updateAllowUsageTracking, updateAllowAutoUpdates, updateDefaultCmsPublishMode, updateHttpTimeout, isConfigFlagEnabled, updateAutoOpenBrowser, } from '@hubspot/local-dev-lib/config';
2
2
  import { CMS_PUBLISH_MODE } from '@hubspot/local-dev-lib/constants/files';
3
3
  import { commaSeparatedValues } from '@hubspot/local-dev-lib/text';
4
4
  import { trackCommandUsage } from './usageTracking.js';
@@ -95,3 +95,13 @@ export async function setHttpTimeout({ accountId, httpTimeout, }) {
95
95
  updateHttpTimeout(newHttpTimeout);
96
96
  uiLogger.success(lib.configOptions.setHttpTimeout.success(newHttpTimeout));
97
97
  }
98
+ export async function setAutoOpenBrowser({ accountId, autoOpenBrowser, }) {
99
+ trackCommandUsage('config-set-auto-open-browser', undefined, accountId);
100
+ updateAutoOpenBrowser(autoOpenBrowser);
101
+ uiLogger.success(autoOpenBrowser
102
+ ? lib.configOptions.setAutoOpenBrowser.enabled
103
+ : lib.configOptions.setAutoOpenBrowser.disabled);
104
+ }
105
+ export function isAutoOpenBrowserEnabled() {
106
+ return isConfigFlagEnabled('autoOpenBrowser', true);
107
+ }
@@ -78,6 +78,8 @@ export declare const APP_AUTH_TYPES: {
78
78
  export declare const FEATURES: {
79
79
  readonly UNIFIED_THEME_PREVIEW: "cms:react:unifiedThemePreview";
80
80
  readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
81
+ readonly SANDBOXES_V2: "sandboxes:v2:enabled";
82
+ readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
81
83
  };
82
84
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
83
85
  UPLOAD_SUCCESS: string;
@@ -88,6 +90,7 @@ export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
88
90
  };
89
91
  export declare const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES: {
90
92
  UPLOAD: string;
93
+ VIEWED_WELCOME_SCREEN: string;
91
94
  };
92
95
  export declare const APP_INSTALLATION_STATES: {
93
96
  readonly NOT_INSTALLED: "NOT_INSTALLED";
@@ -107,3 +110,8 @@ export declare const LOCAL_DEV_SERVER_MESSAGE_TYPES: {
107
110
  readonly INITIAL: "INITIAL";
108
111
  readonly WEBSOCKET_SERVER_CONNECTED: "WEBSOCKET_SERVER_CONNECTED";
109
112
  };
113
+ export declare const CONFIG_LOCAL_STATE_FLAGS: {
114
+ readonly LOCAL_DEV_UI_WELCOME: "LOCAL_DEV_UI_WELCOME";
115
+ };
116
+ export declare const EMPTY_PROJECT = "empty";
117
+ export declare const PROJECT_WITH_APP = "app";
package/lib/constants.js CHANGED
@@ -70,6 +70,8 @@ export const APP_AUTH_TYPES = {
70
70
  export const FEATURES = {
71
71
  UNIFIED_THEME_PREVIEW: 'cms:react:unifiedThemePreview',
72
72
  UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
73
+ SANDBOXES_V2: 'sandboxes:v2:enabled',
74
+ SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
73
75
  };
74
76
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
75
77
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -80,6 +82,7 @@ export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
80
82
  };
81
83
  export const LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES = {
82
84
  UPLOAD: 'client:upload',
85
+ VIEWED_WELCOME_SCREEN: 'client:viewedWelcomeScreen',
83
86
  };
84
87
  export const APP_INSTALLATION_STATES = {
85
88
  NOT_INSTALLED: 'NOT_INSTALLED',
@@ -99,3 +102,8 @@ export const LOCAL_DEV_SERVER_MESSAGE_TYPES = {
99
102
  INITIAL: 'INITIAL',
100
103
  WEBSOCKET_SERVER_CONNECTED: 'WEBSOCKET_SERVER_CONNECTED',
101
104
  };
105
+ export const CONFIG_LOCAL_STATE_FLAGS = {
106
+ LOCAL_DEV_UI_WELCOME: 'LOCAL_DEV_UI_WELCOME',
107
+ };
108
+ export const EMPTY_PROJECT = 'empty';
109
+ export const PROJECT_WITH_APP = 'app';
@@ -5,6 +5,7 @@ import { shouldSuppressError } from './suppressError.js';
5
5
  import { i18n } from '../lang.js';
6
6
  import util from 'util';
7
7
  import { uiCommandReference } from '../ui/index.js';
8
+ import { isProjectValidationError } from '../errors/ProjectValidationError.js';
8
9
  export function logError(error, context) {
9
10
  debugError(error, context);
10
11
  if (isProjectValidationError(error)) {
@@ -84,9 +85,6 @@ export class ApiErrorContext {
84
85
  this.projectName = props.projectName || '';
85
86
  }
86
87
  }
87
- function isProjectValidationError(error) {
88
- return error instanceof Error && error.name === 'ProjectValidationError';
89
- }
90
88
  function isErrorWithMessageOrReason(error) {
91
89
  return (typeof error === 'object' &&
92
90
  error !== null &&
@@ -0,0 +1,4 @@
1
+ export default class ProjectValidationError extends Error {
2
+ constructor(message: string, options?: ErrorOptions);
3
+ }
4
+ export declare function isProjectValidationError(err: unknown): err is ProjectValidationError;
@@ -0,0 +1,9 @@
1
+ export default class ProjectValidationError extends Error {
2
+ constructor(message, options) {
3
+ super(message, options);
4
+ this.name = 'ProjectValidationError';
5
+ }
6
+ }
7
+ export function isProjectValidationError(err) {
8
+ return err instanceof ProjectValidationError;
9
+ }
@@ -7,6 +7,9 @@ export declare const supportedTools: ({
7
7
  } | {
8
8
  name: "Windsurf";
9
9
  value: string;
10
+ } | {
11
+ name: "VSCode";
12
+ value: string;
10
13
  })[];
11
14
  interface McpCommand {
12
15
  command: string;
@@ -15,6 +18,7 @@ interface McpCommand {
15
18
  export declare function addMintlifyMcpServer(installTargets: string[]): Promise<void>;
16
19
  export declare function setupMintlify(derivedTargets?: string[]): Promise<boolean>;
17
20
  export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
21
+ export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
18
22
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
19
23
  export declare function setupCursor(mcpCommand?: McpCommand): boolean;
20
24
  export declare function setupWindsurf(mcpCommand?: McpCommand): boolean;
package/lib/mcp/setup.js CHANGED
@@ -13,11 +13,13 @@ const mcpServerName = 'hubspot-cli-mcp';
13
13
  const claudeCode = 'claude';
14
14
  const windsurf = 'windsurf';
15
15
  const cursor = 'cursor';
16
+ const vscode = 'vscode';
16
17
  const supportedMintlifyClients = [windsurf, cursor];
17
18
  export const supportedTools = [
18
19
  { name: commands.mcp.setup.claudeCode, value: claudeCode },
19
20
  { name: commands.mcp.setup.cursor, value: cursor },
20
21
  { name: commands.mcp.setup.windsurf, value: windsurf },
22
+ { name: commands.mcp.setup.vsCode, value: vscode },
21
23
  ];
22
24
  const defaultMcpCommand = {
23
25
  command: 'hs',
@@ -75,6 +77,9 @@ export async function addMcpServerToConfig(targets) {
75
77
  if (derivedTargets.includes(windsurf)) {
76
78
  await runSetupFunction(setupWindsurf);
77
79
  }
80
+ if (derivedTargets.includes(vscode)) {
81
+ await runSetupFunction(setupVsCode);
82
+ }
78
83
  uiLogger.info(commands.mcp.setup.success(derivedTargets));
79
84
  return derivedTargets;
80
85
  }
@@ -150,6 +155,37 @@ function setupMcpConfigFile(config) {
150
155
  return false;
151
156
  }
152
157
  }
158
+ export async function setupVsCode(mcpCommand = defaultMcpCommand) {
159
+ try {
160
+ SpinniesManager.add('vsCode', {
161
+ text: commands.mcp.setup.spinners.configuringVsCode,
162
+ });
163
+ const mcpConfig = JSON.stringify({
164
+ name: mcpServerName,
165
+ ...buildCommandWithAgentString(mcpCommand, vscode),
166
+ });
167
+ await execAsync(`code --add-mcp '${mcpConfig}'`);
168
+ SpinniesManager.succeed('vsCode', {
169
+ text: commands.mcp.setup.spinners.configuredVsCode,
170
+ });
171
+ return true;
172
+ }
173
+ catch (error) {
174
+ if (error instanceof Error &&
175
+ error.message.includes('code: command not found')) {
176
+ SpinniesManager.fail('vsCode', {
177
+ text: commands.mcp.setup.spinners.vsCodeNotFound,
178
+ });
179
+ }
180
+ else {
181
+ SpinniesManager.fail('vsCode', {
182
+ text: commands.mcp.setup.spinners.failedToConfigureVsCode,
183
+ });
184
+ logError(error);
185
+ }
186
+ return false;
187
+ }
188
+ }
153
189
  export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
154
190
  try {
155
191
  SpinniesManager.add('claudeCode', {
@@ -16,6 +16,7 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => ({
16
16
  cleanup: vi.fn().mockResolvedValue(undefined),
17
17
  },
18
18
  }));
19
+ vi.mock('open');
19
20
  vi.mock('@hubspot/project-parsing-lib');
20
21
  vi.mock('../upload');
21
22
  vi.mock('../config');
@@ -306,4 +307,38 @@ describe('LocalDevProcess', () => {
306
307
  expect(listener).toHaveBeenCalledWith(process.projectNodes);
307
308
  });
308
309
  });
310
+ describe('removeStateListener()', () => {
311
+ it('should remove state listener', () => {
312
+ const listener = vi.fn();
313
+ const key = 'projectNodes';
314
+ // Add the listener first
315
+ process.addStateListener(key, listener);
316
+ // Trigger state change to verify listener is called
317
+ // @ts-expect-error
318
+ process.state.projectNodes = {};
319
+ expect(listener).toHaveBeenCalledTimes(1);
320
+ // Remove the listener
321
+ process.removeStateListener(key, listener);
322
+ // Trigger state change again to verify listener is no longer called
323
+ // @ts-expect-error
324
+ process.state.projectNodes = { newNode: { uid: 'newNode' } };
325
+ expect(listener).toHaveBeenCalledTimes(1); // Should still be 1, not 2
326
+ });
327
+ it('should not affect other listeners when removing one', () => {
328
+ const listener1 = vi.fn();
329
+ const listener2 = vi.fn();
330
+ const key = 'projectNodes';
331
+ // Add two listeners
332
+ process.addStateListener(key, listener1);
333
+ process.addStateListener(key, listener2);
334
+ // Remove only the first listener
335
+ process.removeStateListener(key, listener1);
336
+ // Trigger state change
337
+ // @ts-expect-error
338
+ process.state.projectNodes = {};
339
+ // Only listener2 should be called
340
+ expect(listener1).not.toHaveBeenCalled();
341
+ expect(listener2).toHaveBeenCalled();
342
+ });
343
+ });
309
344
  });