@hubspot/cli 8.0.10-experimental.1 → 8.0.10-experimental.3

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 (111) hide show
  1. package/commands/account/auth.js +15 -5
  2. package/commands/account/use.js +14 -4
  3. package/commands/auth.js +10 -6
  4. package/commands/cms/__tests__/watch.test.js +0 -8
  5. package/commands/cms/function/logs.js +1 -0
  6. package/commands/cms/theme/preview.js +2 -4
  7. package/commands/cms/watch.d.ts +0 -1
  8. package/commands/cms/watch.js +2 -8
  9. package/commands/feedback.js +1 -1
  10. package/commands/hubdb/clear.js +4 -0
  11. package/commands/hubdb/delete.js +4 -0
  12. package/commands/hubdb/fetch.js +4 -0
  13. package/commands/init.js +4 -0
  14. package/commands/mcp/__tests__/start.test.js +8 -1
  15. package/commands/mcp/setup.js +1 -9
  16. package/commands/mcp/start.js +0 -1
  17. package/commands/project/__tests__/create.test.js +1 -1
  18. package/commands/project/create.js +2 -2
  19. package/commands/project/dev/index.js +29 -19
  20. package/commands/project/download.js +5 -1
  21. package/commands/project/watch.js +15 -2
  22. package/commands/sandbox/__tests__/create.test.js +1 -48
  23. package/commands/sandbox/create.js +3 -30
  24. package/commands/testAccount/create.js +4 -0
  25. package/lang/en.d.ts +13 -6
  26. package/lang/en.js +13 -6
  27. package/lib/__tests__/buildAccount.test.js +1 -52
  28. package/lib/__tests__/sandboxes.test.js +1 -29
  29. package/lib/__tests__/serverlessLogs.test.js +79 -64
  30. package/lib/accountAuth.js +4 -0
  31. package/lib/buildAccount.d.ts +1 -6
  32. package/lib/buildAccount.js +9 -42
  33. package/lib/constants.d.ts +1 -3
  34. package/lib/constants.js +1 -3
  35. package/lib/errors/PromptExitError.d.ts +4 -0
  36. package/lib/errors/PromptExitError.js +8 -0
  37. package/lib/generateSelectors.js +1 -2
  38. package/lib/mcp/__tests__/setup.test.js +357 -28
  39. package/lib/mcp/setup.d.ts +1 -0
  40. package/lib/mcp/setup.js +77 -30
  41. package/lib/projects/__tests__/components.test.js +14 -0
  42. package/lib/projects/components.js +12 -2
  43. package/lib/projects/create/__tests__/legacy.test.js +6 -24
  44. package/lib/projects/create/index.js +1 -4
  45. package/lib/projects/create/legacy.js +3 -8
  46. package/lib/projects/create/v2.js +1 -9
  47. package/lib/projects/ensureProjectExists.js +1 -2
  48. package/lib/projects/localDev/AppDevModeInterface.js +4 -0
  49. package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +4 -0
  50. package/lib/projects/localDev/helpers/account.js +5 -11
  51. package/lib/projects/pollProjectBuildAndDeploy.js +90 -85
  52. package/lib/projects/upload.d.ts +1 -0
  53. package/lib/projects/upload.js +37 -46
  54. package/lib/projects/watch.d.ts +2 -1
  55. package/lib/projects/watch.js +32 -24
  56. package/lib/prompts/downloadProjectPrompt.js +11 -10
  57. package/lib/prompts/installAppPrompt.js +3 -2
  58. package/lib/prompts/personalAccessKeyPrompt.js +3 -2
  59. package/lib/prompts/projectDevTargetAccountPrompt.js +13 -16
  60. package/lib/prompts/selectHubDBTablePrompt.js +8 -4
  61. package/lib/prompts/selectPublicAppForMigrationPrompt.js +12 -6
  62. package/lib/sandboxes.d.ts +1 -9
  63. package/lib/sandboxes.js +0 -21
  64. package/lib/serverlessLogs.js +50 -44
  65. package/lib/{cms/devServerProcess.d.ts → theme/cmsDevServerProcess.d.ts} +2 -3
  66. package/lib/theme/cmsDevServerProcess.js +148 -0
  67. package/lib/theme/cmsDevServerRunner.d.ts +14 -0
  68. package/lib/theme/cmsDevServerRunner.js +90 -0
  69. package/lib/usageTracking.js +8 -5
  70. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  71. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  72. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  73. package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
  74. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  75. package/mcp-server/tools/cms/HsListTool.js +1 -1
  76. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
  77. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -2
  78. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -2
  79. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +1 -2
  80. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -2
  81. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -2
  82. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
  83. package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
  84. package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -1
  85. package/mcp-server/tools/project/CreateProjectTool.js +1 -1
  86. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  87. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  88. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  89. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  90. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -2
  91. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -2
  92. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +1 -2
  93. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -2
  94. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
  95. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  96. package/mcp-server/tools/project/constants.d.ts +1 -1
  97. package/mcp-server/utils/__tests__/command.test.js +233 -3
  98. package/mcp-server/utils/__tests__/feedbackTracking.test.js +9 -64
  99. package/mcp-server/utils/command.d.ts +5 -0
  100. package/mcp-server/utils/command.js +24 -0
  101. package/mcp-server/utils/feedbackTracking.js +2 -17
  102. package/package.json +4 -5
  103. package/lib/__tests__/sandboxSync.test.d.ts +0 -1
  104. package/lib/__tests__/sandboxSync.test.js +0 -147
  105. package/lib/cms/devServerProcess.js +0 -200
  106. package/lib/sandboxSync.d.ts +0 -4
  107. package/lib/sandboxSync.js +0 -102
  108. package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
  109. package/mcp-server/utils/__tests__/project.test.js +0 -140
  110. package/mcp-server/utils/project.d.ts +0 -5
  111. package/mcp-server/utils/project.js +0 -18
