@hubspot/cli 7.6.0-beta.11 → 7.6.0-beta.13

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 (158) hide show
  1. package/commands/app/__tests__/migrate.test.js +1 -0
  2. package/commands/getStarted.js +7 -20
  3. package/commands/mcp/setup.d.ts +0 -1
  4. package/commands/mcp/setup.js +11 -11
  5. package/commands/project/__tests__/add.test.js +64 -0
  6. package/commands/project/__tests__/create.test.js +57 -0
  7. package/commands/project/__tests__/deploy.test.js +3 -2
  8. package/commands/project/add.d.ts +1 -1
  9. package/commands/project/add.js +3 -5
  10. package/commands/project/create.js +7 -2
  11. package/commands/project/deploy.js +9 -61
  12. package/commands/project/dev/index.js +1 -1
  13. package/commands/project/dev/unifiedFlow.js +4 -1
  14. package/commands/project/migrate.js +26 -7
  15. package/commands/project/upload.js +2 -2
  16. package/commands/project/validate.js +1 -1
  17. package/commands/project/watch.js +2 -2
  18. package/lang/en.d.ts +20 -5
  19. package/lang/en.js +38 -22
  20. package/lang/en.lyaml +12 -12
  21. package/lib/__tests__/hasFeature.test.js +145 -7
  22. package/lib/__tests__/importData.test.js +1 -1
  23. package/lib/app/__tests__/migrate.test.js +14 -51
  24. package/lib/app/migrate.d.ts +2 -8
  25. package/lib/app/migrate.js +5 -73
  26. package/lib/constants.d.ts +4 -0
  27. package/lib/constants.js +4 -0
  28. package/lib/errorHandlers/index.d.ts +4 -0
  29. package/lib/errorHandlers/index.js +1 -1
  30. package/lib/hasFeature.js +6 -0
  31. package/lib/importData.js +1 -1
  32. package/lib/links.d.ts +1 -0
  33. package/lib/links.js +10 -3
  34. package/lib/mcp/setup.d.ts +0 -2
  35. package/lib/mcp/setup.js +4 -29
  36. package/lib/projects/__tests__/AppDevModeInterface.test.js +71 -44
  37. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  38. package/lib/projects/__tests__/components.test.js +164 -7
  39. package/lib/projects/__tests__/deploy.test.js +164 -0
  40. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  41. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  42. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  43. package/lib/projects/add/__tests__/v3AddComponent.test.js +142 -8
  44. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  45. package/lib/projects/add/legacyAddComponent.js +5 -1
  46. package/lib/projects/add/v3AddComponent.d.ts +2 -1
  47. package/lib/projects/add/v3AddComponent.js +22 -5
  48. package/lib/projects/components.d.ts +1 -0
  49. package/lib/projects/components.js +27 -1
  50. package/lib/projects/create/__tests__/v3.test.js +97 -9
  51. package/lib/projects/create/index.js +2 -2
  52. package/lib/projects/create/legacy.js +1 -1
  53. package/lib/projects/create/v3.d.ts +2 -2
  54. package/lib/projects/create/v3.js +35 -12
  55. package/lib/projects/deploy.d.ts +13 -0
  56. package/lib/projects/deploy.js +63 -0
  57. package/lib/projects/localDev/AppDevModeInterface.d.ts +5 -3
  58. package/lib/projects/localDev/AppDevModeInterface.js +110 -47
  59. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  60. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  61. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  62. package/lib/projects/localDev/LocalDevState.js +9 -1
  63. package/lib/projects/localDev/helpers/project.d.ts +2 -2
  64. package/lib/projects/localDev/helpers/project.js +6 -8
  65. package/lib/projects/platformVersion.d.ts +1 -0
  66. package/lib/projects/platformVersion.js +10 -0
  67. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  68. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  69. package/lib/projects/upload.js +1 -1
  70. package/lib/projects/urls.d.ts +1 -0
  71. package/lib/projects/urls.js +3 -0
  72. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  73. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  74. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  75. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  76. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  77. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  78. package/lib/prompts/installAppPrompt.d.ts +6 -1
  79. package/lib/prompts/installAppPrompt.js +6 -1
  80. package/lib/prompts/projectAddPrompt.js +1 -1
  81. package/lib/prompts/promptUtils.d.ts +5 -0
  82. package/lib/prompts/promptUtils.js +9 -0
  83. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  84. package/lib/theme/__tests__/migrate.test.d.ts +1 -0
  85. package/lib/theme/__tests__/migrate.test.js +233 -0
  86. package/lib/theme/migrate.d.ts +13 -0
  87. package/lib/theme/migrate.js +90 -0
  88. package/lib/ui/index.js +3 -6
  89. package/lib/usageTracking.js +2 -2
  90. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  91. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  92. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  93. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  94. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  95. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  96. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  97. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  98. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  99. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  100. package/mcp-server/tools/cms/HsListTool.js +1 -1
  101. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  102. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  103. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  106. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  107. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  108. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  109. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  110. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  111. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +1 -1
  112. package/mcp-server/tools/index.js +13 -1
  113. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  114. package/mcp-server/tools/project/AddFeatureToProjectTool.js +3 -3
  115. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  116. package/mcp-server/tools/project/CreateProjectTool.js +5 -5
  117. package/mcp-server/tools/project/DeployProjectTool.js +1 -1
  118. package/mcp-server/tools/project/DocFetchTool.js +2 -2
  119. package/mcp-server/tools/project/DocsSearchTool.d.ts +4 -1
  120. package/mcp-server/tools/project/DocsSearchTool.js +7 -7
  121. package/mcp-server/tools/project/GetConfigValuesTool.d.ts +4 -1
  122. package/mcp-server/tools/project/GetConfigValuesTool.js +14 -8
  123. package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -1
  124. package/mcp-server/tools/project/UploadProjectTools.js +2 -2
  125. package/mcp-server/tools/project/ValidateProjectTool.js +1 -1
  126. package/mcp-server/tools/project/__tests__/AddFeatureToProjectTool.test.js +1 -1
  127. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  128. package/mcp-server/tools/project/__tests__/DeployProjectTool.test.js +1 -1
  129. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +2 -2
  130. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +14 -12
  131. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +9 -8
  132. package/mcp-server/tools/project/__tests__/GuidedWalkthroughTool.test.js +1 -1
  133. package/mcp-server/tools/project/__tests__/UploadProjectTools.test.js +1 -1
  134. package/mcp-server/tools/project/__tests__/ValidateProjectTool.test.js +1 -1
  135. package/mcp-server/tools/project/constants.d.ts +1 -1
  136. package/mcp-server/tools/project/constants.js +14 -6
  137. package/mcp-server/utils/__tests__/cliConfig.test.d.ts +1 -0
  138. package/mcp-server/utils/__tests__/cliConfig.test.js +110 -0
  139. package/mcp-server/utils/cliConfig.d.ts +1 -0
  140. package/mcp-server/utils/cliConfig.js +12 -0
  141. package/package.json +4 -3
  142. package/types/LocalDev.d.ts +2 -1
  143. package/types/Projects.d.ts +1 -0
  144. package/ui/components/BoxWithTitle.d.ts +8 -0
  145. package/ui/components/BoxWithTitle.js +9 -0
  146. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  147. package/ui/components/HorizontalSelectPrompt.js +30 -0
  148. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  149. package/ui/components/StatusMessageBoxes.js +31 -0
  150. package/ui/lib/ui-testing-utils.d.ts +9 -0
  151. package/ui/lib/ui-testing-utils.js +47 -0
  152. package/ui/lib/useTerminalSize.d.ts +13 -0
  153. package/ui/lib/useTerminalSize.js +31 -0
  154. package/ui/styles.d.ts +18 -0
  155. package/ui/styles.js +18 -0
  156. package/ui/views/UiSandbox.d.ts +5 -0
  157. package/ui/views/UiSandbox.js +25 -0
  158. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
