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

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 (71) hide show
  1. package/commands/cms/__tests__/watch.test.js +0 -8
  2. package/commands/cms/function/logs.js +1 -0
  3. package/commands/cms/theme/preview.js +64 -11
  4. package/commands/cms/watch.d.ts +0 -1
  5. package/commands/cms/watch.js +2 -8
  6. package/commands/feedback.js +1 -1
  7. package/commands/mcp/__tests__/start.test.js +8 -1
  8. package/commands/mcp/setup.js +1 -9
  9. package/commands/mcp/start.js +0 -1
  10. package/commands/project/__tests__/create.test.js +1 -1
  11. package/commands/project/create.js +2 -2
  12. package/commands/project/watch.js +15 -2
  13. package/lang/en.d.ts +2 -6
  14. package/lang/en.js +2 -6
  15. package/lib/__tests__/serverlessLogs.test.js +71 -65
  16. package/lib/constants.d.ts +1 -1
  17. package/lib/constants.js +1 -1
  18. package/lib/generateSelectors.js +1 -2
  19. package/lib/mcp/__tests__/setup.test.js +357 -28
  20. package/lib/mcp/setup.d.ts +1 -0
  21. package/lib/mcp/setup.js +77 -30
  22. package/lib/projects/create/__tests__/legacy.test.js +6 -24
  23. package/lib/projects/create/index.js +1 -4
  24. package/lib/projects/create/legacy.js +3 -8
  25. package/lib/projects/create/v2.js +1 -9
  26. package/lib/projects/ensureProjectExists.js +1 -2
  27. package/lib/projects/pollProjectBuildAndDeploy.js +90 -85
  28. package/lib/projects/upload.d.ts +1 -0
  29. package/lib/projects/upload.js +37 -46
  30. package/lib/projects/watch.d.ts +2 -1
  31. package/lib/projects/watch.js +32 -24
  32. package/lib/serverlessLogs.js +50 -44
  33. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  34. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  35. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  36. package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
  37. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  38. package/mcp-server/tools/cms/HsListTool.js +1 -1
  39. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
  40. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -2
  41. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -2
  42. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +1 -2
  43. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -2
  44. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -2
  45. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +1 -1
  46. package/mcp-server/tools/project/AddFeatureToProjectTool.js +1 -1
  47. package/mcp-server/tools/project/CreateProjectTool.d.ts +1 -1
  48. package/mcp-server/tools/project/CreateProjectTool.js +1 -1
  49. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  50. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  51. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  52. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  53. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -2
  54. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -2
  55. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +1 -2
  56. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -2
  57. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
  58. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  59. package/mcp-server/tools/project/constants.d.ts +1 -1
  60. package/mcp-server/utils/__tests__/command.test.js +233 -3
  61. package/mcp-server/utils/__tests__/feedbackTracking.test.js +9 -64
  62. package/mcp-server/utils/command.d.ts +5 -0
  63. package/mcp-server/utils/command.js +24 -0
  64. package/mcp-server/utils/feedbackTracking.js +2 -17
  65. package/package.json +4 -4
  66. package/lib/cms/devServerProcess.d.ts +0 -13
  67. package/lib/cms/devServerProcess.js +0 -200
  68. package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
  69. package/mcp-server/utils/__tests__/project.test.js +0 -140
  70. package/mcp-server/utils/project.d.ts +0 -5
  71. package/mcp-server/utils/project.js +0 -18
