@hubspot/cli 8.1.0-beta.0 → 8.2.0-beta.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 (95) 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 +9 -64
  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 +17 -6
  14. package/lang/en.js +18 -7
  15. package/lib/__tests__/commandSuggestion.test.js +2 -0
  16. package/lib/__tests__/serverlessLogs.test.js +79 -64
  17. package/lib/commandSuggestion.js +1 -7
  18. package/lib/constants.d.ts +1 -1
  19. package/lib/constants.js +1 -1
  20. package/lib/generateSelectors.js +1 -2
  21. package/lib/getStartedV2Actions.d.ts +13 -0
  22. package/lib/getStartedV2Actions.js +53 -0
  23. package/lib/mcp/__tests__/setup.test.js +357 -28
  24. package/lib/mcp/setup.d.ts +1 -0
  25. package/lib/mcp/setup.js +77 -30
  26. package/lib/projects/create/__tests__/legacy.test.js +6 -24
  27. package/lib/projects/create/index.js +1 -4
  28. package/lib/projects/create/legacy.js +3 -8
  29. package/lib/projects/create/v2.js +1 -9
  30. package/lib/projects/ensureProjectExists.js +1 -2
  31. package/lib/projects/pollProjectBuildAndDeploy.js +90 -85
  32. package/lib/projects/upload.d.ts +1 -0
  33. package/lib/projects/upload.js +37 -46
  34. package/lib/projects/watch.d.ts +2 -1
  35. package/lib/projects/watch.js +32 -24
  36. package/lib/serverlessLogs.js +50 -44
  37. package/lib/theme/cmsDevServerProcess.d.ts +12 -0
  38. package/lib/theme/cmsDevServerProcess.js +148 -0
  39. package/lib/theme/cmsDevServerRunner.d.ts +14 -0
  40. package/lib/theme/cmsDevServerRunner.js +90 -0
  41. package/lib/usageTracking.js +8 -5
  42. package/mcp-server/tools/cms/HsCreateFunctionTool.js +1 -1
  43. package/mcp-server/tools/cms/HsCreateModuleTool.js +1 -1
  44. package/mcp-server/tools/cms/HsCreateTemplateTool.js +1 -1
  45. package/mcp-server/tools/cms/HsFunctionLogsTool.js +1 -1
  46. package/mcp-server/tools/cms/HsListFunctionsTool.js +1 -1
  47. package/mcp-server/tools/cms/HsListTool.js +1 -1
  48. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +1 -2
  49. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +1 -2
  50. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +1 -2
  51. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +1 -2
  52. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +1 -2
  53. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -2
  54. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
  55. package/mcp-server/tools/project/AddFeatureToProjectTool.js +7 -11
  56. package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
  57. package/mcp-server/tools/project/CreateProjectTool.js +6 -11
  58. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  59. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  60. package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
  61. package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
  62. package/mcp-server/tools/project/GetBuildLogsTool.js +6 -7
  63. package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
  64. package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
  65. package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
  66. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
  67. package/mcp-server/tools/project/UploadProjectTools.js +1 -1
  68. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  69. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -2
  70. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -2
  71. package/mcp-server/tools/project/__tests__/CreateTestAccountTool.test.js +1 -2
  72. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -2
  73. package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +0 -32
  74. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +10 -2
  75. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +2 -2
  76. package/mcp-server/tools/project/constants.d.ts +12 -1
  77. package/mcp-server/tools/project/constants.js +12 -16
  78. package/mcp-server/utils/__tests__/command.test.js +233 -3
  79. package/mcp-server/utils/__tests__/feedbackTracking.test.js +9 -64
  80. package/mcp-server/utils/command.d.ts +5 -0
  81. package/mcp-server/utils/command.js +24 -0
  82. package/mcp-server/utils/feedbackTracking.js +2 -17
  83. package/package.json +4 -5
  84. package/ui/components/getStarted/GetStartedFlow.js +79 -2
  85. package/ui/components/getStarted/reducer.d.ts +20 -0
  86. package/ui/components/getStarted/reducer.js +36 -0
  87. package/ui/components/getStarted/screens/InstallationScreen.d.ts +7 -0
  88. package/ui/components/getStarted/screens/InstallationScreen.js +16 -0
  89. package/ui/components/getStarted/screens/ProjectSetupScreen.js +2 -1
  90. package/ui/lib/constants.d.ts +1 -0
  91. package/ui/lib/constants.js +1 -0
  92. package/mcp-server/utils/__tests__/project.test.d.ts +0 -1
  93. package/mcp-server/utils/__tests__/project.test.js +0 -140
  94. package/mcp-server/utils/project.d.ts +0 -5
  95. 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,16 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import cliProgress from 'cli-progress';
