@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
@@ -15,6 +15,7 @@ import { setAsDefaultAccountPrompt } from '../../lib/prompts/setAsDefaultAccount
15
15
  import { logError } from '../../lib/errorHandlers/index.js';
16
16
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
17
17
  import { uiFeatureHighlight } from '../../lib/ui/index.js';
18
+ import { parseStringToNumber } from '../../lib/parsing.js';
18
19
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
19
20
  import { commands } from '../../lang/en.js';
20
21
  import { uiLogger } from '../../lib/ui/logger.js';
@@ -96,9 +97,19 @@ async function handleConfigMigration() {
96
97
  const describe = commands.account.subcommands.auth.describe;
97
98
  const command = 'auth';
98
99
  async function handler(args) {
99
- const { disableTracking, personalAccessKey: providedPersonalAccessKey, derivedAccountId, } = args;
100
+ const { disableTracking, personalAccessKey: providedPersonalAccessKey, userProvidedAccount, } = args;
101
+ let parsedUserProvidedAccountId;
102
+ if (userProvidedAccount) {
103
+ try {
104
+ parsedUserProvidedAccountId = parseStringToNumber(userProvidedAccount);
105
+ }
106
+ catch (err) {
107
+ uiLogger.error(commands.account.subcommands.auth.errors.invalidAccountIdProvided);
108
+ process.exit(EXIT_CODES.ERROR);
109
+ }
110
+ }
100
111
  if (!disableTracking) {
101
- trackCommandUsage('account-auth', {}, derivedAccountId);
112
+ trackCommandUsage('account-auth', {}, parsedUserProvidedAccountId);
102
113
  await trackAuthAction('account-auth', authType, TRACKING_STATUS.STARTED);
103
114
  }
104
115
  const configMigrationSuccess = await handleConfigMigration();
@@ -112,7 +123,7 @@ async function handler(args) {
112
123
  }
113
124
  loadConfig('');
114
125
  handleExit(deleteEmptyConfigFile);
115
- const updatedConfig = await updateConfigWithNewAccount(args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD, configAlreadyExists, providedPersonalAccessKey, derivedAccountId);
126
+ const updatedConfig = await updateConfigWithNewAccount(args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD, configAlreadyExists, providedPersonalAccessKey, parsedUserProvidedAccountId);
116
127
  if (!updatedConfig) {
117
128
  if (!disableTracking) {
118
129
  await trackAuthAction('account-auth', authType, TRACKING_STATUS.ERROR);
@@ -143,7 +154,7 @@ function accountAuthBuilder(yargs) {
143
154
  yargs.options({
144
155
  account: {
145
156
  describe: commands.account.subcommands.auth.options.account,
146
- type: 'string',
157
+ type: 'number',
147
158
  alias: 'a',
148
159
  },
149
160
  'disable-tracking': {
package/commands/auth.js CHANGED
@@ -40,7 +40,7 @@ async function handler(args) {
40
40
  }
41
41
  }
42
42
  catch (err) {
43
- logError(commands.auth.errors.invalidAccountIdProvided);
43
+ uiLogger.error(commands.auth.errors.invalidAccountIdProvided);
44
44
  process.exit(EXIT_CODES.ERROR);
45
45
  }
46
46
  const authType = (authTypeFlagValue && authTypeFlagValue.toLowerCase()) ||
@@ -5,6 +5,7 @@ type ConfigSetArgs = CommonArgs & ConfigArgs & {
5
5
  allowUsageTracking?: boolean;
6
6
  httpTimeout?: string;
7
7
  allowAutoUpdates?: boolean;
8
+ autoOpenBrowser?: boolean;
8
9
  };
9
10
  declare const configSetCommand: YargsCommandModule<unknown, ConfigSetArgs>;
10
11
  export default configSetCommand;
@@ -2,9 +2,9 @@ import { i18n } from '../../lib/lang.js';
2
2
  import { trackCommandUsage } from '../../lib/usageTracking.js';
3
3
  import { promptUser } from '../../lib/prompts/promptUtils.js';
4
4
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
5
- import { setDefaultCmsPublishMode, setHttpTimeout, setAllowUsageTracking, setAllowAutoUpdates, } from '../../lib/configOptions.js';
5
+ import { setDefaultCmsPublishMode, setHttpTimeout, setAllowUsageTracking, setAllowAutoUpdates, setAutoOpenBrowser, } from '../../lib/configOptions.js';
6
6
  import { commands } from '../../lang/en.js';
7
- import { makeYargsBuilder } from '../../lib/yargsUtils.js';
7
+ import { makeYargsBuilder, getExclusiveConflicts, } from '../../lib/yargsUtils.js';
8
8
  const command = 'set';
9
9
  const describe = commands.config.subcommands.set.describe;
10
10
  async function selectOptions() {
@@ -28,7 +28,7 @@ async function selectOptions() {
28
28
  return configOption;
29
29
  }
30
30
  async function handleConfigUpdate(accountId, args) {
31
- const { allowAutoUpdates, allowUsageTracking, defaultCmsPublishMode, httpTimeout, } = args;
31
+ const { allowAutoUpdates, allowUsageTracking, defaultCmsPublishMode, httpTimeout, autoOpenBrowser, } = args;
32
32
  if (typeof defaultCmsPublishMode !== 'undefined') {
33
33
  await setDefaultCmsPublishMode({ defaultCmsPublishMode, accountId });
34
34
  return true;
@@ -45,6 +45,10 @@ async function handleConfigUpdate(accountId, args) {
45
45
  await setAllowAutoUpdates({ allowAutoUpdates, accountId });
46
46
  return true;
47
47
  }
48
+ else if (typeof autoOpenBrowser !== 'undefined') {
49
+ await setAutoOpenBrowser({ autoOpenBrowser, accountId });
50
+ return true;
51
+ }
48
52
  return false;
49
53
  }
50
54
  async function handler(args) {
@@ -77,13 +81,19 @@ function configSetBuilder(yargs) {
77
81
  type: 'boolean',
78
82
  hidden: true,
79
83
  },
84
+ 'auto-open-browser': {
85
+ describe: commands.config.subcommands.set.options.autoOpenBrowser.describe,
86
+ type: 'boolean',
87
+ hidden: true,
88
+ },
80
89
  })
81
- .conflicts('defaultCmsPublishMode', 'allowUsageTracking')
82
- .conflicts('defaultCmsPublishMode', 'httpTimeout')
83
- .conflicts('allowUsageTracking', 'httpTimeout')
84
- .conflicts('allowAutoUpdates', 'defaultCmsPublishMode')
85
- .conflicts('allowAutoUpdates', 'allowUsageTracking')
86
- .conflicts('allowAutoUpdates', 'httpTimeout')
90
+ .conflicts(getExclusiveConflicts([
91
+ 'default-cms-publish-mode',
92
+ 'allow-usage-tracking',
93
+ 'http-timeout',
94
+ 'allow-auto-updates',
95
+ 'auto-open-browser',
96
+ ]))
87
97
  .example([
88
98
  [
89
99
  '$0 config set',
@@ -1,6 +1,7 @@
1
1
  import { CommonArgs, YargsCommandModule } from '../../types/Yargs.js';
2
2
  interface McpStartArgs extends CommonArgs {
3
3
  aiAgent: string;
4
+ standAloneMode: boolean;
4
5
  }
5
6
  declare const mcpStartCommand: YargsCommandModule<unknown, McpStartArgs>;
6
7
  export default mcpStartCommand;
@@ -8,8 +8,11 @@ import { logError } from '../../lib/errorHandlers/index.js';
8
8
  import { commands } from '../../lang/en.js';
9
9
  import { handleExit } from '../../lib/process.js';
10
10
  import { trackCommandUsage } from '../../lib/usageTracking.js';
11
+ import { fileURLToPath } from 'url';
11
12
  const command = 'start';
12
13
  const describe = undefined; // Leave hidden for now
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
13
16
  async function handler(args) {
14
17
  try {
15
18
  await import('@modelcontextprotocol/sdk/server/mcp.js');
@@ -19,9 +22,10 @@ async function handler(args) {
19
22
  process.exit(EXIT_CODES.ERROR);
20
23
  }
21
24
  trackCommandUsage('mcp-start', {}, args.derivedAccountId);
22
- await startMcpServer(args.aiAgent);
25
+ await startMcpServer(args);
23
26
  }
24
- async function startMcpServer(aiAgent) {
27
+ async function startMcpServer(args) {
28
+ const { aiAgent, standAloneMode } = args;
25
29
  try {
26
30
  const serverPath = path.join(__dirname, '..', '..', 'mcp-server', 'server.js');
27
31
  // Check if server file exists
@@ -29,8 +33,8 @@ async function startMcpServer(aiAgent) {
29
33
  uiLogger.error(commands.mcp.start.errors.serverFileNotFound(serverPath));
30
34
  return;
31
35
  }
32
- uiLogger.info(commands.mcp.start.startingServer);
33
- uiLogger.info(commands.mcp.start.stopInstructions);
36
+ uiLogger.debug(commands.mcp.start.startingServer);
37
+ uiLogger.debug(commands.mcp.start.stopInstructions);
34
38
  const args = [serverPath];
35
39
  // Start the server using ts-node
36
40
  const child = spawn(`node`, args, {
@@ -38,6 +42,7 @@ async function startMcpServer(aiAgent) {
38
42
  env: {
39
43
  ...process.env,
40
44
  HUBSPOT_MCP_AI_AGENT: aiAgent || 'unknown',
45
+ HUBSPOT_MCP_STANDALONE_MODE: `${standAloneMode}`,
41
46
  },
42
47
  });
43
48
  // Handle server process events
@@ -63,6 +68,9 @@ function startBuilder(yargs) {
63
68
  yargs.option('ai-agent', {
64
69
  type: 'string',
65
70
  });
71
+ yargs.option('stand-alone-mode', {
72
+ type: 'boolean',
73
+ });
66
74
  return yargs;
67
75
  }
68
76
  const builder = makeYargsBuilder(startBuilder, command, describe, {
@@ -5,7 +5,8 @@ import { getCwd } from '@hubspot/local-dev-lib/path';
5
5
  import { trackCommandUsage } from '../../lib/usageTracking.js';
6
6
  import { writeProjectConfig, getProjectConfig, } from '../../lib/projects/config.js';
7
7
  import { EMPTY_PROJECT_TEMPLATE_NAME } from '../../lib/projects/create/legacy.js';
8
- import { PROJECT_WITH_APP, EMPTY_PROJECT, } from '../../lib/projects/create/v3.js';
8
+ import { generateComponentPaths } from '../../lib/projects/create/v3.js';
9
+ import { PROJECT_WITH_APP, EMPTY_PROJECT } from '../../lib/constants.js';
9
10
  import { uiBetaTag, uiFeatureHighlight } from '../../lib/ui/index.js';
10
11
  import { debugError, logError } from '../../lib/errorHandlers/index.js';
11
12
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
@@ -14,7 +15,6 @@ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
14
15
  import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
15
16
  import { commands } from '../../lang/en.js';
16
17
  import { uiLogger } from '../../lib/ui/logger.js';
17
- import { generateComponentPaths } from '../../lib/projects/create/v3.js';
18
18
  import { handleProjectCreationFlow, } from '../../lib/projects/create/index.js';
19
19
  const command = ['create', 'init'];
20
20
  const describe = uiBetaTag(commands.project.create.describe, false);
@@ -42,6 +42,7 @@ async function handler(args) {
42
42
  }
43
43
  catch (e) {
44
44
  logError(e);
45
+ uiLogger.error(commands.project.validate.failure(projectConfig.name));
45
46
  process.exit(EXIT_CODES.ERROR);
46
47
  }
47
48
  uiLogger.success(commands.project.validate.success(projectConfig.name));
@@ -1,7 +1,43 @@
1
1
  import yargs from 'yargs';
2
2
  import { addAccountOptions, addConfigOptions, addUseEnvironmentOptions, addTestingOptions, } from '../../../lib/commonOpts.js';
3
3
  import sandboxCreateCommand from '../create.js';
4
+ import { hasFeature } from '../../../lib/hasFeature.js';
5
+ import * as sandboxPrompts from '../../../lib/prompts/sandboxesPrompt.js';
6
+ import * as accountNamePrompt from '../../../lib/prompts/accountNamePrompt.js';
7
+ import * as configUtils from '@hubspot/local-dev-lib/config';
8
+ import * as promptUtils from '../../../lib/prompts/promptUtils.js';
9
+ import { trackCommandUsage } from '../../../lib/usageTracking.js';
10
+ import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
11
+ import * as buildAccount from '../../../lib/buildAccount.js';
12
+ import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
13
+ import { logger } from '@hubspot/local-dev-lib/logger';
14
+ import * as sandboxesLib from '../../../lib/sandboxes.js';
15
+ import * as sandboxSync from '../../../lib/sandboxSync.js';
16
+ import { vi } from 'vitest';
17
+ vi.mock('@hubspot/local-dev-lib/logger');
18
+ vi.mock('@hubspot/local-dev-lib/config');
4
19
  vi.mock('../../../lib/commonOpts');
20
+ vi.mock('../../../lib/hasFeature');
21
+ vi.mock('../../../lib/prompts/sandboxesPrompt');
22
+ vi.mock('../../../lib/prompts/promptUtils');
23
+ vi.mock('../../../lib/prompts/accountNamePrompt');
24
+ vi.mock('../../../lib/sandboxes');
25
+ vi.mock('../../../lib/usageTracking');
26
+ vi.mock('../../../lib/buildAccount');
27
+ vi.mock('../../../lib/sandboxes');
28
+ vi.mock('../../../lib/commonOpts');
29
+ const getAccountConfigSpy = vi.spyOn(configUtils, 'getAccountConfig');
30
+ const promptUserSpy = vi.spyOn(promptUtils, 'promptUser');
31
+ const sandboxTypePromptSpy = vi.spyOn(sandboxPrompts, 'sandboxTypePrompt');
32
+ const processExitSpy = vi.spyOn(process, 'exit');
33
+ const buildSandboxSpy = vi.spyOn(buildAccount, 'buildSandbox');
34
+ const buildV2SandboxSpy = vi.spyOn(buildAccount, 'buildV2Sandbox');
35
+ const getAvailableSyncTypesSpy = vi.spyOn(sandboxesLib, 'getAvailableSyncTypes');
36
+ const syncSandboxSpy = vi.spyOn(sandboxSync, 'syncSandbox');
37
+ const validateSandboxUsageLimitsSpy = vi.spyOn(sandboxesLib, 'validateSandboxUsageLimits');
38
+ const hubspotAccountNamePromptSpy = vi.spyOn(accountNamePrompt, 'hubspotAccountNamePrompt');
39
+ const mockedHasFeatureV2Sandboxes = hasFeature;
40
+ const mockedHasFeatureV2Cli = hasFeature;
5
41
  describe('commands/sandbox/create', () => {
6
42
  const yargsMock = yargs;
7
43
  describe('command', () => {
@@ -28,4 +64,175 @@ describe('commands/sandbox/create', () => {
28
64
  expect(addUseEnvironmentOptions).toHaveBeenCalledWith(yargsMock);
29
65
  });
30
66
  });
67
+ describe('handler', () => {
68
+ let args;
69
+ const sandboxNameFromPrompt = 'sandbox name from prompt';
70
+ const mockSandbox = {
71
+ sandboxHubId: 56789,
72
+ parentHubId: 123456,
73
+ createdAt: '2025-01-01',
74
+ type: 'DEVELOPER',
75
+ archived: false,
76
+ version: 'V1',
77
+ status: 'PENDING',
78
+ name: 'Test Sandbox',
79
+ domain: 'test-sandbox.hubspot.com',
80
+ createdByUser: {
81
+ userId: 11111,
82
+ email: 'test@test.com',
83
+ firstName: 'Test',
84
+ lastName: 'User',
85
+ },
86
+ };
87
+ beforeEach(() => {
88
+ args = {
89
+ derivedAccountId: 1234567890,
90
+ };
91
+ getAccountConfigSpy.mockReturnValue({
92
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
93
+ env: 'prod',
94
+ });
95
+ hubspotAccountNamePromptSpy.mockResolvedValue({
96
+ name: sandboxNameFromPrompt,
97
+ });
98
+ sandboxTypePromptSpy.mockResolvedValue({
99
+ type: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
100
+ });
101
+ promptUserSpy.mockResolvedValue({
102
+ contactRecordsSyncPrompt: false,
103
+ });
104
+ validateSandboxUsageLimitsSpy.mockResolvedValue(undefined);
105
+ mockedHasFeatureV2Sandboxes.mockResolvedValue(false);
106
+ mockedHasFeatureV2Cli.mockResolvedValue(false);
107
+ buildSandboxSpy.mockResolvedValue({
108
+ sandbox: mockSandbox,
109
+ personalAccessKey: 'mock-personal-access-key',
110
+ name: sandboxNameFromPrompt,
111
+ });
112
+ buildV2SandboxSpy.mockResolvedValue({
113
+ sandbox: { ...mockSandbox, version: 'V2' },
114
+ });
115
+ getAvailableSyncTypesSpy.mockResolvedValue([
116
+ { type: 'object-schemas' },
117
+ { type: 'workflows' },
118
+ ]);
119
+ syncSandboxSpy.mockResolvedValue(undefined);
120
+ // Spy on process.exit so our tests don't close when it's called
121
+ // @ts-expect-error Doesn't match the actual signature because then the linter complains about unused variables
122
+ processExitSpy.mockImplementation(() => { });
123
+ });
124
+ it('should load the account config for the correct account id', async () => {
125
+ await sandboxCreateCommand.handler(args);
126
+ expect(getAccountConfigSpy).toHaveBeenCalledTimes(2); // 1st is for parent account, 2nd is for sandbox account
127
+ expect(getAccountConfigSpy).toHaveBeenCalledWith(args.derivedAccountId);
128
+ });
129
+ it('should track the command usage', async () => {
130
+ await sandboxCreateCommand.handler(args);
131
+ expect(trackCommandUsage).toHaveBeenCalledTimes(1);
132
+ expect(trackCommandUsage).toHaveBeenCalledWith('sandbox-create', {}, args.derivedAccountId);
133
+ });
134
+ it('should validate sandbox usage limits', async () => {
135
+ await sandboxCreateCommand.handler(args);
136
+ expect(validateSandboxUsageLimitsSpy).toHaveBeenCalledTimes(1);
137
+ expect(validateSandboxUsageLimitsSpy).toHaveBeenCalledWith({
138
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
139
+ env: 'prod',
140
+ }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, 'prod');
141
+ });
142
+ it('should prompt for the sandbox type if no type is provided in options', async () => {
143
+ await sandboxCreateCommand.handler(args);
144
+ expect(sandboxTypePromptSpy).toHaveBeenCalledTimes(1);
145
+ });
146
+ it('should not prompt for the sandbox type if type is provided in options', async () => {
147
+ await sandboxCreateCommand.handler({
148
+ ...args,
149
+ type: 'developer',
150
+ });
151
+ expect(sandboxTypePromptSpy).toHaveBeenCalledTimes(0);
152
+ });
153
+ it('should not prompt for contact records sync if the sandbox type is developer', async () => {
154
+ await sandboxCreateCommand.handler({
155
+ ...args,
156
+ type: 'developer',
157
+ });
158
+ expect(promptUserSpy).toHaveBeenCalledTimes(0);
159
+ });
160
+ it('should prompt for the contact records sync if the sandbox type is standard', async () => {
161
+ await sandboxCreateCommand.handler({
162
+ ...args,
163
+ type: 'standard',
164
+ });
165
+ expect(promptUserSpy).toHaveBeenCalledTimes(1);
166
+ });
167
+ it('should build a v1 sandbox if the parent account is not ungated for sandboxes:v2:enabled and not ungated for sandboxes:v2:cliEnabled', async () => {
168
+ await sandboxCreateCommand.handler(args);
169
+ expect(buildSandboxSpy).toHaveBeenCalledTimes(1);
170
+ expect(buildSandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {
171
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
172
+ env: 'prod',
173
+ }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, 'prod', undefined // force
174
+ );
175
+ expect(getAvailableSyncTypesSpy).toHaveBeenCalledTimes(1);
176
+ expect(syncSandboxSpy).toHaveBeenCalledTimes(1);
177
+ });
178
+ it('should build a v1 sandbox if the parent account is ungated for sandboxes:v2:enabled but not ungated for sandboxes:v2:cliEnabled', async () => {
179
+ mockedHasFeatureV2Sandboxes.mockResolvedValue(true);
180
+ mockedHasFeatureV2Cli.mockResolvedValue(false);
181
+ await sandboxCreateCommand.handler(args);
182
+ expect(buildSandboxSpy).toHaveBeenCalledTimes(1);
183
+ expect(buildSandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {
184
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
185
+ env: 'prod',
186
+ }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, 'prod', undefined // force
187
+ );
188
+ expect(getAvailableSyncTypesSpy).toHaveBeenCalledTimes(1);
189
+ expect(syncSandboxSpy).toHaveBeenCalledTimes(1);
190
+ });
191
+ it('should build a v2 sandbox if the parent account is ungated for both sandboxes:v2:enabled and sandboxes:v2:cliEnabled', async () => {
192
+ mockedHasFeatureV2Sandboxes.mockResolvedValue(true);
193
+ mockedHasFeatureV2Cli.mockResolvedValue(true);
194
+ await sandboxCreateCommand.handler(args);
195
+ expect(buildV2SandboxSpy).toHaveBeenCalledTimes(1);
196
+ expect(buildV2SandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {
197
+ accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
198
+ env: 'prod',
199
+ }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, false, // syncObjectRecords
200
+ 'prod', undefined // force
201
+ );
202
+ });
203
+ it('should log an error and exit when force is used and invalid sandbox type is provided', async () => {
204
+ await sandboxCreateCommand.handler({
205
+ ...args,
206
+ name: sandboxNameFromPrompt,
207
+ type: 'invalid',
208
+ force: true,
209
+ });
210
+ expect(logger.error).toHaveBeenCalledTimes(1);
211
+ expect(processExitSpy).toHaveBeenCalled();
212
+ expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
213
+ });
214
+ it('should log an error and exit when force is used and no sandbox name is provided', async () => {
215
+ await sandboxCreateCommand.handler({
216
+ ...args,
217
+ type: 'standard',
218
+ force: true,
219
+ });
220
+ expect(logger.error).toHaveBeenCalled();
221
+ expect(processExitSpy).toHaveBeenCalled();
222
+ expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
223
+ });
224
+ it('should error out if the default account type is not standard', async () => {
225
+ getAccountConfigSpy.mockReturnValue({
226
+ accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
227
+ env: 'prod',
228
+ });
229
+ await sandboxCreateCommand.handler({
230
+ ...args,
231
+ type: 'developer',
232
+ });
233
+ expect(logger.error).toHaveBeenCalled();
234
+ expect(processExitSpy).toHaveBeenCalled();
235
+ expect(processExitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
236
+ });
237
+ });
31
238
  });
@@ -1,5 +1,5 @@
1
1
  import { CommonArgs, ConfigArgs, AccountArgs, EnvironmentArgs, TestingArgs, YargsCommandModule } from '../../types/Yargs.js';
2
- type SandboxCreateArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & TestingArgs & {
2
+ export type SandboxCreateArgs = CommonArgs & ConfigArgs & AccountArgs & EnvironmentArgs & TestingArgs & {
3
3
  name?: string;
4
4
  force?: boolean;
5
5
  type?: string;
@@ -13,9 +13,11 @@ import { sandboxTypePrompt } from '../../lib/prompts/sandboxesPrompt.js';
13
13
  import { promptUser } from '../../lib/prompts/promptUtils.js';
14
14
  import { syncSandbox } from '../../lib/sandboxSync.js';
15
15
  import { logError } from '../../lib/errorHandlers/index.js';
16
- import { buildSandbox } from '../../lib/buildAccount.js';
16
+ import { buildSandbox, buildV2Sandbox } from '../../lib/buildAccount.js';
17
17
  import { hubspotAccountNamePrompt } from '../../lib/prompts/accountNamePrompt.js';
18
18
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
19
+ import { hasFeature } from '../../lib/hasFeature.js';
20
+ import { FEATURES } from '../../lib/constants.js';
19
21
  const command = 'create';
20
22
  const describe = uiBetaTag(i18n(`commands.sandbox.subcommands.create.describe`), false);
21
23
  async function handler(args) {
@@ -84,7 +86,11 @@ async function handler(args) {
84
86
  process.exit(EXIT_CODES.ERROR);
85
87
  }
86
88
  }
87
- const sandboxName = name || namePrompt.name;
89
+ const sandboxName = name || (namePrompt && namePrompt.name);
90
+ if (!sandboxName) {
91
+ logger.error(i18n(`commands.sandbox.subcommands.create.failure.optionMissing.name`));
92
+ process.exit(EXIT_CODES.ERROR);
93
+ }
88
94
  let contactRecordsSyncPromptResult = false;
89
95
  if (!force) {
90
96
  const isStandardSandbox = sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX;
@@ -100,8 +106,18 @@ async function handler(args) {
100
106
  contactRecordsSyncPromptResult = contactRecordsSyncPrompt;
101
107
  }
102
108
  }
109
+ // Check if parent portal is ungated for v2 sandboxes
110
+ const isUngatedForV2Cli = await hasFeature(derivedAccountId, FEATURES.SANDBOXES_V2_CLI);
111
+ const isUngatedForV2Sandboxes = await hasFeature(derivedAccountId, FEATURES.SANDBOXES_V2);
112
+ const canCreateV2Sandbox = isUngatedForV2Sandboxes && isUngatedForV2Cli;
103
113
  try {
104
- const result = await buildSandbox(sandboxName, accountConfig, sandboxType, env, force);
114
+ let result;
115
+ if (canCreateV2Sandbox) {
116
+ result = await buildV2Sandbox(sandboxName, accountConfig, sandboxType, contactRecordsSyncPromptResult, env, force);
117
+ }
118
+ else {
119
+ result = await buildSandbox(sandboxName, accountConfig, sandboxType, env, force);
120
+ }
105
121
  const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId);
106
122
  // Check if sandbox account config exists
107
123
  if (!sandboxAccountConfig) {
@@ -111,20 +127,19 @@ async function handler(args) {
111
127
  }));
112
128
  process.exit(EXIT_CODES.ERROR);
113
129
  }
114
- // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically
115
- async function handleSyncSandbox(syncTasks) {
116
- await syncSandbox(sandboxAccountConfig, accountConfig, env, syncTasks);
117
- }
118
- try {
119
- let availableSyncTasks = await getAvailableSyncTypes(accountConfig, sandboxAccountConfig);
120
- if (!contactRecordsSyncPromptResult) {
121
- availableSyncTasks = availableSyncTasks.filter(t => t.type !== SYNC_TYPES.OBJECT_RECORDS);
130
+ if (result && !canCreateV2Sandbox) {
131
+ // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically
132
+ try {
133
+ let availableSyncTasks = await getAvailableSyncTypes(accountConfig, sandboxAccountConfig);
134
+ if (!contactRecordsSyncPromptResult) {
135
+ availableSyncTasks = availableSyncTasks.filter(t => t.type !== SYNC_TYPES.OBJECT_RECORDS);
136
+ }
137
+ await syncSandbox(sandboxAccountConfig, accountConfig, env, availableSyncTasks);
138
+ }
139
+ catch (err) {
140
+ logError(err);
141
+ throw err;
122
142
  }
123
- await handleSyncSandbox(availableSyncTasks);
124
- }
125
- catch (err) {
126
- logError(err);
127
- throw err;
128
143
  }
129
144
  const highlightItems = ['accountsUseCommand', 'projectCreateCommand'];
130
145
  if (sandboxType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) {
@@ -7,7 +7,7 @@ import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
7
7
  import { uiLogger } from '../../lib/ui/logger.js';
8
8
  import { trackCommandUsage } from '../../lib/usageTracking.js';
9
9
  import { commands } from '../../lang/en.js';
10
- import { createDeveloperTestAccountConfigPrompt, } from '../../lib/prompts/createDeveloperTestAccountConfigPrompt.js';
10
+ import { createDeveloperTestAccountConfigPrompt } from '../../lib/prompts/createDeveloperTestAccountConfigPrompt.js';
11
11
  import { fileExists } from '../../lib/validation.js';
12
12
  const command = 'create-config';
13
13
  const describe = commands.testAccount.createConfig.describe;
package/lang/en.d.ts CHANGED
@@ -79,6 +79,7 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
79
79
  readonly personalAccessKey: "Enter existing personal access key";
80
80
  };
81
81
  readonly errors: {
82
+ readonly invalidAccountIdProvided: "--account must be a number.";
82
83
  readonly failedToUpdateConfig: "Failed to update the configuration file. Please try again.";
83
84
  readonly migrationNotConfirmed: `Did not migrate your configuration file. Run ${string} to update your existing config, or use ${string} to switch to the new global configuration.`;
84
85
  readonly mergeNotConfirmed: `Did not merge configuration files. When you are ready to merge the deprecated config file with the global config file, run ${string}.`;
@@ -252,6 +253,9 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
252
253
  readonly allowAutoUpdates: {
253
254
  readonly describe: "Enable or disable auto updates";
254
255
  };
256
+ readonly autoOpenBrowser: {
257
+ readonly describe: "Enable or disable automatic opening of the browser";
258
+ };
255
259
  };
256
260
  };
257
261
  };
@@ -825,9 +829,9 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
825
829
  readonly setup: {
826
830
  readonly installingDocSearch: "Adding the docs-search mcp server";
827
831
  readonly claudeCode: "Claude Code";
828
- readonly claudeDesktop: "Claude Desktop";
829
832
  readonly cursor: "Cursor";
830
833
  readonly windsurf: "Windsurf";
834
+ readonly vsCode: "VSCode";
831
835
  readonly args: {
832
836
  readonly client: "Target applications to configure";
833
837
  readonly docsSearch: "Should the docs search mcp server be installed";
@@ -839,8 +843,6 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
839
843
  };
840
844
  readonly spinners: {
841
845
  readonly failedToConfigure: "Failed to configure the HubSpot mcp server.";
842
- readonly configuringClaudeDesktop: "Configuring Claude Desktop...";
843
- readonly configuredClaudeDesktop: "Configured Claude Desktop";
844
846
  readonly configuringClaudeCode: "Configuring Claude Code...";
845
847
  readonly configuredClaudeCode: "Configured Claude Code";
846
848
  readonly claudeCodeNotFound: "Claude Code not found - skipping configuration";
@@ -853,6 +855,10 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
853
855
  readonly configuringWindsurf: "Configuring Windsurf...";
854
856
  readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
855
857
  readonly configuredWindsurf: "Configured Windsurf";
858
+ readonly configuringVsCode: "Configuring VSCode...";
859
+ readonly failedToConfigureVsCode: "Failed to configure VSCode";
860
+ readonly configuredVsCode: "Configured VSCode";
861
+ readonly vsCodeNotFound: "VSCode not found - skipping configuration";
856
862
  };
857
863
  readonly prompts: {
858
864
  readonly targets: "[--client] Which tools would you like to add the HubSpot CLI MCP server to?";
@@ -1383,6 +1389,7 @@ ${string}`;
1383
1389
  readonly default: "Validate the project before uploading";
1384
1390
  };
1385
1391
  readonly success: (projectName: string) => string;
1392
+ readonly failure: (projectName: string) => string;
1386
1393
  readonly options: {
1387
1394
  readonly profile: {
1388
1395
  readonly describe: "The profile to target for this validation";
@@ -2459,7 +2466,8 @@ export declare const lib: {
2459
2466
  readonly running: (projectName: string, accountIdentifier: string) => string;
2460
2467
  readonly quitHelper: `Press ${string} to stop the local dev server`;
2461
2468
  readonly viewProjectLink: (name: string, accountId: number) => string;
2462
- readonly viewLocalDevUILink: (accountId: number) => string;
2469
+ readonly viewLocalDevUILink: (accountId: number, showWelcomeScreen: boolean) => string;
2470
+ readonly localDevUIAutoMessage: (accountId: number, showWelcomeScreen: boolean) => string;
2463
2471
  readonly viewTestAccountLink: "View developer test account in HubSpot";
2464
2472
  readonly exitingStart: "Stopping local dev server ...";
2465
2473
  readonly exitingSucceed: "Successfully exited";
@@ -2791,6 +2799,11 @@ Run ${string} to upgrade to version ${string}`;
2791
2799
  readonly promptMessage: "Enter http timeout duration";
2792
2800
  readonly success: (timeout: string) => string;
2793
2801
  };
2802
+ readonly setAutoOpenBrowser: {
2803
+ readonly fieldName: "auto open browser";
2804
+ readonly enabled: "Auto opening your browser has been enabled";
2805
+ readonly disabled: "Auto opening your browser has been disabled";
2806
+ };
2794
2807
  };
2795
2808
  readonly commonOpts: {
2796
2809
  readonly options: {