@hubspot/cli 7.7.23-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 (69) hide show
  1. package/commands/account/auth.js +15 -4
  2. package/commands/auth.js +1 -1
  3. package/commands/mcp/start.d.ts +1 -0
  4. package/commands/mcp/start.js +12 -4
  5. package/commands/project/create.js +2 -2
  6. package/commands/project/validate.js +1 -0
  7. package/commands/sandbox/__tests__/create.test.js +207 -0
  8. package/commands/sandbox/create.d.ts +1 -1
  9. package/commands/sandbox/create.js +31 -16
  10. package/lang/en.d.ts +7 -3
  11. package/lang/en.js +12 -5
  12. package/lang/en.lyaml +4 -2
  13. package/lib/__tests__/buildAccount.test.js +62 -4
  14. package/lib/buildAccount.d.ts +4 -1
  15. package/lib/buildAccount.js +57 -2
  16. package/lib/commonOpts.js +25 -0
  17. package/lib/constants.d.ts +4 -0
  18. package/lib/constants.js +4 -0
  19. package/lib/errorHandlers/index.js +1 -3
  20. package/lib/errors/ProjectValidationError.d.ts +4 -0
  21. package/lib/errors/ProjectValidationError.js +9 -0
  22. package/lib/mcp/setup.d.ts +4 -0
  23. package/lib/mcp/setup.js +36 -0
  24. package/lib/projects/__tests__/LocalDevProcess.test.js +35 -0
  25. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +170 -1
  26. package/lib/projects/add/v3AddComponent.js +2 -1
  27. package/lib/projects/create/index.js +2 -2
  28. package/lib/projects/create/v3.d.ts +0 -2
  29. package/lib/projects/create/v3.js +1 -3
  30. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -0
  31. package/lib/projects/localDev/LocalDevProcess.js +3 -0
  32. package/lib/projects/localDev/LocalDevState.d.ts +1 -0
  33. package/lib/projects/localDev/LocalDevState.js +5 -0
  34. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -2
  35. package/lib/projects/localDev/LocalDevWebsocketServer.js +35 -29
  36. package/lib/projects/upload.js +5 -12
  37. package/lib/sandboxes.d.ts +4 -0
  38. package/lib/sandboxes.js +4 -0
  39. package/lib/ui/index.d.ts +6 -0
  40. package/lib/ui/index.js +3 -5
  41. package/mcp-server/tools/index.js +6 -4
  42. package/mcp-server/tools/project/{AddFeatureToProject.d.ts → AddFeatureToProjectTool.d.ts} +4 -4
  43. package/mcp-server/tools/project/{AddFeatureToProject.js → AddFeatureToProjectTool.js} +6 -14
  44. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  45. package/mcp-server/tools/project/CreateProjectTool.js +4 -14
  46. package/mcp-server/tools/project/{DeployProject.d.ts → DeployProjectTool.d.ts} +1 -1
  47. package/mcp-server/tools/project/{DeployProject.js → DeployProjectTool.js} +2 -2
  48. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +20 -0
  49. package/mcp-server/tools/project/GetConfigValuesTool.js +51 -0
  50. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  51. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  52. package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.js → AddFeatureToProjectTool.test.js} +7 -7
  53. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +3 -4
  54. package/mcp-server/tools/project/__tests__/{DeployProject.test.js → DeployProjectTool.test.js} +4 -4
  55. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +198 -0
  56. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +2 -2
  57. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  58. package/mcp-server/tools/project/constants.d.ts +1 -0
  59. package/mcp-server/tools/project/constants.js +11 -0
  60. package/mcp-server/utils/__tests__/command.test.js +76 -3
  61. package/mcp-server/utils/command.d.ts +6 -0
  62. package/mcp-server/utils/command.js +19 -0
  63. package/package.json +2 -2
  64. package/mcp-server/utils/__tests__/project.test.js +0 -79
  65. package/mcp-server/utils/project.d.ts +0 -5
  66. package/mcp-server/utils/project.js +0 -14
  67. /package/mcp-server/tools/project/__tests__/{AddFeatureToProject.test.d.ts → AddFeatureToProjectTool.test.d.ts} +0 -0
  68. /package/mcp-server/tools/project/__tests__/{DeployProject.test.d.ts → DeployProjectTool.test.d.ts} +0 -0
  69. /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()) ||
