@hubspot/cli 7.10.0-experimental.0 → 7.10.1-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 (32) hide show
  1. package/commands/project/__tests__/validate.test.js +285 -27
  2. package/commands/project/deploy.js +14 -6
  3. package/commands/project/dev/index.js +13 -4
  4. package/commands/project/migrate.js +4 -13
  5. package/commands/project/upload.js +8 -2
  6. package/commands/project/validate.js +72 -12
  7. package/lang/en.d.ts +13 -10
  8. package/lang/en.js +14 -11
  9. package/lib/__tests__/projectProfiles.test.js +273 -32
  10. package/lib/middleware/autoUpdateMiddleware.js +25 -22
  11. package/lib/middleware/fireAlarmMiddleware.js +4 -15
  12. package/lib/projectProfiles.d.ts +4 -3
  13. package/lib/projectProfiles.js +78 -32
  14. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -3
  15. package/lib/projects/localDev/LocalDevLogger.js +0 -9
  16. package/lib/projects/localDev/LocalDevProcess.d.ts +0 -1
  17. package/lib/projects/localDev/LocalDevProcess.js +1 -12
  18. package/lib/projects/localDev/LocalDevState.d.ts +0 -3
  19. package/lib/projects/localDev/LocalDevState.js +0 -9
  20. package/mcp-server/utils/config.js +1 -1
  21. package/package.json +3 -4
  22. package/types/LocalDev.d.ts +0 -1
  23. package/ui/components/BoxWithTitle.d.ts +2 -1
  24. package/ui/components/BoxWithTitle.js +2 -2
  25. package/ui/components/StatusMessageBoxes.d.ts +5 -4
  26. package/ui/components/StatusMessageBoxes.js +8 -8
  27. package/lib/projects/localDev/DevSessionManager.d.ts +0 -17
  28. package/lib/projects/localDev/DevSessionManager.js +0 -56
  29. package/lib/projects/localDev/helpers/devSessionsApi.d.ts +0 -9
  30. package/lib/projects/localDev/helpers/devSessionsApi.js +0 -19
  31. package/lib/ui/boxen.d.ts +0 -5
  32. package/lib/ui/boxen.js +0 -26
package/lang/en.js CHANGED
@@ -1824,10 +1824,19 @@ export const commands = {
1824
1824
  default: 'Validate the project before uploading',
1825
1825
  },
1826
1826
  success: (projectName) => `Project ${projectName} is valid and ready to upload`,
1827
- failure: (projectName) => `Project ${projectName} is invalid`,
1827
+ failure: (projectName, profileName) => `Project ${projectName} is invalid${profileName ? `with profile ${profileName} applied` : ''}`,
1828
+ spinners: {
1829
+ validatingProfile: (profileName) => `Validating project with profile "${profileName}"`,
1830
+ profileValidationFailed: (profileName) => `Profile "${profileName}" failed validation`,
1831
+ profileValidationSucceeded: (profileName) => `Project valid with profile "${profileName}" applied`,
1832
+ invalidWithProfile: (profileName, projectName) => `Project is invalid with profile "${profileName}" applied \n ${commands.project.validate.failure(projectName)}`,
1833
+ validatingAllProfiles: 'Validating the project with all profiles',
1834
+ allProfilesValidationSucceeded: 'Project profile validation succeeded',
1835
+ allProfilesValidationFailed: 'Project profile validation failed',
1836
+ },
1828
1837
  options: {
1829
1838
  profile: {
1830
- describe: 'The profile to target for this validation',
1839
+ describe: 'The profile to target for this validation. If no profile is provided, all profiles will be validated.',
1831
1840
  },
1832
1841
  },
1833
1842
  },
@@ -2866,11 +2875,6 @@ export const lib = {
2866
2875
  startError: (message) => `Failed to start local dev server: ${message}`,
2867
2876
  fileChangeError: (message) => `Failed to notify local dev server of file change: ${message}`,
2868
2877
  },