@@ -10,6 +10,7 @@ import { makeYargsBuilder } from '../../lib/yargsUtils.js';
10
10
  import { commands } from '../../lang/en.js';
11
11
  import { uiLogger } from '../../lib/ui/logger.js';
12
12
  import { authenticateNewAccount } from '../../lib/accountAuth.js';
13
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
13
14
  const TRACKING_STATUS = {
14
15
  STARTED: 'started',
15
16
  ERROR: 'error',
@@ -35,11 +36,20 @@ async function handler(args) {
35
36
  await trackAuthAction('account-auth', authType, TRACKING_STATUS.STARTED);
36
37
  }
37
38
  handleExit(deleteConfigFileIfEmpty);
38
- const updatedConfig = await authenticateNewAccount({
39
- env: args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD,
40
- providedPersonalAccessKey,
41
- accountId: parsedUserProvidedAccountId,
42
- });
39
+ let updatedConfig;
40
+ try {
41
+ updatedConfig = await authenticateNewAccount({
42
+ env: args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD,
43
+ providedPersonalAccessKey,
44
+ accountId: parsedUserProvidedAccountId,
45
+ });
46
+ }
47
+ catch (e) {
48
+ if (e instanceof PromptExitError) {
49
+ process.exit(e.exitCode);
50
+ }
51
+ throw e;
52
+ }
43
53
  if (!updatedConfig) {
44
54
  if (!disableTracking) {
45
55
  await trackAuthAction('account-auth', authType, TRACKING_STATUS.ERROR);
@@ -7,6 +7,7 @@ import { uiLogger } from '../../lib/ui/logger.js';
7
7
  import { selectAccountFromConfig, AUTHENTICATE_NEW_ACCOUNT_VALUE, } from '../../lib/prompts/accountsPrompt.js';
8
8
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
9
9
  import { authenticateNewAccount } from '../../lib/accountAuth.js';
10
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
10
11
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
11
12
  const command = 'use [account]';
12
13
  const describe = commands.account.subcommands.use.describe;
@@ -24,10 +25,19 @@ async function handler(args) {
24
25
  }
25
26
  }
26
27
  if (newDefaultAccount === AUTHENTICATE_NEW_ACCOUNT_VALUE) {
27
- const updatedConfig = await authenticateNewAccount({
28
- env: args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD,
29
- setAsDefaultAccount: true,
30
- });
28
+ let updatedConfig;
29
+ try {
30
+ updatedConfig = await authenticateNewAccount({
31
+ env: args.qa ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD,
32
+ setAsDefaultAccount: true,
33
+ });
34
+ }
35
+ catch (e) {
36
+ if (e instanceof PromptExitError) {
37
+ process.exit(e.exitCode);
38
+ }
39
+ throw e;
40
+ }
31
41
  if (!updatedConfig) {
32
42
  process.exit(EXIT_CODES.ERROR);
33
43
  }
package/commands/auth.js CHANGED
@@ -16,6 +16,7 @@ import { authenticateWithOauth } from '../lib/oauth.js';
16
16
  import { EXIT_CODES } from '../lib/enums/exitCodes.js';
17
17
  import { uiFeatureHighlight } from '../lib/ui/index.js';
18
18
  import { logError } from '../lib/errorHandlers/index.js';
19
+ import { PromptExitError } from '../lib/errors/PromptExitError.js';
19
20
  import { commands } from '../lang/en.js';
20
21
  import { uiLogger } from '../lib/ui/logger.js';
21
22
  import { parseStringToNumber } from '../lib/parsing.js';
@@ -90,18 +91,21 @@ async function handler(args) {
90
91
  }
91
92
  break;
92
93
  case PERSONAL_ACCESS_KEY_AUTH_METHOD.value:
93
- const { personalAccessKey } = providedPersonalAccessKey
94
- ? { personalAccessKey: providedPersonalAccessKey }
95
- : await personalAccessKeyPrompt({
96
- env,
97
- account: parsedUserProvidedAccountId,
98
- });
99
94
  try {
95
+ const { personalAccessKey } = providedPersonalAccessKey
96
+ ? { personalAccessKey: providedPersonalAccessKey }
97
+ : await personalAccessKeyPrompt({
98
+ env,
99
+ account: parsedUserProvidedAccountId,
100
+ });
100
101
  token = await getAccessToken(personalAccessKey, env);
101
102
  defaultName = token.hubName ? toKebabCase(token.hubName) : undefined;
102
103
  updatedConfig = await updateConfigWithAccessToken(token, personalAccessKey, env);
103
104
  }
104
105
  catch (e) {
106
+ if (e instanceof PromptExitError) {
107
+ process.exit(e.exitCode);
108
+ }
105
109
  logError(e);
106
110
  }
107
111
  if (!updatedConfig) {
@@ -64,7 +64,6 @@ describe('commands/cms/watch', () => {
64
64
  expect(positionalSpy).toHaveBeenCalledWith('dest', expect.objectContaining({ type: 'string' }));
65
65
  expect(optionSpy).toHaveBeenCalledWith('remove', expect.objectContaining({ type: 'boolean', alias: 'r' }));
66
66
  expect(optionSpy).toHaveBeenCalledWith('initial-upload', expect.objectContaining({ type: 'boolean', alias: 'i' }));
67
- expect(optionSpy).toHaveBeenCalledWith('disable-initial', expect.objectContaining({ type: 'boolean' }));
68
67
  expect(optionSpy).toHaveBeenCalledWith('notify', expect.objectContaining({ type: 'string', alias: 'n' }));
69
68
  expect(optionSpy).toHaveBeenCalledWith('convert-fields', expect.objectContaining({ type: 'boolean' }));
70
69
  });
@@ -78,7 +77,6 @@ describe('commands/cms/watch', () => {
78
77
  derivedAccountId: 123456,
79
78
  remove: false,
80
79
  initialUpload: false,
81
- disableInitial: false,
82
80
  };
83
81
  statSyncSpy.mockReturnValue({
84
82
  isFile: () => false,
@@ -134,7 +132,6 @@ describe('commands/cms/watch', () => {
134
132
  });
135
133
  it('should start watching without initial upload by default', async () => {
136
134
  await watchCommand.handler(args);
137
- expect(uiLogger.info).toHaveBeenCalledWith(expect.stringContaining('not'));
138
135
  expect(getUploadableFileListSpy).not.toHaveBeenCalled();
139
136
  expect(watchSpy).toHaveBeenCalledWith(123456, path.resolve('/test/cwd', 'src'), '/dest', expect.objectContaining({
140
137
  cmsPublishMode: 'publish',
@@ -152,11 +149,6 @@ describe('commands/cms/watch', () => {
152
149
  filePaths: ['file1.js', 'file2.js'],
153
150
  }), null, expect.any(Function), undefined, expect.any(Function));
154
151
  });
155
- it('should show disable initial message when disableInitial is true', async () => {
156
- args.disableInitial = true;
157
- await watchCommand.handler(args);
158
- expect(uiLogger.info).toHaveBeenCalledWith(expect.stringContaining('disable'));
159
- });
160
152
  it('should pass remove option to watch', async () => {
161
153
  args.remove = true;
162
154
  await watchCommand.handler(args);
@@ -35,6 +35,7 @@ const endpointLog = async (accountId, functionPath, options) => {
35
35
  }
36
36
  };
37
37
  await tailLogs(accountId, functionPath, fetchLatest, tailCall, compact);
38
+ process.exit(EXIT_CODES.SUCCESS);
38
39
  }
39
40
  else if (latest) {
40
41
  try {
@@ -3,9 +3,7 @@ import path from 'path';
3
3
  import { commands } from '../../../lang/en.js';
4
4
  import { getCwd } from '@hubspot/local-dev-lib/path';
5
5
  import { getThemeJSONPath } from '@hubspot/local-dev-lib/cms/themes';
6
- // Use subprocess approach to avoid React version conflicts
7
- // import { createDevServer } from '@hubspot/cms-dev-server';
8
- import { spawnDevServer } from '../../../lib/cms/devServerProcess.js';
6
+ import { spawnDevServer } from '../../../lib/theme/cmsDevServerProcess.js';
9
7
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
10
8
  import { previewPrompt, previewProjectPrompt, } from '../../../lib/prompts/previewPrompt.js';
11
9
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
@@ -74,7 +72,7 @@ async function handler(args) {
74
72
  trackCommandUsage('preview', {}, derivedAccountId);
75
73
  // Spawn dev server in isolated subprocess to avoid React version conflicts
76
74
  // File listing and progress bars are handled within the subprocess
77
- spawnDevServer({
75
+ await spawnDevServer({
78
76
  absoluteSrc,
79
77
  accountName: derivedAccountId?.toString(),
80
78
  noSsl,
@@ -5,7 +5,6 @@ export type WatchCommandArgs = ConfigArgs & AccountArgs & EnvironmentArgs & Comm
5
5
  fieldOptions?: string[];
6
6
  remove?: boolean;
7
7
  initialUpload?: boolean;
8
- disableInitial?: boolean;
9
8
  notify?: string;
10
9
  convertFields?: boolean;
11
10
  saveOutput?: boolean;
@@ -15,7 +15,7 @@ import { uiLogger } from '../../lib/ui/logger.js';
15
15
  const command = 'watch [src] [dest]';
16
16
  const describe = commands.cms.subcommands.watch.describe;
17
17
  const handler = async (args) => {
18
- const { remove, initialUpload, disableInitial, notify, derivedAccountId } = args;
18
+ const { remove, initialUpload, notify, derivedAccountId } = args;
19
19
  if (!validateCmsPublishMode(args)) {
20
20
  process.exit(EXIT_CODES.ERROR);
21
21
  }
@@ -40,13 +40,6 @@ const handler = async (args) => {
40
40
  return;
41
41
  }
42
42
  let filesToUpload = [];
43
- if (disableInitial) {
44
- uiLogger.info(commands.cms.subcommands.watch.warnings.disableInitial);
45
- }
46
- else if (!initialUpload) {
47
- uiLogger.info(commands.cms.subcommands.watch.warnings.notUploaded(src));
48
- uiLogger.info(commands.cms.subcommands.watch.warnings.initialUpload);
49
- }
50
43
  if (initialUpload) {
51
44
  filesToUpload = await getUploadableFileList(absoluteSrcPath, args.convertFields);
52
45
  }
@@ -99,6 +92,7 @@ function watchBuilder(yargs) {
99
92
  describe: commands.cms.subcommands.watch.options.initialUpload,
100
93
  type: 'boolean',
101
94
  });
95
+ // TODO: remove this before the next major release
102
96
  yargs.option('disable-initial', {
103
97
  describe: commands.cms.subcommands.watch.options.disableInitial,
104
98
  type: 'boolean',
@@ -4,7 +4,7 @@ import { makeYargsBuilder } from '../lib/yargsUtils.js';
4
4
  import { EXIT_CODES } from '../lib/enums/exitCodes.js';
5
5
  import { commands } from '../lang/en.js';
6
6
  import { uiLogger } from '../lib/ui/logger.js';
7
- const FEEDBACK_URL = 'https://developers.hubspot.com/feedback';
7
+ import { FEEDBACK_URL } from '../lib/constants.js';
8
8
  const command = 'feedback';
9
9
  const describe = commands.project.feedback.describe;
10
10
  async function handler() {
@@ -1,5 +1,6 @@
1
1
  import { uiLogger } from '../../lib/ui/logger.js';
2
2
  import { logError } from '../../lib/errorHandlers/index.js';
3
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
3
4
  import { clearHubDbTableRows } from '@hubspot/local-dev-lib/hubdb';
4
5
  import { publishTable } from '@hubspot/local-dev-lib/api/hubdb';
5
6
  import { selectHubDBTablePrompt } from '../../lib/prompts/selectHubDBTablePrompt.js';
@@ -29,6 +30,9 @@ async function handler(args) {
29
30
  }
30
31
  }
31
32
  catch (e) {
33
+ if (e instanceof PromptExitError) {
34
+ process.exit(e.exitCode);
35
+ }
32
36
  logError(e);
33
37
  }
34
38
  }
@@ -1,5 +1,6 @@
1
1
  import { uiLogger } from '../../lib/ui/logger.js';
2
2
  import { logError } from '../../lib/errorHandlers/index.js';
3
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
3
4
  import { deleteTable } from '@hubspot/local-dev-lib/api/hubdb';
4
5
  import { trackCommandUsage } from '../../lib/usageTracking.js';
5
6
  import { selectHubDBTablePrompt } from '../../lib/prompts/selectHubDBTablePrompt.js';
@@ -34,6 +35,9 @@ async function handler(args) {
34
35
  process.exit(EXIT_CODES.SUCCESS);
35
36
  }
36
37
  catch (e) {
38
+ if (e instanceof PromptExitError) {
39
+ process.exit(e.exitCode);
40
+ }
37
41
  uiLogger.error(commands.hubdb.subcommands.delete.errors.delete(args.tableId || ''));
38
42
  logError(e);
39
43
  }
@@ -1,5 +1,6 @@
1
1
  import { uiLogger } from '../../lib/ui/logger.js';
2
2
  import { logError } from '../../lib/errorHandlers/index.js';
3
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
3
4
  import { downloadHubDbTable } from '@hubspot/local-dev-lib/hubdb';
4
5
  import { selectHubDBTablePrompt } from '../../lib/prompts/selectHubDBTablePrompt.js';
5
6
  import { trackCommandUsage } from '../../lib/usageTracking.js';
@@ -22,6 +23,9 @@ async function handler(args) {
22
23
  uiLogger.success(commands.hubdb.subcommands.fetch.success.fetch(tableId, filePath));
23
24
  }
24
25
  catch (e) {
26
+ if (e instanceof PromptExitError) {
27
+ process.exit(e.exitCode);
28
+ }
25
29
  logError(e);
26
30
  }
27
31
  }
package/commands/init.js CHANGED
@@ -12,6 +12,7 @@ import { setCLILogLevel } from '../lib/commonOpts.js';
12
12
  import { makeYargsBuilder } from '../lib/yargsUtils.js';
13
13
  import { handleExit } from '../lib/process.js';
14
14
  import { debugError, logError } from '../lib/errorHandlers/index.js';
15
+ import { PromptExitError } from '../lib/errors/PromptExitError.js';
15
16
  import { trackCommandUsage, trackAuthAction } from '../lib/usageTracking.js';
16
17
  import { promptUser } from '../lib/prompts/promptUtils.js';
17
18
  import { OAUTH_FLOW, personalAccessKeyPrompt, } from '../lib/prompts/personalAccessKeyPrompt.js';
@@ -150,6 +151,9 @@ async function handler(args) {
150
151
  process.exit(EXIT_CODES.SUCCESS);
151
152
  }
152
153
  catch (err) {
154
+ if (err instanceof PromptExitError) {
155
+ process.exit(err.exitCode);
156
+ }
153
157
  logError(err);
154
158
  if (!disableTracking) {
155
159
  await trackAuthAction('init', authType, TRACKING_STATUS.ERROR, parsedUserProvidedAccountId);
@@ -7,14 +7,20 @@ import * as errorHandlers from '../../../lib/errorHandlers/index.js';
7
7
  import * as usageTrackingLib from '../../../lib/usageTracking.js';
8
8
  import * as processLib from '../../../lib/process.js';
9
9
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
10
- import startCommand from '../start.js';
10
+ // Create a mock execAsync function before importing the module
11
+ const execAsyncMock = vi.fn();
11
12
  vi.mock('yargs');
12
13
  vi.mock('../../../lib/commonOpts');
13
14
  vi.mock('node:child_process');
15
+ vi.mock('node:util', () => ({
16
+ promisify: vi.fn(() => execAsyncMock),
17
+ }));
14
18
  vi.mock('fs');
15
19
  vi.mock('@hubspot/local-dev-lib/config');
16
20
  vi.mock('../../../lib/errorHandlers/index.js');
17
21
  vi.mock('../../../lib/process.js');
22
+ // Import after mocks are set up
23
+ const startCommand = await import('../start.js').then(m => m.default);
18
24
  const spawnSpy = vi.mocked(spawn);
19
25
  const existsSyncSpy = vi.spyOn(fs, 'existsSync');
20
26
  const trackCommandUsageSpy = vi.spyOn(usageTrackingLib, 'trackCommandUsage');
@@ -36,6 +42,7 @@ describe('commands/mcp/start', () => {
36
42
  processExitSpy.mockImplementation(() => { });
37
43
  // Mock config to prevent reading actual config file in CI
38
44
  getConfigAccountIfExistsSpy.mockReturnValue(undefined);
45
+ execAsyncMock.mockClear();
39
46
  });
40
47
  describe('command', () => {
41
48
  it('should have the correct command structure', () => {
@@ -1,21 +1,13 @@
1
1
  import { EXIT_CODES } from '../../lib/enums/exitCodes.js';
2
2
  import { makeYargsBuilder } from '../../lib/yargsUtils.js';
3
3
  import { commands } from '../../lang/en.js';
4
- import { uiLogger } from '../../lib/ui/logger.js';
5
4
  import { addMcpServerToConfig, supportedTools } from '../../lib/mcp/setup.js';
6
5
  import { trackCommandUsage } from '../../lib/usageTracking.js';
7
- import { hasFeature } from '../../lib/hasFeature.js';
8
- import { FEATURES } from '../../lib/constants.js';
9
6
  const command = ['setup'];
10
7
  const describe = commands.mcp.setup.describe;
11
8
  async function handler(args) {
12
9
  const { derivedAccountId } = args;
13
- const hasMcpAccess = await hasFeature(derivedAccountId, FEATURES.MCP_ACCESS);
14
- if (!hasMcpAccess) {
15
- uiLogger.error(commands.mcp.setup.errors.needsMcpAccess(derivedAccountId));
16
- process.exit(EXIT_CODES.ERROR);
17
- }
18
- trackCommandUsage('mcp-setup', {}, args.derivedAccountId);
10
+ await trackCommandUsage('mcp-setup', {}, derivedAccountId);
19
11
  try {
20
12
  await addMcpServerToConfig(args.client);
21
13
  }
@@ -28,7 +28,6 @@ async function startMcpServer(aiAgent) {
28
28
  uiLogger.debug(commands.mcp.start.startingServer);
29
29
  uiLogger.debug(commands.mcp.start.stopInstructions);
30
30
  const args = [serverPath];
31
- // Start the server using ts-node
32
31
  const child = spawn(`node`, args, {
33
32
  stdio: 'inherit',
34
33
  env: {
@@ -50,7 +50,7 @@ describe('commands/project/create', () => {
50
50
  expect(optionsCall['platform-version']).toEqual(expect.objectContaining({
51
51
  describe: 'The target platform version for the new project.',
52
52
  type: 'string',
53
- choices: ['2025.1', '2025.2'],
53
+ choices: ['2025.1', '2025.2', '2026.03-beta'],
54
54
  default: '2025.2',
55
55
  }));
56
56
  });
@@ -20,7 +20,7 @@ import { updateHsMetaFilesWithAutoGeneratedFields } from '../../lib/projects/com
20
20
  import SpinniesManager from '../../lib/ui/SpinniesManager.js';
21
21
  const command = ['create', 'init'];
22
22
  const describe = commands.project.create.describe;
23
- const { v2025_1, v2025_2 } = PLATFORM_VERSIONS;
23
+ const { v2025_1, v2025_2, v2026_03_beta } = PLATFORM_VERSIONS;
24
24
  async function handler(args) {
25
25
  const { derivedAccountId, platformVersion, templateSource } = args;
26
26
  if (templateSource && !templateSource.includes('/')) {
@@ -125,7 +125,7 @@ function projectCreateBuilder(yargs) {
125
125
  'platform-version': {
126
126
  describe: commands.project.create.options.platformVersion.describe,
127
127
  type: 'string',
128
- choices: [v2025_1, v2025_2],
128
+ choices: [v2025_1, v2025_2, v2026_03_beta],
129
129
  default: v2025_2,
130
130
  },
131
131
  'project-base': {
@@ -12,6 +12,7 @@ import { loadProfile } from '../../../lib/projects/projectProfiles.js';
12
12
  import { commands } from '../../../lang/en.js';
13
13
  import { uiLogger } from '../../../lib/ui/logger.js';
14
14
  import { logError } from '../../../lib/errorHandlers/index.js';
15
+ import { PromptExitError } from '../../../lib/errors/PromptExitError.js';
15
16
  import path from 'path';
16
17
  import { listPrompt } from '../../../lib/prompts/promptUtils.js';
17
18
  const command = 'dev';
@@ -99,26 +100,35 @@ async function handler(args) {
99
100
  }
100
101
  }
101
102
  trackCommandUsage('project-dev', {}, targetProjectAccountId);
102
- if (isV2Project(projectConfig.platformVersion)) {
103
- const targetTestingAccountId = testingAccount
104
- ? getConfigAccountIfExists(testingAccount)?.accountId
105
- : undefined;
106
- await unifiedProjectDevFlow({
107
- args,
108
- targetProjectAccountId,
109
- providedTargetTestingAccountId: targetTestingAccountId,
110
- projectConfig,
111
- projectDir,
112
- profileConfig: profile,
113
- });
103
+ try {
104
+ if (isV2Project(projectConfig.platformVersion)) {
105
+ const targetTestingAccountId = testingAccount
106
+ ? getConfigAccountIfExists(testingAccount)?.accountId
107
+ : undefined;
108
+ await unifiedProjectDevFlow({
109
+ args,
110
+ targetProjectAccountId,
111
+ providedTargetTestingAccountId: targetTestingAccountId,
112
+ projectConfig,
113
+ projectDir,
114
+ profileConfig: profile,
115
+ });
116
+ }
117
+ else {
118
+ await deprecatedProjectDevFlow({
119
+ args,
120
+ accountId: targetProjectAccountId,
121
+ projectConfig,
122
+ projectDir,
123
+ });
124
+ }
114
125
  }
115
- else {
116
- await deprecatedProjectDevFlow({
117
- args,
118
- accountId: targetProjectAccountId,
119
- projectConfig,
120
- projectDir,
121
- });
126
+ catch (e) {
127
+ if (e instanceof PromptExitError) {
128
+ process.exit(e.exitCode);
129
+ }
130
+ logError(e);
131
+ process.exit(EXIT_CODES.ERROR);
122
132
  }
123
133
  }
124
134
  function projectDevBuilder(yargs) {
@@ -3,6 +3,7 @@ import { getCwd, sanitizeFileName } from '@hubspot/local-dev-lib/path';
3
3
  import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
4
4
  import { downloadProject, fetchProjectBuilds, } from '@hubspot/local-dev-lib/api/projects';
5
5
  import { logError, ApiErrorContext } from '../../lib/errorHandlers/index.js';
6
+ import { PromptExitError } from '../../lib/errors/PromptExitError.js';
6
7
  import { getProjectConfig } from '../../lib/projects/config.js';
7
8
  import { downloadProjectPrompt } from '../../lib/prompts/downloadProjectPrompt.js';
8
9
  import { commands } from '../../lang/en.js';
@@ -19,10 +20,10 @@ async function handler(args) {
19
20
  process.exit(EXIT_CODES.ERROR);
20
21
  }
21
22
  const { dest, build, derivedAccountId } = args;
22
- const { project: projectName } = await downloadProjectPrompt(args);
23
23
  let buildNumberToDownload = build;
24
24
  trackCommandUsage('project-download', undefined, derivedAccountId);
25
25
  try {
26
+ const { project: projectName } = await downloadProjectPrompt(args);
26
27
  if (!buildNumberToDownload) {
27
28
  const { data: projectBuildsResult } = await fetchProjectBuilds(derivedAccountId, projectName);
28
29
  const { results: projectBuilds } = projectBuildsResult;
@@ -45,6 +46,9 @@ async function handler(args) {
45
46
  process.exit(EXIT_CODES.SUCCESS);
46
47
  }
47
48
  catch (e) {
49
+ if (e instanceof PromptExitError) {
50
+ process.exit(e.exitCode);
51
+ }
48
52
  logError(e, new ApiErrorContext({
49
53
  accountId: derivedAccountId,
50
54
  request: 'project download',
@@ -76,18 +76,30 @@ async function handler(args) {
76
76
  try {
77
77
  const { data: { results: builds }, } = await fetchProjectBuilds(derivedAccountId, projectConfig.name);
78
78
  const hasNoBuilds = !builds || !builds.length;
79
+ const handleWatchTermination = (error) => {
80
+ if (error) {
81
+ logError(error, new ApiErrorContext({ accountId: derivedAccountId }));
82
+ process.exit(EXIT_CODES.ERROR);
83
+ }
84
+ else {
85
+ process.exit(EXIT_CODES.SUCCESS);
86
+ }
87
+ };
79
88
  const startWatching = async () => {
80
- await createWatcher(derivedAccountId, projectConfig, projectDir, handleBuildStatus, handleUserInput);
89
+ await createWatcher(derivedAccountId, projectConfig, projectDir, handleBuildStatus, handleUserInput, handleWatchTermination);
81
90
  };
82
91
  // Upload all files if no build exists for this project yet
83
92
  if (initialUpload || hasNoBuilds) {
84
- const { uploadError } = await handleProjectUpload({
93
+ const { uploadError, projectNotFound } = await handleProjectUpload({
85
94
  accountId: derivedAccountId,
86
95
  projectConfig,
87
96
  projectDir,
88
97
  callbackFunc: startWatching,
89
98
  isUploadCommand: false,
90
99
  });
100
+ if (projectNotFound) {
101
+ process.exit(EXIT_CODES.ERROR);
102
+ }
91
103
  if (uploadError) {
92
104
  if (isSpecifiedError(uploadError, {
93
105
  subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED,
@@ -111,6 +123,7 @@ async function handler(args) {
111
123
  }
112
124
  catch (e) {
113
125
  logError(e, new ApiErrorContext({ accountId: derivedAccountId }));
126
+ process.exit(EXIT_CODES.ERROR);
114
127
  }
115
128
  }
116
129
  function projectWatchBuilder(yargs) {
@@ -1,7 +1,6 @@
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
4
  import * as sandboxPrompts from '../../../lib/prompts/sandboxesPrompt.js';
6
5
  import * as accountNamePrompt from '../../../lib/prompts/accountNamePrompt.js';
7
6
  import * as configUtils from '@hubspot/local-dev-lib/config';
@@ -12,7 +11,6 @@ import * as buildAccount from '../../../lib/buildAccount.js';
12
11
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
13
12
  import { uiLogger } from '../../../lib/ui/logger.js';
14
13
  import * as sandboxesLib from '../../../lib/sandboxes.js';
15
- import * as sandboxSync from '../../../lib/sandboxSync.js';
16
14
  import { vi } from 'vitest';
17
15
  import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
18
16
  vi.mock('@hubspot/local-dev-lib/config');
@@ -29,15 +27,10 @@ const getConfigAccountByIdSpy = vi.spyOn(configUtils, 'getConfigAccountById');
29
27
  const promptUserSpy = vi.spyOn(promptUtils, 'promptUser');
30
28
  const sandboxTypePromptSpy = vi.spyOn(sandboxPrompts, 'sandboxTypePrompt');
31
29
  const processExitSpy = vi.spyOn(process, 'exit');
32
- const buildSandboxSpy = vi.spyOn(buildAccount, 'buildSandbox');
33
30
  const buildV2SandboxSpy = vi.spyOn(buildAccount, 'buildV2Sandbox');
34
31
  const getConfigAccountEnvironmentSpy = vi.spyOn(configUtils, 'getConfigAccountEnvironment');
35
- const getAvailableSyncTypesSpy = vi.spyOn(sandboxesLib, 'getAvailableSyncTypes');
36
- const syncSandboxSpy = vi.spyOn(sandboxSync, 'syncSandbox');
37
32
  const validateSandboxUsageLimitsSpy = vi.spyOn(sandboxesLib, 'validateSandboxUsageLimits');
38
33
  const hubspotAccountNamePromptSpy = vi.spyOn(accountNamePrompt, 'hubspotAccountNamePrompt');
39
- const mockedHasFeatureV2Sandboxes = hasFeature;
40
- const mockedHasFeatureV2Cli = hasFeature;
41
34
  describe('commands/sandbox/create', () => {
42
35
  const yargsMock = yargs;
43
36
  describe('command', () => {
@@ -103,22 +96,10 @@ describe('commands/sandbox/create', () => {
103
96
  contactRecordsSyncPrompt: false,
104
97
  });
105
98
  validateSandboxUsageLimitsSpy.mockResolvedValue(undefined);
106
- mockedHasFeatureV2Sandboxes.mockResolvedValue(false);
107
- mockedHasFeatureV2Cli.mockResolvedValue(false);
108
99
  getConfigAccountEnvironmentSpy.mockReturnValue(ENVIRONMENTS.PROD);
109
- buildSandboxSpy.mockResolvedValue({
110
- sandbox: mockSandbox,
111
- personalAccessKey: 'mock-personal-access-key',
112
- name: sandboxNameFromPrompt,
113
- });
114
100
  buildV2SandboxSpy.mockResolvedValue({
115
101
  sandbox: { ...mockSandbox, version: 'V2' },
116
102
  });
117
- getAvailableSyncTypesSpy.mockResolvedValue([
118
- { type: 'object-schemas' },
119
- { type: 'workflows' },
120
- ]);
121
- syncSandboxSpy.mockResolvedValue(undefined);
122
103
  // Spy on process.exit so our tests don't close when it's called
123
104
  // @ts-expect-error Doesn't match the actual signature because then the linter complains about unused variables
124
105
  processExitSpy.mockImplementation(() => { });
@@ -167,35 +148,7 @@ describe('commands/sandbox/create', () => {
167
148
  });
168
149
  expect(promptUserSpy).toHaveBeenCalledTimes(1);
169
150
  });
170
- 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 () => {
171
- await sandboxCreateCommand.handler(args);
172
- expect(buildSandboxSpy).toHaveBeenCalledTimes(1);
173
- expect(buildSandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {
174
- accountId: 1234567890,
175
- accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
176
- env: 'prod',
177
- }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, 'prod', undefined // force
178
- );
179
- expect(getAvailableSyncTypesSpy).toHaveBeenCalledTimes(1);
180
- expect(syncSandboxSpy).toHaveBeenCalledTimes(1);
181
- });
182
- it('should build a v1 sandbox if the parent account is ungated for sandboxes:v2:enabled but not ungated for sandboxes:v2:cliEnabled', async () => {
183
- mockedHasFeatureV2Sandboxes.mockResolvedValue(true);
184
- mockedHasFeatureV2Cli.mockResolvedValue(false);
185
- await sandboxCreateCommand.handler(args);
186
- expect(buildSandboxSpy).toHaveBeenCalledTimes(1);
187
- expect(buildSandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {
188
- accountId: 1234567890,
189
- accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD,
190
- env: 'prod',
191
- }, HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, 'prod', undefined // force
192
- );
193
- expect(getAvailableSyncTypesSpy).toHaveBeenCalledTimes(1);
194
- expect(syncSandboxSpy).toHaveBeenCalledTimes(1);
195
- });
196
- it('should build a v2 sandbox if the parent account is ungated for both sandboxes:v2:enabled and sandboxes:v2:cliEnabled', async () => {
197
- mockedHasFeatureV2Sandboxes.mockResolvedValue(true);
198
- mockedHasFeatureV2Cli.mockResolvedValue(true);
151
+ it('should build a v2 sandbox', async () => {
199
152
  await sandboxCreateCommand.handler(args);
200
153
  expect(buildV2SandboxSpy).toHaveBeenCalledTimes(1);
201
154
  expect(buildV2SandboxSpy).toHaveBeenCalledWith(sandboxNameFromPrompt, {