@hubspot/cli 7.7.31-experimental.0 → 7.7.33-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/commands/app.js +1 -6
  2. package/commands/getStarted.js +5 -4
  3. package/commands/project/__tests__/add.test.js +3 -5
  4. package/commands/project/__tests__/deploy.test.js +3 -2
  5. package/commands/project/add.js +2 -4
  6. package/commands/project/deploy.js +9 -61
  7. package/commands/project/dev/index.js +1 -1
  8. package/commands/project/dev/unifiedFlow.js +3 -0
  9. package/commands/project/upload.d.ts +2 -2
  10. package/commands/project/upload.js +3 -3
  11. package/commands/project/validate.js +1 -1
  12. package/commands/project/watch.js +2 -2
  13. package/commands/testAccount/create.js +0 -3
  14. package/lang/en.d.ts +8 -26
  15. package/lang/en.js +9 -27
  16. package/lib/__tests__/hasFeature.test.js +145 -7
  17. package/lib/__tests__/importData.test.js +1 -1
  18. package/lib/app/migrate.js +9 -2
  19. package/lib/constants.d.ts +2 -0
  20. package/lib/constants.js +2 -0
  21. package/lib/errorHandlers/index.d.ts +4 -0
  22. package/lib/errorHandlers/index.js +1 -1
  23. package/lib/hasFeature.js +6 -0
  24. package/lib/importData.js +1 -1
  25. package/lib/mcp/setup.js +1 -1
  26. package/lib/projectProfiles.d.ts +1 -1
  27. package/lib/projectProfiles.js +2 -10
  28. package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
  29. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  30. package/lib/projects/__tests__/deploy.test.js +164 -0
  31. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  32. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  33. package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
  34. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  35. package/lib/projects/add/legacyAddComponent.js +5 -1
  36. package/lib/projects/add/v3AddComponent.d.ts +1 -0
  37. package/lib/projects/add/v3AddComponent.js +2 -2
  38. package/lib/projects/create/__tests__/v3.test.js +97 -9
  39. package/lib/projects/create/index.js +2 -2
  40. package/lib/projects/create/legacy.js +1 -1
  41. package/lib/projects/create/v3.d.ts +2 -2
  42. package/lib/projects/create/v3.js +35 -12
  43. package/lib/projects/deploy.d.ts +13 -0
  44. package/lib/projects/deploy.js +63 -0
  45. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
  46. package/lib/projects/localDev/AppDevModeInterface.js +65 -36
  47. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  48. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  49. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  50. package/lib/projects/localDev/LocalDevState.js +9 -1
  51. package/lib/projects/localDev/helpers/project.js +1 -1
  52. package/lib/projects/platformVersion.d.ts +1 -0
  53. package/lib/projects/platformVersion.js +10 -0
  54. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  55. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  56. package/lib/projects/structure.d.ts +2 -2
  57. package/lib/projects/upload.d.ts +1 -2
  58. package/lib/projects/upload.js +1 -2
  59. package/lib/projects/urls.d.ts +1 -0
  60. package/lib/projects/urls.js +3 -0
  61. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  62. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  63. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  64. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  65. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  66. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  67. package/lib/prompts/installAppPrompt.d.ts +6 -1
  68. package/lib/prompts/installAppPrompt.js +6 -1
  69. package/lib/prompts/projectAddPrompt.js +1 -1
  70. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  71. package/mcp-server/tools/cms/HsCreateFunctionTool.d.ts +32 -0
  72. package/mcp-server/tools/cms/HsCreateFunctionTool.js +96 -0
  73. package/mcp-server/tools/cms/HsCreateTemplateTool.d.ts +26 -0
  74. package/mcp-server/tools/cms/HsCreateTemplateTool.js +75 -0
  75. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  76. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  77. package/mcp-server/tools/cms/HsListFunctionsTool.d.ts +23 -0
  78. package/mcp-server/tools/cms/HsListFunctionsTool.js +58 -0
  79. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.d.ts +1 -0
  80. package/mcp-server/tools/cms/__tests__/HsCreateFunctionTool.test.js +251 -0
  81. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.d.ts +1 -0
  82. package/mcp-server/tools/cms/__tests__/HsCreateTemplateTool.test.js +206 -0
  83. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  84. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  85. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.d.ts +1 -0
  86. package/mcp-server/tools/cms/__tests__/HsListFunctionsTool.test.js +120 -0
  87. package/mcp-server/tools/index.js +8 -0
  88. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  89. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  90. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -3
  91. package/mcp-server/tools/project/constants.d.ts +1 -1
  92. package/mcp-server/tools/project/constants.js +6 -4
  93. package/package.json +4 -3
  94. package/types/LocalDev.d.ts +2 -1
  95. package/types/Projects.d.ts +1 -0
  96. package/types/Yargs.d.ts +1 -1
  97. package/ui/components/BoxWithTitle.d.ts +8 -0
  98. package/ui/components/BoxWithTitle.js +9 -0
  99. package/ui/components/HorizontalSelectPrompt.d.ts +8 -0
  100. package/ui/components/HorizontalSelectPrompt.js +30 -0
  101. package/ui/components/StatusMessageBoxes.d.ts +12 -0
  102. package/ui/components/StatusMessageBoxes.js +31 -0
  103. package/ui/lib/ui-testing-utils.d.ts +9 -0
  104. package/ui/lib/ui-testing-utils.js +47 -0
  105. package/ui/lib/useTerminalSize.d.ts +13 -0
  106. package/ui/lib/useTerminalSize.js +31 -0
  107. package/ui/styles.d.ts +18 -0
  108. package/ui/styles.js +18 -0
  109. package/ui/views/UiSandbox.d.ts +5 -0
  110. package/ui/views/UiSandbox.js +25 -0
  111. package/commands/app/__tests__/install.test.js +0 -47
  112. package/commands/app/install.d.ts +0 -8
  113. package/commands/app/install.js +0 -122
  114. /package/{commands/app/__tests__/install.test.d.ts → lib/projects/__tests__/deploy.test.d.ts} +0 -0
  115. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → platformVersion.test.d.ts} +0 -0