2869
- devSession: {
2870
- registrationError: (message) => `Failed to register dev session: ${message}`,
2871
- heartbeatError: (message) => `Failed to send dev session heartbeat: ${message}`,
2872
- deletionError: (message) => `Failed to delete dev session: ${message}`,
2873
- },
2874
2878
  },
2875
2879
  AppDevModeInterface: {
2876
2880
  autoInstallStaticAuthApp: {
@@ -2983,7 +2987,7 @@ export const lib = {
2983
2987
  updateNotification: {
2984
2988
  notifyTitle: chalk.bold('CLI update available'),
2985
2989
  cmsUpdateNotification: (packageName) => `${chalk.bold('The CMS CLI is now the HubSpot CLI')}\n\nTo upgrade, uninstall ${chalk.bold(packageName)}\nand then run ${uiCommandReference('{updateCommand}')}`,
2986
- cliUpdateNotification: `HubSpot CLI version ${chalk.cyan(chalk.bold('{currentVersion}'))} is outdated.\nRun ${uiCommandReference('{updateCommand}')} to upgrade to version ${chalk.cyan(chalk.bold('{latestVersion}'))}`,
2990
+ cliUpdateNotification: (currentVersion, updateCommand, latestVersion) => `HubSpot CLI version ${chalk.cyan(chalk.bold(`${currentVersion}`))} is outdated.\nRun ${uiCommandReference(`${updateCommand}`)} to upgrade to version ${chalk.cyan(chalk.bold(`${latestVersion}`))}`,
2987
2991
  },
2988
2992
  autoUpdateCLI: {
2989
2993
  updateAvailable: (latestVersion) => `There's a new HubSpot CLI version available! Updating to version ${chalk.bold(latestVersion)}`,
@@ -3008,7 +3012,9 @@ export const lib = {
3008
3012
  noProjectConfig: 'No project config found. Please run this command from a project directory.',
3009
3013
  profileNotFound: (profileName) => `Profile ${chalk.bold(profileName)} not found.`,
3010
3014
  missingAccountId: (profileName) => `Profile ${chalk.bold(profileName)} is missing an account id.`,
3015
+ listedAccountNotFound: (accountId, profileName) => `The account ${uiAccountDescription(accountId)} is defined in your profile ${chalk.bold(profileName)}, but is missing in your config file`,
3011
3016
  failedToLoadProfile: (profileName) => `Failed to load profile ${chalk.bold(profileName)}.`,
3017
+ profileNotValid: (profileName, errors) => `Profile "${profileName}" is not valid:\n\t- ${errors.join('\n\t- ')}`,
3012
3018
  },
3013
3019
  },
3014
3020
  },
@@ -3099,9 +3105,6 @@ export const lib = {
3099
3105
  legacyFileDetected: (filename, platformVersion) => `The ${chalk.bold(filename)} file is not supported on platform version ${chalk.bold(platformVersion)} and will be ignored.`,
3100
3106
  },
3101
3107
  },
3102
- boxen: {
3103
- failedToLoad: 'Failed to load boxen util.',
3104
- },
3105
3108
  importData: {
3106
3109
  errors: {
3107
3110
  incorrectAccountType: (derivedAccountId) => `The account ${uiAccountDescription(derivedAccountId)} is not a standard account, developer test account, or app developer account.`,
@@ -1,22 +1,26 @@
1
1
  import path from 'path';
2
- import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, } from '@hubspot/project-parsing-lib';
2
+ import { loadHsProfileFile, getHsProfileFilename, getAllHsProfiles, validateProfileVariables, } from '@hubspot/project-parsing-lib';
3
3
  import { lib } from '../../lang/en.js';
4
4
  import { uiBetaTag, uiLine } from '../ui/index.js';
5
5
  import { uiLogger } from '../ui/logger.js';
6
- import { EXIT_CODES } from '../enums/exitCodes.js';
7
- import { logProfileHeader, logProfileFooter, loadProfile, exitIfUsingProfiles, } from '../projectProfiles.js';
6
+ import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
7
+ import { logProfileHeader, logProfileFooter, loadProfile, enforceProfileUsage, loadAndValidateProfile, validateProjectForProfile, } from '../projectProfiles.js';
8
+ import { handleTranslate } from '../projects/upload.js';
9
+ import SpinniesManager from '../ui/SpinniesManager.js';
10
+ import { commands } from '../../lang/en.js';
8
11
  // Mock dependencies
9
12
  vi.mock('@hubspot/project-parsing-lib');
13
+ vi.mock('@hubspot/local-dev-lib/config');
10
14
  vi.mock('../ui');
11
15
  vi.mock('../ui/logger');
12
16
  vi.mock('../../lang/en');
13
- // Mock process.exit
14
- const mockExit = vi.spyOn(process, 'exit').mockImplementation(code => {
15
- throw new Error(`Process.exit called with code ${code}`);
16
- });
17
+ vi.mock('../projects/upload');
18
+ vi.mock('../ui/SpinniesManager');
17
19
  const mockedLoadHsProfileFile = loadHsProfileFile;
18
20
  const mockedGetHsProfileFilename = getHsProfileFilename;
19
21
  const mockedGetAllHsProfiles = getAllHsProfiles;
22
+ const mockedValidateProfileVariables = validateProfileVariables;
23
+ const mockedGetConfigAccountById = getConfigAccountById;
20
24
  const mockedUiBetaTag = uiBetaTag;
21
25
  const mockedUiLine = uiLine;
22
26
  const mockedUiLogger = uiLogger;
@@ -68,62 +72,299 @@ describe('lib/projectProfiles', () => {
68
72
  const mockProfile = {
69
73
  accountId: 123,
70
74
  };
71
- it('should return undefined when project config is missing', () => {
72
- const result = loadProfile(null, mockProjectDir, mockProfileName);
73
- expect(result).toBeUndefined();
74
- expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.noProjectConfig);
75
+ beforeEach(() => {
76
+ vi.clearAllMocks();
77
+ });
78
+ it('should throw error when project config is missing', () => {
79
+ expect(() => loadProfile(null, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
75
80
  });
76
- it('should return undefined when profile is not found', () => {
81
+ it('should throw error when project dir is missing', () => {
82
+ expect(() => loadProfile(mockProjectConfig, null, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.noProjectConfig);
83
+ });
84
+ it('should throw error when profile is not found', () => {
77
85
  mockedLoadHsProfileFile.mockReturnValue(null);
78
86
  const filename = 'test-profile.hsprofile';
79
87
  mockedGetHsProfileFilename.mockReturnValue(filename);
80
- const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
81
- expect(result).toBeUndefined();
82
- expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
88
+ expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.profileNotFound(filename));
83
89
  });
84
- it('should return undefined when profile has no account ID', () => {
90
+ it('should throw error when profile has no account ID', () => {
85
91
  mockedLoadHsProfileFile.mockReturnValue({});
86
92
  const filename = 'test-profile.hsprofile';
87
93
  mockedGetHsProfileFilename.mockReturnValue(filename);
88
- const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
89
- expect(result).toBeUndefined();
90
- expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
94
+ expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.missingAccountId(filename));
91
95
  });
92
- it('should return undefined when profile loading fails', () => {
96
+ it('should throw error when profile loading fails', () => {
93
97
  mockedLoadHsProfileFile.mockImplementation(() => {
94
98
  throw new Error('Load failed');
95
99
  });
96
100
  const filename = 'test-profile.hsprofile';
97
101
  mockedGetHsProfileFilename.mockReturnValue(filename);
98
- const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
99
- expect(result).toBeUndefined();
100
- expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(filename));
102
+ expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(filename));
103
+ });
104
+ it('should throw error when account is not found in config', () => {
105
+ mockedLoadHsProfileFile.mockReturnValue(mockProfile);
106
+ mockedGetConfigAccountById.mockImplementation(() => {
107
+ throw new Error('Account not found');
108
+ });
109
+ const filename = 'test-profile.hsprofile';
110
+ mockedGetHsProfileFilename.mockReturnValue(filename);
111
+ expect(() => loadProfile(mockProjectConfig, mockProjectDir, mockProfileName)).toThrow(lib.projectProfiles.loadProfile.errors.listedAccountNotFound(mockProfile.accountId, filename));
101
112
  });
102
113
  it('should return profile when loading succeeds', () => {
103
114
  mockedLoadHsProfileFile.mockReturnValue(mockProfile);
115
+ mockedGetConfigAccountById.mockReturnValue({
116
+ accountId: mockProfile.accountId,
117
+ });
104
118
  const result = loadProfile(mockProjectConfig, mockProjectDir, mockProfileName);
105
119
  expect(result).toEqual(mockProfile);
106
120
  expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
121
+ expect(mockedGetConfigAccountById).toHaveBeenCalledWith(mockProfile.accountId);
107
122
  });
108
123
  });
109
- describe('exitIfUsingProfiles()', () => {
124
+ describe('enforceProfileUsage()', () => {
110
125
  const mockProjectConfig = {
111
126
  srcDir: 'src',
112
127
  name: 'test-project',
113
128
  platformVersion: '1.0.0',
114
129
  };
115
130
  const mockProjectDir = '/test/project';
116
- it('should not exit when no profiles exist', async () => {
131
+ beforeEach(() => {
132
+ vi.clearAllMocks();
133
+ });
134
+ it('should not throw when no profiles exist', async () => {
117
135
  mockedGetAllHsProfiles.mockResolvedValue([]);
118
- await exitIfUsingProfiles(mockProjectConfig, mockProjectDir);
119
- expect(mockedUiLogger.error).not.toHaveBeenCalled();
120
- expect(mockExit).not.toHaveBeenCalled();
136
+ await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).resolves.toBeUndefined();
121
137
  });
122
- it('should exit with error when profiles exist', async () => {
138
+ it('should throw error when profiles exist', async () => {
123
139
  mockedGetAllHsProfiles.mockResolvedValue(['profile1', 'profile2']);
124
- await expect(exitIfUsingProfiles(mockProjectConfig, mockProjectDir)).rejects.toThrow(`Process.exit called with code ${EXIT_CODES.ERROR}`);
125
- expect(mockedUiLogger.error).toHaveBeenCalledWith(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
126
- expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
140
+ await expect(enforceProfileUsage(mockProjectConfig, mockProjectDir)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
141
+ });
142
+ it('should not throw when project config is null', async () => {
143
+ await expect(enforceProfileUsage(null, mockProjectDir)).resolves.toBeUndefined();
144
+ });
145
+ it('should not throw when project dir is null', async () => {
146
+ await expect(enforceProfileUsage(mockProjectConfig, null)).resolves.toBeUndefined();
147
+ });
148
+ });
149
+ describe('loadAndValidateProfile()', () => {
150
+ const mockProjectConfig = {
151
+ srcDir: 'src',
152
+ name: 'test-project',
153
+ platformVersion: '1.0.0',
154
+ };
155
+ const mockProjectDir = '/test/project';
156
+ const mockProfileName = 'test-profile';
157
+ const mockProfile = {
158
+ accountId: 123,
159
+ variables: {
160
+ key1: 'value1',
161
+ key2: 'value2',
162
+ },
163
+ };
164
+ beforeEach(() => {
165
+ vi.clearAllMocks();
166
+ });
167
+ it('should enforce profile usage when no profile name provided', async () => {
168
+ mockedGetAllHsProfiles.mockResolvedValue([]);
169
+ const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined);
170
+ expect(result).toBeUndefined();
171
+ expect(mockedGetAllHsProfiles).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir));
172
+ });
173
+ it('should throw when profiles exist but no profile name provided', async () => {
174
+ mockedGetAllHsProfiles.mockResolvedValue(['profile1']);
175
+ await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, undefined)).rejects.toThrow(lib.projectProfiles.exitIfUsingProfiles.errors.noProfileSpecified);
176
+ });
177
+ it('should load and return account ID when profile is valid', async () => {
178
+ mockedLoadHsProfileFile.mockReturnValue(mockProfile);
179
+ mockedGetConfigAccountById.mockReturnValue({
180
+ accountId: mockProfile.accountId,
181
+ });
182
+ mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
183
+ mockedValidateProfileVariables.mockReturnValue({ success: true });
184
+ const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
185
+ expect(result).toBe(mockProfile.accountId);
186
+ expect(mockedLoadHsProfileFile).toHaveBeenCalledWith(path.join(mockProjectDir, mockProjectConfig.srcDir), mockProfileName);
187
+ expect(mockedValidateProfileVariables).toHaveBeenCalledWith(mockProfile.variables, mockProfileName);
188
+ });
189
+ it('should log profile header and footer when not silent', async () => {
190
+ mockedLoadHsProfileFile.mockReturnValue(mockProfile);
191
+ mockedGetConfigAccountById.mockReturnValue({
192
+ accountId: mockProfile.accountId,
193
+ });
194
+ mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
195
+ mockedValidateProfileVariables.mockReturnValue({ success: true });
196
+ await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, false);
197
+ expect(mockedUiBetaTag).toHaveBeenCalled();
198
+ expect(mockedUiLine).toHaveBeenCalled();
199
+ expect(mockedUiLogger.log).toHaveBeenCalled();
200
+ });
201
+ it('should not log when silent is true', async () => {
202
+ mockedLoadHsProfileFile.mockReturnValue(mockProfile);
203
+ mockedGetConfigAccountById.mockReturnValue({
204
+ accountId: mockProfile.accountId,
205
+ });
206
+ mockedValidateProfileVariables.mockReturnValue({ success: true });
207
+ await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName, true);
208
+ expect(mockedUiBetaTag).not.toHaveBeenCalled();
209
+ expect(mockedUiLine).not.toHaveBeenCalled();
210
+ });
211
+ it('should throw error when profile variables are invalid', async () => {
212
+ const invalidProfile = {
213
+ accountId: 123,
214
+ variables: {
215
+ invalid: 'value',
216
+ },
217
+ };
218
+ const validationErrors = ['Variable "invalid" is not allowed'];
219
+ mockedLoadHsProfileFile.mockReturnValue(invalidProfile);
220
+ mockedGetConfigAccountById.mockReturnValue({
221
+ accountId: invalidProfile.accountId,
222
+ });
223
+ mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
224
+ mockedValidateProfileVariables.mockReturnValue({
225
+ success: false,
226
+ errors: validationErrors,
227
+ });
228
+ await expect(loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName)).rejects.toThrow(lib.projectProfiles.loadProfile.errors.profileNotValid('test-profile.hsprofile', validationErrors));
229
+ });
230
+ it('should not validate when profile has no variables', async () => {
231
+ const profileWithoutVars = {
232
+ accountId: 123,
233
+ };
234
+ mockedLoadHsProfileFile.mockReturnValue(profileWithoutVars);
235
+ mockedGetConfigAccountById.mockReturnValue({
236
+ accountId: profileWithoutVars.accountId,
237
+ });
238
+ mockedGetHsProfileFilename.mockReturnValue('test-profile.hsprofile');
239
+ const result = await loadAndValidateProfile(mockProjectConfig, mockProjectDir, mockProfileName);
240
+ expect(result).toBe(profileWithoutVars.accountId);
241
+ expect(mockedValidateProfileVariables).not.toHaveBeenCalled();
242
+ });
243
+ });
244
+ describe('validateProjectForProfile()', () => {
245
+ const mockProjectConfig = {
246
+ srcDir: 'src',
247
+ name: 'test-project',
248
+ platformVersion: '2025.2',
249
+ };
250
+ const mockProjectDir = '/test/project';
251
+ const mockProfileName = 'test-profile';
252
+ const mockDerivedAccountId = 123;
253
+ const mockProfileFilename = 'test-profile.hsprofile';
254
+ const mockProfile = {
255
+ accountId: mockDerivedAccountId,
256
+ };
257
+ beforeEach(() => {
258
+ vi.clearAllMocks();
259
+ mockedGetHsProfileFilename.mockReturnValue(mockProfileFilename);
260
+ vi.mocked(SpinniesManager.init);
261
+ vi.mocked(SpinniesManager.add);
262
+ vi.mocked(SpinniesManager.succeed);
263
+ vi.mocked(SpinniesManager.fail);
264
+ // Mock dependencies for loadAndValidateProfile
265
+ mockedGetAllHsProfiles.mockResolvedValue([]);
266
+ mockedLoadHsProfileFile.mockReturnValue(mockProfile);
267
+ mockedGetConfigAccountById.mockReturnValue({
268
+ accountId: mockDerivedAccountId,
269
+ });
270
+ mockedValidateProfileVariables.mockReturnValue({ success: true });
271
+ vi.mocked(handleTranslate).mockResolvedValue(undefined);
272
+ });
273
+ it('should return empty array when validation succeeds', async () => {
274
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
275
+ expect(result).toEqual([]);
276
+ expect(SpinniesManager.init).toHaveBeenCalled();
277
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
278
+ text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
279
+ indent: 0,
280
+ });
281
+ expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
282
+ text: commands.project.validate.spinners.profileValidationSucceeded(mockProfileFilename),
283
+ });
284
+ });
285
+ it('should call handleTranslate with profile account ID from profile', async () => {
286
+ await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
287
+ expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, mockDerivedAccountId, false, mockProfileName);
288
+ });
289
+ it('should call handleTranslate with different profile account ID when profile has different ID', async () => {
290
+ const profileAccountId = 456;
291
+ const profileWithDifferentId = {
292
+ accountId: profileAccountId,
293
+ };
294
+ mockedLoadHsProfileFile.mockReturnValue(profileWithDifferentId);
295
+ mockedGetConfigAccountById.mockReturnValue({
296
+ accountId: profileAccountId,
297
+ });
298
+ await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
299
+ expect(handleTranslate).toHaveBeenCalledWith(mockProjectDir, mockProjectConfig, profileAccountId, false, mockProfileName);
300
+ });
301
+ it('should return error when profile has no accountId', async () => {
302
+ // @ts-expect-error causing an error on purpose
303
+ const profileWithoutId = {};
304
+ mockedLoadHsProfileFile.mockReturnValue(profileWithoutId);
305
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
306
+ expect(result.length).toBeGreaterThan(0);
307
+ expect(SpinniesManager.fail).toHaveBeenCalled();
308
+ expect(handleTranslate).not.toHaveBeenCalled();
309
+ });
310
+ it('should indent spinners when indentSpinners is true', async () => {
311
+ await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, true);
312
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
313
+ text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
314
+ indent: 4,
315
+ });
316
+ });
317
+ it('should not indent spinners when indentSpinners is false', async () => {
318
+ await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId, false);
319
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
320
+ text: commands.project.validate.spinners.validatingProfile(mockProfileFilename),
321
+ indent: 0,
322
+ });
323
+ });
324
+ it('should return error array when profile loading fails', async () => {
325
+ mockedLoadHsProfileFile.mockReturnValue(null);
326
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
327
+ expect(result.length).toBeGreaterThan(0);
328
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
329
+ text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
330
+ });
331
+ expect(handleTranslate).not.toHaveBeenCalled();
332
+ });
333
+ it('should return error when profile file loading throws', async () => {
334
+ mockedLoadHsProfileFile.mockImplementation(() => {
335
+ throw new Error('Failed to load profile file');
336
+ });
337
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
338
+ expect(result.length).toBeGreaterThan(0);
339
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
340
+ text: commands.project.validate.spinners.profileValidationFailed(mockProfileFilename),
341
+ });
342
+ expect(handleTranslate).not.toHaveBeenCalled();
343
+ });
344
+ it('should return error array when translation fails', async () => {
345
+ const error = new Error('Translation failed');
346
+ vi.mocked(handleTranslate).mockRejectedValue(error);
347
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
348
+ expect(result).toHaveLength(2);
349
+ expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
350
+ expect(result[1]).toBe(error);
351
+ expect(SpinniesManager.fail).toHaveBeenCalledWith(`validatingProfile-${mockProfileName}`, {
352
+ text: commands.project.validate.spinners.invalidWithProfile(mockProfileFilename, mockProjectConfig.name),
353
+ });
354
+ });
355
+ it('should return string error when translation fails with non-Error', async () => {
356
+ const error = 'Translation error';
357
+ vi.mocked(handleTranslate).mockRejectedValue(error);
358
+ const result = await validateProjectForProfile(mockProjectConfig, mockProjectDir, mockProfileName, mockDerivedAccountId);
359
+ expect(result).toHaveLength(2);
360
+ expect(result[0]).toBe(commands.project.validate.failure(mockProjectConfig.name));
361
+ expect(result[1]).toBe(error);
362
+ });
363
+ it('should use correct spinner name based on profile name', async () => {
364
+ const customProfileName = 'custom-profile';
365
+ await validateProjectForProfile(mockProjectConfig, mockProjectDir, customProfileName, mockDerivedAccountId);
366
+ expect(SpinniesManager.add).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
367
+ expect(SpinniesManager.succeed).toHaveBeenCalledWith(`validatingProfile-${customProfileName}`, expect.any(Object));
127
368
  });
