@hubspot/cli 7.7.32-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 (77) hide show
  1. package/commands/getStarted.js +5 -4
  2. package/commands/project/__tests__/add.test.js +3 -5
  3. package/commands/project/__tests__/deploy.test.js +3 -2
  4. package/commands/project/add.js +2 -4
  5. package/commands/project/deploy.js +9 -61
  6. package/commands/project/dev/index.js +1 -1
  7. package/commands/project/dev/unifiedFlow.js +3 -0
  8. package/commands/project/upload.js +2 -2
  9. package/commands/project/validate.js +1 -1
  10. package/commands/project/watch.js +2 -2
  11. package/lang/en.d.ts +7 -3
  12. package/lang/en.js +8 -4
  13. package/lib/__tests__/hasFeature.test.js +145 -7
  14. package/lib/__tests__/importData.test.js +1 -1
  15. package/lib/app/migrate.js +9 -2
  16. package/lib/constants.d.ts +2 -0
  17. package/lib/constants.js +2 -0
  18. package/lib/errorHandlers/index.d.ts +4 -0
  19. package/lib/errorHandlers/index.js +1 -1
  20. package/lib/hasFeature.js +6 -0
  21. package/lib/importData.js +1 -1
  22. package/lib/projects/__tests__/AppDevModeInterface.test.js +61 -44
  23. package/lib/projects/__tests__/LocalDevProcess.test.js +1 -0
  24. package/lib/projects/__tests__/deploy.test.js +164 -0
  25. package/lib/projects/__tests__/platformVersion.test.d.ts +1 -0
  26. package/lib/projects/__tests__/{buildAndDeploy.test.js → platformVersion.test.js} +2 -2
  27. package/lib/projects/add/__tests__/legacyAddComponent.test.js +49 -6
  28. package/lib/projects/add/__tests__/v3AddComponent.test.js +71 -1
  29. package/lib/projects/add/legacyAddComponent.d.ts +1 -1
  30. package/lib/projects/add/legacyAddComponent.js +5 -1
  31. package/lib/projects/add/v3AddComponent.d.ts +1 -0
  32. package/lib/projects/add/v3AddComponent.js +2 -2
  33. package/lib/projects/create/__tests__/v3.test.js +97 -9
  34. package/lib/projects/create/index.js +2 -2
  35. package/lib/projects/create/legacy.js +1 -1
  36. package/lib/projects/create/v3.d.ts +2 -2
  37. package/lib/projects/create/v3.js +35 -12
  38. package/lib/projects/deploy.d.ts +13 -0
  39. package/lib/projects/deploy.js +63 -0
  40. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -2
  41. package/lib/projects/localDev/AppDevModeInterface.js +65 -36
  42. package/lib/projects/localDev/DevServerManagerV2.js +1 -0
  43. package/lib/projects/localDev/LocalDevProcess.js +3 -1
  44. package/lib/projects/localDev/LocalDevState.d.ts +5 -2
  45. package/lib/projects/localDev/LocalDevState.js +9 -1
  46. package/lib/projects/localDev/helpers/project.js +1 -1
  47. package/lib/projects/platformVersion.d.ts +1 -0
  48. package/lib/projects/platformVersion.js +10 -0
  49. package/lib/projects/{buildAndDeploy.d.ts → pollProjectBuildAndDeploy.d.ts} +0 -1
  50. package/lib/projects/{buildAndDeploy.js → pollProjectBuildAndDeploy.js} +0 -10
  51. package/lib/projects/upload.js +1 -1
  52. package/lib/projects/urls.d.ts +1 -0
  53. package/lib/projects/urls.js +3 -0
  54. package/lib/prompts/__tests__/projectAddPrompt.test.d.ts +1 -0
  55. package/lib/prompts/__tests__/projectAddPrompt.test.js +143 -0
  56. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.d.ts +1 -0
  57. package/lib/prompts/__tests__/selectProjectTemplatePrompt.test.js +160 -0
  58. package/lib/prompts/createDeveloperTestAccountConfigPrompt.js +1 -0
  59. package/lib/prompts/importDataFilePathPrompt.js +4 -2
  60. package/lib/prompts/installAppPrompt.d.ts +6 -1
  61. package/lib/prompts/installAppPrompt.js +6 -1
  62. package/lib/prompts/projectAddPrompt.js +1 -1
  63. package/lib/prompts/selectProjectTemplatePrompt.js +1 -1
  64. package/mcp-server/tools/cms/HsFunctionLogsTool.d.ts +32 -0
  65. package/mcp-server/tools/cms/HsFunctionLogsTool.js +76 -0
  66. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.d.ts +1 -0
  67. package/mcp-server/tools/cms/__tests__/HsFunctionLogsTool.test.js +183 -0
  68. package/mcp-server/tools/index.js +2 -0
  69. package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +3 -3
  70. package/mcp-server/tools/project/CreateProjectTool.d.ts +3 -3
  71. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -3
  72. package/mcp-server/tools/project/constants.d.ts +1 -1
  73. package/mcp-server/tools/project/constants.js +6 -4
  74. package/package.json +3 -3
  75. package/types/LocalDev.d.ts +2 -1
  76. package/types/Projects.d.ts +1 -0
  77. /package/lib/projects/__tests__/{buildAndDeploy.test.d.ts → deploy.test.d.ts} +0 -0