4
3
  import { commands } from '../../../lang/en.js';
5
4
  import { getCwd } from '@hubspot/local-dev-lib/path';
6
- import { FILE_UPLOAD_RESULT_TYPES } from '@hubspot/local-dev-lib/constants/files';
7
5
  import { getThemeJSONPath } from '@hubspot/local-dev-lib/cms/themes';
8
- import { createDevServer } from '@hubspot/cms-dev-server';
9
- import { getUploadableFileList } from '../../../lib/upload.js';
6
+ import { spawnDevServer } from '../../../lib/theme/cmsDevServerProcess.js';
10
7
  import { trackCommandUsage } from '../../../lib/usageTracking.js';
11
8
  import { previewPrompt, previewProjectPrompt, } from '../../../lib/prompts/previewPrompt.js';
12
9
  import { EXIT_CODES } from '../../../lib/enums/exitCodes.js';
13
- import { ApiErrorContext, logError } from '../../../lib/errorHandlers/index.js';
14
10
  import { getProjectConfig } from '../../../lib/projects/config.js';
15
11
  import { findProjectComponents } from '../../../lib/projects/structure.js';
16
12
  import { ComponentTypes } from '../../../types/Projects.js';
@@ -73,67 +69,16 @@ async function determineSrcAndDest(args) {
73
69
  async function handler(args) {
74
70
  const { derivedAccountId, noSsl, resetSession, port, generateFieldsTypes } = args;
75
71
  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
- }
129
72
  trackCommandUsage('preview', {}, derivedAccountId);
130
- if (port) {
131
- process.env['PORT'] = port.toString();
132
- }
133
- createDevServer(absoluteSrc, false, '', '', !noSsl, generateFieldsTypes, {
134
- filePaths,
73
+ // Spawn dev server in isolated subprocess to avoid React version conflicts
74
+ // File listing and progress bars are handled within the subprocess
75
+ await spawnDevServer({
76
+ absoluteSrc,
77
+ accountName: derivedAccountId?.toString(),
78
+ noSsl,
79
+ port,
80
+ generateFieldsTypes,
135
81
  resetSession: resetSession || false,
136
- startProgressBar,
137
82
  dest,
138
83
  });
139
84
  }
@@ -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
@@ -43,6 +43,11 @@ export declare const commands: {
43
43
  checkOutConfig: (configPath: string) => string;
44
44
  pressEnterToInstall: (accountName: string) => string;
45
45
  pressKeyToExit: string;
46
+ installingApp: (appName: string, accountName: string) => string;
47
+ installInstructions: string;
48
+ browserFailedToOpen: (url: string) => string;
49
+ pollingTimeout: (minutes: number) => string;
50
+ pressEnterToContinueSetup: string;
46
51
  prompts: {
47
52
  selectOptionV2: string;
48
53
  options: {
@@ -596,11 +601,6 @@ export declare const commands: {
596
601
  src: string;
597
602
  dest: string;
598
603
  };
599
- warnings: {
600
- disableInitial: string;
601
- initialUpload: string;
602
- notUploaded: (path: string) => string;
603
- };
604
604
  };
605
605
  fetch: {
606
606
  describe: string;
@@ -1256,7 +1256,6 @@ export declare const commands: {
1256
1256
  };
1257
1257
  success: (derivedTargets: string[]) => string;
1258
1258
  errors: {
1259
- needsMcpAccess: (accountId?: number) => string;
1260
1259
  errorParsingJsonFIle: (filename: string, errorMessage: string) => string;
1261
1260
  };
1262
1261
  spinners: {
@@ -1290,6 +1289,8 @@ export declare const commands: {
1290
1289
  prompts: {
1291
1290
  targets: string;
1292
1291
  targetsRequired: string;
1292
+ standaloneMode: string;
1293
+ cliVersion: string;
1293
1294
  };
1294
1295
  };
1295
1296
  start: {
@@ -3976,4 +3977,14 @@ export declare const lib: {
3976
3977
  copyingProjectFilesFailed: string;
3977
3978
  };
3978
3979
  };
3980
+ theme: {
3981
+ cmsDevServerProcess: {
3982
+ installStarted: (targetVersion: string) => string;
3983
+ installSucceeded: string;
3984
+ installFailed: string;
3985
+ serverStartError: (error: Error) => string;
3986
+ serverExit: (code: number) => string;
3987
+ serverKill: (signal: NodeJS.Signals) => string;
3988
+ };
3989
+ };
3979
3990
  };
package/lang/en.js CHANGED
@@ -51,6 +51,11 @@ export const commands = {
51
51
  checkOutConfig: (configPath) => `Check out ${chalk.cyan(configPath)} for the full configuration.`,
52
52
  pressEnterToInstall: (accountName) => `? Press ${chalk.bold('<enter>')} to continue installing and previewing this app in ${chalk.bold(accountName)}`,
53
53
  pressKeyToExit: `Press any key to exit...`,
54
+ installingApp: (appName, accountName) => `Installing ${chalk.bold(appName)} in ${chalk.bold(accountName)}...`,
55
+ installInstructions: `We'll take you to your HubSpot account and walk you through installing your app.`,
56
+ browserFailedToOpen: (url) => `⚠️ Failed to open browser automatically. Please open this URL manually:\n${chalk.cyan(url)}`,
57
+ pollingTimeout: (minutes) => `⚠️ Installation polling timed out after ${minutes} minutes. The app may still be installing in the background.`,
58
+ pressEnterToContinueSetup: `Press ${chalk.bold('<enter>')} to continue with card setup...`,
54
59
  prompts: {
55
60
  selectOptionV2: 'Choose a component type to get started',
56
61
  options: {
@@ -604,11 +609,6 @@ export const commands = {
604
609
  src: 'Path to the local directory your files are in, relative to your current working directory',
605
610
  dest: 'Path in HubSpot Design Tools. Can be a net new path',
606
611
  },
607
- warnings: {
608
- disableInitial: `Passing the "${chalk.bold('--disable-initial')}" option is no longer necessary. Running "${uiCommandReference('hs watch')}" no longer uploads the watched directory by default.`,
609
- initialUpload: `To upload the directory run "${uiCommandReference('hs upload')}" beforehand or add the "${chalk.bold('--initial-upload')}" option when running "${uiCommandReference('hs watch')}".`,
610
- notUploaded: (path) => `The "${uiCommandReference('hs watch')}" command no longer uploads the watched directory when started. The directory "${path}" was not uploaded.`,
611
- },
612
612
  },
613
613
  fetch: {
614
614
  describe: 'Fetch a file, directory or module from HubSpot and write to a path on your computer.',
@@ -1266,7 +1266,6 @@ export const commands = {
1266
1266
  },
1267
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')}.`,
1268
1268
  errors: {
1269
- 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))}`,
1270
1269
  errorParsingJsonFIle: (filename, errorMessage) => `Unable to update ${chalk.bold(filename)} due to invalid JSON: ${errorMessage}`,
1271
1270
  },
1272
1271
  spinners: {
@@ -1306,6 +1305,8 @@ export const commands = {
1306
1305
  prompts: {
1307
1306
  targets: '[--client] Which tools would you like to add the HubSpot CLI MCP server to?',
1308
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):',
1309
1310
  },
1310
1311
  },
1311
1312
  start: {
@@ -3076,7 +3077,7 @@ export const lib = {
3076
3077
  updateSucceeded: (latestVersion) => `Successfully updated HubSpot CLI to version ${chalk.bold(latestVersion)}`,
3077
3078
  notInstalledGlobally: 'Cannot auto-update the HubSpot CLI because NPM is not installed globally',
3078
3079
  updateFailed: (latestVersion) => `Failed to update HubSpot CLI to version ${chalk.bold(latestVersion)}`,
3079
- enableAutoUpdatesMessage: `The HubSpot CLI can automatically keep itself up to date.\n\nThis helps ensure compatibility with the HubSpot platform. You can change this later at any time.\n\nRun${uiCommandReference('hs config set --allow-auto-updates=true')}`,
3080
+ enableAutoUpdatesMessage: `The HubSpot CLI can automatically keep itself up to date.\n\nThis helps ensure compatibility with the HubSpot platform. You can change this later at any time.\n\nRun ${uiCommandReference('hs config set --allow-auto-updates=true')}`,
3080
3081
  },
3081
3082
  },
3082
3083
  projectProfiles: {
@@ -3999,4 +4000,14 @@ export const lib = {
3999
4000
  copyingProjectFilesFailed: 'Unable to copy migrated project files',
4000
4001
  },
4001
4002
  },
4003
+ theme: {
4004
+ cmsDevServerProcess: {
4005
+ installStarted: (targetVersion) => `Installing cms-dev-server ${targetVersion}...`,
4006
+ installSucceeded: 'cms-dev-server setup complete',
4007
+ installFailed: 'Failed to install cms-dev-server',
4008
+ serverStartError: (error) => `Failed to start dev server: ${error}`,
4009
+ serverExit: (code) => `Dev server exited with code ${code}`,
4010
+ serverKill: (signal) => `Dev server killed with signal ${signal}`,
4011
+ },
4012
+ },
4002
4013
  };
@@ -71,6 +71,8 @@ describe('lib/commandSuggestion', () => {
71
71
  // Create a mock yargs builder with strict method
72
72
  const mockYargsBuilder = {
73
73
  strict: vi.fn().mockReturnThis(),
74
+ help: vi.fn().mockReturnThis(),
75
+ version: vi.fn().mockReturnThis(),
74
76
  };
75
77
  await commandModule.builder(mockYargsBuilder);
76
78
  expect(mockYargsBuilder.strict).toHaveBeenCalledWith(false);
@@ -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,87 @@ 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
+ // eslint-disable-next-line
45
+ config: { headers: {} },
46
+ }));
47
+ const tailCall = vi.fn(() => Promise.resolve({
48
+ data: {
49
+ results: [],
50
+ paging: {
51
+ next: {
52
+ after: 'somehash',
57
53
  },
58
54
  },
59
- status: 200,
60
- statusText: 'OK',
61
- headers: {},
62
- config: {},
63
- });
64
- });
55
+ },
56
+ status: 200,
57
+ statusText: 'OK',
58
+ headers: {},
59
+ // eslint-disable-next-line
60
+ config: { headers: {} },
61
+ }));
65
62
  // @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();
63
+ const tailPromise = tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
64
+ await vi.advanceTimersByTimeAsync(0);
68
65
  expect(fetchLatest).toHaveBeenCalled();
66
+ expect(tailCall).toHaveBeenCalledTimes(1);
67
+ await vi.advanceTimersByTimeAsync(5000);
69
68
  expect(tailCall).toHaveBeenCalledTimes(2);
69
+ terminateTailLogs();
70
+ await tailPromise;
70
71
  });
71
72
  it('outputs results', async () => {
72
73
  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',
74
+ const fetchLatest = vi.fn(() => Promise.resolve({
75
+ data: {
76
+ id: '1234',
77
+ executionTime: 510,
78
+ log: 'Log message',
79
+ error: {
80
+ message: '',
81
+ type: '',
82
+ stackTrace: [],
83
+ statusCode: null,
84
84
  },
85
- status: 200,
86
- statusText: 'OK',
87
- headers: {},
88
- config: {},
89
- });
90
- });
85
+ status: 'SUCCESS',
86
+ createdAt: 1620232011451,
87
+ memory: '70/128 MB',
88
+ duration: '53.40 ms',
89
+ },
90
+ status: 200,
91
+ statusText: 'OK',
92
+ headers: {},
93
+ // eslint-disable-next-line
94
+ config: { headers: {} },
95
+ }));
91
96
  const latestLogResponse = {
92
97
  results: [
93
98
  {
@@ -117,12 +122,23 @@ describe('lib/serverlessLogs', () => {
117
122
  },
118
123
  },
119
124
  };
120
- const tailCall = vi.fn(() => Promise.resolve({ data: latestLogResponse }));
125
+ const tailCall = vi.fn(() => Promise.resolve({
126
+ data: latestLogResponse,
127
+ status: 200,
128
+ statusText: 'OK',
129
+ headers: {},
130
+ // eslint-disable-next-line
131
+ config: { headers: {} },
132
+ }));
121
133
  // @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();
134
+ const tailPromise = tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
135
+ await vi.advanceTimersByTimeAsync(0);
124
136
  expect(outputLogs).toHaveBeenCalledWith(latestLogResponse, expect.objectContaining({ compact }));
137
+ expect(tailCall).toHaveBeenCalledTimes(1);
138
+ await vi.advanceTimersByTimeAsync(5000);
125
139
  expect(tailCall).toHaveBeenCalledTimes(2);
140
+ terminateTailLogs();
141
+ await tailPromise;
126
142
  });
127
143
  it('handles no logs', async () => {
128
144
  const compact = false;
@@ -141,8 +157,7 @@ describe('lib/serverlessLogs', () => {
141
157
  statusCode: 404,
142
158
  }));
143
159
  await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact);
144
- vi.runOnlyPendingTimers();
145
- expect(tailCall).toHaveBeenCalledTimes(2);
160
+ expect(tailCall).toHaveBeenCalledTimes(1);
146
161
  });
147
162
  });
148
163
  });