@@ -1,6 +1,8 @@
1
1
  import { confirm, Separator as _Separator, select, input, checkbox, password, number, } from '@inquirer/prompts';
2
2
  import { EXIT_CODES } from '../enums/exitCodes.js';
3
+ import chalk from 'chalk';
3
4
  export const Separator = new _Separator();
5
+ export const PROMPT_THEME = { prefix: { idle: chalk.green('?') } };
4
6
  function isUserCancellationError(error) {
5
7
  return error instanceof Error && error.name === 'ExitPromptError';
6
8
  }
@@ -103,6 +105,7 @@ function handleRawListPrompt(config) {
103
105
  pageSize: config.pageSize,
104
106
  default: config.default,
105
107
  loop: config.loop,
108
+ theme: PROMPT_THEME,
106
109
  }).then(resp => ({ [config.name]: resp }));
107
110
  }
108
111
  function handleNumberPrompt(config) {
@@ -110,6 +113,7 @@ function handleNumberPrompt(config) {
110
113
  message: config.message,
111
114
  default: config.default,
112
115
  validate: config.validate,
116
+ theme: PROMPT_THEME,
113
117
  }).then(resp => ({ [config.name]: resp }));
114
118
  }
115
119
  function handlePasswordPrompt(config) {
@@ -117,6 +121,7 @@ function handlePasswordPrompt(config) {
117
121
  message: config.message,
118
122
  mask: '*',
119
123
  validate: config.validate,
124
+ theme: PROMPT_THEME,
120
125
  }).then(resp => ({ [config.name]: resp }));
121
126
  }
122
127
  function handleCheckboxPrompt(config) {
@@ -127,6 +132,7 @@ function handleCheckboxPrompt(config) {
127
132
  pageSize: config.pageSize,
128
133
  validate: config.validate,
129
134
  loop: config.loop,
135
+ theme: PROMPT_THEME,
130
136
  }).then(resp => ({ [config.name]: resp }));