@@ -4,6 +4,7 @@ import { getProjectComponentListFromRepo } from '../../create/legacy.js';
4
4
  import { projectAddPrompt } from '../../../prompts/projectAddPrompt.js';
5
5
  import { logger } from '@hubspot/local-dev-lib/logger';
6
6
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
7
+ import { trackCommandUsage } from '../../../usageTracking.js';
7
8
  import { ComponentTypes, } from '../../../../types/Projects.js';
8
9
  import { commands } from '../../../../lang/en.js';
9
10
  vi.mock('../../structure');
@@ -11,21 +12,25 @@ vi.mock('../../create/legacy');
11
12
  vi.mock('../../../prompts/projectAddPrompt');
12
13
  vi.mock('@hubspot/local-dev-lib/logger');
13
14
  vi.mock('@hubspot/local-dev-lib/github');
15
+ vi.mock('../../../usageTracking.js');
14
16
  const mockedFindProjectComponents = vi.mocked(findProjectComponents);
15
17
  const mockedGetProjectComponentListFromRepo = vi.mocked(getProjectComponentListFromRepo);
16
18
  const mockedProjectAddPrompt = vi.mocked(projectAddPrompt);
17
19
  const mockedLogger = vi.mocked(logger);
18
20
  const mockedCloneGithubRepo = vi.mocked(cloneGithubRepo);
21
+ const mockedTrackCommandUsage = vi.mocked(trackCommandUsage);
19
22
  describe('lib/projects/add/legacyAddComponent', () => {
20
23
  const mockProjectConfig = {
21
24
  name: 'test-project',
22
25
  srcDir: 'src',
23
26
  platformVersion: 'v1',
24
27
  };
28
+ const accountId = 1234567890;
25
29
  const mockArgs = { name: 'test-component', type: 'module' };
26
30
  const projectDir = '/path/to/project';
27
31
  beforeEach(() => {
28
32
  vi.resetAllMocks();
33
+ mockedTrackCommandUsage.mockResolvedValue();
29
34
  });
30
35
  describe('legacyAddComponent()', () => {
31
36
  it('successfully adds a component to a project without public apps', async () => {
@@ -58,7 +63,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
58
63
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
59
64
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
60
65
  mockedCloneGithubRepo.mockResolvedValue(true);
61
- await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
66
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
62
67
  expect(mockedFindProjectComponents).toHaveBeenCalledWith(projectDir);
63
68
  expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
64
69
  expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
@@ -67,6 +72,9 @@ describe('lib/projects/add/legacyAddComponent', () => {
67
72
  branch: 'main',
68
73
  hideLogs: true,
69
74
  }));
75
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
76
+ type: 'module',
77
+ }, accountId);
70
78
  expect(mockedLogger.log).toHaveBeenCalledWith(commands.project.add.creatingComponent('test-project'));
71
79
  expect(mockedLogger.success).toHaveBeenCalledWith(commands.project.add.success('new-component'));
72
80
  });
@@ -97,7 +105,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
97
105
  },
98
106
  ];
99
107
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
100
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
108
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.projectContainsPublicApp);
101
109
  expect(mockedGetProjectComponentListFromRepo).not.toHaveBeenCalled();
102
110
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
103
111
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
@@ -118,7 +126,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
118
126
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
119
127
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
120
128
  mockedCloneGithubRepo.mockResolvedValue(true);
121
- await legacyAddComponent(mockArgs, projectDir, mockProjectConfig);
129
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
122
130
  expect(mockedGetProjectComponentListFromRepo).toHaveBeenCalledWith('v1');
123
131
  expect(mockedProjectAddPrompt).toHaveBeenCalledWith(mockComponentList, mockArgs);