@@ -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) {
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}.`;
@@ -828,9 +829,9 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
828
829
  readonly setup: {
829
830
  readonly installingDocSearch: "Adding the docs-search mcp server";
830
831
  readonly claudeCode: "Claude Code";
831
- readonly claudeDesktop: "Claude Desktop";
832
832
  readonly cursor: "Cursor";
833
833
  readonly windsurf: "Windsurf";
834
+ readonly vsCode: "VSCode";
834
835
  readonly args: {
835
836
  readonly client: "Target applications to configure";
836
837
  readonly docsSearch: "Should the docs search mcp server be installed";
@@ -842,8 +843,6 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
842
843
  };
843
844
  readonly spinners: {
844
845
  readonly failedToConfigure: "Failed to configure the HubSpot mcp server.";
845
- readonly configuringClaudeDesktop: "Configuring Claude Desktop...";
846
- readonly configuredClaudeDesktop: "Configured Claude Desktop";
847
846
  readonly configuringClaudeCode: "Configuring Claude Code...";
848
847
  readonly configuredClaudeCode: "Configured Claude Code";
849
848
  readonly claudeCodeNotFound: "Claude Code not found - skipping configuration";
@@ -856,6 +855,10 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
856
855
  readonly configuringWindsurf: "Configuring Windsurf...";
857
856
  readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
858
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";
859
862
  };
860
863
  readonly prompts: {
861
864
  readonly targets: "[--client] Which tools would you like to add the HubSpot CLI MCP server to?";
@@ -1386,6 +1389,7 @@ ${string}`;
1386
1389
  readonly default: "Validate the project before uploading";
1387
1390
  };
1388
1391
  readonly success: (projectName: string) => string;
1392
+ readonly failure: (projectName: string) => string;
1389
1393
  readonly options: {
1390
1394
  readonly profile: {
1391
1395
  readonly describe: "The profile to target for this validation";
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')}.`,
@@ -831,9 +831,9 @@ export const commands = {
831
831
  setup: {
832
832
  installingDocSearch: 'Adding the docs-search mcp server',
833
833
  claudeCode: 'Claude Code',
834
- claudeDesktop: 'Claude Desktop',
835
834
  cursor: 'Cursor',
836
835
  windsurf: 'Windsurf',
836
+ vsCode: 'VSCode',
837
837
  args: {
838
838
  client: 'Target applications to configure',
839
839
  docsSearch: 'Should the docs search mcp server be installed',
@@ -845,20 +845,26 @@ export const commands = {
845
845
  },
846
846
  spinners: {
847
847
  failedToConfigure: 'Failed to configure the HubSpot mcp server.',
848
- configuringClaudeDesktop: 'Configuring Claude Desktop...',
849
- configuredClaudeDesktop: 'Configured Claude Desktop',
848
+ // Claude
850
849
  configuringClaudeCode: 'Configuring Claude Code...',
851
850
  configuredClaudeCode: 'Configured Claude Code',
852
851
  claudeCodeNotFound: 'Claude Code not found - skipping configuration',
853
852
  claudeCodeInstallFailed: 'Claude Code CLI not working - skipping configuration',
854
853
  failedToConfigureClaudeDesktop: 'Failed to configure Claude Desktop',
854
+ // Cursor
855
855
  configuringCursor: 'Configuring Cursor...',
856
856
  failedToConfigureCursor: 'Failed to configure Cursor',
857
857
  configuredCursor: 'Configured Cursor',
858
858
  alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
859
+ // Windsurf
859
860
  configuringWindsurf: 'Configuring Windsurf...',
860
861
  failedToConfigureWindsurf: 'Failed to configure Windsurf',
861
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',
862
868
  },
863
869
  prompts: {
864
870
  targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
@@ -1378,6 +1384,7 @@ export const commands = {
1378
1384
  default: 'Validate the project before uploading',
1379
1385
  },
1380
1386
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
1387
+ failure: (projectName) => `Project ${projectName} is invalid`,
1381
1388
  options: {
1382
1389
  profile: {
1383
1390
  describe: 'The profile to target for this validation',
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 }}"