@@ -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 {
@@ -1,14 +1,16 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import cliProgress from 'cli-progress';
3
4
  import { commands } from '../../../lang/en.js';
4
5
  import { getCwd } from '@hubspot/local-dev-lib/path';
6
+ import { FILE_UPLOAD_RESULT_TYPES } from '@hubspot/local-dev-lib/constants/files';
5
7
  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';
8
+ import { createDevServer } from '@hubspot/cms-dev-server';
9
+ import { getUploadableFileList } from '../../../lib/upload.js';
9
10
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
10
11
  import { previewPrompt, previewProjectPrompt, } from '../../../lib/prompts/previewPrompt.js';
11
12
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
13
+ import { ApiErrorContext, logError } from '../../../lib/errorHandlers/index.js';
12
14
  import { getProjectConfig } from '../../../lib/projects/config.js';
13
15
  import { findProjectComponents } from '../../../lib/projects/structure.js';
14
16
  import { ComponentTypes } from '../../../types/Projects.js';
@@ -71,16 +73,67 @@ async function determineSrcAndDest(args) {
71
73
  async function handler(args) {
72
74
  const { derivedAccountId, noSsl, resetSession, port, generateFieldsTypes } = args;
73
75
  const { absoluteSrc, dest } = await determineSrcAndDest(args);
76
+ const filePaths = await getUploadableFileList(absoluteSrc, false);
77
+ function startProgressBar(numFiles) {
78
+ const initialUploadProgressBar = new cliProgress.SingleBar({
79
+ gracefulExit: true,
80
+ format: '[{bar}] {percentage}% | {value}/{total} | {label}',
81
+ hideCursor: true,
82
+ }, cliProgress.Presets.rect);
83
+ initialUploadProgressBar.start(numFiles, 0, {
84
+ label: commands.cms.subcommands.theme.subcommands.preview
85
+ .initialUploadProgressBar.start,
86
+ });
87
+ let uploadsHaveStarted = false;
88
+ const uploadOptions = {
89
+ onAttemptCallback: () => {
90
+ /* Intentionally blank */
91
+ },
92
+ onSuccessCallback: () => {
93
+ initialUploadProgressBar.increment();
94
+ if (!uploadsHaveStarted) {
95
+ uploadsHaveStarted = true;
96
+ initialUploadProgressBar.update(0, {
97
+ label: commands.cms.subcommands.theme.subcommands.preview
98
+ .initialUploadProgressBar.uploading,
99
+ });
100
+ }
101
+ },
102
+ onFirstErrorCallback: () => {
103
+ /* Intentionally blank */
104
+ },
105
+ onRetryCallback: () => {
106
+ /* Intentionally blank */
107
+ },
108
+ onFinalErrorCallback: () => initialUploadProgressBar.increment(),
109
+ onFinishCallback: (results) => {
110
+ initialUploadProgressBar.update(numFiles, {
111
+ label: commands.cms.subcommands.theme.subcommands.preview
112
+ .initialUploadProgressBar.finish,
113
+ });
114
+ initialUploadProgressBar.stop();
115
+ results.forEach(result => {
116
+ if (result.resultType == FILE_UPLOAD_RESULT_TYPES.FAILURE) {
117
+ uiLogger.error(commands.cms.subcommands.theme.subcommands.preview.errors.uploadFailed(result.file, dest));
118
+ logError(result.error, new ApiErrorContext({
119
+ accountId: derivedAccountId,
120
+ request: dest,
121
+ payload: result.file,
122
+ }));
123
+ }
124
+ });
125
+ },
126
+ };
127
+ return uploadOptions;
128
+ }
74
129
  trackCommandUsage('preview', {}, derivedAccountId);
75
- // Spawn dev server in isolated subprocess to avoid React version conflicts
76
- // File listing and progress bars are handled within the subprocess
77
- spawnDevServer({
78
- absoluteSrc,
79
- accountName: derivedAccountId?.toString(),
80
- noSsl,
81
- port,
82
- generateFieldsTypes,
130
+ if (port) {
131
+ process.env['PORT'] = port.toString();
132
+ }
133
+ createDevServer(absoluteSrc, false, '', '', !noSsl, generateFieldsTypes, {
134
+ filePaths,
83
135
  resetSession: resetSession || false,
136
+ startProgressBar,
84
137
  dest,
85
138
  });
86
139
  }
@@ -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() {
@@ -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': {
@@ -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) {
package/lang/en.d.ts CHANGED
@@ -601,11 +601,6 @@ export declare const commands: {
601
601
  src: string;
602
602
  dest: string;
603
603
  };
604
- warnings: {
605
- disableInitial: string;
606
- initialUpload: string;
607
- notUploaded: (path: string) => string;
608
- };
609
604
  };
610
605
  fetch: {
611
606
  describe: string;
@@ -1261,7 +1256,6 @@ export declare const commands: {
1261
1256
  };
1262
1257
  success: (derivedTargets: string[]) => string;
1263
1258
  errors: {
1264
- needsMcpAccess: (accountId?: number) => string;
1265
1259
  errorParsingJsonFIle: (filename: string, errorMessage: string) => string;
1266
1260
  };
1267
1261
  spinners: {
@@ -1295,6 +1289,8 @@ export declare const commands: {
1295
1289
  prompts: {
1296
1290
  targets: string;
1297
1291
  targetsRequired: string;
1292
+ standaloneMode: string;
1293
+ cliVersion: string;
1298
1294
  };
1299
1295
  };
1300
1296
  start: {
package/lang/en.js CHANGED
@@ -609,11 +609,6 @@ export const commands = {
609
609
  src: 'Path to the local directory your files are in, relative to your current working directory',
610
610
  dest: 'Path in HubSpot Design Tools. Can be a net new path',
611
611
  },
612
- warnings: {
613
- disableInitial: `Passing the "${chalk.bold('--disable-initial')}" option is no longer necessary. Running "${uiCommandReference('hs watch')}" no longer uploads the watched directory by default.`,
614
- initialUpload: `To upload the directory run "${uiCommandReference('hs upload')}" beforehand or add the "${chalk.bold('--initial-upload')}" option when running "${uiCommandReference('hs watch')}".`,
615
- notUploaded: (path) => `The "${uiCommandReference('hs watch')}" command no longer uploads the watched directory when started. The directory "${path}" was not uploaded.`,
616
- },
617
612
  },
618
613
  fetch: {
619
614
  describe: 'Fetch a file, directory or module from HubSpot and write to a path on your computer.',
@@ -1271,7 +1266,6 @@ export const commands = {
1271
1266
  },
1272
1267
  success: (derivedTargets) => `You can now use the HubSpot CLI MCP Server in ${derivedTargets.join(', ')}. ${chalk.bold('You may need to restart these tools to apply the changes')}.`,
1273
1268
  errors: {
1274
- needsMcpAccess: (accountId) => `You must opt in to the developer MCP beta to use this feature on ${uiAccountDescription(accountId)}. Try again with a different account or ${uiLink('join the beta now', getProductUpdatesUrl('239890', accountId))}`,
1275
1269
  errorParsingJsonFIle: (filename, errorMessage) => `Unable to update ${chalk.bold(filename)} due to invalid JSON: ${errorMessage}`,
1276
1270
  },
1277
1271
  spinners: {
@@ -1311,6 +1305,8 @@ export const commands = {
1311
1305
  prompts: {
1312
1306
  targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
1313
1307
  targetsRequired: 'Must choose at least one app to configure.',
1308
+ standaloneMode: 'Do you want to run in standalone mode? (This will use npx @hubspot/cli instead of the installed hs command)',
1309
+ cliVersion: 'Specify a CLI version to pin (leave blank for latest):',
1314
1310
  },
1315
1311
  },
1316
1312
  start: {
@@ -1,6 +1,6 @@
1
- import mockStdIn from 'mock-stdin';
2
1
  import { outputLogs } from '../ui/serverlessFunctionLogs.js';
3
2
  import { tailLogs } from '../serverlessLogs.js';
3
+ import { handleKeypress } from '../process.js';
4
4
  vi.mock('../ui/serverlessFunctionLogs');
5
5
  vi.mock('../ui/SpinniesManager', () => ({
6
6
  default: {
@@ -12,82 +12,79 @@ vi.mock('../ui/SpinniesManager', () => ({
12
12
  stopAll: vi.fn(),
13
13
  },
14
14
  }));
15
+ vi.mock('../process');
15
16
  vi.useFakeTimers();
16
17
  const ACCOUNT_ID = 123;
18
+ function terminateTailLogs() {
19
+ const keypressCallback = vi.mocked(handleKeypress).mock.calls[0][0];
20
+ keypressCallback({ name: 'q' });
21
+ }
17
22
  describe('lib/serverlessLogs', () => {
18
23
  describe('tailLogs()', () => {
19
- let stdinMock;
20
- beforeEach(() => {
21
- // @ts-ignore - we don't need to mock the entire process object
22
- vi.spyOn(process, 'exit').mockImplementation(() => { });
23
- stdinMock = mockStdIn.stdin();
24
- });
25
24
  afterEach(() => {
26
25
  vi.clearAllTimers();
27
- stdinMock.restore();
26
+ vi.clearAllMocks();
28
27
  });
29
28
  it('calls tailCall() to get the next results', async () => {
30
29
  const compact = false;
31
- const fetchLatest = vi.fn(() => {
32
- return Promise.resolve({
33
- data: {
34
- id: '1234',
35
- executionTime: 510,
36
- log: 'Log message',
37
- error: { message: '', type: '', stackTrace: [] },
38
- status: 'SUCCESS',
39
- createdAt: 1620232011451,
40
- memory: '70/128 MB',
41
- duration: '53.40 ms',
42
- },
43
- status: 200,
44
- statusText: 'OK',
45
- headers: {},
46
- config: {},
47
- });
48
- });
49
- const tailCall = vi.fn(() => {
50
- return Promise.resolve({
51
- data: {
52
- results: [],
53
- paging: {
54
- next: {
55
- after: 'somehash',
56
- },
30
+ const fetchLatest = vi.fn(() => Promise.resolve({
31
+ data: {
32
+ id: '1234',
33
+ executionTime: 510,
34
+ log: 'Log message',
35
+ error: { message: '', type: '', stackTrace: [] },
36
+ status: 'SUCCESS',
37
+ createdAt: 1620232011451,
38
+ memory: '70/128 MB',
39
+ duration: '53.40 ms',
40
+ },
41
+ status: 200,
42
+ statusText: 'OK',
43
+ headers: {},
44
+ config: { headers: {} },
45
+ }));
46
+ const tailCall = vi.fn(() => Promise.resolve({
47
+ data: {
48
+ results: [],
49
+ paging: {
50
+ next: {
51
+ after: 'somehash',
57
52
  },
58
53
  },
59
- status: 200,
60
- statusText: 'OK',
61
- headers: {},
62
- config: {},
63
- });
64
- });
54
+ },
55
+ status: 200,
56
+ statusText: 'OK',
57
+ headers: {},
58
+ config: { headers: {} },
59
+ }));
65
60
  // @ts-ignore - headers is not used in the actual function and does not need to be mocked
66
- await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
67
- vi.runOnlyPendingTimers();
61
+ const tailPromise = tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
62
+ await vi.advanceTimersByTimeAsync(0);
68
63
  expect(fetchLatest).toHaveBeenCalled();
64
+ expect(tailCall).toHaveBeenCalledTimes(1);
65
+ await vi.advanceTimersByTimeAsync(5000);
69
66
  expect(tailCall).toHaveBeenCalledTimes(2);
67
+ terminateTailLogs();
68
+ await tailPromise;
70
69
  });
71
70
  it('outputs results', async () => {
72
71
  const compact = false;
73
- const fetchLatest = vi.fn(() => {
74
- return Promise.resolve({
75
- data: {
76
- id: '1234',
77
- executionTime: 510,
78
- log: 'Log message',
79
- error: { message: '', type: '', stackTrace: [], statusCode: null },
80
- status: 'SUCCESS',
81
- createdAt: 1620232011451,
82
- memory: '70/128 MB',
83
- duration: '53.40 ms',
84
- },
85
- status: 200,
86
- statusText: 'OK',
87
- headers: {},
88
- config: {},
89
- });
90
- });
72
+ const fetchLatest = vi.fn(() => Promise.resolve({
73
+ data: {
74
+ id: '1234',
75
+ executionTime: 510,
76
+ log: 'Log message',
77
+ error: { message: '', type: '', stackTrace: [], statusCode: null },
78
+ status: 'SUCCESS',
79
+ createdAt: 1620232011451,
80
+ memory: '70/128 MB',
81
+ duration: '53.40 ms',
82
+ },
83
+ status: 200,
84
+ statusText: 'OK',
85
+ headers: {},
86
+ config: { headers: {} },
87
+ }));
91
88
  const latestLogResponse = {
92
89
  results: [
93
90
  {
@@ -117,12 +114,22 @@ describe('lib/serverlessLogs', () => {
117
114
  },
118
115
  },
119
116
  };
120
- const tailCall = vi.fn(() => Promise.resolve({ data: latestLogResponse }));
117
+ const tailCall = vi.fn(() => Promise.resolve({
118
+ data: latestLogResponse,
119
+ status: 200,
120
+ statusText: 'OK',
121
+ headers: {},
122
+ config: { headers: {} },
123
+ }));
121
124
  // @ts-ignore - headers is not used in the actual function and does not need to be mocked
122
- await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
123
- vi.runOnlyPendingTimers();
125
+ const tailPromise = tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
126
+ await vi.advanceTimersByTimeAsync(0);
124
127
  expect(outputLogs).toHaveBeenCalledWith(latestLogResponse, expect.objectContaining({ compact }));
128
+ expect(tailCall).toHaveBeenCalledTimes(1);
129
+ await vi.advanceTimersByTimeAsync(5000);
125
130
  expect(tailCall).toHaveBeenCalledTimes(2);
131
+ terminateTailLogs();
132
+ await tailPromise;
126
133
  });
127
134
  it('handles no logs', async () => {
128
135
  const compact = false;
@@ -141,8 +148,7 @@ describe('lib/serverlessLogs', () => {
141
148
  statusCode: 404,
142
149
  }));
143
150
  await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
144
- vi.runOnlyPendingTimers();
145
- expect(tailCall).toHaveBeenCalledTimes(2);
151
+ expect(tailCall).toHaveBeenCalledTimes(1);
146
152
  });
147
153
  });
148
154
  });
@@ -81,7 +81,6 @@ export declare const FEATURES: {
81
81
  readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
82
82
  readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
83
83
  readonly APPS_HOME: "UIE:AppHome";
84
- readonly MCP_ACCESS: "Developers:CLIMCPAccess";
85
84
  readonly THEME_MIGRATION_2025_2: "Developers:ProjectThemeMigrations:2025.2";
86
85
  readonly AGENT_TOOLS: "ThirdPartyAgentTools";
87
86
  };
@@ -145,3 +144,4 @@ export declare const ACCOUNT_LEVELS: {
145
144
  readonly ENTERPRISE: "ENTERPRISE";
146
145
  };
147
146
  export declare const ACCOUNT_LEVEL_CHOICES: readonly ["FREE", "STARTER", "PROFESSIONAL", "ENTERPRISE"];
147
+ export declare const FEEDBACK_URL = "https://developers.hubspot.com/feedback";
package/lib/constants.js CHANGED
@@ -73,7 +73,6 @@ export const FEATURES = {
73
73
  SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
74
74
  APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
75
75
  APPS_HOME: 'UIE:AppHome',
76
- MCP_ACCESS: 'Developers:CLIMCPAccess',
77
76
  THEME_MIGRATION_2025_2: 'Developers:ProjectThemeMigrations:2025.2',
78
77
  AGENT_TOOLS: 'ThirdPartyAgentTools',
79
78
  };
@@ -146,3 +145,4 @@ export const ACCOUNT_LEVEL_CHOICES = [
146
145
  ACCOUNT_LEVELS.PROFESSIONAL,
147
146
  ACCOUNT_LEVELS.ENTERPRISE,
148
147
  ];
148
+ export const FEEDBACK_URL = 'https://developers.hubspot.com/feedback';
@@ -1,5 +1,4 @@
1
1
  import fs from 'fs';
2
- import { EXIT_CODES } from './enums/exitCodes.js';
3
2
  import { commands } from '../lang/en.js';
4
3
  import { uiLogger } from './ui/logger.js';
5
4
  const CSS_COMMENTS_REGEX = new RegExp(/\/\*.*\*\//, 'g');
@@ -12,7 +11,7 @@ export function findFieldsJsonPath(basePath) {
12
11
  const _path = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
13
12
  if (!fs.existsSync(_path)) {
14
13
  uiLogger.error(commands.cms.subcommands.theme.subcommands.generateSelectors.errors.invalidPath(basePath));
15
- process.exit(EXIT_CODES.ERROR);
14
+ return null;
16
15
  }
17
16
  const files = fs.readdirSync(_path);
18
17
  if (files.includes('fields.json')) {