124
132
  expect(mockedCloneGithubRepo).toHaveBeenCalled();
@@ -140,7 +148,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
140
148
  ];
141
149
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
142
150
  mockedGetProjectComponentListFromRepo.mockResolvedValue([]);
143
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
151
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
144
152
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
145
153
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
146
154
  });
@@ -162,7 +170,7 @@ describe('lib/projects/add/legacyAddComponent', () => {
162
170
  mockedFindProjectComponents.mockResolvedValue(mockComponents);
163
171
  // @ts-expect-error Breaking stuff on purpose
164
172
  mockedGetProjectComponentListFromRepo.mockResolvedValue(null);
165
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
173
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToFetchComponentList);
166
174
  expect(mockedProjectAddPrompt).not.toHaveBeenCalled();
167
175
  expect(mockedCloneGithubRepo).not.toHaveBeenCalled();
168
176
  });
@@ -196,9 +204,44 @@ describe('lib/projects/add/legacyAddComponent', () => {
196
204
  mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
197
205
  mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
198
206
  mockedCloneGithubRepo.mockRejectedValue(new Error('Clone failed'));
199
- await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
207
+ await expect(legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId)).rejects.toThrow(commands.project.add.error.failedToDownloadComponent);
200
208
  expect(mockedCloneGithubRepo).toHaveBeenCalled();
201
209
  expect(mockedLogger.success).not.toHaveBeenCalled();
202
210
  });
211
+ it('calls trackCommandUsage with correct component type', async () => {
212
+ const mockComponents = [
213
+ {
214
+ type: ComponentTypes.PrivateApp,
215
+ config: {
216
+ name: 'private-app',
217
+ description: '',
218
+ uid: '',
219
+ scopes: [],
220
+ public: false,
221
+ },
222
+ runnable: true,
223
+ path: '/path/to/private-app',
224
+ },
225
+ ];
226
+ const mockComponentList = [
227
+ { label: 'Card Component', path: 'card-component', type: 'card' },
228
+ ];
229
+ const mockPromptResponse = {
230
+ name: 'new-card',
231
+ componentTemplate: {
232
+ label: 'Card Component',
233
+ path: 'card-template-path',
234
+ type: 'card',
235
+ },
236
+ };
237
+ mockedFindProjectComponents.mockResolvedValue(mockComponents);
238
+ mockedGetProjectComponentListFromRepo.mockResolvedValue(mockComponentList);
239
+ mockedProjectAddPrompt.mockResolvedValue(mockPromptResponse);
240
+ mockedCloneGithubRepo.mockResolvedValue(true);
241
+ await legacyAddComponent(mockArgs, projectDir, mockProjectConfig, accountId);
242
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
243
+ type: 'card',
244
+ }, accountId);
245
+ });
203
246
  });
204
247
  });
@@ -33,7 +33,11 @@ describe('lib/projects/add/v3AddComponent', () => {
33
33
  srcDir: 'src',
34
34
  platformVersion: 'v3',
35
35
  };
36
- const mockArgs = { name: 'test-component', type: 'module' };
36
+ const mockArgs = {
37
+ name: 'test-component',
38
+ type: 'module',
39
+ derivedAccountId: 1234,
40
+ };
37
41
  const projectDir = '/path/to/project';
38
42
  const mockAccountId = 123;
39
43
  const mockComponentTemplate = {
@@ -245,5 +249,71 @@ describe('lib/projects/add/v3AddComponent', () => {
245
249
  type: '',
246
250
  }, mockAccountId);
247
251
  });
252
+ it('should track usage with cliSelector when available', async () => {
253
+ const mockAppMeta = {
254
+ config: {
255
+ distribution: 'private',
256
+ auth: { type: 'oauth' },
257
+ },
258
+ };
259
+ const mockComponentTemplateWithCliSelector = {
260
+ label: 'Workflow Action Tool',
261
+ path: 'workflow-action-tool',
262
+ type: 'workflow-action',
263
+ cliSelector: 'workflow-action-tool',
264
+ supportedAuthTypes: ['oauth'],
265
+ supportedDistributions: ['private'],
266
+ };
267
+ const mockPromptResponse = {
268
+ componentTemplate: [mockComponentTemplateWithCliSelector],
269
+ };
270
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
271
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
272
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
273
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
274
+ mockedCloneGithubRepo.mockResolvedValue(true);
275
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
276
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
277
+ type: 'workflow-action-tool',
278
+ }, mockAccountId);
279
+ });
280
+ it('should track usage with cliSelector for multiple components', async () => {
281
+ const mockAppMeta = {
282
+ config: {
283
+ distribution: 'private',
284
+ auth: { type: 'oauth' },
285
+ },
286
+ };
287
+ const mockComponentWithCliSelector = {
288
+ label: 'Workflow Action Tool',
289
+ path: 'workflow-action-tool',
290
+ type: 'workflow-action',
291
+ cliSelector: 'workflow-action-tool',
292
+ supportedAuthTypes: ['oauth'],
293
+ supportedDistributions: ['private'],
294
+ };
295
+ const mockComponentWithoutCliSelector = {
296
+ label: 'Regular Module',
297
+ path: 'module',
298
+ type: 'module',
299
+ supportedAuthTypes: ['oauth'],
300
+ supportedDistributions: ['private'],
301
+ };
302
+ const mockPromptResponse = {
303
+ componentTemplate: [
304
+ mockComponentWithCliSelector,
305
+ mockComponentWithoutCliSelector,
306
+ ],
307
+ };
308
+ mockedGetConfigForPlatformVersion.mockResolvedValue(mockConfig);
309
+ mockedGetProjectMetadata.mockResolvedValue(mockProjectMetadata);
310
+ mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockAppMeta));
311
+ mockedProjectAddPromptV3.mockResolvedValue(mockPromptResponse);
312
+ mockedCloneGithubRepo.mockResolvedValue(true);
313
+ await v3AddComponent(mockArgs, projectDir, mockProjectConfig, mockAccountId);
314
+ expect(mockedTrackCommandUsage).toHaveBeenCalledWith('project-add', {
315
+ type: 'workflow-action-tool,module',
316
+ }, mockAccountId);
317
+ });
248
318
  });
