@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
@@ -1,35 +1,173 @@
1
1
  import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth';
2
- import { hasFeature } from '../hasFeature.js';
2
+ import { http } from '@hubspot/local-dev-lib/http';
3
+ import { hasFeature, hasUnfiedAppsAccess } from '../hasFeature.js';
4
+ import { FEATURES } from '../constants.js';
3
5
  vi.mock('@hubspot/local-dev-lib/api/localDevAuth');
6
+ vi.mock('@hubspot/local-dev-lib/http');
4
7
  const mockedFetchEnabledFeatures = fetchEnabledFeatures;
8
+ const mockedHttp = http;
5
9
  describe('lib/hasFeature', () => {
6
10
  describe('hasFeature()', () => {
7
11
  const accountId = 123;
8
- beforeEach(() => {
12
+ afterEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ it('should return true if the feature is enabled', async () => {
9
16
  mockedFetchEnabledFeatures.mockResolvedValueOnce({
10
17
  data: {
11
18
  enabledFeatures: {
12
19
  'feature-1': true,
13
- 'feature-2': false,
14
- 'feature-3': true,
15
20
  },
16
21
  },
17
22
  });
18
- });
19
- it('should return true if the feature is enabled', async () => {
20
23
  // @ts-expect-error test data
21
24
  const result = await hasFeature(accountId, 'feature-1');
22
25
  expect(result).toBe(true);
23
26
  });
24
- it('should return false if the feature is not enabled', async () => {
27
+ it('should return false if the feature is disabled', async () => {
28
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
29
+ data: {
30
+ enabledFeatures: {
31
+ 'feature-2': false,
32
+ },
33
+ },
34
+ });
25
35
  // @ts-expect-error test data
26
36
  const result = await hasFeature(accountId, 'feature-2');
27
37
  expect(result).toBe(false);
28
38
  });
29
39
  it('should return false if the feature is not present', async () => {
40
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
41
+ data: {
42
+ enabledFeatures: {},
43
+ },
44
+ });
30
45
  // @ts-expect-error test data
31
46
  const result = await hasFeature(accountId, 'feature-4');
32
47
  expect(result).toBe(false);
33
48
  });
49
+ it('should return true for APPS_HOME feature when not present in enabled features (defaults on)', async () => {
50
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
51
+ data: {
52
+ enabledFeatures: {},
53
+ },
54
+ });
55
+ const result = await hasFeature(accountId, FEATURES.APPS_HOME);
56
+ expect(result).toBe(true);
57
+ });
58
+ it('should respect explicit setting for APPS_HOME feature even when it defaults on', async () => {
59
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
60
+ data: {
61
+ enabledFeatures: {
62
+ [FEATURES.APPS_HOME]: false,
63
+ },
64
+ },
65
+ });
66
+ const result = await hasFeature(accountId, FEATURES.APPS_HOME);
67
+ expect(result).toBe(false);
68
+ });
69
+ it('should handle truthy values correctly', async () => {
70
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
71
+ data: {
72
+ enabledFeatures: {
73
+ 'feature-truthy': 'yes',
74
+ },
75
+ },
76
+ });
77
+ // @ts-expect-error test data
78
+ const truthyResult = await hasFeature(accountId, 'feature-truthy');
79
+ expect(truthyResult).toBe(true);
80
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
81
+ data: {
82
+ enabledFeatures: {
83
+ 'feature-number': 1,
84
+ },
85
+ },
86
+ });
87
+ // @ts-expect-error test data
88
+ const numberResult = await hasFeature(accountId, 'feature-number');
89
+ expect(numberResult).toBe(true);
90
+ });
91
+ it('should handle falsy values correctly', async () => {
92
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
93
+ data: {
94
+ enabledFeatures: {
95
+ 'feature-null': null,
96
+ },
97
+ },
98
+ });
99
+ // @ts-expect-error test data
100
+ const nullResult = await hasFeature(accountId, 'feature-null');
101
+ expect(nullResult).toBe(false);
102
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
103
+ data: {
104
+ enabledFeatures: {
105
+ 'feature-zero': 0,
106
+ },
107
+ },
108
+ });
109
+ // @ts-expect-error test data
110
+ const zeroResult = await hasFeature(accountId, 'feature-zero');
111
+ expect(zeroResult).toBe(false);
112
+ mockedFetchEnabledFeatures.mockResolvedValueOnce({
113
+ data: {
114
+ enabledFeatures: {
115
+ 'feature-empty': '',
116
+ },
117
+ },
118
+ });
119
+ // @ts-expect-error test data
120
+ const emptyResult = await hasFeature(accountId, 'feature-empty');
121
+ expect(emptyResult).toBe(false);
122
+ });
123
+ it('should propagate errors from fetchEnabledFeatures', async () => {
124
+ const error = new Error('API error');
125
+ mockedFetchEnabledFeatures.mockRejectedValueOnce(error);
126
+ await expect(hasFeature(accountId, FEATURES.UNIFIED_APPS)).rejects.toThrow('API error');
127
+ });
128
+ });
129
+ describe('hasUnfiedAppsAccess()', () => {
130
+ const accountId = 123;
131
+ afterEach(() => {
132
+ vi.clearAllMocks();
133
+ });
134
+ it('should return true when API returns true', async () => {
135
+ // @ts-expect-error Don't want to mock the full response object
136
+ mockedHttp.get.mockResolvedValueOnce({ data: true });
137
+ const result = await hasUnfiedAppsAccess(accountId);
138
+ expect(result).toBe(true);
139
+ expect(mockedHttp.get).toHaveBeenCalledWith(accountId, {
140
+ url: 'developer-tooling/external/developer-portal/has-unified-dev-platform-access',
141
+ });
142
+ });
143
+ it('should return false when API returns false', async () => {
144
+ // @ts-expect-error Don't want to mock the full response object
145
+ mockedHttp.get.mockResolvedValueOnce({ data: false });
146
+ const result = await hasUnfiedAppsAccess(accountId);
147
+ expect(result).toBe(false);
148
+ });
149
+ it('should handle truthy values correctly', async () => {
150
+ // @ts-expect-error Don't want to mock the full response object
151
+ mockedHttp.get.mockResolvedValueOnce({ data: 'yes' });
152
+ const result = await hasUnfiedAppsAccess(accountId);
153
+ expect(result).toBe(true);
154
+ });
155
+ it('should handle falsy values correctly', async () => {
156
+ // @ts-expect-error Don't want to mock the full response object
157
+ mockedHttp.get.mockResolvedValueOnce({ data: null });
158
+ const result = await hasUnfiedAppsAccess(accountId);
159
+ expect(result).toBe(false);
160
+ });
161
+ it('should handle undefined response data', async () => {
162
+ // @ts-expect-error Don't want to mock the full response object
163
+ mockedHttp.get.mockResolvedValueOnce({ data: undefined });
164
+ const result = await hasUnfiedAppsAccess(accountId);
165
+ expect(result).toBe(false);
166
+ });
167
+ it('should propagate errors from http.get', async () => {
168
+ const error = new Error('Network error');
169
+ mockedHttp.get.mockRejectedValueOnce(error);
170
+ await expect(hasUnfiedAppsAccess(accountId)).rejects.toThrow('Network error');
171
+ });
34
172
  });