131
137
  }
132
138
  function handleConfirmPrompt(config) {
@@ -140,6 +146,7 @@ function handleInputPrompt(config) {
140
146
  default: config.default,
141
147
  validate: config.validate,
142
148
  transformer: config.transformer,
149
+ theme: PROMPT_THEME,
143
150
  }).then(resp => ({ [config.name]: resp }));
144
151
  }
145
152
  function handleSelectPrompt(config) {
@@ -150,6 +157,7 @@ function handleSelectPrompt(config) {
150
157
  default: config.default,
151
158
  pageSize: config.pageSize,
152
159
  loop: config.loop,
160
+ theme: PROMPT_THEME,
153
161
  }).then(resp => ({ [config.name]: resp }));
154
162
  }
155
163
  export async function confirmPrompt(message, options = {}) {
@@ -157,6 +165,7 @@ export async function confirmPrompt(message, options = {}) {
157
165
  const choice = await confirm({
158
166
  message,
159
167
  default: defaultAnswer,
168
+ theme: PROMPT_THEME,
160
169
  });
161
170
  return choice;
162
171
  }
@@ -13,7 +13,7 @@ export async function selectProjectTemplatePrompt(promptOptions, projectTemplate
13
13
  if (template instanceof Separator || !template.value) {
14
14
  return;
15
15
  }
16
- if (promptOptions.features?.includes(template.value.type)) {
16
+ if (promptOptions.features?.includes(template.value.cliSelector || template.value.type)) {
17
17
  if (template.disabled) {
18
18
  throw new Error(`Cannot create project with template '${template.value.type}'. Reasons: ${template.disabled}`);
19
19
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,233 @@
1
+ import { getProjectThemeDetails, migrateThemes, } from '@hubspot/project-parsing-lib';
2
+ import { confirmPrompt } from '../../prompts/promptUtils.js';
3
+ import { writeProjectConfig, } from '../../projects/config.js';
4
+ import { ensureProjectExists } from '../../projects/ensureProjectExists.js';
5
+ import { useV3Api } from '../../projects/platformVersion.js';
6
+ import { fetchMigrationApps } from '../../app/migrate.js';
7
+ import { getHasMigratableThemes, validateMigrationAppsAndThemes, handleThemesMigration, migrateThemes2025_2, } from '../migrate.js';
8
+ import { lib } from '../../../lang/en.js';
9
+ vi.mock('@hubspot/local-dev-lib/logger');
10
+ vi.mock('@hubspot/project-parsing-lib');
11
+ vi.mock('../../prompts/promptUtils');
12
+ vi.mock('../../projects/config');
13
+ vi.mock('../../projects/ensureProjectExists');
14
+ vi.mock('../../projects/platformVersion');
15
+ vi.mock('../../app/migrate');
16
+ const mockedGetProjectThemeDetails = getProjectThemeDetails;
17
+ const mockedMigrateThemes = migrateThemes;
18
+ const mockedConfirmPrompt = confirmPrompt;
19
+ const mockedWriteProjectConfig = writeProjectConfig;
20
+ const mockedEnsureProjectExists = ensureProjectExists;
21
+ const mockedUseV3Api = useV3Api;
22
+ const mockedFetchMigrationApps = fetchMigrationApps;
23
+ const ACCOUNT_ID = 123;
24
+ const PROJECT_NAME = 'Test Project';
25
+ const PLATFORM_VERSION = '2025.2';
26
+ const MOCK_PROJECT_DIR = '/mock/project/dir';
27
+ const createLoadedProjectConfig = (name) => ({
28
+ projectConfig: { name, srcDir: 'src', platformVersion: '2024.1' },
29
+ projectDir: MOCK_PROJECT_DIR,
30
+ });
31
+ describe('lib/theme/migrate', () => {
32
+ beforeEach(() => {
33
+ mockedUseV3Api.mockReturnValue(false);
34
+ });
35
+ describe('getHasMigratableThemes', () => {
36
+ it('should return false when no projectConfig is provided', async () => {
37
+ const result = await getHasMigratableThemes();
38
+ expect(result).toEqual({
39
+ hasMigratableThemes: false,
40
+ migratableThemesCount: 0,
41
+ });
42
+ });
43
+ it('should return false when projectConfig is missing required properties', async () => {
44
+ const invalidProjectConfig = {
45
+ projectConfig: { name: undefined, srcDir: 'src' },
46
+ projectDir: undefined,
47
+ };
48
+ const result = await getHasMigratableThemes(invalidProjectConfig);
49
+ expect(result).toEqual({
50
+ hasMigratableThemes: false,
51
+ migratableThemesCount: 0,
52
+ });
53
+ });
54
+ it('should return true when there are legacy themes', async () => {
55
+ mockedGetProjectThemeDetails.mockResolvedValue({
56
+ legacyThemeDetails: [
57
+ {
58
+ configFilepath: 'src/theme.json',
59
+ themePath: 'src/theme',
60
+ themeConfig: {
61
+ secret_names: ['my-secret'],
62
+ },
63
+ },
64
+ ],
65
+ legacyReactThemeDetails: [],
66
+ });
67
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
68
+ const result = await getHasMigratableThemes(projectConfig);
69
+ expect(result).toEqual({
70
+ hasMigratableThemes: true,
71
+ migratableThemesCount: 1,
72
+ });
73
+ });
74
+ it('should return true when there are legacy React themes', async () => {
75
+ mockedGetProjectThemeDetails.mockResolvedValue({
76
+ legacyThemeDetails: [],
77
+ legacyReactThemeDetails: [
78
+ {
79
+ configFilepath: 'src/react-theme.json',
80
+ themePath: 'src/react-theme',
81
+ themeConfig: {
82
+ secretNames: ['my-secret'],
83
+ },
84
+ },
85
+ ],
86
+ });
87
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
88
+ const result = await getHasMigratableThemes(projectConfig);
89
+ expect(result).toEqual({
90
+ hasMigratableThemes: true,
91
+ migratableThemesCount: 1,
92
+ });
93
+ });
94
+ it('should return true when there are both legacy and React themes', async () => {
95
+ mockedGetProjectThemeDetails.mockResolvedValue({
96
+ legacyThemeDetails: [
97
+ {
98
+ configFilepath: 'src/theme.json',
99
+ themePath: 'src/theme',
100
+ themeConfig: {
101
+ secret_names: ['my-secret'],
102
+ },
103
+ },
104
+ ],
105
+ legacyReactThemeDetails: [
106
+ {
107
+ configFilepath: 'src/react-theme.json',
108
+ themePath: 'src/react-theme',
109
+ themeConfig: {
110
+ secretNames: ['my-secret'],
111
+ },
112
+ },
113
+ ],
114
+ });
115
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
116
+ const result = await getHasMigratableThemes(projectConfig);
117
+ expect(result).toEqual({
118
+ hasMigratableThemes: true,
119
+ migratableThemesCount: 2,
120
+ });
121
+ });
122
+ });
123
+ describe('validateMigrationAppsAndThemes', () => {
124
+ it('should throw an error when themes are already migrated (v3 API)', async () => {
125
+ mockedUseV3Api.mockReturnValue(true);
126
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
127
+ await expect(validateMigrationAppsAndThemes(0, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAlreadyMigrated);
128
+ });
129
+ it('should throw an error when apps and themes are both present', async () => {
130
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
131
+ await expect(validateMigrationAppsAndThemes(1, projectConfig)).rejects.toThrow(lib.migrate.errors.project.themesAndAppsNotAllowed);
132
+ });
133
+ it('should throw an error when no project config is provided', async () => {
134
+ await expect(validateMigrationAppsAndThemes(0)).rejects.toThrow(lib.migrate.errors.project.noProjectForThemesMigration);
135
+ });
136
+ it('should not throw an error when validation passes', async () => {
137
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
138
+ await expect(validateMigrationAppsAndThemes(0, projectConfig)).resolves.not.toThrow();
139
+ });
140
+ });
141
+ describe('handleThemesMigration', () => {
142
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
143
+ beforeEach(() => {
144
+ mockedMigrateThemes.mockResolvedValue({
145
+ migrated: true,
146
+ failureReason: undefined,
147
+ legacyThemeDetails: [],
148
+ legacyReactThemeDetails: [],
149
+ });
150
+ mockedWriteProjectConfig.mockReturnValue(true);
151
+ });
152
+ it('should throw an error when project config is invalid', async () => {
153
+ const invalidProjectConfig = {
154
+ projectConfig: { name: PROJECT_NAME, srcDir: undefined },
155
+ projectDir: undefined,
156
+ };
157
+ await expect(handleThemesMigration(invalidProjectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
158
+ });
159
+ it('should successfully migrate themes and update project config', async () => {
160
+ await handleThemesMigration(projectConfig, PLATFORM_VERSION);
161
+ expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
162
+ expect(mockedWriteProjectConfig).toHaveBeenCalledWith(`${MOCK_PROJECT_DIR}/hsproject.json`, expect.objectContaining({
163
+ platformVersion: PLATFORM_VERSION,
164
+ }));
165
+ });
166
+ it('should throw an error when theme migration fails', async () => {
167
+ mockedMigrateThemes.mockResolvedValue({
168
+ migrated: false,
169
+ failureReason: 'Migration failed',
170
+ legacyThemeDetails: [],
171
+ legacyReactThemeDetails: [],
172
+ });
173
+ await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow('Migration failed');
174
+ });
175
+ it('should throw an error when project config write fails', async () => {
176
+ mockedWriteProjectConfig.mockReturnValue(false);
177
+ await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.failedToUpdateProjectConfig);
178
+ });
179
+ it('should throw an error when migrateThemes throws an exception', async () => {
180
+ mockedMigrateThemes.mockRejectedValue(new Error('Unexpected error'));
181
+ await expect(handleThemesMigration(projectConfig, PLATFORM_VERSION)).rejects.toThrow(lib.migrate.errors.project.failedToMigrateThemes);
182
+ });
183
+ });
184
+ describe('migrateThemes2025_2', () => {
185
+ const options = {
186
+ platformVersion: PLATFORM_VERSION,
187
+ };
188
+ const projectConfig = createLoadedProjectConfig(PROJECT_NAME);
189
+ const themeCount = 2;
190
+ beforeEach(() => {
191
+ mockedEnsureProjectExists.mockResolvedValue({ projectExists: true });
192
+ mockedFetchMigrationApps.mockResolvedValue({
193
+ migratableApps: [],
194
+ unmigratableApps: [],
195
+ });
196
+ mockedConfirmPrompt.mockResolvedValue(true);
197
+ mockedMigrateThemes.mockResolvedValue({
198
+ migrated: true,
199
+ failureReason: undefined,
200
+ legacyThemeDetails: [],
201
+ legacyReactThemeDetails: [],
202
+ });
203
+ mockedWriteProjectConfig.mockReturnValue(true);
204
+ });
205
+ it('should throw an error when project config is invalid', async () => {
206
+ const invalidProjectConfig = {
207
+ projectConfig: undefined,
208
+ projectDir: MOCK_PROJECT_DIR,
209
+ };
210
+ await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, invalidProjectConfig)).rejects.toThrow(lib.migrate.errors.project.invalidConfig);
211
+ });
212
+ it('should throw an error when project does not exist', async () => {
213
+ mockedEnsureProjectExists.mockResolvedValue({ projectExists: false });
214
+ await expect(migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig)).rejects.toThrow(lib.migrate.errors.project.doesNotExist(ACCOUNT_ID));
215
+ });
216
+ it('should proceed with migration when user confirms', async () => {
217
+ await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
218
+ expect(mockedFetchMigrationApps).toHaveBeenCalledWith(ACCOUNT_ID, PLATFORM_VERSION, projectConfig);
219
+ expect(mockedConfirmPrompt).toHaveBeenCalledWith(lib.migrate.prompt.proceed, { defaultAnswer: false });
220
+ expect(mockedMigrateThemes).toHaveBeenCalledWith(MOCK_PROJECT_DIR, `${MOCK_PROJECT_DIR}/src`);
221
+ });
222
+ it('should exit without migrating when user cancels', async () => {
223
+ mockedConfirmPrompt.mockResolvedValue(false);
224
+ await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
225
+ expect(mockedMigrateThemes).not.toHaveBeenCalled();
226
+ });
227
+ it('should validate migration apps and themes', async () => {
228
+ await migrateThemes2025_2(ACCOUNT_ID, options, themeCount, projectConfig);
229
+ // The validation is called internally, so we verify it through the error handling
230
+ expect(mockedFetchMigrationApps).toHaveBeenCalled();
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,13 @@
1
+ import { ArgumentsCamelCase } from 'yargs';
2
+ import { LoadedProjectConfig } from '../projects/config.js';
3
+ import { AccountArgs, CommonArgs, ConfigArgs, EnvironmentArgs } from '../../types/Yargs.js';
4
+ export type MigrateThemesArgs = CommonArgs & AccountArgs & EnvironmentArgs & ConfigArgs & {
5
+ platformVersion: string;
6
+ };
7
+ export declare function getHasMigratableThemes(projectConfig?: LoadedProjectConfig): Promise<{
8
+ hasMigratableThemes: boolean;
9
+ migratableThemesCount: number;
10
+ }>;
11
+ export declare function validateMigrationAppsAndThemes(hasApps: number, projectConfig?: LoadedProjectConfig): Promise<void>;
12
+ export declare function handleThemesMigration(projectConfig: LoadedProjectConfig, platformVersion: string): Promise<void>;
13
+ export declare function migrateThemes2025_2(derivedAccountId: number, options: ArgumentsCamelCase<MigrateThemesArgs>, themeCount: number, projectConfig: LoadedProjectConfig): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import path from 'path';
2
+ import { migrateThemes, getProjectThemeDetails, } from '@hubspot/project-parsing-lib';
3
+ import { writeProjectConfig } from '../projects/config.js';
4
+ import { ensureProjectExists } from '../projects/ensureProjectExists.js';
5
+ import SpinniesManager from '../ui/SpinniesManager.js';
6
+ import { lib } from '../../lang/en.js';
7
+ import { PROJECT_CONFIG_FILE } from '../constants.js';
8
+ import { uiLogger } from '../ui/logger.js';
9
+ import { debugError } from '../errorHandlers/index.js';
10
+ import { useV3Api } from '../projects/platformVersion.js';
11
+ import { confirmPrompt } from '../prompts/promptUtils.js';
12
+ import { fetchMigrationApps } from '../app/migrate.js';
13
+ export async function getHasMigratableThemes(projectConfig) {
14
+ if (!projectConfig?.projectConfig?.name || !projectConfig?.projectDir) {
15
+ return { hasMigratableThemes: false, migratableThemesCount: 0 };
16
+ }
17
+ const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
18
+ const { legacyThemeDetails, legacyReactThemeDetails } = await getProjectThemeDetails(projectSrcDir);
19
+ return {
20
+ hasMigratableThemes: legacyThemeDetails.length > 0 || legacyReactThemeDetails.length > 0,
21
+ migratableThemesCount: legacyThemeDetails.length + legacyReactThemeDetails.length,
22
+ };
23
+ }
24
+ export async function validateMigrationAppsAndThemes(hasApps, projectConfig) {
25
+ if (useV3Api(projectConfig?.projectConfig?.platformVersion)) {
26
+ throw new Error(lib.migrate.errors.project.themesAlreadyMigrated);
27
+ }
28
+ if (hasApps > 0 && projectConfig) {
29
+ throw new Error(lib.migrate.errors.project.themesAndAppsNotAllowed);
30
+ }
31
+ if (!projectConfig) {
32
+ throw new Error(lib.migrate.errors.project.noProjectForThemesMigration);
33
+ }
34
+ }
35
+ export async function handleThemesMigration(projectConfig, platformVersion) {
36
+ if (!projectConfig?.projectDir || !projectConfig?.projectConfig?.srcDir) {
37
+ throw new Error(lib.migrate.errors.project.invalidConfig);
38
+ }
39
+ const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
40
+ let migrated = false;
41
+ let failureReason;
42
+ try {
43
+ const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
44
+ migrated = migrationResult.migrated;
45
+ failureReason = migrationResult.failureReason;
46
+ }
47
+ catch (error) {
48
+ debugError(error);
49
+ throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
50
+ }
51
+ if (!migrated) {
52
+ throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
53
+ }
54
+ const newProjectConfig = { ...projectConfig.projectConfig };
55
+ newProjectConfig.platformVersion = platformVersion;
56
+ const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
57
+ const success = writeProjectConfig(projectConfigPath, newProjectConfig);
58
+ if (!success) {
59
+ throw new Error(lib.migrate.errors.project.failedToUpdateProjectConfig);
60
+ }
61
+ uiLogger.log('');
62
+ uiLogger.log(lib.migrate.success.themesMigrationSuccess(platformVersion));
63
+ }
64
+ export async function migrateThemes2025_2(derivedAccountId, options, themeCount, projectConfig) {
65
+ SpinniesManager.init();
66
+ if (!projectConfig?.projectConfig || !projectConfig?.projectDir) {
67
+ throw new Error(lib.migrate.errors.project.invalidConfig);
68
+ }
69
+ const { projectExists } = await ensureProjectExists(derivedAccountId, projectConfig.projectConfig.name, { allowCreate: false, noLogs: true });
70
+ if (!projectExists) {
71
+ throw new Error(lib.migrate.errors.project.doesNotExist(derivedAccountId));
72
+ }
73
+ SpinniesManager.add('checkingForMigratableComponents', {
74
+ text: lib.migrate.spinners.checkingForMigratableComponents,
75
+ });
76
+ const { migratableApps, unmigratableApps } = await fetchMigrationApps(derivedAccountId, options.platformVersion, projectConfig);
77
+ const hasApps = [...migratableApps, ...unmigratableApps].length;
78
+ SpinniesManager.remove('checkingForMigratableComponents');
79
+ await validateMigrationAppsAndThemes(hasApps, projectConfig);
80
+ uiLogger.log(lib.migrate.prompt.themesMigration(themeCount));
81
+ const proceed = await confirmPrompt(lib.migrate.prompt.proceed, {
82
+ defaultAnswer: false,
83
+ });
84
+ if (proceed) {
85
+ await handleThemesMigration(projectConfig, options.platformVersion);
86
+ }
87
+ else {
88
+ uiLogger.log(lib.migrate.exitWithoutMigrating);
89
+ }
90
+ }
package/lib/ui/index.js CHANGED
@@ -65,16 +65,13 @@ export function uiCommandReference(command, withQuotes = true) {
65
65
  }
66
66
  export function uiFeatureHighlight(features, title) {
67
67
  uiInfoSection(title ? title : i18n(`lib.ui.featureHighlight.defaultTitle`), () => {
68
- features.forEach((c, i) => {
69
- const featureKey = `lib.ui.featureHighlight.featureKeys.${c}`;
68
+ features.forEach(feature => {
69
+ const featureKey = `lib.ui.featureHighlight.featureKeys.${feature}`;
70
70
  const message = i18n(`${featureKey}.message`, {
71
71
  command: uiCommandReference(i18n(`${featureKey}.command`)),
72
72
  link: uiLink(i18n(`${featureKey}.linkText`), i18n(`${featureKey}.url`)),
73
73
  });
74
- if (i !== 0) {
75
- logger.log('');
76
- }
77
- logger.log(message);
74
+ logger.log(` - ${message}`);
78
75
  });
79
76
  });
80
77
  }
@@ -44,7 +44,7 @@ export async function trackCommandUsage(command, meta = {}, accountId) {
44
44
  action: 'cli-command',
45
45
  command,
46
46
  authType,
47
- ...meta,
47
+ meta,
48
48
  accountId,
49
49
  });
50
50
  }
@@ -133,12 +133,12 @@ async function trackCliInteraction({ action, accountId, command, authType, meta
133
133
  }
134
134
  }
135
135
  try {
136
+ logger.debug('Sent usage tracking command event: %o', usageTrackingEvent);
136
137
  return trackUsage('cli-interaction', EventClass.INTERACTION, usageTrackingEvent, accountId);
137
138
  }
138
139
  catch (error) {
139
140
  debugError(error);
140
141
  }
141
- logger.debug('Sent usage tracking command event: %o', usageTrackingEvent);
142
142
  }
143
143
  catch (e) {
144
144
  debugError(e);
@@ -0,0 +1,32 @@
1
+ import { TextContentResponse, Tool } from '../../types.js';
2
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ declare const inputSchemaZodObject: z.ZodObject<{
5
+ absoluteCurrentWorkingDirectory: z.ZodString;
6
+ dest: z.ZodOptional<z.ZodString>;
7
+ functionsFolder: z.ZodOptional<z.ZodString>;
8
+ filename: z.ZodOptional<z.ZodString>;
9
+ endpointMethod: z.ZodOptional<z.ZodEnum<["DELETE", "GET", "PATCH", "POST", "PUT"]>>;
10
+ endpointPath: z.ZodOptional<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ absoluteCurrentWorkingDirectory: string;
13
+ dest?: string | undefined;
14
+ functionsFolder?: string | undefined;
15
+ filename?: string | undefined;
16
+ endpointMethod?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT" | undefined;
17
+ endpointPath?: string | undefined;
18
+ }, {
19
+ absoluteCurrentWorkingDirectory: string;
20
+ dest?: string | undefined;
21
+ functionsFolder?: string | undefined;
22
+ filename?: string | undefined;
23
+ endpointMethod?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT" | undefined;
24
+ endpointPath?: string | undefined;
25
+ }>;
26
+ export type HsCreateFunctionInputSchema = z.infer<typeof inputSchemaZodObject>;
27
+ export declare class HsCreateFunctionTool extends Tool<HsCreateFunctionInputSchema> {
28
+ constructor(mcpServer: McpServer);
29
+ handler({ dest, functionsFolder, filename, endpointMethod, endpointPath, absoluteCurrentWorkingDirectory, }: HsCreateFunctionInputSchema): Promise<TextContentResponse>;
30
+ register(): RegisteredTool;
31
+ }
32
+ export {};
@@ -0,0 +1,96 @@
1
+ import { Tool } from '../../types.js';
2
+ import { z } from 'zod';
3
+ import { absoluteCurrentWorkingDirectory } from '../project/constants.js';
4
+ import { runCommandInDir } from '../../utils/project.js';
5
+ import { formatTextContents, formatTextContent } from '../../utils/content.js';
6
+ import { trackToolUsage } from '../../utils/toolUsageTracking.js';
7
+ import { addFlag } from '../../utils/command.js';
8
+ import { HTTP_METHODS } from '../../../types/Cms.js';
9
+ const inputSchema = {
10
+ absoluteCurrentWorkingDirectory,
11
+ dest: z
12
+ .string()
13
+ .describe('The destination path where the function should be created on the current computer.')
14
+ .optional(),
15
+ functionsFolder: z
16
+ .string()
17
+ .describe('Folder name for function creation. Required for non-interactive function creation. If the user has not specified the folder name, ask them to provide it.')
18
+ .optional(),
19
+ filename: z
20
+ .string()
21
+ .describe('Function filename. Required for non-interactive function creation. If the user has not specified the filename, ask them to provide it.')
22
+ .optional(),
23
+ endpointMethod: z
24
+ .enum(HTTP_METHODS)
25
+ .describe(`HTTP method for the function endpoint. Must be one of: ${HTTP_METHODS.join(', ')}. Defaults to GET.`)
26
+ .optional(),
27
+ endpointPath: z
28
+ .string()
29
+ .describe('API endpoint path for the function. Required for non-interactive function creation. If the user has not specified the endpoint path, ask them to provide it.')
30
+ .optional(),
31
+ };
32
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
+ const inputSchemaZodObject = z.object({ ...inputSchema });
34
+ const toolName = 'create-cms-function';
35
+ export class HsCreateFunctionTool extends Tool {
36
+ constructor(mcpServer) {
37
+ super(mcpServer);
38
+ }
39
+ async handler({ dest, functionsFolder, filename, endpointMethod, endpointPath, absoluteCurrentWorkingDirectory, }) {
40
+ await trackToolUsage(toolName);
41
+ const content = [];
42
+ // Require functions folder
43
+ if (!functionsFolder) {
44
+ content.push(formatTextContent(`Ask the user to provide the folder name for the function.`));
45
+ }
46
+ // Require filename
47
+ if (!filename) {
48
+ content.push(formatTextContent(`Ask the user to provide the filename for the function.`));
49
+ }
50
+ // Require endpoint path
51
+ if (!endpointPath) {
52
+ content.push(formatTextContent(`Ask the user to provide the API endpoint path for the function.`));
53
+ }
54
+ // If we have missing required information, return the prompts
55
+ if (content.length > 0) {
56
+ return {
57
+ content,
58
+ };
59
+ }
60
+ // Build the command
61
+ let command = 'hs create function';
62
+ if (dest) {
63
+ command += ` "${dest}"`;
64
+ }
65
+ // Add function-specific flags
66
+ if (functionsFolder) {
67
+ command = addFlag(command, 'functions-folder', functionsFolder);
68
+ }
69
+ if (filename) {
70
+ command = addFlag(command, 'filename', filename);
71
+ }
72
+ if (endpointMethod) {
73
+ command = addFlag(command, 'endpoint-method', endpointMethod);
74
+ }
75
+ else {
76
+ command = addFlag(command, 'endpoint-method', 'GET');
77
+ }
78
+ if (endpointPath) {
79
+ command = addFlag(command, 'endpoint-path', endpointPath);
80
+ }
81
+ try {
82
+ const { stdout, stderr } = await runCommandInDir(absoluteCurrentWorkingDirectory, command);
83
+ return formatTextContents(stdout, stderr);
84
+ }
85
+ catch (error) {
86
+ return formatTextContents(error instanceof Error ? error.message : `${error}`);
87
+ }
88
+ }
89
+ register() {
90
+ return this.mcpServer.registerTool(toolName, {
91
+ title: 'Create HubSpot CMS Serverless Function',
92
+ description: `Creates a new HubSpot CMS serverless function using the hs create function command. Functions can be created non-interactively by specifying functionsFolder, filename, and endpointPath. Supports all HTTP methods (${HTTP_METHODS.join(', ')}).`,
93
+ inputSchema,
94
+ }, this.handler);
95
+ }
96
+ }
@@ -0,0 +1,38 @@
1
+ import { TextContentResponse, Tool } from '../../types.js';
2
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ declare const inputSchemaZodObject: z.ZodObject<{
5
+ absoluteCurrentWorkingDirectory: z.ZodString;
6
+ userSuppliedName: z.ZodOptional<z.ZodString>;
7
+ dest: z.ZodOptional<z.ZodString>;
8
+ moduleLabel: z.ZodOptional<z.ZodString>;
9
+ reactType: z.ZodOptional<z.ZodBoolean>;
10
+ contentTypes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
11
+ global: z.ZodOptional<z.ZodBoolean>;
12
+ availableForNewContent: z.ZodOptional<z.ZodBoolean>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ absoluteCurrentWorkingDirectory: string;
15
+ dest?: string | undefined;
16
+ global?: boolean | undefined;
17
+ moduleLabel?: string | undefined;
18
+ reactType?: boolean | undefined;
19
+ contentTypes?: string | undefined;
20
+ availableForNewContent?: boolean | undefined;
21
+ userSuppliedName?: string | undefined;
22
+ }, {
23
+ absoluteCurrentWorkingDirectory: string;
24
+ dest?: string | undefined;
25
+ global?: boolean | undefined;
26
+ moduleLabel?: string | undefined;
27
+ reactType?: boolean | undefined;
28
+ contentTypes?: string | undefined;
29
+ availableForNewContent?: boolean | undefined;
30
+ userSuppliedName?: string | undefined;
31
+ }>;
32
+ export type HsCreateModuleInputSchema = z.infer<typeof inputSchemaZodObject>;
33
+ export declare class HsCreateModuleTool extends Tool<HsCreateModuleInputSchema> {
34
+ constructor(mcpServer: McpServer);
35
+ handler({ userSuppliedName, dest, moduleLabel, reactType, contentTypes, global, availableForNewContent, absoluteCurrentWorkingDirectory, }: HsCreateModuleInputSchema): Promise<TextContentResponse>;
36
+ register(): RegisteredTool;
37
+ }
38
+ export {};