249
319
  });
@@ -2,4 +2,4 @@ import { ProjectConfig } from '../../../types/Projects.js';
2
2
  export declare function legacyAddComponent(args: {
3
3
  name?: string;
4
4
  type?: string;
5
- }, projectDir: string, projectConfig: ProjectConfig): Promise<void>;
5
+ }, projectDir: string, projectConfig: ProjectConfig, derivedAccountId: number): Promise<void>;
@@ -8,7 +8,8 @@ import path from 'path';
8
8
  import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, DEFAULT_PROJECT_TEMPLATE_BRANCH, } from '../../constants.js';
9
9
  import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
10
10
  import { uiLogger } from '../../ui/logger.js';
11
- export async function legacyAddComponent(args, projectDir, projectConfig) {
11
+ import { trackCommandUsage } from '../../usageTracking.js';
12
+ export async function legacyAddComponent(args, projectDir, projectConfig, derivedAccountId) {
12
13
  // We currently only support adding private apps to projects
13
14
  let projectContainsPublicApp = false;
14
15
  try {
@@ -27,6 +28,9 @@ export async function legacyAddComponent(args, projectDir, projectConfig) {
27
28
  throw new Error(commands.project.add.error.failedToFetchComponentList);
28
29
  }
29
30
  const projectAddPromptResponse = await projectAddPrompt(components, args);
31
+ trackCommandUsage('project-add', {
32
+ type: projectAddPromptResponse.componentTemplate.type,
33
+ }, derivedAccountId);
30
34
  try {
31
35
  const componentPath = path.join(projectDir, projectConfig.srcDir, projectAddPromptResponse.name);
32
36
  await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, componentPath, {
@@ -5,4 +5,5 @@ export declare function v3AddComponent(args: {
5
5
  features?: string[];
6
6
  auth?: string;
7
7
  distribution?: string;
8
+ derivedAccountId: number;
8
9
  }, projectDir: string, projectConfig: ProjectConfig, accountId: number): Promise<void>;
@@ -46,9 +46,9 @@ export async function v3AddComponent(args, projectDir, projectConfig, accountId)
46
46
  derivedDistribution = apps[0].config?.distribution;
47
47
  derivedAuthType = apps[0].config?.auth?.type;
48
48
  }
49
- const componentTemplateChoices = calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, currentProjectMetadata);
49
+ const componentTemplateChoices = await calculateComponentTemplateChoices(components, derivedAuthType, derivedDistribution, args.derivedAccountId, currentProjectMetadata);
50
50
  const projectAddPromptResponse = await projectAddPromptV3(componentTemplateChoices, args.features);
51
- const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.type);
51
+ const componentTypes = projectAddPromptResponse.componentTemplate?.map(componentTemplate => componentTemplate.cliSelector || componentTemplate.type);
52
52
  await trackCommandUsage('project-add', {
53
53
  type: componentTypes?.join(','),
54
54
  }, accountId);
@@ -26,9 +26,9 @@ describe('lib/projects/create/v3', () => {
26
26
  card: { count: 3, maxCount: 3, hsMetaFiles: [] },
27
27
  },
28
28
  };
29
- it('returns enabled components when they meet all requirements', () => {
30
- const choices = calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', mockProjectMetadataForChoices);
31
- expect(choices).toHaveLength(3); // includes separator
29
+ it('returns enabled components when they meet all requirements', async () => {
30
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'private', 123, mockProjectMetadataForChoices);
31
+ expect(choices).toHaveLength(4); // includes separator
32
32
  expect(choices[0]).toEqual({
33
33
  name: 'Module Component',
34
34
  value: mockComponents[0],
@@ -39,8 +39,8 @@ describe('lib/projects/create/v3', () => {
39
39
  disabled: expect.stringContaining('maximum'),
40
40
  });
41
41
  });
42
- it('disables components when auth type is not supported', () => {
43
- const choices = calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', mockProjectMetadataForChoices);
42
+ it('disables components when auth type is not supported', async () => {
43
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'privatekey', 'private', 123, mockProjectMetadataForChoices);
44
44
  // All components should be disabled, so they come after the separator
45
45
  expect(choices[1]).toEqual({
46
46
  name: expect.stringContaining('Module Component'),
@@ -48,8 +48,8 @@ describe('lib/projects/create/v3', () => {
48
48
  disabled: expect.stringContaining('privatekey'),
49
49
  });
50
50
  });
51
- it('disables components when distribution is not supported', () => {
52
- const choices = calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', mockProjectMetadataForChoices);
51
+ it('disables components when distribution is not supported', async () => {
52
+ const choices = await calculateComponentTemplateChoices(mockComponents, 'oauth', 'enterprise', 123, mockProjectMetadataForChoices);
53
53
  // All components should be disabled, so they come after the separator
54
54
  expect(choices[1]).toEqual({
55
55
  name: expect.stringContaining('Module Component'),
@@ -57,7 +57,7 @@ describe('lib/projects/create/v3', () => {
57
57
  disabled: expect.stringContaining('enterprise'),
58
58
  });
59
59
  });
60
- it('handles components without auth type or distribution restrictions', () => {
60
+ it('handles components without auth type or distribution restrictions', async () => {
61
61
  const componentsWithoutRestrictions = [
62
62
  {
63
63
  label: 'Unrestricted Component',
@@ -65,7 +65,7 @@ describe('lib/projects/create/v3', () => {
65
65
  type: 'module',
66
66
  },
67
67
  ];
68
- const choices = calculateComponentTemplateChoices(componentsWithoutRestrictions, 'oauth', 'private', {
68
+ const choices = await calculateComponentTemplateChoices(componentsWithoutRestrictions, 'oauth', 'private', 123, {
69
69
  hsMetaFiles: [],
70
70
  components: { module: { count: 0, maxCount: 5, hsMetaFiles: [] } },
71
71
  });
@@ -74,5 +74,93 @@ describe('lib/projects/create/v3', () => {
74
74
  value: componentsWithoutRestrictions[0],
75
75
  });
76
76
  });
77
+ it('handles components with cliSelector field (metadata compatibility)', async () => {
78
+ const componentWithCliSelector = [
79
+ {
80
+ label: 'Workflow Action Tool',
81
+ path: 'workflow-action-tool',
82
+ type: 'workflow-action',
83
+ cliSelector: 'workflow-action-tool',
84
+ supportedAuthTypes: ['oauth'],
85
+ supportedDistributions: ['private'],
86
+ },
87
+ ];
88
+ const projectMetadataWithWorkflowAction = {
89
+ hsMetaFiles: [],
90
+ components: {
91
+ 'workflow-action': { count: 2, maxCount: 3, hsMetaFiles: [] },
92
+ },
93
+ };
94
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 213, projectMetadataWithWorkflowAction);
95
+ expect(choices).toHaveLength(1); // no disabled components
96
+ expect(choices[0]).toEqual({
97
+ name: 'Workflow Action Tool',
98
+ value: componentWithCliSelector[0],
99
+ });
100
+ });
101
+ it('disables component when project metadata count exceeds maximum', async () => {
102
+ const componentWithCliSelector = [
103
+ {
104
+ label: 'Workflow Action Tool',
105
+ path: 'workflow-action-tool',
106
+ type: 'workflow-action',
107
+ cliSelector: 'workflow-action-tool',
108
+ supportedAuthTypes: ['oauth'],
109
+ supportedDistributions: ['private'],
110
+ },
111
+ ];
112
+ const projectMetadataAtMaxWorkflowAction = {
113
+ hsMetaFiles: [],
114
+ components: {
115
+ 'workflow-action': { count: 3, maxCount: 3, hsMetaFiles: [] },
116
+ },
117
+ };
118
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, projectMetadataAtMaxWorkflowAction);
119
+ expect(choices).toHaveLength(3); // includes separators
120
+ expect(choices[1]).toEqual({
121
+ name: expect.stringContaining('Workflow Action Tool'),
122
+ value: componentWithCliSelector[0],
123
+ disabled: expect.stringContaining('maximum'),
124
+ });
125
+ });
126
+ it('handles undefined projectMetadata without throwing errors', async () => {
127
+ const componentWithCliSelector = [
128
+ {
129
+ label: 'Workflow Action Tool',
130
+ path: 'workflow-action-tool',
131
+ type: 'workflow-action',
132
+ cliSelector: 'workflow-action-tool',
133
+ supportedAuthTypes: ['oauth'],
134
+ supportedDistributions: ['private'],
135
+ },
136
+ ];
137
+ const choices = await calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123, undefined);
138
+ expect(choices).toHaveLength(1); // no disabled components
139
+ expect(choices[0]).toEqual({
140
+ name: 'Workflow Action Tool',
141
+ value: componentWithCliSelector[0],
142
+ });
143
+ });
144
+ it('handles projectMetadata with undefined components property (after fix)', async () => {
145
+ const componentWithCliSelector = [
146
+ {
147
+ label: 'Workflow Action Tool',
148
+ path: 'workflow-action-tool',
149
+ type: 'workflow-action',
150
+ cliSelector: 'workflow-action-tool',
151
+ supportedAuthTypes: ['oauth'],
152
+ supportedDistributions: ['private'],
153
+ },
154
+ ];
155
+ const projectMetadataWithoutComponents = {
156
+ hsMetaFiles: [],
157
+ components: undefined,
158
+ };
159
+ // This test verifies the null check fix works
160
+ // Currently this will fail because the fix checks for projectMetadata.components
161
+ await expect(async () => calculateComponentTemplateChoices(componentWithCliSelector, 'oauth', 'private', 123,
162
+ // @ts-expect-error breaking stuff on purpose
163
+ projectMetadataWithoutComponents)).rejects.toThrow();
164
+ });
77
165
  });
78
166
  });
@@ -1,7 +1,7 @@
1
1
  import { selectProjectTemplatePrompt, } from '../../prompts/selectProjectTemplatePrompt.js';
2
2
  import { projectNameAndDestPrompt } from '../../prompts/projectNameAndDestPrompt.js';
3
3
  import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, EMPTY_PROJECT, } from '../../constants.js';
4
- import { useV3Api } from '../buildAndDeploy.js';
4
+ import { useV3Api } from '../platformVersion.js';
5
5
  import { v3ComponentFlow } from './v3.js';
6
6
  import { getProjectTemplateListFromRepo } from './legacy.js';
7
7
  import { uiLogger } from '../../ui/logger.js';
@@ -12,7 +12,7 @@ export async function handleProjectCreationFlow(args) {
12
12
  const repo = templateSource || HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH;
13
13
  const projectNameAndDestPromptResponse = await projectNameAndDestPrompt(args);
14
14
  if (useV3Api(platformVersion)) {
15
- const { componentTemplateChoices, authType, distribution, repoConfig, projectContents, } = await v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution);
15
+ const { componentTemplateChoices, authType, distribution, repoConfig, projectContents, } = await v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, args.derivedAccountId);
16
16
  const selectProjectTemplatePromptResponse = await selectProjectTemplatePrompt(args, undefined, projectContents !== EMPTY_PROJECT ? componentTemplateChoices : undefined);
17
17
  return {
18
18
  authType,
@@ -3,7 +3,7 @@ import { DEFAULT_PROJECT_TEMPLATE_BRANCH, HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH
3
3
  import { EXIT_CODES } from '../../enums/exitCodes.js';
4
4
  import { debugError } from '../../errorHandlers/index.js';
5
5
  import { uiLogger } from '../../ui/logger.js';
6
- import { useV3Api } from '../buildAndDeploy.js';
6
+ import { useV3Api } from '../platformVersion.js';
7
7
  import { lib } from '../../../lang/en.js';
8
8
  const PROJECT_TEMPLATE_PROPERTIES = ['name', 'label', 'path'];
9
9
  export const EMPTY_PROJECT_TEMPLATE_NAME = 'no-template';
@@ -6,7 +6,7 @@ export declare function createV3App(providedAuth: string | undefined, providedDi
6
6
  authType: string;
7
7
  distribution: string;
8
8
  }>;
9
- export declare function calculateComponentTemplateChoices(components: ComponentTemplate[], authType: string | undefined, distribution: string | undefined, projectMetadata?: ProjectMetadata): (ComponentTemplateChoice | Separator)[];
9
+ export declare function calculateComponentTemplateChoices(components: ComponentTemplate[], authType: string | undefined, distribution: string | undefined, accountId: number, projectMetadata?: ProjectMetadata): Promise<(ComponentTemplateChoice | Separator)[]>;
10
10
  type V3ComponentInfo = {
11
11
  authType?: string;
12
12
  distribution?: string;
@@ -14,7 +14,7 @@ type V3ComponentInfo = {
14
14
  projectContents?: string;
15
15
  componentTemplateChoices?: (ComponentTemplateChoice | Separator)[];
16
16
  };
17
- export declare function v3ComponentFlow(platformVersion: string, projectBase: string | undefined, providedAuth: string | undefined, providedDistribution: string | undefined): Promise<V3ComponentInfo>;
17
+ export declare function v3ComponentFlow(platformVersion: string, projectBase: string | undefined, providedAuth: string | undefined, providedDistribution: string | undefined, accountId: number): Promise<V3ComponentInfo>;
18
18
  export declare function generateComponentPaths({ selectProjectTemplatePromptResponse, platformVersion, repoConfig, projectContents, authType, distribution, }: {
19
19
  selectProjectTemplatePromptResponse: SelectProjectTemplatePromptResponse;
20
20
  platformVersion: string;
@@ -1,13 +1,15 @@
1
1
  import { Separator } from '@inquirer/prompts';
2
- import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_PROJECT, PROJECT_WITH_APP, } from '../../constants.js';
2
+ import { marketplaceDistribution, oAuth, privateDistribution, staticAuth, EMPTY_PROJECT, PROJECT_WITH_APP, FEATURES, } from '../../constants.js';
3
3
  import { commands, lib } from '../../../lang/en.js';
4
4
  import { listPrompt } from '../../prompts/promptUtils.js';
5
5
  import chalk from 'chalk';
6
- import { useV3Api } from '../buildAndDeploy.js';
6
+ import { useV3Api } from '../platformVersion.js';
7
7
  import path from 'path';
8
8
  import { getConfigForPlatformVersion } from './legacy.js';
9
9
  import { logError } from '../../errorHandlers/index.js';
10
10
  import { EXIT_CODES } from '../../enums/exitCodes.js';
11
+ import { hasFeature } from '../../hasFeature.js';
12
+ import { AppEventsKey, PagesKey, } from '@hubspot/project-parsing-lib/src/lib/constants.js';
11
13
  export async function createV3App(providedAuth, providedDistribution) {
12
14
  let authType;
13
15
  if (providedAuth &&
@@ -47,16 +49,26 @@ export async function createV3App(providedAuth, providedDistribution) {
47
49
  authType: authType,
48
50
  };
49
51
  }
50
- export function calculateComponentTemplateChoices(components, authType, distribution, projectMetadata) {
52
+ const componentTypeToGateMap = {
53
+ [AppEventsKey]: FEATURES.APP_EVENTS,
54
+ [PagesKey]: FEATURES.APPS_HOME,
55
+ };
56
+ export async function calculateComponentTemplateChoices(components, authType, distribution, accountId, projectMetadata) {
51
57
  const enabledComponents = [];
52
58
  const disabledComponents = [];
53
- components.forEach(template => {
59
+ for (const template of components) {
54
60
  const { supportedAuthTypes, supportedDistributions } = template;
55
61
  const disabledReasons = [];
56
62
  if (projectMetadata) {
57
- const { count, maxCount } = projectMetadata.components[template.type];
58
- if (count >= maxCount) {
59
- disabledReasons.push(commands.project.add.error.maxExceeded(maxCount));
63
+ const componentMetadata = projectMetadata.components[template.type];
64
+ if (!componentMetadata) {
65
+ disabledReasons.push(commands.project.add.error.invalidComponentType(template.type));
66
+ }
67
+ else {
68
+ const { count, maxCount } = componentMetadata;
69
+ if (count >= maxCount) {
70
+ disabledReasons.push(commands.project.add.error.maxExceeded(maxCount));
71
+ }
60
72
  }
61
73
  }
62
74
  if (Array.isArray(supportedAuthTypes) &&
@@ -69,9 +81,15 @@ export function calculateComponentTemplateChoices(components, authType, distribu
69
81
  !supportedDistributions.includes(distribution)) {
70
82
  disabledReasons.push(commands.project.add.error.distributionNotAllowed(distribution));
71
83
  }
84
+ if (componentTypeToGateMap[template.type]) {
85
+ const isUngated = await hasFeature(accountId, componentTypeToGateMap[template.type]);
86
+ if (!isUngated) {
87
+ disabledReasons.push(commands.project.add.error.portalDoesNotHaveAccessToThisFeature(accountId));
88
+ }
89
+ }
72
90
  if (disabledReasons.length > 0) {
73
91
  disabledComponents.push({
74
- name: `[${chalk.yellow('DISABLED')}] ${template.label}`,
92
+ name: `[${chalk.yellow('DISABLED')}] ${template.label} -`,
75
93
  value: template,
76
94
  disabled: disabledReasons.join(' '),
77
95
  });
@@ -82,12 +100,17 @@ export function calculateComponentTemplateChoices(components, authType, distribu
82
100
  value: template,
83
101
  });
84
102
  }
85
- });
103
+ }
86
104
  return disabledComponents.length
87
- ? [...enabledComponents, new Separator(), ...disabledComponents]
105
+ ? [
106
+ ...enabledComponents,
107
+ new Separator(),
108
+ ...disabledComponents,
109
+ new Separator(),
110
+ ]
88
111
  : [...enabledComponents];
89
112
  }
90
- export async function v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution) {
113
+ export async function v3ComponentFlow(platformVersion, projectBase, providedAuth, providedDistribution, accountId) {
91
114
  let repoConfig = undefined;
92
115
  let authType;
93
116
  let distribution;
@@ -113,7 +136,7 @@ export async function v3ComponentFlow(platformVersion, projectBase, providedAuth
113
136
  authType = selectedAuthType;
114
137
  distribution = selectedDistribution;
115
138
  }
116
- const componentTemplateChoices = calculateComponentTemplateChoices(repoConfig?.components || [], authType, distribution);
139
+ const componentTemplateChoices = await calculateComponentTemplateChoices(repoConfig?.components || [], authType, distribution, accountId);
117
140
  return {
118
141
  componentTemplateChoices,
119
142
  authType,
@@ -0,0 +1,13 @@
1
+ import { Deploy } from '@hubspot/local-dev-lib/types/Deploy';
2
+ export declare function validateBuildIdForDeploy(buildId: number, deployedBuildId: number | undefined, latestBuildId: number, projectName: string | undefined, accountId: number): boolean | string;
3
+ export declare function logDeployErrors(errorData: {
4
+ message: string;
5
+ errors: Array<{
6
+ message: string;
7
+ subCategory: string;
8
+ context: {
9
+ COMPONENT_NAME: string;
10
+ };
11
+ }>;
12
+ }): void;
13
+ export declare function handleProjectDeploy(targetAccountId: number, projectName: string, buildId: number, useV3Api: boolean, force: boolean): Promise<Deploy | undefined>;
@@ -0,0 +1,63 @@
1
+ import { uiLogger } from '../ui/logger.js';
2
+ import { commands } from '../../lang/en.js';
3
+ import { PROJECT_ERROR_TYPES } from '../constants.js';
4
+ import { deployProject } from '@hubspot/local-dev-lib/api/projects';
5
+ import { pollDeployStatus } from './pollProjectBuildAndDeploy.js';
6
+ export function validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId) {
7
+ if (Number(buildId) > latestBuildId) {
8
+ return commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName);
9
+ }
10
+ if (Number(buildId) === deployedBuildId) {
11
+ return commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName);
12
+ }
13
+ return true;
14
+ }
15
+ export function logDeployErrors(errorData) {
16
+ uiLogger.error(errorData.message);
17
+ errorData.errors.forEach(err => {
18
+ // This is how the pre-deploy check manifests itself in < 2025.2 projects
19
+ if (err.subCategory === PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS) {
20
+ uiLogger.log(commands.project.deploy.errors.deployContainsRemovals(err.context.COMPONENT_NAME));
21
+ }
22
+ else {
23
+ uiLogger.log(err.message);
24
+ }
25
+ });
26
+ }
27
+ function handleBlockedDeploy(deployResp) {
28
+ const deployCanBeForced = deployResp.issues.every(issue => issue.blockingMessages.every(message => message.isWarning));
29
+ uiLogger.log('');
30
+ if (deployCanBeForced) {
31
+ uiLogger.warn(commands.project.deploy.errors.deployWarningsHeader);
32
+ uiLogger.log('');
33
+ }
34
+ else {
35
+ uiLogger.error(commands.project.deploy.errors.deployBlockedHeader);
36
+ uiLogger.log('');
37
+ }
38
+ deployResp.issues.forEach(issue => {
39
+ if (issue.blockingMessages.length > 0) {
40
+ issue.blockingMessages.forEach(message => {
41
+ uiLogger.log(commands.project.deploy.errors.deployIssueComponentWarning(issue.uid, issue.componentTypeName, message.message));
42
+ });
43
+ }
44
+ else {
45
+ uiLogger.log(commands.project.deploy.errors.deployIssueComponentGeneric(issue.uid, issue.componentTypeName));
46
+ }
47
+ uiLogger.log('');
48
+ });
49
+ }
50
+ export async function handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force) {
51
+ const { data: deployResp } = await deployProject(targetAccountId, projectName, buildId, useV3Api, force);
52
+ if (!deployResp || deployResp.buildResultType !== 'DEPLOY_QUEUED') {
53
+ if (deployResp?.buildResultType === 'DEPLOY_BLOCKED') {
54
+ handleBlockedDeploy(deployResp);
55
+ }
56
+ else {
57
+ uiLogger.error(commands.project.deploy.errors.deploy);
58
+ }
59
+ return;
60
+ }
61
+ const deployResult = await pollDeployStatus(targetAccountId, projectName, Number(deployResp.id), buildId);
62
+ return deployResult;
63
+ }
@@ -15,11 +15,9 @@ declare class AppDevModeInterface {
15
15
  private get appNode();
16
16
  private get appData();
17
17
  private set appData(value);
18
- private isAutomaticallyInstallable;
19
18
  private getAppInstallUrl;
20
19
  private fetchAppData;
21
20
  private checkMarketplaceAppInstalls;
22
- private autoInstallStaticAuthApp;
23
21
  private installAppOrOpenInstallUrl;
24
22
  private checkTestAccountAppInstallation;
25
23
  private onDevServerMessage;