128
369
  });
129
370
  });
@@ -1,13 +1,14 @@
1
1
  import updateNotifier from 'update-notifier';
2
2
  import { getConfig } from '@hubspot/local-dev-lib/config';
3
3
  import { pkg } from '../jsonLoader.js';
4
- import { UI_COLORS } from '../ui/index.js';
5
4
  import SpinniesManager from '../ui/SpinniesManager.js';
6
5
  import { lib } from '../../lang/en.js';
7
6
  import { DEFAULT_PACKAGE_MANAGER, isGloballyInstalled, executeInstall, } from '../npm.js';
8
7
  import { debugError } from '../errorHandlers/index.js';
9
8
  import { uiLogger } from '../ui/logger.js';
10
9
  import { isTargetedCommand } from './commandTargetingUtils.js';
10
+ import { renderInline } from '../../ui/index.js';
11
+ import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
11
12
  // Default behavior is to check for notifications at most once per day
12
13
  // update-notifier stores the last checked date in the user's home directory
13
14
  const notifier = updateNotifier({
@@ -16,24 +17,16 @@ const notifier = updateNotifier({
16
17
  shouldNotifyInNpmScript: true,
17
18
  });
18
19
  const CMS_CLI_PACKAGE_NAME = '@hubspot/cms-cli';
19
- async function updateNotification() {
20
- notifier.notify({
20
+ async function updateNotification(currentVersion, latestVersion, updateCommand) {
21
+ await renderInline(getWarningBox({
22
+ title: pkg.name === CMS_CLI_PACKAGE_NAME
23
+ ? ''
24
+ : lib.middleware.updateNotification.notifyTitle,
21
25
  message: pkg.name === CMS_CLI_PACKAGE_NAME
22
26
  ? lib.middleware.updateNotification.cmsUpdateNotification(CMS_CLI_PACKAGE_NAME)
23
- : lib.middleware.updateNotification.cliUpdateNotification,
24
- defer: false,
25
- boxenOptions: {
26
- borderColor: UI_COLORS.MARIGOLD_DARK,
27
- margin: 1,
28
- padding: 1,
29
- textAlignment: 'center',
30
- borderStyle: 'round',
31
- title: pkg.name === CMS_CLI_PACKAGE_NAME
32
- ? undefined
33
- : lib.middleware.updateNotification.notifyTitle,
34
- },
35
- isGlobal: await isGloballyInstalled('hs'),
36
- });
27
+ : lib.middleware.updateNotification.cliUpdateNotification(currentVersion, updateCommand, latestVersion),
28
+ textCentered: true,
29
+ }));
37
30
  }
38
31
  const SKIP_AUTO_UPDATE_COMMANDS = {
39
32
  config: {
@@ -44,8 +37,16 @@ const preventAutoUpdateForCommand = (commandParts) => {
44
37
  return isTargetedCommand(commandParts, SKIP_AUTO_UPDATE_COMMANDS);
45
38
  };
46
39
  export async function autoUpdateCLI(argv) {
47
- // This lets us back to default update-notifier behavior
48
40
  let showManualInstallHelp = true;
41
+ let isGlobalInstall = null;
42
+ const checkGlobalInstall = async () => {
43
+ if (isGlobalInstall === null) {
44
+ isGlobalInstall =
45
+ (await isGloballyInstalled(DEFAULT_PACKAGE_MANAGER)) &&
46
+ (await isGloballyInstalled('hs'));
47
+ }
48
+ return isGlobalInstall;
49
+ };
49
50
  let config;
50
51
  try {
51
52
  config = getConfig();
@@ -71,8 +72,7 @@ export async function autoUpdateCLI(argv) {
71
72
  text: lib.middleware.autoUpdateCLI.updateAvailable(notifier.update.latest),
72
73
  });
73
74
  try {
74
- if ((await isGloballyInstalled(DEFAULT_PACKAGE_MANAGER)) &&
75
- (await isGloballyInstalled('hs'))) {
75
+ if (await checkGlobalInstall()) {
76
76
  await executeInstall(['@hubspot/cli@latest'], '-g');
77
77
  showManualInstallHelp = false;
78
78
  SpinniesManager.succeed('cliAutoUpdate', {
@@ -95,7 +95,10 @@ export async function autoUpdateCLI(argv) {
95
95
  }
96
96
  }
97
97
  }
98
- if (showManualInstallHelp) {
99
- await updateNotification();
98
+ if (showManualInstallHelp &&
99
+ notifier.update &&
100
+ process.stdout.isTTY &&
101
+ !notifier.update.current.includes('-')) {
102
+ await updateNotification(notifier.update.current, notifier.update.latest, `npm i ${(await checkGlobalInstall()) ? '-g' : ''} @hubspot/cli`);
100
103
  }
101
104
  }
@@ -2,7 +2,6 @@ import chalk from 'chalk';
2
2
  import { fetchFireAlarms } from '@hubspot/local-dev-lib/api/fireAlarm';
3
3
  import { debugError } from '../errorHandlers/index.js';
4
4
  import { pkg } from '../jsonLoader.js';
5
- import { logInBox } from '../ui/boxen.js';
6
5
  import { renderInline } from '../../ui/index.js';
7
6
  import { getWarningBox } from '../../ui/components/StatusMessageBoxes.js';
8
7
  /*
@@ -100,20 +99,10 @@ async function logFireAlarms(accountId, command, version) {
100
99
  }
101
100
  return acc;
102
101
  }, '');
103
- if (!process.env.HUBSPOT_ENABLE_INK) {
104
- await logInBox({
105
- contents: notifications,
106
- options: {
107
- title: 'Notifications',
108
- },
109
- });
110
- }
111
- else {
112
- await renderInline(getWarningBox({
113
- title: 'Notifications',
114
- message: notifications,
115
- }));
116
- }
102
+ await renderInline(getWarningBox({
103
+ title: 'Notifications',
104
+ message: notifications,
105
+ }));
117
106
  }
118
107
  }
119
108
  export async function checkFireAlarms(argv) {
@@ -2,6 +2,7 @@ import { HsProfileFile } from '@hubspot/project-parsing-lib/src/lib/types.js';
2
2
  import { ProjectConfig } from '../types/Projects.js';
3
3
  export declare function logProfileHeader(profileName: string): void;
4
4
  export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
5
- export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
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): Promise<number | undefined>;
5
+ export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | never;
6
+ export declare function enforceProfileUsage(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
7
+ export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string | undefined, silent?: boolean): Promise<number | undefined>;
8
+ export declare function validateProjectForProfile(projectConfig: ProjectConfig, projectDir: string, profileName: string, derivedAccountId: number, indentSpinners?: boolean): Promise<(string | Error)[]>;