35
173
  });
@@ -43,7 +43,7 @@ describe('lib/importData', () => {
43
43
  data: { id: '123' },
44
44
  });
45
45
  await handleImportData(targetAccountId, dataFileNames, importRequest);
46
- expect(mockUiLogger.success).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
46
+ expect(mockUiLogger.info).toHaveBeenCalledWith(lib.importData.viewImportLink('https://app.hubspot.com', targetAccountId, '123'));
47
47
  });
48
48
  it('should log the correct error message', async () => {
49
49
  mockCreateImport.mockRejectedValue(new Error('test-error'));
@@ -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);
package/lib/mcp/setup.js CHANGED
@@ -182,7 +182,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
182
182
  });
183
183
  await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
184
184
  }
185
- await execAsync(`claude mcp add-json "${mcpServerName}" '${mcpConfig}' --scope user`);
185
+ await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
186
186
  SpinniesManager.succeed('claudeCode', {
187
187
  text: commands.mcp.setup.spinners.configuredClaudeCode,
188
188
  });
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
5
  export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
6
6
  export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
7
- export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined, useEnv?: boolean): Promise<number | undefined>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
@@ -38,12 +38,7 @@ export function loadProfile(projectConfig, projectDir, profileName) {
38
38
  uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
39
39
  return;
40
40
  }
41
- return {
42
- ...profile,
43
- accountId: process.env.HUBSPOT_ACCOUNT_ID
44
- ? Number(process.env.HUBSPOT_ACCOUNT_ID)
45
- : profile.accountId,
46
- };
41
+ return profile;
47
42
  }
48
43
  catch (e) {
49
44
  uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
@@ -59,7 +54,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
59
54
  }
60
55
  }
61
56
  }
62
- export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile, useEnv = false) {
57
+ export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
63
58
  if (argsProfile) {
64
59
  logProfileHeader(argsProfile);
65
60
  const profile = loadProfile(projectConfig, projectDir, argsProfile);
@@ -68,9 +63,6 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
68
63
  process.exit(EXIT_CODES.ERROR);
69
64
  }
70
65
  logProfileFooter(profile, true);
71
- if (useEnv) {
72
- return Number(process.env.HUBSPOT_ACCOUNT_ID);
73
- }
74
66
  return profile.accountId;
75
67
  }
76
68
  else {
@@ -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
+ });
@@ -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);