@@ -22,7 +22,7 @@ import { hasUnfiedAppsAccess } from '../hasFeature.js';
22
22
  import { getProjectBuildDetailUrl, getProjectDetailUrl, } from '../projects/urls.js';
23
23
  import { uiLogger } from '../ui/logger.js';
24
24
  import { debugError } from '../errorHandlers/index.js';
25
- import { useV3Api } from '../projects/buildAndDeploy.js';
25
+ import { useV3Api } from '../projects/platformVersion.js';
26
26
  export function getUnmigratableReason(reasonCode, projectName, accountId) {
27
27
  switch (reasonCode) {
28
28
  case UNMIGRATABLE_REASONS.UP_TO_DATE:
@@ -236,13 +236,20 @@ export async function handleThemesMigration(projectConfig, platformVersion) {
236
236
  throw new Error(lib.migrate.errors.project.invalidConfig);
237
237
  }
238
238
  const projectSrcDir = path.resolve(projectConfig.projectDir, projectConfig.projectConfig.srcDir);
239
+ let migrated = false;
240
+ let failureReason;
239
241
  try {
240
- await migrateThemes(projectConfig.projectDir, projectSrcDir);
242
+ const migrationResult = await migrateThemes(projectConfig.projectDir, projectSrcDir);
243
+ migrated = migrationResult.migrated;
244
+ failureReason = migrationResult.failureReason;
241
245
  }
242
246
  catch (error) {
243
247
  debugError(error);
244
248
  throw new Error(lib.migrate.errors.project.failedToMigrateThemes);
245
249
  }
250
+ if (!migrated) {
251
+ throw new Error(failureReason || lib.migrate.errors.project.failedToMigrateThemes);
252
+ }
246
253
  const newProjectConfig = { ...projectConfig.projectConfig };
247
254
  newProjectConfig.platformVersion = platformVersion;
248
255
  const projectConfigPath = path.join(projectConfig.projectDir, PROJECT_CONFIG_FILE);
@@ -80,6 +80,8 @@ export declare const FEATURES: {
80
80
  readonly UNIFIED_APPS: "Developers:UnifiedApps:PrivateBeta";
81
81
  readonly SANDBOXES_V2: "sandboxes:v2:enabled";
82
82
  readonly SANDBOXES_V2_CLI: "sandboxes:v2:cliEnabled";
83
+ readonly APP_EVENTS: "Developers:UnifiedApps:AppEventsAccess";
84
+ readonly APPS_HOME: "UIE:AppHome";
83
85
  };
84
86
  export declare const LOCAL_DEV_UI_MESSAGE_SEND_TYPES: {
85
87
  UPLOAD_SUCCESS: string;
package/lib/constants.js CHANGED
@@ -72,6 +72,8 @@ export const FEATURES = {
72
72
  UNIFIED_APPS: 'Developers:UnifiedApps:PrivateBeta',
73
73
  SANDBOXES_V2: 'sandboxes:v2:enabled',
74
74
  SANDBOXES_V2_CLI: 'sandboxes:v2:cliEnabled',
75
+ APP_EVENTS: 'Developers:UnifiedApps:AppEventsAccess',
76
+ APPS_HOME: 'UIE:AppHome',
75
77
  };
76
78
  export const LOCAL_DEV_UI_MESSAGE_SEND_TYPES = {
77
79
  UPLOAD_SUCCESS: 'server:uploadSuccess',
@@ -12,3 +12,7 @@ export declare class ApiErrorContext {
12
12
  projectName?: string;
13
13
  });
14
14
  }
15
+ export declare function isErrorWithMessageOrReason(error: unknown): error is {
16
+ message?: string;
17
+ reason?: string;
18
+ };
@@ -85,7 +85,7 @@ export class ApiErrorContext {
85
85
  this.projectName = props.projectName || '';
86
86
  }
87
87
  }
88
- function isErrorWithMessageOrReason(error) {
88
+ export function isErrorWithMessageOrReason(error) {
89
89
  return (typeof error === 'object' &&
90
90
  error !== null &&
91
91
  ('message' in error || 'reason' in error));
package/lib/hasFeature.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { http } from '@hubspot/local-dev-lib/http';
2
2
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
3
+ import { FEATURES } from './constants.js';
4
+ const FEATURES_THAT_DEFAULT_ON = [FEATURES.APPS_HOME];
3
5
  export async function hasFeature(accountId, feature) {
4
6
  const { data: { enabledFeatures }, } = await fetchEnabledFeatures(accountId);
7
+ if (enabledFeatures[feature] === undefined &&
8
+ FEATURES_THAT_DEFAULT_ON.includes(feature)) {
9
+ return true;
10
+ }
5
11
  return Boolean(enabledFeatures[feature]);
6
12
  }
7
13
  export async function hasUnfiedAppsAccess(accountId) {
package/lib/importData.js CHANGED
@@ -10,7 +10,7 @@ export async function handleImportData(targetAccountId, dataFileNames, importReq
10
10
  const baseUrl = getHubSpotWebsiteOrigin(getEnv());
11
11
  const response = await createImport(targetAccountId, importRequest, dataFileNames);
12
12
  const importId = response.data.id;
13
- uiLogger.success(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
13
+ uiLogger.info(lib.importData.viewImportLink(baseUrl, targetAccountId, importId));
14
14
  }
15
15
  catch (error) {
16
16
  uiLogger.error(lib.importData.errors.failedToImportData);
@@ -240,16 +240,21 @@ describe('AppDevModeInterface', () => {
240
240
  await newAppDevModeInterface.setup({});
241
241
  expect(process.exit).toHaveBeenCalledWith(0);
242
242
  });
243
- it('should auto-install static auth app on test account', async () => {
244
- fetchAppInstallationData.mockResolvedValue({
245
- data: {
246
- isInstalledWithScopeGroups: false,
247
- previouslyAuthorizedScopeGroups: [],
248
- },
249
- });
250
- await appDevModeInterface.setup({});
251
- expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
252
- });
243
+ // @TODO: Restore test account auto install functionality
244
+ // it('should auto-install static auth app on test account', async () => {
245
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
246
+ // data: {
247
+ // isInstalledWithScopeGroups: false,
248
+ // previouslyAuthorizedScopeGroups: [],
249
+ // },
250
+ // });
251
+ // await appDevModeInterface.setup({});
252
+ // expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
253
+ // 123,
254
+ // 67890,
255
+ // [1, 2, 3]
256
+ // );
257
+ // });
253
258
  it('should open browser for OAuth app installation', async () => {
254
259
  const oauthAppNode = {
255
260
  ...mockAppNode,
@@ -288,7 +293,12 @@ describe('AppDevModeInterface', () => {
288
293
  },
289
294
  });
290
295
  await appDevModeInterface.setup({});
291
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
296
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
297
+ appUid: 'test-app-uid',
298
+ projectAccountId: 12345,
299
+ projectName: 'test-project',
300
+ testingAccountId: 67890,
301
+ });
292
302
  });
293
303
  it('should handle errors during setup', async () => {
294
304
  const error = new Error('Setup failed');
@@ -318,39 +328,46 @@ describe('AppDevModeInterface', () => {
318
328
  await appDevModeInterface.setup({});
319
329
  expect(process.exit).toHaveBeenCalledWith(1);
320
330
  });
321
- it('should exit if user declines auto-install', async () => {
322
- // Set up conditions for automatic installation
323
- getAccountConfig.mockReturnValue({
324
- parentAccountId: 12345, // matches targetProjectAccountId
325
- });
326
- isDeveloperTestAccount.mockReturnValue(true);
327
- fetchAppInstallationData.mockResolvedValue({
328
- data: {
329
- isInstalledWithScopeGroups: false,
330
- previouslyAuthorizedScopeGroups: [],
331
- },
332
- });
333
- installAppAutoPrompt.mockResolvedValue(false);
334
- // Create a new instance to trigger the exit during setup
335
- const newAppDevModeInterface = new AppDevModeInterface({
336
- localDevState: mockLocalDevState,
337
- localDevLogger: mockLocalDevLogger,
338
- });
339
- // The setup method catches the error, so we check that process.exit was called
340
- await newAppDevModeInterface.setup({});
341
- expect(process.exit).toHaveBeenCalledWith(0);
342
- });
343
- it('should fallback to browser install if auto-install fails', async () => {
344
- fetchAppInstallationData.mockResolvedValue({
345
- data: {
346
- isInstalledWithScopeGroups: false,
347
- previouslyAuthorizedScopeGroups: [],
348
- },
349
- });
350
- installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
351
- await appDevModeInterface.setup({});
352
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
353
- });
331
+ // @TODO: Restore test account auto install functionality
332
+ // it('should exit if user declines auto-install', async () => {
333
+ // // Set up conditions for automatic installation
334
+ // (getAccountConfig as Mock).mockReturnValue({
335
+ // parentAccountId: 12345, // matches targetProjectAccountId
336
+ // });
337
+ // (isDeveloperTestAccount as Mock).mockReturnValue(true);
338
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
339
+ // data: {
340
+ // isInstalledWithScopeGroups: false,
341
+ // previouslyAuthorizedScopeGroups: [],
342
+ // },
343
+ // });
344
+ // (installAppAutoPrompt as Mock).mockResolvedValue(false);
345
+ // // Create a new instance to trigger the exit during setup
346
+ // const newAppDevModeInterface = new AppDevModeInterface({
347
+ // localDevState: mockLocalDevState,
348
+ // localDevLogger: mockLocalDevLogger,
349
+ // });
350
+ // // The setup method catches the error, so we check that process.exit was called
351
+ // await newAppDevModeInterface.setup({});
352
+ // expect(process.exit).toHaveBeenCalledWith(0);
353
+ // });
354
+ // @TODO: Restore test account auto install functionality
355
+ // it('should fallback to browser install if auto-install fails', async () => {
356
+ // (fetchAppInstallationData as Mock).mockResolvedValue({
357
+ // data: {
358
+ // isInstalledWithScopeGroups: false,
359
+ // previouslyAuthorizedScopeGroups: [],
360
+ // },
361
+ // });
362
+ // (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
363
+ // new Error('Install failed')
364
+ // );
365
+ // await appDevModeInterface.setup({});
366
+ // expect(installAppBrowserPrompt).toHaveBeenCalledWith(
367
+ // 'http://static-install-url',
368
+ // false
369
+ // );
370
+ // });
354
371
  });
355
372
  describe('start()', () => {
356
373
  it('should return early if no app node exists', async () => {
@@ -39,6 +39,7 @@ describe('LocalDevProcess', () => {
39
39
  targetTestingAccountId: 456,
40
40
  projectId: 789,
41
41
  initialProjectNodes: {},
42
+ initialProjectProfileData: {},
42
43
  env: ENVIRONMENTS.PROD,
43
44
  projectName: 'test-project',
44
45
  };
@@ -0,0 +1,164 @@
1
+ import { vi } from 'vitest';
2
+ import { validateBuildIdForDeploy, logDeployErrors, handleProjectDeploy, } from '../deploy.js';
3
+ import { uiLogger } from '../../ui/logger.js';
4
+ import { commands } from '../../../lang/en.js';
5
+ import { PROJECT_ERROR_TYPES } from '../../constants.js';
6
+ import { deployProject } from '@hubspot/local-dev-lib/api/projects';
7
+ import { pollDeployStatus } from '../pollProjectBuildAndDeploy.js';
8
+ // Mock external dependencies
9
+ vi.mock('../../ui/logger.js');
10
+ vi.mock('@hubspot/local-dev-lib/api/projects');
11
+ vi.mock('../pollProjectBuildAndDeploy.js');
12
+ const mockUiLogger = vi.mocked(uiLogger);
13
+ const mockDeployProject = vi.mocked(deployProject);
14
+ const mockPollDeployStatus = vi.mocked(pollDeployStatus);
15
+ describe('lib/projects/deploy', () => {
16
+ beforeEach(() => {
17
+ vi.resetAllMocks();
18
+ });
19
+ describe('validateBuildIdForDeploy()', () => {
20
+ const accountId = 12345;
21
+ const projectName = 'test-project';
22
+ it('returns true when build ID is valid for deployment', () => {
23
+ const buildId = 5;
24
+ const deployedBuildId = 3;
25
+ const latestBuildId = 10;
26
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
27
+ expect(result).toBe(true);
28
+ });
29
+ it('returns error message when build ID does not exist', () => {
30
+ const buildId = 15;
31
+ const deployedBuildId = 3;
32
+ const latestBuildId = 10;
33
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
34
+ expect(result).toBe(commands.project.deploy.errors.buildIdDoesNotExist(accountId, buildId, projectName));
35
+ });
36
+ it('returns error message when build is already deployed', () => {
37
+ const buildId = 3;
38
+ const deployedBuildId = 3;
39
+ const latestBuildId = 10;
40
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
41
+ expect(result).toBe(commands.project.deploy.errors.buildAlreadyDeployed(accountId, buildId, projectName));
42
+ });
43
+ it('handles edge case when deployedBuildId is undefined', () => {
44
+ const buildId = 5;
45
+ const deployedBuildId = undefined;
46
+ const latestBuildId = 10;
47
+ const result = validateBuildIdForDeploy(buildId, deployedBuildId, latestBuildId, projectName, accountId);
48
+ expect(result).toBe(true);
49
+ });
50
+ });
51
+ describe('logDeployErrors()', () => {
52
+ it('logs main error message and individual error messages', () => {
53
+ const errorData = {
54
+ message: 'Deploy failed with errors',
55
+ errors: [
56
+ {
57
+ message: 'Component error 1',
58
+ subCategory: 'SOME_ERROR',
59
+ context: { COMPONENT_NAME: 'test-component' },
60
+ },
61
+ {
62
+ message: 'Component error 2',
63
+ subCategory: 'ANOTHER_ERROR',
64
+ context: { COMPONENT_NAME: 'another-component' },
65
+ },
66
+ ],
67
+ };
68
+ logDeployErrors(errorData);
69
+ expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy failed with errors');
70
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 1');
71
+ expect(mockUiLogger.log).toHaveBeenCalledWith('Component error 2');
72
+ });
73
+ it('handles DEPLOY_CONTAINS_REMOVALS error type specially', () => {
74
+ const errorData = {
75
+ message: 'Deploy contains removals',
76
+ errors: [
77
+ {
78
+ message: 'Component will be removed',
79
+ subCategory: PROJECT_ERROR_TYPES.DEPLOY_CONTAINS_REMOVALS,
80
+ context: { COMPONENT_NAME: 'removed-component' },
81
+ },
82
+ ],
83
+ };
84
+ logDeployErrors(errorData);
85
+ expect(mockUiLogger.error).toHaveBeenCalledWith('Deploy contains removals');
86
+ expect(mockUiLogger.log).toHaveBeenCalledWith(commands.project.deploy.errors.deployContainsRemovals('removed-component'));
87
+ });
88
+ it('handles empty errors array', () => {
89
+ const errorData = {
90
+ message: 'No specific errors',
91
+ errors: [],
92
+ };
93
+ logDeployErrors(errorData);
94
+ expect(mockUiLogger.error).toHaveBeenCalledWith('No specific errors');
95
+ expect(mockUiLogger.log).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+ describe('handleProjectDeploy()', () => {
99
+ const targetAccountId = 12345;
100
+ const projectName = 'test-project';
101
+ const buildId = 5;
102
+ const useV3Api = true;
103
+ const force = false;
104
+ it('successfully deploys and returns deploy result', async () => {
105
+ const mockDeployResponseData = {
106
+ id: 'deploy-123',
107
+ buildResultType: 'DEPLOY_QUEUED',
108
+ links: {
109
+ status: 'http://status-url',
110
+ },
111
+ };
112
+ const mockDeployResult = {
113
+ deployId: 123,
114
+ buildId: 5,
115
+ status: 'SUCCESS',
116
+ enqueuedAt: '2023-01-01T00:00:00Z',
117
+ startedAt: '2023-01-01T00:01:00Z',
118
+ finishedAt: '2023-01-01T00:05:00Z',
119
+ portalId: targetAccountId,
120
+ projectName: 'test-project',
121
+ userId: 456,
122
+ source: 'HUBSPOT_USER',
123
+ subdeployStatuses: [],
124
+ };
125
+ mockDeployProject.mockResolvedValue({
126
+ data: mockDeployResponseData,
127
+ });
128
+ mockPollDeployStatus.mockResolvedValue(mockDeployResult);
129
+ const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
130
+ expect(mockDeployProject).toHaveBeenCalledWith(targetAccountId, projectName, buildId, useV3Api, force);
131
+ expect(result).toEqual(mockDeployResult);
132
+ });
133
+ it('handles blocked deploy with warnings', async () => {
134
+ const mockBlockedResponse = {
135
+ buildResultType: 'DEPLOY_BLOCKED',
136
+ issues: [
137
+ {
138
+ uid: 'component-1',
139
+ componentTypeName: 'module',
140
+ errorMessages: [],
141
+ blockingMessages: [
142
+ {
143
+ message: 'This is a warning',
144
+ isWarning: true,
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ };
150
+ mockDeployProject.mockResolvedValue({
151
+ data: mockBlockedResponse,
152
+ });
153
+ const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
154
+ expect(mockUiLogger.warn).toHaveBeenCalledWith(commands.project.deploy.errors.deployWarningsHeader);
155
+ expect(result).toBeUndefined();
156
+ });
157
+ it('handles general deploy failure', async () => {
158
+ mockDeployProject.mockResolvedValue({ data: null });
159
+ const result = await handleProjectDeploy(targetAccountId, projectName, buildId, useV3Api, force);
160
+ expect(mockUiLogger.error).toHaveBeenCalledWith(commands.project.deploy.errors.deploy);
161
+ expect(result).toBeUndefined();
162
+ });
163
+ });
164
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
- import { useV3Api } from '../buildAndDeploy.js';
2
- describe('buildAndDeploy', () => {
1
+ import { useV3Api } from '../platformVersion.js';
2
+ describe('platformVersion', () => {
3
3
  describe('useV3Api', () => {
4
4
  it('returns true if platform version is UNSTABLE', () => {
5
5
  expect(useV3Api('UNSTABLE')).toBe(true);
@@ -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>;